Fait dire aux boutons repliables s'ils sont ouverts ou fermés

« aria-expanded="@_ouvert" » ne rend pas « true » ou « false » : Blazor y voit
un attribut de présence, comme disabled. Vrai donnait aria-expanded="", faux
retirait l'attribut — dans les deux cas, un lecteur d'écran n'apprenait rien,
et un bouton replié ne s'annonçait plus comme repliable.

Le défaut valait pour les six boutons de l'application. Une fonction nommée
remplace le booléen nu, qui se relisait comme correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-22 01:39:28 +02:00
co-authored by Claude Opus 5
parent e85867014c
commit c168959e12
8 changed files with 63 additions and 13 deletions
+13 -5
View File
@@ -3353,16 +3353,24 @@ un cran trop haut qu'un catalogue qui ne s'affiche plus. Un test le verrouille.
reste un vrai lien vers `/series/{id}`.
⚠️ **`aria-expanded="@Deplie"` ne fait pas ce qu'on croit** : Blazor traite un booléen comme un
attribut de *présence* et rend `aria-expanded=""`, que les lecteurs d'écran ne savent pas lire.
L'état doit être écrit en toutes lettres (`@(Deplie ? "true" : "false")`). Constaté ici, mesuré
dans le navigateur — **le même défaut subsiste ailleurs** (`ArbreSerie`, le bouton « Filtrer » du
catalogue), non corrigé faute d'être dans la demande.
attribut de *présence*, à la manière de `disabled` — vrai rend `aria-expanded=""`, faux **retire
l'attribut**. Or ARIA attend les chaînes « true » et « false », et un lecteur d'écran ne sait lire
aucune des deux formes produites : replié, le bouton ne s'annonce plus comme repliable du tout.
Le défaut était passé inaperçu **six fois** — il ne se voit pas à l'écran, et aucun test ne rend
de balisage. `Services/Aria.cs` porte donc la mise en garde et la conversion
(`aria-expanded="@Aria.Etat(_ouvert)"`), appliquée partout : bascule du menu, bouton « Filtrer »
du catalogue et de la bibliographie, dépli d'une œuvre bibliographique, arbre des séries, blocs
du catalogue. ⚠️ **Une fonction plutôt qu'un ternaire recopié** : un booléen nu se lit comme
correct à la relecture, un appel nommé se remarque quand il manque.
Vérifié en exécution sur les six : « false » à l'état replié, « true » une fois ouvert.
**Vérifié en exécution** (375 px puis 320 px) : le cycle *La Légende de Drizzt* coiffe ses deux
sous-séries, chaque niveau se replie et se déplie indépendamment, l'état d'un sous-niveau survit
au repli de son cycle, le compteur reste « 6 livres » dans tous les cas, le repli survit à une
recherche (« salvatore ») et à son effacement, le nom mène toujours à `/series/6`, aucun
débordement horizontal (`scrollWidth` = 320) et aucune erreur de console. 627 tests au vert.
débordement horizontal (`scrollWidth` = 320) et aucune erreur de console. 628 tests au vert.
**Vérifié en exécution** (320 px puis 1280 px, base de développement garnie de trois tomes et
d'un livre hors série) : blocs « L'Elfe noir » (2 tomes, dans l'ordre de **lecture** — l'inverse
+1 -1
View File
@@ -16,7 +16,7 @@
<div class="arbre-ligne">
@if (Enfants.Count > 0)
{
<button type="button" class="arbre-plier" aria-expanded="@Deplie"
<button type="button" class="arbre-plier" aria-expanded="@Aria.Etat(Deplie)"
aria-label="@($"{(Deplie ? "Replier" : "Déplier")} « {Serie.Titre} »")"
@onclick="Basculer">
<span aria-hidden="true">@(Deplie ? "▾" : "▸")</span>
@@ -13,10 +13,9 @@
*@
<li class="groupe-serie" @key="Entree.Serie!.Id">
<div class="groupe-serie-entete">
@* ⚠️ « aria-expanded="@Deplie" » ne marche PAS : Blazor traite un booléen comme un attribut
de présence et rend « aria-expanded="" ». L'état doit être écrit en toutes lettres. *@
@* ⚠️ Aria.Etat, et non le booléen nu : voir la mise en garde qu'il porte. *@
<button type="button" class="groupe-serie-plier"
aria-expanded="@(Deplie ? "true" : "false")"
aria-expanded="@Aria.Etat(Deplie)"
aria-label="@($"{(Deplie ? "Replier" : "Déplier")} « {Entree.Serie!.Titre} »")"
@onclick="Basculer">
<span aria-hidden="true">@(Deplie ? "▾" : "▸")</span>
+1 -1
View File
@@ -61,7 +61,7 @@
}
<button type="button" class="bandeau-icone bandeau-bascule" @onclick="BasculerMenu"
aria-expanded="@_menuOuvert" aria-controls="menu-principal"
aria-expanded="@Aria.Etat(_menuOuvert)" aria-controls="menu-principal"
aria-label="@(_menuOuvert ? "Fermer le menu" : "Ouvrir le menu")">
<span aria-hidden="true">@(_menuOuvert ? "\u2715" : "\u2630")</span>
</button>
+2 -2
View File
@@ -165,7 +165,7 @@ else if (_bibliographie is { } biblio)
<div class="actions-formulaire">
<button type="button" class="bouton bouton-discret"
@onclick="() => _filtresOuverts = !_filtresOuverts"
aria-expanded="@_filtresOuverts">
aria-expanded="@Aria.Etat(_filtresOuverts)">
Filtrer (@NombreFiltres)
</button>
@@ -302,7 +302,7 @@ else if (_bibliographie is { } biblio)
{
<button type="button" class="carte-titre oeuvre-titre-bouton"
@onclick="() => BasculerOeuvre(oeuvre.Titre)"
aria-expanded="@_oeuvresOuvertes.Contains(oeuvre.Titre)">
aria-expanded="@Aria.Etat(_oeuvresOuvertes.Contains(oeuvre.Titre))">
@oeuvre.Titre
</button>
}
+1 -1
View File
@@ -15,7 +15,7 @@
<h1 class="titre-page">Catalogue</h1>
<button type="button" class="bouton bouton-discret bouton-compact"
aria-expanded="@_filtresOuverts" aria-controls="filtres-catalogue"
aria-expanded="@Aria.Etat(_filtresOuverts)" aria-controls="filtres-catalogue"
@onclick="() => _filtresOuverts = !_filtresOuverts">
Filtrer@(FiltresActifs > 0 ? $" ({FiltresActifs})" : "")
</button>
+22
View File
@@ -0,0 +1,22 @@
namespace MaBibli.Client.Services;
/// <summary>
/// Les états ARIA, écrits en toutes lettres.
/// </summary>
/// <remarks>
/// ⚠️ <b><c>aria-expanded="@_ouvert"</c> ne fait pas ce qu'on croit.</b> Blazor traite un booléen
/// comme un attribut de <i>présence</i> — à la manière de <c>disabled</c> ou <c>hidden</c> : vrai
/// rend <c>aria-expanded=""</c>, faux <b>retire l'attribut</b>. Or ARIA attend les chaînes
/// « true » et « false », et un lecteur d'écran ne sait lire ni l'une ni l'autre de ces deux
/// formes : replié, le bouton ne s'annonce plus comme repliable du tout.
/// <para>
/// Le défaut est passé inaperçu cinq fois (2026-08-22) parce qu'il ne se voit pas à l'écran et
/// qu'aucun test ne rend de balisage. D'où cette fonction : elle se remarque à la relecture là
/// où un booléen nu se lit comme correct.
/// </para>
/// </remarks>
public static class Aria
{
/// <summary>« true » ou « false », jamais un attribut de présence.</summary>
public static string Etat(bool valeur) => valeur ? "true" : "false";
}
+21
View File
@@ -0,0 +1,21 @@
using MaBibli.Client.Services;
namespace MaBibli.Tests;
/// <summary>
/// L'état ARIA s'écrit en toutes lettres (2026-08-22).
/// </summary>
/// <remarks>
/// ⚠️ Ce que ce test verrouille n'est pas la fonction — elle tient en une ligne — mais le
/// <b>contrat</b> : « false » est une valeur, et non l'absence d'attribut. C'est exactement ce
/// que Blazor faisait à sa place, et ce qui rendait un bouton replié muet aux lecteurs d'écran.
/// </remarks>
public class AriaTests
{
[Fact]
public void L_etat_est_toujours_une_chaine()
{
Assert.Equal("true", Aria.Etat(true));
Assert.Equal("false", Aria.Etat(false));
}
}