Skip to content

test(v2): add axe accessibility gate + keyboard nav tests to Storybook#3691

Merged
gantoine merged 3 commits into
masterfrom
posthog-code/v2-a11y-audit
Jul 6, 2026
Merged

test(v2): add axe accessibility gate + keyboard nav tests to Storybook#3691
gantoine merged 3 commits into
masterfrom
posthog-code/v2-a11y-audit

Conversation

@gantoine

@gantoine gantoine commented Jul 6, 2026

Copy link
Copy Markdown
Member

Description
Explain the changes or enhancements you are proposing with this pull request.

Adds an automated accessibility audit layer to the v2 UI, fixes the violations it surfaces, and adds keyboard-operability tests. Targets the unlabeled-control problem in #1848.

1. Axe wired into the vitest Storybook runner (frontend/test/storybook.test.ts)

Every v2 lib story renders into an owned canvas and is scanned by axe-core, gated per story by the @storybook/addon-a11y a11y.test parameter:

  • error (global default, .storybook/preview.ts) fails on any violation,
  • todo warns without failing (a ratchet escape hatch),
  • off skips.

color-contrast is disabled in this layer (happy-dom has no layout); see follow-ups. New dependency: axe-core (devDependency).

2. Keyboard-operability play() tests added to RSwitch, RTabNav, RRating (Tab walks focus in order; Enter/Space flip aria-checked / aria-selected).

3. Fixed the violations. Turning the gate on surfaced 55 failing stories. All are now resolved except one:

  • Icon-only buttons / decorative images labeled (RToolbar, RTooltip, RCarousel, RTabNav, RCheckbox).
  • RProgressCircular: optional label; renders aria-hidden when decorative (indeterminate + no label/slot), else named via label or slot content. Cascades to RSpinner, RSelect/RTextField loading spinners.
  • RTextField: interactive prepend/append-inner adornment button now named from a *InnerLabel prop (falls back to the tooltip); form fields labeled.
  • RDateField: role="combobox" moved off the <label> wrapper onto the field container (restores the input's label + uses an allowed role).
  • RTable: layout-only scroll/body containers marked presentational so rows/cells resolve their required table parent in every state.
  • RMenuItem / RDropzone / RSlider: supplied the menu parent / input label / slider aria-label the components already support.

Remaining (1 todo, intentionally deferred)

RSelect (Clearable): the clear <button> is nested inside the activator <button> (nested-interactive). The correct fix lifts the clear/chip-close controls out of the activator (select-only combobox pattern), which moves the field's border/hover/focus/error chrome onto a wrapper. This is a visual-layout refactor that needs in-browser verification, so it's left as a tracked todo rather than shipped blind.

Suggested follow-ups

  • Land the RSelect activator refactor (with browser verification).
  • Add @storybook/addon-vitest with Playwright browser mode to also catch color contrast + focus visibility in both themes.

AI-assistance disclosure
This PR was written with AI assistance (PostHog Code / Claude). All changes were reviewed; the full local test/typecheck/lint suite passes (axe runner 363, full vitest 469, vue-tsc, trunk).

Checklist
Please check all that apply.

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Screenshots (if applicable)

N/A (test-tooling + a11y semantics; no visual-layout changes)

Refs #1848


Created with PostHog Code

@socket-security

socket-security Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​axe-core@​4.12.0 ⏵ 4.12.1100 +110010099 +170

View full report

Wire axe-core into the vitest Storybook runner so every v2 lib story is
scanned for accessibility violations, gated per story by the
@storybook/addon-a11y `a11y.test` parameter (error / todo / off) with a
global default of "error".

Fix the unlabeled-control violations this surfaces (icon-only buttons,
decorative images) and mark the remaining component-level gaps as `todo`
so CI stays green while the backlog stays visible. Add keyboard-operability
play() tests to RSwitch, RTabNav, and RRating.

Refs #1848

Generated-By: PostHog Code
Task-Id: b44aba2c-90c9-407d-8c80-5bbe77ecd6c1
@gantoine
gantoine force-pushed the posthog-code/v2-a11y-audit branch from 16fc7f1 to 1eb4d1c Compare July 6, 2026 02:10
gantoine added 2 commits July 5, 2026 22:30
Resolve 7 of the 8 ratcheted `todo` stories with real fixes and flip them
back to the `error` gate:

- RProgressCircular: add optional `label`; render aria-hidden when the ring
  is decorative (indeterminate + no label/slot), else name it via label or
  slot content. Cascades to RSpinner, RSelect (Loading), RTextField (Loading).
- RTextField: label the interactive prepend/append-inner adornment button
  from a new `*InnerLabel` prop (falls back to the tooltip).
- RDateField: move role="combobox" off the RTextField `<label>` wrapper onto
  the field container, restoring the input's label and using an allowed role.
- RTable: mark the layout-only scroll/body containers presentational so
  rows/cells resolve their required table parent in all states.
- RMenuItem, RDropzone, RSlider: supply the menu parent / input label /
  slider aria-label the components already support.

RSelect (Clearable) stays `todo`: the clear button is nested inside the
activator button and the correct fix is a visual-layout refactor that needs
in-browser verification.

Refs #1848

Generated-By: PostHog Code
Task-Id: b44aba2c-90c9-407d-8c80-5bbe77ecd6c1
Remove redundant explanation and shorten the remaining comments added by
the accessibility work to the "why" only.

Generated-By: PostHog Code
Task-Id: b44aba2c-90c9-407d-8c80-5bbe77ecd6c1
@gantoine
gantoine marked this pull request as ready for review July 6, 2026 12:16
Copilot AI review requested due to automatic review settings July 6, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an automated accessibility audit layer to the v2 Storybook/Vitest test suite (via axe-core), and fixes/ratchets accessibility + keyboard-operability issues across v2 library components/stories (notably unlabeled controls referenced in #1848).

Changes:

  • Add an axe-core gate to the Vitest Storybook runner with per-story a11y.test controls (error/todo/off).
  • Add Storybook play() keyboard navigation tests for key primitives (switch/tab nav/rating).
  • Fix a11y violations across v2 components and stories (labels for icon-only controls, improved roles/ARIA semantics, and component-level accessibility props).

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
frontend/test/storybook.test.ts Adds Storybook-driven rendering + per-story axe-core scanning with ratcheting modes.
frontend/.storybook/preview.ts Sets the global default a11y.test mode to error to enforce the gate.
frontend/package.json Adds axe-core devDependency.
frontend/package-lock.json Locks axe-core version update/addition.
frontend/src/v2/lib/structural/RTooltip/RTooltip.stories.ts Labels icon-only tooltip activators to satisfy accessible-name requirements.
frontend/src/v2/lib/structural/RToolbar/RToolbar.stories.ts Labels icon-only toolbar buttons in stories.
frontend/src/v2/lib/primitives/RTabNav/RTabNav.vue Makes tab item images decorative (alt="") to avoid redundant/incorrect accessible names.
frontend/src/v2/lib/primitives/RTabNav/RTabNav.stories.ts Adds play() keyboard navigation coverage for tab selection.
frontend/src/v2/lib/primitives/RProgressCircular/RProgressCircular.vue Adds optional label and decorative behavior for unnamed indeterminate spinners.
frontend/src/v2/lib/primitives/RProgressCircular/RProgressCircular.stories.ts Exercises the new label control and labels determinate examples.
frontend/src/v2/lib/overlays/RCarousel/RCarousel.stories.ts Marks thumbnail images decorative (alt="") to satisfy a11y checks.
frontend/src/v2/lib/menus/RMenuItem/RMenuItem.stories.ts Adds required role="menu" parents for menuitem-role stories.
frontend/src/v2/lib/forms/RTextField/RTextField.vue Adds prependInnerLabel/appendInnerLabel to name interactive adornment buttons.
frontend/src/v2/lib/forms/RTextField/RTextField.stories.ts Ensures form examples provide visible labels/tooltips needed for accessibility.
frontend/src/v2/lib/forms/RSwitch/RSwitch.stories.ts Adds play() keyboard toggle coverage for switch semantics.
frontend/src/v2/lib/forms/RSlider/RSlider.stories.ts Provides default and per-instance labeling guidance for an unlabeled slider UI.
frontend/src/v2/lib/forms/RSelect/RSelect.stories.ts Adds an explicit a11y.test: "todo" escape hatch for the known nested-interactive issue.
frontend/src/v2/lib/forms/RRating/RRating.stories.ts Adds play() keyboard navigation coverage for rating radio semantics.
frontend/src/v2/lib/forms/RDropzone/RDropzone.stories.ts Supplies an input label for the underlying file input.
frontend/src/v2/lib/forms/RDateField/RDateField.vue Moves role="combobox" and related ARIA attributes to a valid container to preserve labeling.
frontend/src/v2/lib/forms/RCheckbox/RCheckbox.stories.ts Updates no-visible-label example to provide an accessible name.
frontend/src/v2/lib/data/RTable/RTable.vue Adds table semantics and marks layout-only wrappers presentational to satisfy required ARIA structure.
Files not reviewed (1)
  • frontend/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gantoine
gantoine merged commit 5866af2 into master Jul 6, 2026
12 checks passed
@gantoine
gantoine deleted the posthog-code/v2-a11y-audit branch July 6, 2026 12: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.

2 participants