speedster-js Guide

Accessibilité

Un défaut d'accessibilité ne se manifeste jamais à l'exécution : la page s'affiche, le clic fonctionne, et seul l'utilisateur qui navigue au clavier ou au lecteur d'écran rencontre le mur.

C'est donc vérifié au build, sur le balisage, avec la position exacte du problème.

Panneau.spd:9:3 — avertissement a11y-img-alt
<img> n'a pas d'`alt`. Décrivez l'image, ou écrivez `alt=""` pour la déclarer
décorative — l'attribut vide est une information, son absence n'en est pas une.
 7 |   <h2></h2>
 8 |   <div on:click={basculer}>Ouvrir</div>
 9 |   <img src="/logo.png" />
   |   ^^^^

Les règles

Code Ce qu'il signale
a11y-img-alt Image sans alt
a11y-missing-name Bouton réduit à une icône, champ que nul <label> ne désigne
a11y-label-association <label> sans for ni contrôle imbriqué
a11y-anchor-href <a> sans href : ni focalisable, ni activable au clavier
a11y-click-non-interactive on:click sur un élément inerte
a11y-click-events-have-key-events Rôle interactif déclaré, mais seul le clic est géré
a11y-interactive-tabindex Rôle interactif sur un élément non focalisable
a11y-unknown-aria Attribut aria-* inexistant
a11y-unknown-role Rôle ARIA inexistant
a11y-aria-hidden-focusable aria-hidden sur un élément resté dans l'ordre de tabulation
a11y-presentation-interactive role="none" sur un élément focalisable
a11y-positive-tabindex tabindex positif : la tabulation de toute la page est réordonnée
a11y-empty-heading Titre sans texte
a11y-heading-order Titre qui saute un niveau : h1 puis h3
a11y-duplicate-id Identifiant écrit deux fois — ou une seule, mais dans une liste
a11y-iframe-title <iframe> sans title
a11y-media-captions <video> sans <track kind="captions">
a11y-distracting-element <marquee>, <blink>

a11y-unknown-aria et a11y-unknown-role proposent le bon terme : aria-lable devient « vouliez-vous aria-label ? ».


Sévérité

Avertissement par défaut. Rien ne casse un build sans qu'on l'ait demandé : l'accessibilité se corrige, elle ne s'impose pas au milieu d'une session de travail.

speedster({ accessibility: 'error' })      // plugin de build
speedster-check src --a11y error           # intégration continue
speedster-check src --a11y off

En error, un manquement fait échouer la vérification au même titre qu'une erreur de type — ce qu'un projet à jour veut, pour ne pas régresser.


Neutraliser une règle

Un avertissement qu'on ne peut pas taire est un avertissement qu'on apprend à ignorer — tous, y compris les justes. Un commentaire suffit, et il ne survit pas à la compilation : le balisage émis est identique.

<!-- a11y-ignore-next -->
<div on:click={fermerLaFenetre}></div>

<!-- a11y-ignore-next a11y-img-alt -->
<img src={pixelDeSuivi} />

Il ne porte que sur l'élément suivant, jamais sur ses descendants : la portée est visible à l'œil nu.


Pourquoi vous ne verrez pas de faux positifs

Une règle qui se trompe une fois sur dix finit désactivée en bloc, et le projet perd les neuf autres. Dès qu'une valeur est calculée, la vérification qui en dépend est abandonnée plutôt que devinée.

<img src="a.png" alt={legende} />   <!-- rien à signaler : l'attribut est là -->
<img {...attributs} />              <!-- l'étalement peut tout apporter : silence -->
<div role={role} on:click={agir} /> <!-- un rôle existe, on ignore lequel : silence -->
<input id={identifiant} />          <!-- peut correspondre à un `for` calculé -->

C'est la distinction entre « absent » et « inconnu », et c'est la seule raison pour laquelle ces règles sont utilisables au quotidien.

Les branches qui s'excluent le sont vraiment :

{#if court}<h1>…</h1>{:else}<h3>…</h3>{/if}   <!-- pas un saut de niveau -->
{#if vide}<p id="etat">…</p>{:else}<p id="etat">…</p>{/if}   <!-- pas un doublon -->

Et l'association <label for> ↔ champ ne se lit pas en descendant : les cibles sont relevées sur tout le composant, donc un libellé écrit après son champ associe correctement.


Ce qui n'est pas vérifiable ici

Le balisage produit par un composant enfant relève de sa compilation ; ce qu'on lui passe en contenu est écrit dans le parent, et donc vérifié dans le parent.

Deux choses restent hors de portée : l'ordre des titres entre composants, et l'unicité des identifiants sur la page assemblée. Un composant ignore où il sera monté, et combien de fois — les deviner produirait exactement les faux positifs qu'on cherche à éviter.


À l'exécution

Deux primitives, pour ce que le balisage ne peut pas dire seul :

import { announce, moveFocus } from '@ajustor/speedster-runtime'

announce('3 tâches supprimées')
announce('Connexion perdue', 'assertive')

moveFocus(panneau)

Voir Mouvement.

Et un détail qui compte : aria-*, contenteditable, draggable et spellcheck sont écrits littéralement. aria-expanded={false} produit aria-expanded="false", et non un attribut absent — l'absence et le « faux » ne disent pas la même chose à une technologie d'assistance.


Ce que ces règles ne remplacent pas

Elles vérifient le balisage, pas l'expérience. Le contraste des couleurs, la pertinence d'un texte de remplacement, la logique d'un parcours au clavier, le sens d'un ordre de lecture : rien de tout cela ne se lit dans un fichier .spd.

Un audit reste un audit. Ce que ces règles apportent, c'est de ne plus le passer à chercher des alt manquants.