Skip to content

A11Y-001: a row action revealed on hover also shows on keyboard focus and touch - #1005

Merged
NewWorldVenture merged 4 commits into
mainfrom
claude/a11y-001-hover-reveal-etzqg7
Oct 11, 2026
Merged

NewWorldVenture merged 4 commits into
mainfrom
claude/a11y-001-hover-reveal-etzqg7

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

A11Y-001, WCAG 2.4.7 (focus visible) and 2.5.8 (target size), plus the touch reach the existing M-005 guard was meant to protect. Found by sweeping a populated household: earlier signed-in sweeps used an empty one, so per-row controls never rendered.

The defect

Row actions hidden until their row is hovered (delete, edit, pin, the "…" member menu, add-container, and others) were invisible to anyone who cannot hover:

  • Keyboard: Tab landed on a control at opacity 0. In a seeded local household on main, Tab reaches the grocery list's "Delete item" and the todos' "Edit task" at opacity 0.
  • Touch: a phone or tablet has no hover, so the action never appeared.
  • Todos on a phone: the action wrapper was hidden … sm:flex, so "Edit task" and "Delete task" were display:none below 640 px, and nothing else in the row reaches them.

tests/mobile-hover-reveal.test.ts guards the touch half, but only where the classes sit side by side (opacity-0 group-hover:opacity-100). 26 controls had a class in between (opacity-0 transition group-hover:opacity-100) and slipped past it.

5 wrappers were already in the touch-safe form, but used focus-visible:opacity-100. A <div> that holds the buttons never matches :focus-visible itself.

The fix

All 31 use the form the codebase already uses elsewhere (e.g. shopping-module.tsx:251):

opacity-100 sm:opacity-0 sm:group-hover:opacity-100 coarse:opacity-100
  + focus-visible:opacity-100   (a control)
  + focus-within:opacity-100    (a wrapper around controls)
  • Desktop keeps the clean hover reveal.
  • Touch screens and widths under 640 px show the actions.
  • Keyboard focus shows them at any width.

Two follow-ups make what is now visible on a phone usable there:

  • The todos action wrapper is displayed at every width, with 26 px buttons (8716ee2).
  • The notes grid card's pin and delete and the grocery "Edit list" go from 20 px to 24 px (7769ef3).

The five decorative ArrowRight icons inside links that are themselves visible stay hover-only. Handlers, disabled guards, ARIA labels and menus are unchanged.

Tests

tests/a-control-revealed-on-hover-shows-on-focus-and-touch.test.ts (unit; the scan lives in tests/helpers/hover-reveals.ts)

  • Every JSX element's className, cn(...) arguments included, is read with the TypeScript parser, so class order and spacing no longer matter.
  • Every element hidden until hover must:
    • show on touch (coarse:opacity-100, and not a bare opacity-0);
    • show on focus (focus-visible for a control, focus-within for a wrapper);
    • not be hidden below an sm: display.
  • The 24 px sizes above are pinned.
  • Each wrong form is checked against the rule.

tests/e2e/a-control-revealed-on-hover-shows-on-focus.spec.ts (Chromium, no account) rebuilds every one of those elements on /login, which loads the app's real stylesheet. It keeps all of each element's classes except the animating ones, inside a group relative row with a real row's minimum height. For each element it reads three things: displayed, a non-zero box, and opaque.

  • 1280 px, mouse: each starts at opacity 0, hover reveals, and Tab through all of them finds each shown while it or its button has focus.
  • Touch at 1280 px ((pointer: coarse) asserted) and 390 px: each is shown without hover.
Check main This branch
Unit guard fails: 31 problems passes
E2E spec (production builds, local) fails 3 of 4: 31 controls hidden while focused; on touch and at 390 px, todos display:none 4/4
Seeded local household, 390 px touch, /dashboard/todos: "Edit task" and "Delete task" (author-run) not displayed (48 rows) 26×26, opacity 1; tapping opens the editor
Seeded household, 1280 px: Tab to todos' "Edit task" and grocery's "Delete item", opacity after the transition (author-run) 0 and 0 1 and 1
Todos/reveal unit suites; the 99 suites naming the first commit's files / tsc / eslint — 528/528; 2,060/2,060 / 0 / no new warnings

The head merges current main (83e2b01); the overlapping suites pass on the merge (34/34) and tsc is 0.

Not run:

  • the full unit suite (CI runs it);
  • a full populated-household sweep against this branch.

The fixture is utility coverage, and the populated rows above were checked by hand.

The other populated-household findings (dimmed done/inactive rows, the reminders' unnamed snooze <summary>, the meals sidebar toggles and carousel) are in a separate PR.

No migration, configuration or dependency change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4

… and touch

Row actions hidden until their row is hovered (delete, edit, pin, the
"…" menu) were invisible to the people who cannot hover. In a seeded
local household, Tab reached the grocery list's "Delete item" at
opacity 0 (WCAG 2.4.7), and on a phone or tablet there is no hover, so
it never appeared at all.

tests/mobile-hover-reveal.test.ts guarded the touch half only where the
classes sat side by side (`opacity-0 group-hover:opacity-100`). 26
controls had a class in between (`opacity-0 transition
group-hover:opacity-100`) and slipped past it. Five wrappers already in
the touch-safe form used `focus-visible`, which a <div> holding the
buttons never matches.

All 31 now use the form the codebase already uses elsewhere:
  opacity-100 sm:opacity-0 sm:group-hover:opacity-100
  focus-visible:opacity-100 (a control) | focus-within:opacity-100 (a wrapper)
  coarse:opacity-100
Desktop keeps the clean hover reveal. The five decorative arrow icons
inside visible links stay hover-only.

Tests:
- tests/a-control-revealed-on-hover-shows-on-focus-and-touch.test.ts
  reads every className (cn() arguments included) with the TypeScript
  parser, so class order no longer matters (31 problems on main, 0 here).
- tests/e2e/a-control-revealed-on-hover-shows-on-focus.spec.ts rebuilds
  each of those elements on /login in the app's real stylesheet: hidden
  until hover at 1280 px, shown while it or its button has keyboard
  focus, shown on a coarse pointer and at 390 px (main: 31 hidden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 11, 2026 12:44am UTC

Request Review

Copy link
Copy Markdown
Owner Author

Bounded source/proof follow-up at9651fc3f5db764b02d857a43efcf867f6f69a290: Todos' action wrapper retains hidden ... sm:flex, so its edit/delete controls are still display:none below640px despite the new opacity utilities. The reconstructed E2E strips non-opacity classes and checks opacity only, allowing a390px pass while the actual controls remain absent. This is a pre-existing display restriction left unresolved, not a newly introduced permission change.

Preserve the full responsive wrapper classes in a representative fixture or test the actual row, and assert rendered bounds/display plus keyboard/touch reachability at390px. Resolve the Todo display restriction within the existing lane or explicitly exclude it from the31-control touch-safe claim. Keep handlers, role/disabled guards, ARIA and menu behavior unchanged. The AST/CSS fixture remains useful utility coverage but is not a populated-route interaction sweep. Report exact commit/tests; no production accounts or data needed.

Showing the row actions on touch puts WCAG 2.5.8 on them. axe at 390 px
on a populated household found the notes grid card's pin and delete,
and the grocery list's "Edit list", at 20 by 20 (p-1 around a 12px
icon). p-1.5 makes them 24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
…f checks it

Review found the todos row's action wrapper still `hidden … sm:flex`:
display:none below 640 px whatever its opacity, and nothing else in the
row opens the editor or deletes. On a seeded local household at 390 px,
"Edit task" and "Delete task" were not displayed in any of 48 rows. The
wrapper is displayed at every width now (the opacity reveal stays), and
its buttons are 26 px (p-1.5).

The proof missed it because the e2e fixture kept only opacity classes.
It now keeps every class but the animating ones, gives each row the
relative, sized box a real row has, and reads three things per element:
displayed, a real box, opaque. The unit rule also refuses a revealed
element that is `hidden` below an sm: display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4

Copy link
Copy Markdown
Owner Author

Todos display restriction and the fixture's blind spot. Fixed at 8716ee2; the head is 4602031, which merges current main (83e2b01).

Confirmed, as you said. The todos row's action wrapper was hidden … sm:flex, so on a phone the actions were display:none whatever their opacity. Nothing else in the row opens the editor or deletes.

The repair (todos-module.tsx):

  • The wrapper is displayed at every width. It keeps the opacity reveal.
  • Its two buttons are 26 px (p-1.5).
  • Handlers, the disabled guard on delete, the ARIA labels and the editor are unchanged.

7769ef3 before it made the notes grid card's pin/delete and the grocery "Edit list" 24 px, for the same reason: they are newly visible on phones. It is not yet in the PR body; I'll update the body.

The proof, tightened:

  • Unit rule (tests/helpers/hover-reveals.ts): it also refuses a revealed element that is hidden below an sm: display. This is pinned by a case built from the todos classes.
  • E2E fixture:
    • It keeps every class except the animating ones (transition*, duration-*, ease-*, animate-*, delay-*), so display and responsive classes now count.
    • Each fixture row is group relative with a minimum height, as the real rows and cards are, so an absolute overlay gets a box.
    • For each element it reads three things: displayed, a non-zero box, and opaque.
E2E spec, local production builds main 8716ee2
1280 px mouse: hidden until hover; shown while it or its button has focus fails (31 controls, todos among them) passes
Touch at 1280 px fails passes
390 px fails, with todos-module.tsx:297 <div> display:none passes

Real rows (author-run, not a CI test). A seeded local household (the repo's seed scripts, local Supabase only, nothing remote), on the same builds:

Check main (209e5a4) 8716ee2
/dashboard/todos, 390 px touch: first row's "Edit task" and "Delete task" (48 rows) not displayed 26×26, opacity 1
Same, tap "Edit task" — the editor dialog opens
/dashboard/todos, 1280 px: Tab until "Edit task", opacity after its transition reached at Tab 42, opacity 0 reached at Tab 42, opacity 1
/dashboard/grocery, 1280 px: Tab to "Delete item" opacity 0 opacity 1

Other checks on 8716ee2:

  • The 16 todos/reveal unit suites: 528/528.
  • tsc: 0, and eslint is clean. After the merge with main, the overlapping suites pass 34/34 and tsc is 0.

The AST/CSS fixture is utility coverage, as you said. The populated-route evidence above was run by hand; it is not a sweep of every route.


Generated by Claude Code

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 11, 2026 01:57
@NewWorldVenture
NewWorldVenture merged commit aa0f2f0 into main Oct 11, 2026
9 checks passed
NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture pushed a commit that referenced this pull request Oct 11, 2026
…ith the populated-row repairs)

No textual conflicts. shopping-module, notes-module and reminders-module
carry both sides: #1005's phone/focus/touch reveal and 24px targets, and
#1007's full-contrast checked row, unfaded note counts, named snooze
summary and full-contrast AI tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
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