Skip to content

a11y: tabs and radio group semantics (breaks e2e selectors) #13

Description

@devRMA

Two accessibility improvements deliberately left out of the a11y PR because each one breaks Playwright selectors, and one of them changes product behaviour. Splitting them out so they can be reviewed on their own terms.

1. AUTO/MANUAL should be a radio group

components/organisms/journey-form.tsx renders the pair as two buttons with aria-pressed. Assistive tech announces them as two independent toggles: the mutual exclusion is invisible, they are two separate tab stops, and there is no arrow-key navigation between them.

Cheapest correct fix is two visually-hidden <input type="radio" name="exit-mode"> with styled <label>s — the platform then supplies grouping, aria-checked, arrow keys and the roving tab stop for zero JS. (role="radiogroup" plus hand-written key handling reaches the same result with more code to maintain.)

Cost:tests/e2e/work-calculator.spec.ts uses page.click('button:has-text("MANUAL")'), which stops matching once it is no longer a <button>. Becomes page.getByRole("radio", { name: "MANUAL" }).check().

2. The Jornada / Custo-da-Hora nav should not be aria-pressed buttons

app/page.tsx wraps them in <nav> and marks them with aria-pressed. They swap the main panel, so they are tabs, not navigation.

Two options, and this is the decision to make:

(a) role="tablist"role="tab" + aria-selected + aria-controls on the buttons, role="tabpanel" on the panel, drop the <nav>. Keeps them as <button>, so no e2e selector changes.

(b) Real links with the view in the URL<Link href="/?view=work"> plus aria-current="page", reading view via useSearchParams. This additionally fixes two things (a) does not: the tab becomes shareable and bookmarkable (activeView is currently unshareable useState), and Cmd-click / middle-click start working because they would be real anchors.

Cost of (b): breaks both spec files — salary-calculator.spec.ts and work-calculator.spec.ts both use button:has-text(...). They become page.getByRole("link", { name: "Jornada" }).click().

My recommendation is (b): the URL-state win is worth more than the selector churn, and the selector change is mechanical.

3. Minor items from the same audit, unblocked and cheap

  • salary-calculator.tsx, tax-details-panel.tsx, extra-entry-row.tsx — currency fields set value={formatCurrencySimple(...)}, reformatting on every keystroke. When React's written value differs from what was typed, the browser resets the caret to the end: appending feels fine, editing mid-string throws the caret to the end each character. components/atoms/masked-input.tsx already solves this correctly for date and time with local state committed on completion — route currency through the same pattern, or format on blur only.
  • lib/utils.tsformatCurrency and formatCurrencySimple construct a fresh Intl.NumberFormat on every call, i.e. once per keystroke on a controlled input. Hoist both to module scope.
  • journey-form.tsx — "Resetar Horários" wipes every entered time with no confirmation and no undo.
  • Inputs lack name, autoComplete="off", enterKeyHint and spellCheck={false}. Cheapest is defaults in components/atoms/input.tsx.
  • components/molecules/side-ads.tsx — the ads render from 2xl (>=1536px) while the content container is 2xl:max-w-[1600px]. At 1536px the ads sit at x=16..176 and x=1360..1520 while content spans 32..1504, so they overlap. Gate at min-[1980px].
  • app/page.tsxpt-32 pb-32 is 256px of fixed vertical padding; at 375x667 in landscape only 200px of 375px is usable. Use pt-24 pb-28 sm:pt-32 sm:pb-32.
  • No env(safe-area-inset-bottom) on the bottom-anchored nav, cookie banner or privacy button; bottom-8 is 32px against the 34px iOS home indicator.
  • app/layout.tsx — no <link rel="preconnect"> for pagead2.googlesyndication.com or googletagmanager.com, both fetched at runtime.
  • Times are formatted with a hardcoded format(d, "HH:mm:ss"); Intl.DateTimeFormat("pt-BR", { timeStyle: "medium" }) respects the user's locale.

Found while auditing the UI against the Web Interface Guidelines for #6 and #7. Contrast ratios, element sizes and overlap coordinates in the linked PR were measured against the built CSS and a real browser, not estimated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions