Skip to content

Improve semantic HTML and accessibility of ProductTable filters and presets #18114

@myelinated-wackerow

Description

@myelinated-wackerow

Context

The shared ProductTable component powers the filter UI on /wallets/find-wallet and /layer-2/networks. An accessibility/semantic-HTML audit surfaced several issues:

  1. Persona cards (top of /wallets/find-wallet) use <h3> inside a <button> for typography. The headings break the document outline and aren't real section headings — the cards are filter checkboxes, not content sections.
  2. Sidebar filter group titles ("Device", "Network support", etc.) are <p> tags inside accordion triggers — there's no programmatic grouping of the related controls beneath them.
  3. Switch toggles in SwitchFilterInput.tsx have no accessible name. The visible label is a sibling <p>, not associated via htmlFor or aria-labelledby. A screen reader announces "switch, checked" with no identifying name.
  4. Description text for Switch toggles also has no aria-describedby link to the control.

Phase 1 — adding an sr-only <h2> to fix the h1h3 outline jump on /wallets/find-wallet — is addressed in #18112. This issue covers the remaining work.

Affected pages

  • /wallets/find-wallet
  • /layer-2/networks (benefits automatically from changes to shared ProductTable components)

Goals

  • Filter groups carry real semantic grouping via <fieldset> + <legend>.
  • Persona cards are checkbox controls, not headings.
  • Every Switch and Checkbox has a programmatically associated label (and description, where one exists).
  • Visual output on both pages is unchanged.
  • Improvements to shared components benefit both consumer pages.

Non-goals (explicit)

  • Do not replace Radix Checkbox/Switch with native <input> elements. The Radix primitives are already WCAG-equivalent and already styled in src/components/ui/. The accessibility wins in this issue are upstream of those primitives.
  • Do not change visible styling. Reviewers should be able to confirm via Chromatic baselines that nothing visual moved.
  • Do not hand-edit non-English JSON files under src/intl/. The intl-pipeline propagates English changes (src/scripts/intl-pipeline/, spec at tests/specs/PIPELINE-SPEC.md).
  • Do not introduce a new component-*.json i18n namespace for this work — strings fit cleanly into existing namespaces.

Foundation: shadcn Field component family

This work uses the shadcn Field component family (docs), which provides FieldSet, FieldLegend, Field, FieldLabel, and FieldDescription — covering both the grouping problem and the label-association problem with one idiomatic API.

Install (once, as part of PR 1):

pnpm dlx shadcn@latest add field

Visual-diff expectation: shadcn ships defaults that rarely match this project's visuals. After install, review and override className on FieldSet/FieldLegend/FieldDescription (and the others as needed) to preserve current spacing, typography, and layout on both /wallets/find-wallet and /layer-2/networks. This is the standard shadcn-adoption flow in this repo.

Dependency note: the install likely adds @radix-ui/react-label if not present. Confirm via the package.json diff during review.

Plan

Broken into two PRs.

PR 1 — Sidebar a11y (low-risk, no visual change, benefits both pages)

Install shadcn Field:

  • Run pnpm dlx shadcn@latest add field.
  • Tune className defaults across src/components/ui/field.tsx to match this project's design system; verify on both consumer pages before moving on.

Sidebar filter group structure:

  • src/components/ProductTable/Filter.tsx:
    • Wrap filter.items.map(...) (line ~107) in <FieldSet> with an sr-only <FieldLegend> matching the existing visible group title (filter.title).
    • For nested options under Mobile/Desktop/Browser (lines ~119–134), wrap in a nested <FieldSet> with an sr-only <FieldLegend> describing the sub-group (e.g. "Mobile platforms", "Desktop platforms", "Browser engines").

Switch label association:

  • src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx:
    • Replace the sibling <p>{label}</p> and description <p> with the shadcn Field composition: <Field> containing <FieldLabel>, the existing <Switch>, and (when present) <FieldDescription>. Let Field/FieldLabel handle id wiring and aria-describedby instead of doing it by hand.

Translations (English source only — pipeline handles the rest):

  • src/intl/en/table.json — add keys for the nested sub-group legends (e.g. table-mobile-platforms, table-desktop-platforms, table-browser-engines). Outer group legends already exist as filter.title, no new keys needed.

Files touched:

src/components/ui/field.tsx                                   (NEW, via shadcn add)
src/components/ProductTable/Filter.tsx
src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx
src/intl/en/table.json
package.json / pnpm-lock.yaml                                  (likely diff from shadcn install)

PR 2 — Persona cards refactor (/wallets/find-wallet only)

File: src/components/ProductTable/PresetFilters.tsx

  • Wrap the presets.map(...) grid in <FieldSet> with an sr-only <FieldLegend> (e.g. "Filter wallets by user type").
  • Replace the outer <button> with a <FieldLabel> (or <label> composed via Field) containing a Radix <Checkbox> from src/components/ui/checkbox (visually hidden via sr-only but keeping focus reachability).
  • Replace the fake-checkbox <div> indicator with a <span aria-hidden> driven off the Checkbox's data-state (or off peer-data-[state=checked] selectors on the wrapper).
  • Drop the <h3> — persona title becomes a <span> with the same typography classes.
  • Wire onCheckedChange to call existing handleSelectPreset(idx).

Translations:

  • src/intl/en/page-wallets-find-wallet.json — add a key for the persona group legend (e.g. page-find-wallet-persona-legend). Persona titles already have keys.

Files touched:

src/components/ProductTable/PresetFilters.tsx
src/intl/en/page-wallets-find-wallet.json
src/components/FindWalletProductTable/FindWalletProductTable.stories.tsx   (likely needs story tweaks)

Translation rules (read carefully)

  • Every new visible and sr-only string must use an src/intl/en/*.json key.
  • Find-wallet-only strings → src/intl/en/page-wallets-find-wallet.json.
  • Strings used by the shared ProductTable (rendered on both find-wallet and L2 networks) → src/intl/en/table.json.
  • Do not edit any other locale's JSON. The intl-pipeline (src/scripts/intl-pipeline/) propagates from English.

Component policy

  • Use existing Radix-based ui/checkbox and ui/switch primitives.
  • Add the shadcn Field family via pnpm dlx shadcn@latest add field, then override className defaults to preserve current visuals.
  • Do not introduce other new ui/* components for this work.

Acceptance criteria

  • /wallets/find-wallet heading outline: h1h2 (sr-only) → (no further headings inside the finder UI). Verified with browser DevTools accessibility tree.
  • Each sidebar filter group on both pages renders as a <fieldset> (via FieldSet) with an sr-only <legend> matching the visible group title.
  • Each Switch in the sidebar has a connected <label> and (where applicable) aria-describedby pointing at the description, both wired via the shadcn Field composition.
  • Persona cards on /wallets/find-wallet render as a <fieldset> of checkbox controls; no <h3> elements remain inside them; no <button> element remains as the toggle.
  • Chromatic shows zero visual diffs on both /wallets/find-wallet and /layer-2/networks.
  • Screen reader walkthrough (VoiceOver or NVDA) confirms:
    • Persona group is announced as a group with a name; each persona is announced as a checkbox with a name and checked state.
    • Each sidebar filter group is announced with a name; each switch is announced with a name and (where applicable) description.
  • npx tsc --noEmit clean.
  • pnpm lint clean.
  • Storybook stories for affected components updated.

Testing checklist

  • pnpm dev and visually compare both pages against dev baseline.
  • pnpm storybook — verify changed component stories.
  • pnpm chromatic (or rely on CI) — review visual diffs.
  • Screen-reader pass on both pages.
  • Keyboard-only navigation pass: Tab order and focus visibility on personas, accordion triggers, and switches.

References

Generated by Claude Opus 4.7

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triage 📥This issue needs triaged before being worked on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions