Skip to content

CODAP-1150: migrate color picker from Chakra UI to React Aria#2492

Open
kswenson wants to merge 15 commits intomainfrom
CODAP-1150-color-picker-react-aria
Open

CODAP-1150: migrate color picker from Chakra UI to React Aria#2492
kswenson wants to merge 15 commits intomainfrom
CODAP-1150-color-picker-react-aria

Conversation

@kswenson
Copy link
Copy Markdown
Member

@kswenson kswenson commented Mar 24, 2026

Summary

  • Migrate color picker components from Chakra UI to React Aria for accessible focus management, keyboard navigation, and ARIA semantics
  • Replace swatch grid with React Aria ListBox (single selection, grid layout) for keyboard navigation with independent focus and selection
  • Delete ~30 lines of manual adjustPosition() positioning logic. React Aria handles positioning with shouldFlip={false} and a new shared useColorPickerPopoverOffset hook for viewport-aware popover positioning.
  • Update swatch grid layout from 4×4 to 8×2 per Zeplin design spec (22px swatches, 5px gaps, 235px width), with a reserved 3rd row for the custom color swatch so the More/Less button position is stable
  • Add 22 ColorPickerPalette unit tests, 11 PointColorSetting unit tests, and 1 Cypress test for expanded popover viewport containment
  • Fix popover disappearing when expanded near viewport bottom (dynamic offset calculation)
  • Accessibility improvements: aria-label on Popovers, human-readable color swatch names, aria-expanded on More/Less button, missing labels on case card editor, WCAG-compliant swatch border contrast
  • Update Cypress helpers for ListBox migration (data-selected, popover dismiss via Escape)
  • Remove nested <Dialog> wrapper from inside <Popover> in all consumers — the nested [role=dialog] caused React Aria's Popover to disable its own focus management (FocusScope containment and auto-focus)
  • Fix keyboard events (Enter, Tab, Arrow keys) leaking from the portal-rendered popover to RDG's cell editor via React synthetic event bubbling through the component tree. Arrow key propagation is scoped to the swatch grid so react-colorful sliders still work.
  • Fix text tile color picker to accept color on click-outside (previously rejected, requiring explicit "Set Color" for every change)

Files migrated: color-picker-palette.tsx, point-color-setting.tsx, color-text-editor.tsx, color-cell-text-editor.tsx, format-text-color-button.tsx
New files: use-color-picker-popover-offset.ts

Test plan

  • Manual: Graph inspector Fill Color / Border Color swatches
  • Manual: Case card color attribute editing
  • Manual: Case table color attribute editing
  • Manual: Map point color setting
  • Manual: Text toolbar color button
  • Manual: Color picker expanded near viewport bottom stays fully visible
  • Manual: ESC key closes popover from any focused element in the palette
  • Manual: Selecting a swatch then closing preserves the selected color
  • Manual: Tab key cycles within popover (does not escape to RDG cell navigation)
  • Manual: Enter key on More/Less button toggles expanded picker (does not close cell editor)
  • Manual: Arrow keys work in react-colorful saturation/hue sliders when expanded
  • Manual: Click-outside text tile color picker accepts the color change
  • CI: lint, build, unit tests pass
  • CI: Cypress regression suite

🤖 Generated with Claude Code

kswenson and others added 8 commits March 24, 2026 11:17
…DAP-1150)

Replace Chakra PopoverContent/PopoverBody/PopoverArrow/Button/ButtonGroup/Flex
with plain HTML and React Aria RadioGroup/Radio for the color swatch grid.
Remove all positioning logic (React Aria Popover handles positioning in a
later task). Delete the SCSS :export block and its TypeScript declaration file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused isPaletteOpen prop and with-color-picker CSS class.
Fix deprecated grid-gap to gap. Fix invalid justify-content: right
to flex-end.

Part of CODAP-1150.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… DialogTrigger (CODAP-1150)

Replace Chakra Popover/PopoverTrigger/Portal with React Aria
DialogTrigger + Button + Popover + Dialog. Remove useOutsidePointerDown
(React Aria handles outside-click dismissal). Replace string-based
openPopover state with boolean isOpen. Use isAcceptingRef pattern to
distinguish accept-close from dismiss-close. Update tests to work with
real React Aria components instead of Chakra mocks, and add tests for
Escape-to-reject, accept-without-reject, and disabled behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ialogTrigger (CODAP-1150)

Replace Chakra Popover/PopoverTrigger/PopoverAnchor/Portal with React Aria
DialogTrigger/Popover/Dialog for the case card inline color editor. Replace
Chakra forwardRef/useMergeRefs with React equivalents. Keep useOutsidePointerDown
for submit-on-outside-click of the entire editor. Use isCancellingRef pattern
to distinguish Escape (cancel) from outside-click (submit) on palette close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ia DialogTrigger (CODAP-1150)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Aria (CODAP-1150)

Replace Chakra Popover/PopoverTrigger/Portal/useDisclosure with React Aria
Popover + Dialog in controlled mode. Use triggerRef approach (rather than
DialogTrigger) since InspectorButton already wraps a React Aria Button in
a TooltipTrigger. Add isAcceptingRef pattern to distinguish accept-close
from dismiss-close. Remove removed ColorPickerPalette props (initialColor,
buttonRef, placement).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part of CODAP-1150.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 tests covering RadioGroup rendering, swatch selection, arrow key
navigation, non-standard color swatch, More/Less toggle, Accept/Reject
callbacks, color picker expand/collapse, and light swatch class.

Part of CODAP-1150.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kswenson kswenson added the v3 CODAP v3 label Mar 24, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 71.34503% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.33%. Comparing base (678513b) to head (8ac026d).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
.../components/case-tile-common/color-text-editor.tsx 13.95% 37 Missing ⚠️
...3/src/components/text/format-text-color-button.tsx 57.14% 9 Missing ⚠️
...c/components/case-table/color-cell-text-editor.tsx 89.47% 2 Missing ⚠️
v3/src/components/common/color-picker-palette.tsx 97.29% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2492       +/-   ##
===========================================
+ Coverage   70.23%   86.33%   +16.09%     
===========================================
  Files         768      769        +1     
  Lines       42980    43031       +51     
  Branches    10280    10686      +406     
===========================================
+ Hits        30189    37152     +6963     
+ Misses      12785     5864     -6921     
- Partials        6       15        +9     
Flag Coverage Δ
cypress 69.73% <65.13%> (+30.36%) ⬆️
jest 58.71% <46.78%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress
Copy link
Copy Markdown

cypress bot commented Mar 24, 2026

codap-v3    Run #10896

Run Properties:  status check passed Passed #10896  •  git commit 8ac026dd1f: fix: color picker accept/reject semantics and arrow key handling
Project codap-v3
Branch Review CODAP-1150-color-picker-react-aria
Run status status check passed Passed #10896
Run duration 02m 06s
Commit git commit 8ac026dd1f: fix: color picker accept/reject semantics and arrow key handling
Committer Kirk Swenson
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

Add Current Status section documenting 3 known bugs (gray border,
tab cycling, arrow key losing custom color), implementation deviations
from plan (FormatTextColorButton, useOutsidePointerDown kept in
ColorTextEditor, React Aria Radio structure), and updated File Map.

Part of CODAP-1150.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kswenson kswenson force-pushed the CODAP-1150-color-picker-react-aria branch from d8ec856 to 1d080b0 Compare March 24, 2026 18:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates CODAP v3 color picker UI from Chakra UI popovers/buttons to React Aria Components to improve accessible focus management, keyboard navigation, and ARIA semantics across inspector, text toolbar, and table/card editors.

Changes:

  • Replaces Chakra popovers with React Aria DialogTrigger/Popover/Dialog and rewrites the swatch grid using React Aria ListBox/ListBoxItem.
  • Introduces useColorPickerPopoverOffset() to keep the expanded palette within the viewport.
  • Adds/updates unit tests and Cypress selectors to match the new DOM/ARIA behavior, plus new translation strings for accessible labels.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
v3/src/utilities/translation/lang/en-US.json5 Adds dialog label + named swatch labels for accessible announcements.
v3/src/components/text/format-text-color-button.tsx Migrates text toolbar color popover to React Aria and wires palette + offset hook.
v3/src/components/data-display/inspector/point-color-setting.tsx Migrates inspector swatch popover to React Aria and adds commit/reject handling.
v3/src/components/data-display/inspector/point-color-setting.test.tsx Updates tests to target React Aria behavior instead of Chakra mocks.
v3/src/components/common/use-color-picker-popover-offset.ts New hook to compute/maintain vertical offset when palette expands.
v3/src/components/common/color-picker-palette.tsx Rewrites palette UI with React Aria ListBox, adds non-standard swatch support.
v3/src/components/common/color-picker-palette.test.tsx New test suite validating swatches, expansion, accept/reject, and non-standard swatch behavior.
v3/src/components/common/color-picker-palette.scss.d.ts Removes no-longer-needed SCSS exports typings.
v3/src/components/common/color-picker-palette.scss Updates palette layout/styling (8×2 grid, 22px swatches, new buttons).
v3/src/components/case-tile-common/color-text-editor.tsx Migrates case card color editor popover to React Aria; preserves outside-click submit behavior.
v3/src/components/case-table/color-cell-text-editor.tsx Migrates case table color editor popover to React Aria and wires offset hook.
v3/doc/specs/2026-03-19-codap-1150-color-picker-plan.md Adds implementation plan/spec and known bugs documentation.
v3/doc/specs/2026-03-19-codap-1150-color-picker-a11y-design.md Adds accessibility design notes and migration approach.
v3/cypress/support/elements/color-picker-palette.ts Updates Cypress selectors for React Aria data-selected and new structure.
v3/cypress/e2e/graph.spec.ts Adjusts E2E interactions to close popovers via Escape; adds viewport overflow test for expanded picker.
v3/cypress/e2e/graph-legend.spec.ts Updates E2E assertions to use data-selected and closes popover via Escape.
Comments suppressed due to low confidence (1)

v3/src/components/case-tile-common/color-text-editor.tsx:98

  • handleInputColorChange() calls setInputValue() and then calls handleUpdateValue(), which calls setInputValue() again. This double state update is redundant and can cause extra renders. Consider having handleInputColorChange() delegate entirely to handleUpdateValue(event.target.value) (or removing the setInputValue() from one of the two functions).
  function handleInputColorChange(event: ChangeEvent<HTMLInputElement>) {
    setInputValue(event.target.value)
    handleUpdateValue(event.target.value)
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +53
const handleAcceptColor = useCallback((color: string) => {
updateValue(color)
initialColorRef.current = color
closePopover()
}, [closePopover, updateValue])
isAcceptingRef.current = true
setIsOpen(false)
}, [updateValue])
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleAcceptColor() closes the dialog via setIsOpen(false), which bypasses handleOpenChange(false) where you reset the popover offset (and where reject-vs-accept logic lives). This can leave a non-zero popoverOffset applied the next time the picker opens (especially after expanding via “More”). Consider centralizing all close paths through handleOpenChange(false) (or calling resetPopoverOffset() here) so offset cleanup is guaranteed.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +71
const handleReject = useCallback(() => {
handleRejectColor()
setIsOpen(false)
}, [handleRejectColor])
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleReject() closes via setIsOpen(false) instead of going through handleOpenChange(false), so resetPopoverOffset() won't run and the reject path may leave state (e.g. offset) dirty for the next open. Also, reject currently calls onColorChange(initialColorRef.current) but does not restore local inputValue, so the next time the popover opens it may render the palette using a stale inputValue that no longer matches the reverted swatchBackgroundColor. Recommend resetting inputValue back to initialColorRef.current (or syncing it from swatchBackgroundColor on open/close) and ensuring all dismiss/close paths run the same cleanup.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cad8e93.

handleAcceptColor and handleReject now route through handleOpenChange(false). Also reset inputValue on close to prevent stale values on next open.

Comment on lines +51 to +60
const handleAccept = useCallback((color: string) => {
handleSetColor(color)
onClose()
}
isAcceptingRef.current = true
setIsOpen(false)
}, [handleSetColor])

function handleReject() {
const handleReject = useCallback(() => {
handleSetColor(initialColorRef.current)
onClose()
}
setIsOpen(false)
}, [handleSetColor])
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleAccept() / handleReject() close the popover by calling setIsOpen(false), but the offset cleanup (resetPopoverOffset()) only runs inside handleOpenChange(false). With controlled isOpen, onOpenChange is typically only invoked for user-initiated state changes, so closing programmatically can leave a non-zero popoverOffset applied on the next open. Consider routing all closes through the same cleanup path (e.g. call handleOpenChange(false) or call resetPopoverOffset() directly in accept/reject).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cad8e93.

handleAccept and handleReject now route through handleOpenChange(false).

@emcelroy emcelroy marked this pull request as ready for review March 27, 2026 19:24
kswenson and others added 3 commits March 27, 2026 17:31
…color picker

Remove the nested <Dialog> inside <Popover> in all four color picker
consumers. The nested [role=dialog] caused React Aria's Popover to
disable its own focus management (FocusScope containment and auto-focus),
leaving focus in the parent component instead of the popover portal.

Fix keyboard event leaking from the portal-rendered color picker to
RDG's EditCell handler. React synthetic events bubble through the
component tree (not the DOM tree), so Enter/Arrow/Tab events from the
popover reached RDG and triggered cell navigation. Two complementary
fixes: (1) stop propagation for Enter/Arrow keys in ColorPickerPalette's
capture handler, and (2) guard RDG's handleCellKeyDown to skip events
when focus is outside the gridcell (Tab must propagate so FocusScope
can trap it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use explicit grid-template-rows: repeat(3, 22px) so the grid always
reserves space for a third row. This keeps the More/Less button in a
stable position regardless of whether a custom (17th) swatch is present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix FormatTextColorButton to accept color on click-outside (not reject).
Replace isAcceptingRef pattern with simpler model: close = accept,
Escape = reject. This matches the expected behavior where previewing
colors does not require explicit Set Color to commit.

Scope arrow key stopPropagation to the swatch grid area only, so
react-colorful saturation/hue sliders still receive arrow key events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Member Author

@kswenson kswenson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Looks good -- I encountered some focus management issues in a couple places, some keyboard handling issues in the case table, and some other issues that I went ahead and fixed. See commit messages for details, but one of the issues was that Claude/we determined that the pattern of Dialog within Popover was preventing focus trapping from working correctly in some cases, so the Dialogs have been removed. According to Claude, Popover functions like a dialog and having both of them is redundant. You may want to review the changes in turn. I've appended the rest of a review developed in conjunction with Claude Code 🤖, and an accesslint accessibility audit. Feel free to address or ignore any suggestions found there.


PR Review Summary

Changes: 14 files, +898 / -462 lines

What it does

Migrates the color picker UI from Chakra UI components to React Aria for accessible focus management, keyboard navigation, and ARIA semantics. This is part of a broader effort to move CODAP v3 off Chakra UI. The migration touches 5 consumer components (PointColorSetting, ColorTextEditor, ColorCellTextEditor, FormatTextColorButton, and ColorPickerPalette itself) plus Cypress helpers and tests.

The approach

ColorPickerPalette is rebuilt from scratch: the old Chakra PopoverContent/Button/ButtonGroup/Flex structure is replaced with a React Aria ListBox (single-selection, grid layout) for the swatch grid, plain HTML buttons for More/Less and Cancel/Set Color, and a clean CSS grid layout (8×2 instead of 4×4). The ListBox gives keyboard navigation with independent focus and selection for free — arrow keys move focus without changing the selected color, and click/Enter selects.

Consumer components (PointColorSetting, ColorTextEditor, ColorCellTextEditor, FormatTextColorButton) each replace Chakra Popover/PopoverTrigger/Portal with React Aria DialogTrigger/Popover/Dialog. The old manual positioning logic (~30 lines of adjustPosition() with SCSS :export variables) is deleted entirely. React Aria handles most positioning, with shouldFlip={false} and a new shared useColorPickerPopoverOffset hook to handle the case where a tile is near the viewport bottom.

FormatTextColorButton uses a triggerRef approach (rather than DialogTrigger) because its trigger button is already wrapped in a React Aria TooltipTrigger via InspectorButton.

Key patterns:

  • isAcceptingRef / isCancellingRef patterns distinguish accept-close from dismiss-close (Escape/outside-click)
  • nonStandardIdRef keeps the 17th (custom color) swatch's ListBoxItem ID stable across renders
  • useColorPickerPopoverOffset dynamically shifts the popover upward when the expanded picker would overflow the viewport

Assessment

This is a well-structured, thorough migration. The approach is sound — React Aria's ListBox is the right primitive for a single-selection swatch grid, and the consumer-side DialogTrigger/Popover/Dialog pattern is consistent and clean across all 4 consumers. Good test coverage: 22 unit tests for ColorPickerPalette, 11 for PointColorSetting, and a new Cypress test for viewport containment.

The accessibility improvements are substantial: human-readable color names on every swatch, aria-label on dialogs and buttons, aria-expanded on the More/Less toggle, proper focus management via ListBox, and WCAG-compliant swatch border contrast (charcoal-dark-1 instead of charcoal-light-1).

Issues

  1. Magic number in useColorPickerPopoverOffset (color-picker-palette.tsx:28): The expandedAddition = 280 is a hardcoded estimate of expanded picker height. If the color picker's layout changes, this will silently break. A comment noting what this approximates would help, though measuring after render would be the ideal approach.

  2. handlePaletteOpenChange stale closure in ColorTextEditor (color-text-editor.tsx:83-95): The callback closes over inputValue and isPaletteOpen which change frequently. The deps array correctly lists them, but this means a new callback is created on every keystroke in the color text input. Since this is passed as a prop to DialogTrigger, it could cause unnecessary re-renders of the popover. This is a minor perf concern, not a bug.

  3. Deleted Cypress test for "clicks outside to close" (graph-legend.spec.ts:668-671): The old test for "Hides color picker when user clicks outside" was removed. React Aria handles outside-click dismissal, so the behavior should still work — but removing the test means there's no regression coverage for it. Consider whether this scenario is covered elsewhere.

  4. Cypress viewport containment test is fragile (graph.spec.ts:532-545): The test drags the graph to innerHeight - 150 and then checks popover bounds. This depends on the exact graph tile height and popover size. If either changes, the test may pass trivially (popover doesn't actually overflow) or fail spuriously.

None of these are blockers. Issues 1 and 3 are worth noting; 2 and 4 are informational.


Accessibility Audit

Summary: 1 Critical, 3 High, 4 Medium issues. The migration to React Aria provides a strong accessibility foundation — proper ARIA roles via ListBox, translated color name labels, focus-visible styling, and aria-selected state. The issues below are areas where the implementation could go further.

Critical

A1. FormatTextColorButton: Popover without DialogTrigger may break focus return (format-text-color-button.tsx:72-91)

  • WCAG 2.4.3, 2.1.2 — Uses standalone <Popover> with triggerRef instead of <DialogTrigger> (unlike the other 3 consumers). Focus may be lost to <body> when the popover closes. The preventFocusLoss pointer handler (e.preventDefault()) for Slate editor focus preservation may also interfere with the popover's focus management.
  • FIXED in review commit 7a3c5f2df: Removed the nested <Dialog> wrapper from all four consumers. The <Dialog> was causing the Popover to set isDialog = false, which disabled both FocusScope containment and focusSafely auto-focus. With <Dialog> removed, the Popover manages focus correctly — shouldContainFocus: true, auto-focus on mount, and focus restoration on close.

High

A2. Non-standard (17th) swatch uses raw hex as aria-label (color-picker-palette.tsx:131)

  • WCAG 1.1.1, 4.1.2 — Custom color swatch gets aria-label="#3a7fb2", announced as gibberish by screen readers. Standard swatches correctly use names like "Red", "Blue". Suggest: "Custom color: #3a7fb2" via a translation string.
  • Not addressed — remains as a future improvement.

A3. "More"/"Less" button lacks accessible context (color-picker-palette.tsx:140-148)

  • WCAG 4.1.2 — Button has aria-expanded (good) but generic text "more"/"less". No aria-controls linking to the expanded region. Screen reader users hear "more, button, expanded" with no indication of what it controls.
  • Not addressed — remains as a future improvement.

A4. Expanded color picker sliders lack keyboard instructions (color-picker-palette.tsx:152-153)

  • WCAG 3.3.2 — The react-colorful saturation picker requires 2D arrow key navigation, a non-obvious interaction. No visible or programmatic instructions. Consider adding a hex input field for keyboard users.
  • Not addressed — a react-colorful library limitation. Would require upstream changes or a custom wrapper.

Medium

A5. Cancel/Set Color visual distinction — borderline pass since text labels differentiate them, but visual hierarchy relies partly on color.

A6. Escape key double-dismiss risk (color-picker-palette.tsx:101-106) — capture-phase Escape handler may conflict with React Aria's own Escape handling, potentially running rejection logic twice.

  • Likely mitigated in review commit 7a3c5f2df: The handleKeyDownCapture calls e.stopPropagation() on Escape before calling onReject(), which should prevent React Aria from also processing the Escape event.

A7. White swatch minimal visual distinction — 1px charcoal-dark-1 border on white background passes WCAG (7.57:1) but is thin.

A8. Asymmetric grid when 17th swatch present — 8-column grid with 17 items creates a lone item in the third row; keyboard navigation may feel odd.

  • Mitigated in review commit 592789044: The grid now uses grid-template-rows: repeat(3, 22px) to reserve space for a third row, so the layout is consistent with or without the 17th swatch.

Positive Findings

  • All 16 palette colors have translated human-readable aria-label values
  • Popover now properly manages focus (FocusScope containment + auto-focus) after <Dialog> removal
  • ListBox with layout="grid" provides proper ARIA roles, arrow keys, selection management
  • [data-focus-visible] styling only shows for keyboard focus (not mouse)
  • All color contrast ratios pass WCAG AA
  • autoFocus on ListBox gives keyboard users immediate interaction target
  • data-selected / aria-selected properly communicates selected state

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants