The canvas is unreachable by keyboard: arrow-key nudging exists but nothing can select without a pointer #84

Closed
opened 2026-07-21 22:09:56 +00:00 by steve · 0 comments
Owner

Accessibility, and one part of it is a self-defeating feature rather than a missing one.

The canvas has no keyboard path in

  • web/src/editor/GardenCanvas.tsx:234-240 — the <svg> has no role, no aria-label, no <title>, no tabIndex
  • web/src/editor/ObjectShape.tsx:64 and PlopMarker.tsx:50 — objects and plops are <g onPointerDown={...}: not focusable, no role, no accessible name

A screen reader gets an empty graphic where the garden is.

The part that makes it worse

Arrow-key nudging is implemented (GardenEditorPage.tsx:184-256), with grid snapping and Escape to peel back layers (:163-175). But it only acts on the current selection, and there is no keyboard way to make a selection — selection happens exclusively through onPointerDown.

So the keyboard feature is unusable by exactly the people who need a keyboard. Someone who can use a pointer to select doesn't need arrow-key nudging; someone who can't, can't reach it.

Scoped first slice

Full canvas accessibility is large. A worthwhile first cut is small and makes the existing nudge feature reachable:

  1. role="application" + aria-label on the <svg>, and a <title> naming the garden.
  2. Make each object focusable (tabIndex, role="button" or role="img") with an aria-label of name + kind + size.
  3. Enter/Space selects the focused object.

That turns a dead feature into a working one. Roving-tabindex between plops inside a focused bed, and announcing positions usefully, are natural follow-ups.

Two smaller a11y items worth folding in

  • Modal has no focus trap and no focus restoration (web/src/components/ui/Modal.tsx:28-56). It sets role="dialog" aria-modal="true" and focuses the card, but Tab walks straight out into the page behind (nothing is inert/aria-hidden), and on close focus lands on <body> rather than the trigger. Every destructive confirmation goes through this componentDeleteGardenModal, ClearBedModal, DeletePlantModal, DeleteSeedLotModal, ShareGardenModal. Well-bounded standard fix.
  • EditorRail tabs aren't tabs (web/src/editor/EditorRail.tsx:61). They use aria-current="page" on plain buttons instead of role="tablist"/role="tab"/aria-selected with arrow-key roving focus, so a screen reader announces four unrelated buttons rather than a tab set.

Noted as clean

Form controls are actually good — TextField/Select wire htmlFor/id/aria-describedby correctly through useFieldId. No changes needed there.

Accessibility, and one part of it is a self-defeating feature rather than a missing one. ## The canvas has no keyboard path in - `web/src/editor/GardenCanvas.tsx:234-240` — the `<svg>` has no `role`, no `aria-label`, no `<title>`, no `tabIndex` - `web/src/editor/ObjectShape.tsx:64` and `PlopMarker.tsx:50` — objects and plops are `<g onPointerDown={...}`: not focusable, no role, no accessible name A screen reader gets an empty graphic where the garden is. ## The part that makes it worse Arrow-key nudging is implemented (`GardenEditorPage.tsx:184-256`), with grid snapping and Escape to peel back layers (`:163-175`). But it only acts on the **current selection**, and there is **no keyboard way to make a selection** — selection happens exclusively through `onPointerDown`. So the keyboard feature is unusable by exactly the people who need a keyboard. Someone who can use a pointer to select doesn't need arrow-key nudging; someone who can't, can't reach it. ## Scoped first slice Full canvas accessibility is large. A worthwhile first cut is small and makes the existing nudge feature reachable: 1. `role="application"` + `aria-label` on the `<svg>`, and a `<title>` naming the garden. 2. Make each object focusable (`tabIndex`, `role="button"` or `role="img"`) with an `aria-label` of name + kind + size. 3. Enter/Space selects the focused object. That turns a dead feature into a working one. Roving-tabindex between plops inside a focused bed, and announcing positions usefully, are natural follow-ups. ## Two smaller a11y items worth folding in - **`Modal` has no focus trap and no focus restoration** (`web/src/components/ui/Modal.tsx:28-56`). It sets `role="dialog" aria-modal="true"` and focuses the card, but Tab walks straight out into the page behind (nothing is `inert`/`aria-hidden`), and on close focus lands on `<body>` rather than the trigger. **Every destructive confirmation goes through this component** — `DeleteGardenModal`, `ClearBedModal`, `DeletePlantModal`, `DeleteSeedLotModal`, `ShareGardenModal`. Well-bounded standard fix. - **`EditorRail` tabs aren't tabs** (`web/src/editor/EditorRail.tsx:61`). They use `aria-current="page"` on plain buttons instead of `role="tablist"`/`role="tab"`/`aria-selected` with arrow-key roving focus, so a screen reader announces four unrelated buttons rather than a tab set. ## Noted as clean Form controls are actually good — `TextField`/`Select` wire `htmlFor`/`id`/`aria-describedby` correctly through `useFieldId`. No changes needed there.
steve closed this issue 2026-07-22 03:34:08 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#84