store: changing a cart line's fulfilment reorders the list and drops keyboard focus #92

Closed
opened 2026-09-06 12:25:36 +00:00 by yuris · 0 comments
Owner

Found during review of #91 by two independent reviewers. Pre-existing on main, untouched by that PR, and larger than the two accessibility defects it fixed.

Symptom

A keyboard user cannot browse the cart's fulfilment options. Arrowing between them commits each option, teleports the line to the bottom of the list, and drops focus to <body>.

Three causes compounding

1. Every mutation destroys focus. document.activeElement is BODY after increment, after a mode switch, and after remove. The line is keyed key={${line.slug}-${line.installAtShop}}, so changing fulfilment changes the key, React unmounts the line, and the focused control ceases to exist.

2. setMode reorders the cart. It is remove-then-add, so the changed line moves to the end. Measured: switching line 1 (Oli) to pasang reordered a four-line cart to [Bridgestone, Shockbreaker, Ban Serep, Oli].

3. Native radios commit on arrow-key. With a real radiogroup, ArrowDown selects — it does not merely move a cursor. So exploring the two options is not possible without choosing them.

Combined effect

ArrowDown on Kirim ke alamat saya → pasang is committed → the line jumps to the bottom → focus lands on <body> → roughly 20 Tab presses to get back to where the user was. Repeat to change their mind.

Why it is not just cosmetic

It is a WCAG 2.4.3 (Focus Order) and 3.2.2 (On Input) concern: a change of setting causes a change of context — list reordering plus focus loss — that the user was not warned about.

Suggested direction

Not prescribing an implementation, but the three causes suggest:

  • Stop rekeying on a mutable field. The key encodes installAtShop, which is precisely what the control changes. A stable identity — a line id assigned on add — would let React keep the DOM node and the focus with it.
  • Make setMode an update, not remove+add, so position is preserved. The existing merge behaviour (when a sibling line already sits in the target mode) still needs handling, but merging two lines is a different operation from moving one.
  • Restore focus explicitly after any mutation that must remount, so the control the user was on is refocused.

Verification for whoever takes it

With two or more lines: focus the first line's Kirim ke alamat saya, press ArrowDown, and assert (a) the line keeps its position in the list, and (b) document.activeElement is still inside that line. Both fail today.

Found during review of #91 by two independent reviewers. Pre-existing on `main`, untouched by that PR, and larger than the two accessibility defects it fixed. ## Symptom A keyboard user cannot browse the cart's fulfilment options. Arrowing between them commits each option, teleports the line to the bottom of the list, and drops focus to `<body>`. ## Three causes compounding **1. Every mutation destroys focus.** `document.activeElement` is `BODY` after increment, after a mode switch, and after remove. The line is keyed `key={`${line.slug}-${line.installAtShop}`}`, so changing fulfilment changes the key, React unmounts the line, and the focused control ceases to exist. **2. `setMode` reorders the cart.** It is remove-then-add, so the changed line moves to the end. Measured: switching line 1 (Oli) to `pasang` reordered a four-line cart to `[Bridgestone, Shockbreaker, Ban Serep, Oli]`. **3. Native radios commit on arrow-key.** With a real `radiogroup`, ArrowDown *selects* — it does not merely move a cursor. So exploring the two options is not possible without choosing them. ## Combined effect ArrowDown on `Kirim ke alamat saya` → `pasang` is committed → the line jumps to the bottom → focus lands on `<body>` → roughly 20 Tab presses to get back to where the user was. Repeat to change their mind. ## Why it is not just cosmetic It is a WCAG 2.4.3 (Focus Order) and 3.2.2 (On Input) concern: a change of setting causes a change of context — list reordering plus focus loss — that the user was not warned about. ## Suggested direction Not prescribing an implementation, but the three causes suggest: - **Stop rekeying on a mutable field.** The key encodes `installAtShop`, which is precisely what the control changes. A stable identity — a line id assigned on add — would let React keep the DOM node and the focus with it. - **Make `setMode` an update, not remove+add**, so position is preserved. The existing merge behaviour (when a sibling line already sits in the target mode) still needs handling, but merging two lines is a different operation from moving one. - **Restore focus explicitly** after any mutation that must remount, so the control the user was on is refocused. ## Verification for whoever takes it With two or more lines: focus the first line's `Kirim ke alamat saya`, press ArrowDown, and assert (a) the line keeps its position in the list, and (b) `document.activeElement` is still inside that line. Both fail today.
yuris closed this issue 2026-09-28 20:57:51 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage#92
No description provided.