Skip to content

FSHSP-106 fix(a11y): clear the mechanical axe violations (151 → 79) - #87

Open
LBU4SH wants to merge 8 commits into
mainfrom
fix/a11y-input-combobox
Open

FSHSP-106 fix(a11y): clear the mechanical axe violations (151 → 79)#87
LBU4SH wants to merge 8 commits into
mainfrom
fix/a11y-input-combobox

Conversation

@LBU4SH

@LBU4SH LBU4SH commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Premier lot de FSHSP-106 : toutes les violations axe qui ne demandent aucune décision de design — donc rien qui touche aux couleurs, qui sont bloquées sur les variables Figma.

Mesure

Baseline re-mesurée sur main (605 stories) : 151 occurrences / 135 stories — pas les 136 du ticket, écrit avant que le datepicker gagne 18 stories.

Règle Avant Après
aria-allowed-attr 29 0
aria-input-field-name 14 0
scrollable-region-focusable 6 0
aria-prohibited-attr 4 0
aria-required-parent 2 0
empty-table-header 2 0
button-name 1 0
aria-valid-attr-value 13 2
aria-required-children 4 1
nested-interactive 11 11
color-contrast 65 65
Total 151 79

77 stories en échec au lieu de 135, aucune story devenue rouge, aucune erreur de chargement. Lint et les 154 tests unitaires passent.

Un commit par règle, relisible indépendamment.

Deux trouvailles qui ne sont pas des correctifs d'attribut

ui-button avalait son contenu projeté (af31015). hasProjectedContent est détecté depuis .ui-button-content, dont le rendu était conditionné à isIconOnly() — qui dérive de cette détection. Un ui-button avec une icône et du seul contenu projeté ne rendait donc jamais ce contenu, restait en mode icône seule et se retrouvait sans nom accessible. La détection ne pouvait pas aboutir : l'élément qu'elle inspecte n'existait pas. C'était l'origine réelle du button-name.

Le kit n'avait aucun utilitaire sr-only (fca6ba9), alors que ui-datepicker, ui-select, ui-file-upload et ui-read-only recopient le même bloc dans leur SCSS. Un aria-label sur un <th> vide ne satisfait pas empty-table-header — axe veut du texte réellement présent, mesuré et confirmé.

Point à trancher en revue

fca6ba9 est le seul commit qui élargit la surface CSS publiée (.sr-only dans les helpers, d'où le type feat et l'entrée en Added). Il s'annule seul, sans toucher aux sept autres, si vous préférez le sortir en ticket dédié.

Réserve

Le role="combobox" de ui-input (079c575) n'a pas été testé au lecteur d'écran. axe valide la légalité de l'attribut, pas l'expérience : reste à vérifier que NVDA/VoiceOver, en mode formulaire, ne captent pas les flèches dont la grille du datepicker a besoin. À faire avant de retirer continue-on-error du job a11y.

Reste sur le ticket

  • nested-interactive (11) + aria-required-children (1) — pattern chips-dans-combobox de ui-input-tags/ui-autocomplete, et ui-stepper vertical
  • aria-valid-attr-value (2) — aria-controls vers un panneau non monté, demande un registre des panneaux rendus
  • color-contrast (65) — passe designer, détail des 7 paires de couleurs en commentaire sur FSHSP-106

🤖 Generated with Claude Code

LBU4SH added 8 commits August 24, 2026 20:31
ui-input forwarded aria-expanded to the native input, but that attribute is
not allowed on the implicit textbox role: screen readers dropped it, so a
ui-datepicker never announced that a calendar could open, nor its state.

Promote the input to role="combobox" (the ARIA 1.2 role that accepts
aria-expanded) only when ariaExpanded is provided, leaving plain text fields
announced as before. ui-datepicker is the only consumer of these attributes
in the kit.

Clears the 25 aria-allowed-attr violations of the a11y job (136 -> 111
occurrences, 122 -> 97 failing stories), with no new violation and no
cascade on aria-required-children, verified by a full rebuild + axe pass.
`min`/`max` are declared with `numberAttribute`, which turns an unset input into
NaN rather than null. The spinbutton then rendered aria-valuemin="NaN" and
aria-valuemax="NaN" — an invalid ARIA value, so the bound was not just useless
but discarded by assistive tech.

Emit both attributes only for a finite bound: no bound, no attribute.

Clears 11 of the aria-valid-attr-value violations of the a11y job.
The tags listbox only got an accessible name when no visible `label` was set:
with a label, the name went to the inner <input> and the listbox announced
nothing. Fall back to `label` for the listbox name.

In ui-autocomplete the `role="listbox"` also spanned the whole box, including
the combobox <input> — a listbox may contain only options/groups. Wrap the
option tags alone, with `display: contents` so the layout is untouched (same
treatment ui-input-tags already had).

Clears 14 aria-input-field-name and 3 aria-required-children violations.
Both pickers rendered `role="gridcell"` buttons straight inside `role="grid"`,
with no `role="row"` in between — required by the grid pattern, and present on
the day grid. Screen readers got no row structure to walk in the month and year
views, and axe flagged the whole grid as invalid.

Chunk the cells into the rows the CSS actually renders (3 columns for months,
2 for years) and mark the wrappers `display: contents`, so the rendered grid is
byte-for-byte the same.

Clears 2 aria-required-parent (22 nodes) and 2 aria-required-children.
aria-label is prohibited on a role-less <span>: the name the consumer passed was
simply dropped, so these components had no accessible name at all.

Take role="img" when an aria-label is provided — and role="group" for a
removable chip, whose × button must not end up inside an img, which hides its
children from assistive tech. No label, no role: unlabelled usage is unchanged.

Clears the 4 aria-prohibited-attr violations (ui-avatar's badge included, it is
a ui-badge instance).
…hable

A scrolling region has to be focusable, or its overflowing content can only be
reached with a mouse.

ui-table: the viewport takes tabindex=0 whenever `scrollable` is on.

ui-modal: the body region is always `overflow-y: auto`, but an unconditional tab
stop inserts itself into the focus order of every dialog — a focus-trap unit
test caught exactly that. It therefore becomes focusable only once it actually
overflows AND holds nothing focusable itself; since the content is projected,
neither is derivable from the inputs, so both are measured (ResizeObserver).

Clears the 6 scrollable-region-focusable violations.
`hasProjectedContent` is detected from the `.ui-button-content` element, but that
element was only rendered `@if (label() || !isIconOnly())` — and `isIconOnly()`
derives from that very detection. A ui-button given an `icon` and nothing but
projected content therefore never rendered the content, stayed in icon-only mode,
and ended up with no accessible name at all. The detection could never succeed:
the element it inspects did not exist.

Always render the wrapper. The SCSS already carried
`.ui-button-content:empty { display: none; }` ("empty → removed, avoids phantom
gap") precisely for this, so a genuine icon-only button is unchanged.

Clears the button-name violation on the Projected Content story — which was not
a story oversight but this bug, rendering an empty button.
…ble headers

axe's empty-table-header wants text that is actually present: an aria-label on
the <th> does not satisfy it (measured — the violation survived that attempt).
The kit had no screen-reader-only utility, while four components hand-roll the
same block in their own SCSS (ui-datepicker, ui-select, ui-file-upload,
ui-read-only), so add it once to the published helpers sheet.

Use it to name the selection and row-expansion columns of the ui-table stories.
Verified: both stories now report zero violations, and the headers still measure
56px and 68px — the absolutely-positioned span adds nothing to the column.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant