Commit df48c296 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Quatorze jeux de commandes qui ignoraient l'intitulé posé au-dessus d'eux

Le produit nomme aussi ses groupes par une « span.etiq », qui n'est qu'un style :
aucune association, aucun rôle. Le lecteur d'écran lisait le texte, puis une
série de boutons sans savoir de quel jeu ils relèvent — « Ajouter, Ajouter,
Ajouter » sous « Le catalogue ».

── Mon « sept » était faux, et la façon dont il l'était compte

Je l'avais tiré d'un grep qui ne regardait qu'une ligne après l'intitulé. En
balayant les 170 « .etiq » du produit, une cinquantaine coiffent quelque chose
d'interactif — mais la plupart sont des titres de panneau, suivis d'un bouton
« Fermer ». Ce ne sont pas des noms de groupe, et les nommer aurait ajouté du
bruit.

J'ai donc trié à la main, retenu douze, écrit la garantie — et la garantie en a
trouvé deux de plus que ma relecture avait manqués : le champ d'échéance du
détail d'action et le catalogue de tuiles du cockpit. Quatre fois dans cette
session j'ai énuméré à la main et compté faux ; à chaque fois qu'une règle
exécutable a repris le compte, elle a trouvé le reste. La règle avant la
relecture, désormais.

── Les quatorze

Envoi en masse (à qui, qui porte ces étiquettes, sauf), pilotage de crise
(gravité), mobilisation (mobilisations préparées), intégrations (portées),
cockpit (le catalogue), détail d'action (échéance), et les cinq sections des deux
rails de navigation — « Cockpit et salons », « Transverse », « Organisation »,
« Paramètres de l'organisation », « Instance ». Ces cinq-là sont le meilleur
rapport de l'ensemble : c'est par le rail qu'on se déplace pendant une crise,
et il porte vingt-quatre destinations.

── Un champ sans aucun nom, trouvé en chemin

Le « datetime-local » de l'échéance n'avait ni étiquette, ni « aria-label », ni
rien : un lecteur d'écran annonçait une zone de saisie muette. Nommé. Il aurait
dû tomber dans le lot des vingt champs nommés en début de session ; il y avait
échappé.

── Ce que je n'ai pas touché, et pourquoi

Les titres de panneau : ils nomment une région, pas un jeu de commandes.
Les listes lisibles — invitations en attente, membres, observations de clôture,
appareils mobiles : du contenu, pas des commandes.
Les blocs dont les commandes portent déjà leur propre nom, comme la nouvelle
période d'astreinte.
Et trois barres d'onglets — conduite, tableau des actions, veille externe :
elles demandent « role=tablist » et « aria-selected », pas un groupe. Signalées
dans le commentaire de la garantie, à traiter à part.

1179 garanties au vert. Vérifié à l'écran, dans l'arbre d'accessibilité :
« group Organisation », « group Cockpit et salons », « group À qui »,
« group Le catalogue », « textbox Échéance » là où il n'y avait qu'un « div » et
un champ muet. Mise en page inchangée.

Co-Authored-By: Claude (RCA)
parent a3b663cd
Loading
Loading
Loading
Loading
Loading
+37 −0
Changes for apps/api/src/verification/interface.ts: 37 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -452,6 +452,43 @@ export async function verifierInterface(r: Rapport): Promise<void> {
  r.verifie("EX-19", "aucune étiquette n'est posée sans le champ qu'elle nomme",
    orphelines.length === 0, orphelines.slice(0, 4).join(", "));

  // ── Un jeu de boutons porte le nom qui le coiffe (EX-19) ─────────────────
  //
  // Le contrôle ci-dessus ne voit que les « label ». Le produit nomme aussi ses
  // groupes par une « span.etiq », qui n'est qu'un style : le lecteur d'écran
  // lisait le texte, puis une série de boutons sans savoir de quel jeu ils
  // relèvent — « Ajouter, Ajouter, Ajouter » sous « Le catalogue ».
  //
  // Ce contrôle a trouvé deux groupes que ma relecture avait manqués, après en
  // avoir manqué d'autres à chaque fois que j'ai énuméré à la main. C'est la
  // raison d'être de la règle : elle se rejoue, la relecture non.
  //
  // Elle ne porte que sur la forme la plus nette — une « span.etiq » fermée sur
  // sa ligne, suivie dans les deux lignes d'un « div.ligne » ou « div.pile » qui
  // contient des boutons. Ce qu'elle ne voit pas : un titre de panneau, une
  // liste d'éléments lisibles, une barre d'onglets. Ceux-là se tranchent au cas
  // par cas, et trois barres d'onglets restent à traiter en « role=tablist ».
  const anonymes: string[] = [];
  for (const f of fichiers) {
    if (!f.endsWith(".tsx")) continue;
    const lignes = (await readFile(f, "utf8")).split("\n");
    for (let i = 0; i < lignes.length; i++) {
      const etiq = lignes[i]!;
      if (!etiq.includes('<span className="etiq">') || !etiq.includes("</span>")) continue;
      for (const j of [i + 1, i + 2]) {
        const cont = lignes[j];
        if (cont === undefined || !/<div [^\n]*className="(?:ligne|pile)"/.test(cont)) continue;
        if (lignes.slice(j + 1, j + 9).some((x) => x.includes("<button"))
          && !cont.includes('role="group"')) {
          anonymes.push(`${f.slice(web.length)}:${i + 1}`);
        }
        break;
      }
    }
  }
  r.verifie("EX-19", "aucun jeu de boutons n'ignore l'intitulé qui le coiffe",
    anonymes.length === 0, anonymes.slice(0, 4).join(", "));

  r.verifie("EX-21", "une clé absente retombe sur le français, pas sur un identifiant",
    noyau.includes("EN[fr] ?? fr"));
  r.verifie("EX-21", "la langue du document suit la langue choisie",
+2 −2
Changes for apps/web/src/coque/Coque.tsx: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -437,7 +437,7 @@ export function Coque({ criseId }: { criseId: string }) {
        <nav className={`rail${railOuvert ? " ouvert" : ""}`}
          aria-label={t("Navigation de la crise")}
          onClick={() => definirRailOuvert(false)}>
          <div className="groupe">
          <div role="group" aria-label={t("Cockpit et salons")} className="groupe">
            <span className="etiq">{t("Cockpit et salons")}</span>
            <button className={`item${module === "cockpit" ? " actif" : ""}`}
              aria-current={module === "cockpit" ? "page" : undefined}
@@ -509,7 +509,7 @@ export function Coque({ criseId }: { criseId: string }) {
            </button>
          </div>

          <div className="groupe">
          <div role="group" aria-label={t("Transverse")} className="groupe">
            <span className="etiq">{t("Transverse")}</span>
            {/* En tête : c'est le premier écran d'une crise qui vient de
                s'ouvrir, et il ne sert à rien s'il faut le chercher. */}
+2 −2
Changes for apps/web/src/ecrans/ActionDetail.tsx: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -107,8 +107,8 @@ export function ActionDetail({
      {/* ── Quand elle est due ────────────────────────────────────────────── */}
      <div style={{ marginTop: 16 }}>
        <span className="etiq">{t("Échéance")}</span>
        <div className="ligne" style={{ gap: 8, marginTop: 8, flexWrap: "wrap" }}>
          <input type="datetime-local" value={echeance}
        <div role="group" aria-label={t("Échéance")} className="ligne" style={{ gap: 8, marginTop: 8, flexWrap: "wrap" }}>
          <input type="datetime-local" value={echeance} aria-label={t("Échéance")}
            onChange={(e) => definirEcheance(e.target.value)} />
          {echeance && (
            <button className="discret petit" onClick={() => definirEcheance("")}>
+1 −1
Changes for apps/web/src/ecrans/Cockpit.tsx: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -227,7 +227,7 @@ function Composer({

      <div style={{ marginTop: 14 }}>
        <span className="etiq">{t("Le catalogue")}</span>
        <div className="pile" style={{ marginTop: 6 }}>
        <div role="group" aria-label={t("Le catalogue")} className="pile" style={{ marginTop: 6 }}>
          {CATALOGUE_TUILES.filter((d) => !choix.includes(d.cle)).map((d) => (
            <div key={d.cle} className="ligne" style={{ alignItems: "flex-start", gap: 8 }}>
              <div style={{ flex: 1 }}>
+3 −3
Changes for apps/web/src/ecrans/EnvoiMasse.tsx: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -146,7 +146,7 @@ export function EnvoiMasse({ organisationId, criseId }: {
          <div className="carte">
            <span className="etiq">{t("À qui")}</span>

            <div className="ligne" style={{ gap: 6, marginTop: 10 }}>
            <div role="group" aria-label={t("À qui")} className="ligne" style={{ gap: 6, marginTop: 10 }}>
              {([["sms", "SMS"], ["courriel", "Courriel"],
                 ["voix", t("Appel vocal")]] as const).map(([g, libelle]) => (
                <button key={g} className={genre === g ? "accent petit" : "petit"}
@@ -257,7 +257,7 @@ export function EnvoiMasse({ organisationId, criseId }: {
                pression, on ne rédige pas une requête. */}
            <div style={{ marginTop: 12 }}>
              <span className="etiq">{t("Qui porte toutes ces étiquettes")}</span>
              <div className="ligne" style={{ flexWrap: "wrap", gap: 6, marginTop: 5 }}>
              <div role="group" aria-label={t("Qui porte toutes ces étiquettes")} className="ligne" style={{ flexWrap: "wrap", gap: 6, marginTop: 5 }}>
                {(vocabulaire.data?.etiquettes ?? []).map((e) => (
                  <button key={e.nom} className={et.includes(e.nom) ? "accent petit" : "petit"}
                    onClick={() => definirEt(et.includes(e.nom)
@@ -279,7 +279,7 @@ export function EnvoiMasse({ organisationId, criseId }: {
            {(vocabulaire.data?.etiquettes ?? []).length > 0 && (
              <div style={{ marginTop: 10 }}>
                <span className="etiq">{t("Sauf")}</span>
                <div className="ligne" style={{ flexWrap: "wrap", gap: 6, marginTop: 5 }}>
                <div role="group" aria-label={t("Sauf")} className="ligne" style={{ flexWrap: "wrap", gap: 6, marginTop: 5 }}>
                  {(vocabulaire.data?.etiquettes ?? []).map((e) => (
                    <button key={e.nom}
                      className={sauf.includes(e.nom) ? "accent petit" : "petit"}
Loading