Address Gadfly findings on #84
Build image / build-and-push (push) Successful in 10s

- Object aria-label uses kindDef().label (the canonical "In-ground") instead of
  an ad-hoc kind.replace() that produced "in ground" and diverged from the UI.
  4+ models flagged this.
- aria-current, not aria-pressed, for the selected object — selection isn't a
  toggle, which is what aria-pressed means; aria-current marks the active item.
- Modal focus trap made robust: if focus is NOT inside the dialog (fell to
  <body> because the focused control was removed — ShareGardenModal's
  remove-share button — or disabled while busy, or externally stolen), Tab now
  pulls it back in instead of escaping. The previous branches only handled
  focus being exactly at a known boundary.
- Focus restore checks opener.isConnected before calling focus(): the delete/
  clear flows this targets often remove the element that opened the dialog, and
  a disconnected node's focus() silently no-ops.
- Hoisted the focusable-element selector to a module constant, and excluded
  input[type="hidden"] (it matched input:not([disabled]) and, at a boundary,
  broke the wrap).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
2026-07-21 23:33:38 -04:00
co-authored by Claude Opus 4.8
parent bfc5d9a871
commit 4b348dcbc0
2 changed files with 35 additions and 16 deletions
+12 -6
View File
@@ -1,6 +1,6 @@
import { memo, type KeyboardEvent, type PointerEvent } from 'react'
import { objectTransform } from './shared'
import { objectDisplayName } from './kinds'
import { kindDef, objectDisplayName } from './kinds'
import type { EditorObject } from './types'
const DEFAULT_FILL = '#8a8a8a'
@@ -73,10 +73,13 @@ export const ObjectShape = memo(function ObjectShape({
const stroke = selected ? '#2f7a3e' : '#00000033'
const strokeWidth = selected ? 2 : 1
// A concise accessible name: the object's label plus its kind, e.g.
// "North Bed, raised bed". The dimensions aren't included — they need the
// garden's unit context this component doesn't hold — so they're a follow-up.
const label = `${objectDisplayName(object)}, ${object.kind.replace(/_/g, ' ')}`
// A concise accessible name: the object's label plus its kind's canonical
// label, e.g. "North Bed, In-ground" — reusing kindDef so it never diverges
// from what the UI shows (an ad-hoc kind.replace() gave "in ground"). The
// dimensions aren't included; they need the garden's unit context this
// component doesn't hold, so they're a follow-up.
const kindLabel = kindDef(object.kind)?.label ?? object.kind
const label = `${objectDisplayName(object)}, ${kindLabel}`
// Keyboard focus needs to be VISIBLE — that's the point of making the canvas
// keyboard-reachable. The `object-shape` class carries a :focus-visible rule
@@ -93,7 +96,10 @@ export const ObjectShape = memo(function ObjectShape({
role="button"
tabIndex={0}
aria-label={label}
aria-pressed={selected}
// aria-current, not aria-pressed: selecting an object isn't a toggle (a
// toggle is what aria-pressed means). aria-current marks it as the active
// item among the objects. Omitted, not "false", when unselected.
aria-current={selected || undefined}
style={{ cursor: 'pointer' }}
>
{object.shape === 'circle' ? (