Skip to content

a11y: name the eight icon-only links, and make the a11y scan enforce it for <a> - #5825

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-5674
Sep 2, 2026
Merged

a11y: name the eight icon-only links, and make the a11y scan enforce it for <a>#5825
atomantic merged 1 commit into
mainfrom
claim/issue-5674

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

  • Eight icon-only links had no accessible name — a screen reader announced "link" with no destination on the media lightbox and media card download links, the brain link-row and mood-board "open in new tab" links, the file browser and uploads download links, and the two LoRA source links. Seven leaned on title (mouse-hover-only, not reliably read as the accessible name); MediaLightbox had neither title nor aria-label, and MoodBoardReferenceStrip marked its only child aria-hidden="true", computing a literally empty name. Each now carries an aria-label alongside whatever title it already had.
  • <a> joins CONTROL_NAME_RULES in client/src/a11yConventions.test.js, reusing the existing per-tag scan and getting the stale-allowlist burn-down check for free. PREEXISTING_ANCHOR_NAME_ALLOWLIST is empty and documented to stay that way. href joins the semantic-anchor attributes so an offender line names a link a reader can actually find.
  • The recognizer learned the naming path a link has and a form control does not — a link is named by its own content:
    • its body's text, without which the rule would report every ordinary text link in the tree;
    • a descendant image's alt, without which three thumbnail links (<a><img alt={caption} /></a>) would have been "fixed" with an aria-label that overrode the alt;
    • a same-file component that renders text, without which a card link (<a><TicketCard /></a>) would have been "fixed" the same destructive way. Only local declarations that render text are credited — the imported icons every real offender wraps (<Download />, <ExternalLink />) stay reported, and so does a file-local icon wrapper.
  • All of it reads the body through one aria-hidden-masked preparation, so an anchor whose only content is hidden still reports as unnamed. hasAccessibleControlName now takes the tag-index node rather than a (tag, index) pair, since the body boundary is the lexer's answer to give.

The alt-restricted-to-<img>/<area> and same-file-component branches came out of the local review round.

Test plan

  • cd client && npx vitest run src/a11yConventions.test.js — 46 passed. The new reads a name for <a> from its own content, and from nothing else fixture pins both directions so the rule cannot go vacuous: text and {label} bodies named; a bare icon child, a title-only anchor, an aria-hidden-only child, an empty/absent alt, an alt on a <div> or a component, and a local icon-only component all unnamed; aria-label, <img alt>, and a local text-rendering component all named.
  • cd client && npm test — 10736 tests, all green except one pre-existing MediaCollections.test.jsx navigation flake that passes in isolation and touches nothing in this diff.
  • cd client && npm run lint — clean.

Closes #5674

…it for <a> (#5674)

`a11yConventions.test.js` required an accessible name from every icon-only
<button>, <input>, <select> and <textarea>, but never asked it of an <a>. Eight
links in the tree rendered nothing but a Lucide icon, so a screen reader
announced "link" with no destination: the media lightbox and media card download
links, the brain link-row and mood-board "open in new tab" links, the file
browser and uploads download links, and the two LoRA source links. Seven leaned
on `title`, which is mouse-hover-only and is not reliably read as the accessible
name; two were worse — MediaLightbox had neither `title` nor `aria-label`, and
MoodBoardReferenceStrip marked its only child `aria-hidden="true"`, computing a
literally empty name.

Each link now carries an `aria-label` alongside whatever `title` it already had,
and <a> joins CONTROL_NAME_RULES so a new one fails the suite. Its allowlist is
empty and documented to stay that way.

The recognizer needed a naming path no form control has, because a link is named
by its own content. It credits an anchor whose body renders text, without which
the rule would report every ordinary text link in the tree; a descendant image's
`alt`, without which three thumbnail links would have been "fixed" with an
aria-label that OVERRODE the alt; and a same-file component that renders text,
without which a card link (KanbanBoard's `<a><TicketCard /></a>`) would have
been "fixed" the same destructive way. Only local declarations that render text
are credited, so the imported icons every real offender wraps stay reported.

All of it reads the body through one `aria-hidden`-masked preparation, so an
anchor whose only child is hidden still reports as unnamed.
`hasAccessibleControlName` now takes the tag-index node rather than a
`(tag, index)` pair, since the body boundary is the lexer's answer to give.
@atomantic
atomantic merged commit ecfa6d8 into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5674 branch September 2, 2026 06:57
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.

Icon-only links have no accessible name — a11yConventions.test.js enforces the rule for buttons but never asks it of an anchor

1 participant