Skip to content

fix(ui): give nav links distinct focus identities - #24

Merged
acamarata merged 1 commit into
mainfrom
fix/nav-link-focus-identity
Sep 13, 2026
Merged

acamarata merged 1 commit into
mainfrom
fix/nav-link-focus-identity

Conversation

@acamarata

Copy link
Copy Markdown
Contributor

The failure

nself-org/web's Accessibility (WCAG AA) gate went red on main
(run 34751962108):

Error: /products: 2 links have no accessible name
(only 1 may fall back to the shared href)

Why it is this repo's bug, not web's

The commit that went red changed exactly one file — a docs page
(licensing/offline.mdx) that links to nothing on /products. The two offending
links come from the shared header and footer in this package.

/products is linked from three renderings, none of which carried a data-testid
or aria-label:

Location File
Desktop nav Header.tsx (navItems.map, hidden lg:flex)
Mobile nav Header.tsx (navItems.map, inside the collapsible panel)
Footer link groups Footer.tsx (group.links.map)

tests/keyboard/keyboard-nav.spec.ts identifies a link by
data-testid ?? aria-label ?? href and skips anything with
offsetParent === null. At desktop width the mobile panel is hidden, but the desktop
nav link and the footer link are both visible and both fall back to the bare href —
two "unnamed" links for one destination, which the test reports as a collapsed focus
identity. Its own comment records this exact failure mode from 2026-09-11, where three
links collapsed into one identity and tripped a false focus-trap.

That also explains why it looked intermittent: it is viewport- and
render-visibility-dependent, not random.

The fix

A test-only disambiguator on all three renderings: nav-desktop-<label>,
nav-mobile-<label>, footer-<group>-<label>.

Deliberately not an aria-label. That would satisfy the gate while making two
otherwise-identical links announce differently to screen readers, for no user benefit
— worse accessibility in exchange for a green check. data-testid is invisible to
assistive tech and is the identity the test prefers first, so it fixes the real
collapsed-identity problem without touching what users hear.

Nothing user-visible changes.

Verification

pnpm build in @nself-web/ui is clean, and all three expressions are present in the
built output:

"data-testid": `nav-desktop-${item.label}`
"data-testid": `nav-mobile-${item.label}`
"data-testid": `footer-${group.title}-${link.label}`

The authoritative check is web's a11y gate against a build that consumes this — web
clones this repo fresh in CI, so it picks the change up once merged.

web's Playwright keyboard-nav gate went red on main:

  Error: /products: 2 links have no accessible name
  (only 1 may fall back to the shared href)

The same destination is linked from three places — the desktop nav, the
mobile nav (Header.tsx) and the footer link groups (Footer.tsx) — and none
carried a data-testid or aria-label. The test identifies a link by
data-testid ?? aria-label ?? href, so at desktop width the visible desktop
nav link and the visible footer link both fall back to the bare href,
collapse into one identity, and read as a focus trap. That is the same
failure mode the test's own comment records from 2026-09-11.

Adds a test-only disambiguator to all three renderings:
nav-desktop-<label>, nav-mobile-<label>, footer-<group>-<label>.

Deliberately NOT an aria-label: that would satisfy the gate but get
announced to screen readers, making two identical links read differently
for no user benefit. data-testid is invisible to assistive tech and is the
identity the test prefers first.

Nothing user-visible changes. tsup build clean, all three expressions
present in dist/index.js.
@acamarata
acamarata merged commit 8a5cc53 into main Sep 13, 2026
5 checks passed
@acamarata
acamarata deleted the fix/nav-link-focus-identity branch September 13, 2026 16:48
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