#64 — Tech Debt: Selectable-Grid Picker Pattern Duplicated Across 3 Dialogs #64
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#64
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tech Debt: Selectable-Grid Picker Pattern Duplicated Across 3 Dialogs
Reported by: Frontend Engineer, spotted during
#24's code review, 2026-07-22Symptom
The same "grid of selectable buttons" idiom — map over values, render a
buttonwithcn(...)-composed classes, aring-2 ring-foreground(orring-offset) highlight when selected,role="group"on the wrapper,aria-pressedper button — is now hand-implemented independently in:ReactUi/src/components/ListAppearanceDialog.tsx(colour grid, icon grid — two copies already)ReactUi/src/components/LabelPicker.tsx(colour grid)ReactUi/src/components/CreateListDialog.tsx(template grid, added in#24)Each copy has near-identical Tailwind class strings and identical interaction logic
(select-on-click, disabled-while-saving, selected-state styling), but no shared component. A future
style or accessibility fix (e.g. changing the selected-state ring colour, adding keyboard
arrow-key navigation) has to be found and applied in four places across three files; it's easy to
update some and miss others, producing inconsistent selection UI across dialogs.
Expected behaviour
A single shared component (e.g.
ReactUi/src/components/ui/option-grid.tsx, name TBD by whoeverpicks this up) parameterized by
items,value,onChange,disabled, arenderLabel/renderItemcallback, and the grid layout classes — used by all four existing call sites (colour, icon, label
colour, template) with no visual or behavioral change.
Acceptance criteria
ListAppearanceDialog,LabelPicker, andCreateListDialogcontinue to pass unmodified (or with only trivial selector updates)
Notes for whoever picks this up
Purely a maintenance/consistency cleanup — no functional bug today. Low priority; bundle it with
other frontend tech-debt work rather than picking it up standalone unless a related story touches
one of these dialogs anyway.
Agents involved
Blockers
None.
design (
64_shared_picker_grid_component_design.md)#64— Design NoteComponent shape
ReactUi/src/components/ui/option-grid.tsx'sOptionGrid<T>factors out only the genuinelyshared, behavioral part of the four (now five, counting
#28'sEmailNotificationDialog, addedthis same session) hand-rolled picker grids: the
role="group"wrapper, one<button>per item,aria-pressed/onClick/disabledwiring, andkey. Per-item visual differences (circularswatch vs. icon square vs. full-width text row) stay with each caller via
itemClassNameandrenderItem— folding those into the shared component too would have made it a much larger,more special-cased abstraction for comparatively little further deduplication, since the actual
visual shapes genuinely differ across the five call sites.
item === value(reference/value equality) is the selection check — safe here because everycall site's items are plain string literal unions (
TodoListColor,TodoListIcon,LabelColor,TodoListTemplate,EmailNotificationMode), never objects.Migrated call sites
ListAppearanceDialog.tsx(colour + icon grids),LabelPicker.tsx(colour grid),CreateListDialog.tsx(template grid),EmailNotificationDialog.tsx(mode grid) — four files,five grids total (matching the reuse-angle review finding from
#28's own code review, whichflagged
EmailNotificationDialogas a fifth undedup'd instance).Behavioral alignment:
EmailNotificationDialogconformed to the majority conventionFour of the five existing grids used toggle-button semantics (
role="group"+aria-pressed),not a true ARIA
radiogroup/radio— even though all five are genuinely single-select,mutually-exclusive choices, which
radiogroupdescribes more precisely.EmailNotificationDialog(written this same session, for
#28) usedradiogroup/radioinstead, since it was designed inisolation before this cleanup ticket. Rather than have
OptionGridsupport two different ARIApatterns (or migrate the other four to
radiogroup, a larger behavioral change this ticket's ownAC explicitly rules out — "No visual regression... same classes/behaviour at each of the four call
sites"), conformed
EmailNotificationDialogto the established majority convention instead, sinceit's the one component here safe to adjust (written this session, no accumulated external
dependents). Updated its own test file and one assertion in
TodoListHeader.test.tsxaccordingly— both were the only two files needing a selector change to keep passing, matching AC's "trivial
selector updates" allowance.
Verification
All 458 pre-existing frontend tests continue to pass unmodified except the two files above.
ReactUi/src/components/ui/option-grid.test.tsxis new, testing the shared component directly(group role, per-item
aria-pressed,onChangewiring,disabledpropagation, optionalgetItemAriaLabel/getTitle).