Commit 41f245c3 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Six champs, zéro étiquette : le formulaire des destinataires n'annonçait rien

Vingt champs de l'interface n'avaient aucun nom accessible. Le pire est celui
des destinataires : six champs, et ni étiquette associée, ni « placeholder », ni
« aria-label ». Un lecteur d'écran y annonce six zones de texte anonymes, là où
l'œil lit « Nom », « Organisme », « Adresse électronique », « Téléphone »,
« Langue », « Ce qui autorise l'envoi ».

Le motif était toujours le même : une étiquette posée à côté du champ, sans
« htmlFor », et un champ sans « id ». Cela se voit à l'écran et ne s'associe
pas. Ailleurs dans le produit c'est fait — quatre-vingt-seize étiquettes sur
cent quarante-quatre portent déjà leur « htmlFor ».

Deux traitements, selon ce que l'écran montre déjà.

Là où une étiquette existe, elle est associée : destinataires, modèles de
message, étiquettes, canaux de crise, recensement. Là où la rangée est compacte
à dessein — les périodes d'astreinte, les délais de relance et d'escalade — un
« aria-label » nomme le champ sans ajouter une ligne qui déferait la mise en
page : « Début de la période », « Fin de la période », « Relance, en minutes ».

Le cas des astreintes méritait le détour : deux champs « datetime-local »
séparés par une flèche, que rien ne distinguait l'un de l'autre.

Et quatre apostrophes droites dans du texte visible, dans un produit qui écrit
« l’est » partout ailleurs.

Restent onze étiquettes qui nomment un groupe de boutons ou de cases plutôt
qu'un champ — « Criticité », « Gravité », « Cellules à ouvrir ». Elles pointent
vers rien. Le correctif est « role="group" » ou « fieldset », qui touche au
style : signalé, pas fait.

Co-Authored-By: Claude (RCA)
parent ba4824b7
Loading
Loading
Loading
Loading
+3 −3
Changes for apps/web/src/ecrans/CanauxCrise.tsx: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -98,7 +98,7 @@ export function CanauxCrise({ criseId }: { criseId: string }) {
    <div className="scene">
      <header>
        <div className="etiq">Canaux</div>
        <h1>Ce qui est ouvert, ce qui ne l'est pas</h1>
        <h1>Ce qui est ouvert, ce qui ne l’est pas</h1>
      </header>

      <div className="contenu">
@@ -114,8 +114,8 @@ export function CanauxCrise({ criseId }: { criseId: string }) {

          {ouvert && (
            <div style={{ marginTop: 12 }}>
              <label>Nom du canal</label>
              <input value={libelle} placeholder="Juridique et assurances"
              <label htmlFor="canal-nom">Nom du canal</label>
              <input id="canal-nom" value={libelle} placeholder="Juridique et assurances"
                onChange={(e) => definirLibelle(e.target.value)} />

              <label className="ligne" style={{ gap: 8, marginTop: 10, cursor: "pointer" }}>
+2 −1
Changes for apps/web/src/ecrans/Mobilisation.tsx: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -348,12 +348,13 @@ export function Mobilisation({ criseId }: { criseId: string }) {
              <span className="discret" style={{ fontSize: 13 }}>
                Relance
                <input inputMode="numeric" value={relance} style={{ maxWidth: 64, margin: "0 6px" }}
                  aria-label="Relance, en minutes"
                  onChange={(e) => definirRelance(Number(e.target.value) || 10)} />
                min
              </span>
              <span className="discret" style={{ fontSize: 13 }}>
                Escalade vers le suppléant
                <input inputMode="numeric" value={escalade}
                <input inputMode="numeric" value={escalade} aria-label="Escalade vers le suppléant, en minutes"
                  style={{ maxWidth: 64, margin: "0 6px" }}
                  onChange={(e) => definirEscalade(Number(e.target.value) || 25)} />
                min
+4 −4
Changes for apps/web/src/ecrans/Recensement.tsx: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -83,7 +83,7 @@ export function Recensement({ criseId, organisationId }: {
            <div className={e.besoin_aide > 0 ? "carte crit" : "carte"}
              style={{ marginBottom: 16 }}>
              <div className="etiq" style={{ marginBottom: 8 }}>
                {e.besoin_aide > 0 ? "Demandent de l'aide" : "Personne ne demande d'aide"}
                {e.besoin_aide > 0 ? "Demandent de l’aide" : "Personne ne demande d’aide"}
              </div>
              {e.a_secourir.map((p) => (
                <div key={p.nom} className="ligne" style={{ marginBottom: 4 }}>
@@ -158,8 +158,8 @@ export function Recensement({ criseId, organisationId }: {
          <div className="etiq" style={{ marginBottom: 10 }}>
            {encours ? "Lancer un autre recensement" : "Lancer un recensement"}
          </div>
          <label>Ce qu'on demande</label>
          <textarea rows={3} value={message}
          <label htmlFor="rec-message">Ce qu’on demande</label>
          <textarea id="rec-message" rows={3} value={message}
            placeholder="Incendie sur le site de Brest. Dites-nous si vous êtes en sécurité."
            onChange={(ev) => definirMessage(ev.target.value)} />

@@ -176,7 +176,7 @@ export function Recensement({ criseId, organisationId }: {
          <label className="ligne" style={{ gap: 8, marginTop: 8, cursor: "pointer" }}>
            <input type="checkbox" checked={avecAnnuaire}
              onChange={(ev) => definirAvecAnnuaire(ev.target.checked)} />
            <span style={{ fontSize: 13.5 }}>Y joindre l'annuaire de crise</span>
            <span style={{ fontSize: 13.5 }}>Y joindre l’annuaire de crise</span>
          </label>

          <button className="accent" style={{ marginTop: 12 }}
+12 −12
Changes for apps/web/src/ecrans/preparation/Destinataires.tsx: 12 added lines, 12 removed lines.
Original line number Diff line number Diff line
@@ -218,28 +218,28 @@ export function Destinataires({ organisationId }: { organisationId: string }) {
        <div className="carte" style={{ marginBottom: 16 }}>
          <div className="grille deux">
            <div>
              <label>Nom</label>
              <input value={saisie.nom}
              <label htmlFor="dest-nom">Nom</label>
              <input id="dest-nom" value={saisie.nom}
                onChange={(e) => definirSaisie({ ...saisie, nom: e.target.value })} />
            </div>
            <div>
              <label>Organisme</label>
              <input value={saisie.organisme}
              <label htmlFor="dest-organisme">Organisme</label>
              <input id="dest-organisme" value={saisie.organisme}
                onChange={(e) => definirSaisie({ ...saisie, organisme: e.target.value })} />
            </div>
            <div>
              <label>Adresse électronique</label>
              <input type="email" value={saisie.email}
              <label htmlFor="dest-email">Adresse électronique</label>
              <input id="dest-email" type="email" value={saisie.email}
                onChange={(e) => definirSaisie({ ...saisie, email: e.target.value })} />
            </div>
            <div>
              <label>Téléphone</label>
              <input value={saisie.telephone}
              <label htmlFor="dest-tel">Téléphone</label>
              <input id="dest-tel" value={saisie.telephone}
                onChange={(e) => definirSaisie({ ...saisie, telephone: e.target.value })} />
            </div>
          </div>
          <label style={{ marginTop: 8 }}>Langue</label>
          <select value={saisie.langue}
          <label htmlFor="dest-langue" style={{ marginTop: 8 }}>Langue</label>
          <select id="dest-langue" value={saisie.langue}
            onChange={(e) => definirSaisie({ ...saisie, langue: e.target.value })}>
            <option value="">— non renseignée —</option>
            {LANGUES_COURANTES.map((l) => (
@@ -250,8 +250,8 @@ export function Destinataires({ organisationId }: { organisationId: string }) {
            Non renseignée, l’alerte partira dans la langue par défaut de la campagne —
            jamais rien.
          </div>
          <label style={{ marginTop: 8 }}>Ce qui autorise l'envoi</label>
          <select value={saisie.base_legale}
          <label htmlFor="dest-base" style={{ marginTop: 8 }}>Ce qui autorise l’envoi</label>
          <select id="dest-base" value={saisie.base_legale}
            onChange={(e) => definirSaisie({ ...saisie, base_legale: e.target.value })}>
            {BASES.map(([c, l]) => <option key={c} value={c}>{l}</option>)}
          </select>
+6 −4
Changes for apps/web/src/ecrans/preparation/Disponibilites.tsx: 6 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -158,6 +158,7 @@ export function Disponibilites({ organisationId }: { organisationId: string }) {
        <div className="ligne">
          <span className="etiq">Trous d’astreinte</span>
          <select className="petit pousse" value={horizon}
            aria-label="Horizon du calcul des trous d’astreinte"
            onChange={(e) => definirHorizon(Number(e.target.value))}>
            <option value={7}>7 jours</option>
            <option value={14}>14 jours</option>
@@ -206,7 +207,8 @@ export function Disponibilites({ organisationId }: { organisationId: string }) {
        <div className="carte" style={{ marginBottom: 14 }}>
          <span className="etiq">Nouvelle période</span>
          <div className="ligne" style={{ gap: 8, marginTop: 8, flexWrap: "wrap" }}>
            <select value={personneId} onChange={(e) => definirPersonne(e.target.value)}>
            <select value={personneId} aria-label="Personne"
              onChange={(e) => definirPersonne(e.target.value)}>
              <option value="">— choisir une personne —</option>
              {personnes.map((p) => (
                <option key={p.id} value={p.id}>
@@ -214,17 +216,17 @@ export function Disponibilites({ organisationId }: { organisationId: string }) {
                </option>
              ))}
            </select>
            <select value={genre}
            <select value={genre} aria-label="Astreinte ou absence"
              onChange={(e) => definirGenre(e.target.value as "astreinte" | "absence")}>
              <option value="astreinte">Astreinte</option>
              <option value="absence">Absence</option>
            </select>
          </div>
          <div className="ligne" style={{ gap: 8, marginTop: 8, flexWrap: "wrap" }}>
            <input type="datetime-local" value={du}
            <input type="datetime-local" value={du} aria-label="Début de la période"
              onChange={(e) => definirDu(e.target.value)} />
            <span className="discret">→</span>
            <input type="datetime-local" value={au}
            <input type="datetime-local" value={au} aria-label="Fin de la période"
              onChange={(e) => definirAu(e.target.value)} />
          </div>
          <input style={{ marginTop: 8, maxWidth: 420 }} value={motif} maxLength={200}
Loading