Skip to content

fix(ui): name every control, tie validation to its field, skip link, page titles and focus - #99

Merged
jbouder merged 2 commits into
mainfrom
fix/ui-a11y
Oct 19, 2026
Merged

jbouder merged 2 commits into
mainfrom
fix/ui-a11y

Conversation

@jbouder

@jbouder jbouder commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Closes #59.

What changed

  • Every form control has a name:
    • The Namespace and App type selects use aria-labelledby (their <Label>s had no htmlFor, and a select trigger isn't labelable).
    • The env-var editor is a <fieldset>/<legend>. Each row's inputs are named ("Variable 1 name", "Value of LOG_LEVEL"), and the icon-only remove button is "Remove LOG_LEVEL".
    • The Public switch is labelled and described by its text.
  • Validation: the form's validation names the field it's about. That control gets aria-invalid and aria-describedby="form-error", and the message is <p id="form-error" role="alert"> in the destructive colour instead of a muted <p>.
  • Skip link: "Skip to content" is the first focusable element and jumps to <main id="main" tabIndex={-1}>.
  • Page titles and focus: a new usePageTitle hook sets document.title to " · " on every route. After a client-side navigation it moves focus to the page's <h1> (tabIndex={-1}) once the heading renders, since pages show a spinner first, so screen readers announce the new page. The first page after a full load keeps the browser's focus.
  • Table and status semantics:
    • Sortable headers carry aria-sort, and filter chips carry aria-pressed.
    • Condition rows on the detail page add sr-only "Passing:" / "Failing:", with the icons aria-hidden.
    • Metrics placeholders are h2 (axe flagged h3 directly under the h1).
  • axe on every page: new tests/pages/a11y.test.tsx runs axe-core on Dashboard, Apps, App detail, Edit, Launch and Metrics (inside the real Layout), in light and dark. color-contrast is disabled because jsdom does no layout or painting. Before the fixes above it caught an unnamed toggle on Launch and Edit and the heading-order skip on Metrics. The same file tests the validation wiring, heading focus after navigating, and the skip link.

Lint rules and dependency: Biome's recommended preset includes the a11y rules and has run in CI since #80. It's why the env group is a <fieldset>. I used axe-core directly rather than vitest-axe, which was last published in 2022; it's a dev dependency only.

Testing

  • npm run check:ci, npm run build and npm test pass (66 tests).
  • Not checked by hand: a screen-reader pass on the cluster. Signing in to the nebari-local UI needs Keycloak credentials.

This PR overlaps #96, #97 and #98 in app-form.tsx and apps.tsx. I'll rebase whichever merges later.

🤖 Generated with Claude Code

https://claude.ai/code/session_016pvMUQJoLkBDk6B94TTm45

jbouder and others added 2 commits October 19, 2026 12:24
…page titles and focus

- Launch/edit form: Namespace and App type selects are labelled
  (aria-labelledby); env-var rows are a fieldset with named inputs and a
  named remove button; the Public switch is labelled and described. The
  validation message is a role="alert" with id form-error, and the field
  it is about gets aria-invalid + aria-describedby.
- Layout: a "Skip to content" link to <main id="main">.
- usePageTitle sets "<page> · <app title>" as document.title on every
  route and, after a client-side navigation, moves focus to the page's h1.
- Apps table: aria-sort on sortable headers, aria-pressed on filter chips.
  Conditions on the detail page carry sr-only "Passing:"/"Failing:" text.
  Metrics placeholders are h2, not h3 under an h1.
- tests/pages/a11y.test.tsx runs axe-core on all six pages in light and
  dark (contrast excluded: jsdom does no layout), plus validation wiring,
  heading focus and the skip link. Biome's recommended a11y rules already
  run in CI (#80).

Closes #59

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pvMUQJoLkBDk6B94TTm45
@jbouder
jbouder merged commit bcbf38d into main Oct 19, 2026
24 checks passed
@jbouder
jbouder deleted the fix/ui-a11y branch October 19, 2026 16:46
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.

UI accessibility: label every control, associate validation errors, skip link and route focus, sort/pressed semantics

1 participant