Repository navigation
a11y(frontend): complete color radio-group keyboard contract #730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
742da8c
🎨 Palette: 커스텀 라디오 그룹(색상 선택기) 접근성 개선
seonghobae a18ef31
Merge branch 'main' into palette/color-swatch-a11y-10094327760965026187
seonghobae 64ecff1
chore: drop stray pnpm-lock.yaml (repo uses npm; breaks central cover…
seonghobae 8793882
Merge branch 'main' into palette/color-swatch-a11y-10094327760965026187
opencode-agent[bot] 9da2ea9
🎨 Palette: 커스텀 라디오 그룹(색상 선택기) 접근성 개선
seonghobae 85de93d
Merge branch 'main' into palette/color-swatch-a11y-10094327760965026187
opencode-agent[bot] 30991c5
chore(frontend): remove unrelated package-manager drift
seonghobae cbb049c
chore(docs): preserve canonical palette history
seonghobae 881114a
fix(a11y): implement radio-group roving focus
seonghobae a55bb33
test(a11y): cover radio roving tabindex and arrow navigation
seonghobae f8be5e1
docs(a11y): record radio-group evidence in APA 7
seonghobae 4ce05b3
test(a11y): assert current radio nodes after rerender
seonghobae cac4d09
docs: record color radio keyboard contract
seonghobae 33098bf
test(a11y): close radio review and coverage gaps
seonghobae e34178e
chore: merge protected main into radio-group branch
seonghobae 44377be
🎨 Palette: jsdom 포커스 테스트 타이밍 문제 해결
seonghobae dfb9fc5
Merge branch 'main' into palette/color-swatch-a11y-10094327760965026187
opencode-agent[bot] 3432c41
🎨 Palette: jsdom 테스트 포커스 이동 안정화 (setTimeout 교체)
seonghobae 2a46e1c
fix(deps): preserve protected-main Vite version
seonghobae 9c4dd2f
fix(deps): restore canonical frontend lock
seonghobae cb6ead2
chore(frontend): exact-head dependency drift remediation
seonghobae 91e73d5
chore(frontend): exact-head dependency drift remediation
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Color radio-group accessibility evidence | ||
|
|
||
| ## Decision | ||
|
|
||
| The Business Group color selector uses the WAI-ARIA radio-group pattern with one radio in the page tab sequence. The checked color receives `tabIndex={0}` and every other color receives `tabIndex={-1}`. Right and Down Arrow move focus and selection to the next color; Left and Up Arrow move to the previous color; navigation wraps at both ends. Native button activation preserves Space and click behavior. | ||
|
|
||
| This contract is covered by focused regression tests for checked state, roving tabindex, forward and backward arrow navigation, wrapping, unrelated-key handling, and the no-selection fallback. | ||
|
|
||
| ## Evidence | ||
|
|
||
| The W3C Accessible Rich Internet Applications Authoring Practices Guide specifies that a radio group contains `radio` elements with `aria-checked`, that only one radio participates in the tab sequence, and that arrow keys move focus and selection with wrapping. | ||
|
|
||
| ## Reference | ||
|
|
||
| World Wide Web Consortium. (n.d.). *Radio group pattern*. WAI-ARIA Authoring Practices Guide. Retrieved August 4, 2026, from https://www.w3.org/WAI/ARIA/apg/patterns/radio/ | ||
|
|
||
| World Wide Web Consortium. (n.d.). *Radio group example using roving tabindex*. WAI-ARIA Authoring Practices Guide. Retrieved August 4, 2026, from https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio/ | ||
98 changes: 98 additions & 0 deletions
98
frontend/src/components/modals/GroupModal.radioKeyboard.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import '@testing-library/jest-dom/vitest' | ||
| import { cleanup, fireEvent, render, screen } from '@testing-library/react' | ||
| import { useState } from 'react' | ||
| import { afterEach, describe, expect, it, vi } from 'vitest' | ||
|
|
||
| import { BUSINESS_GROUP_COLORS } from '../../erd/businessGroups' | ||
| import { GroupModal } from './GroupModal' | ||
|
|
||
| afterEach(() => { | ||
| cleanup() | ||
| }) | ||
|
|
||
| function RadioGroupHarness({ initialColor }: { initialColor: string }) { | ||
| const [color, setColor] = useState(initialColor) | ||
| return ( | ||
| <GroupModal | ||
| isOpen | ||
| businessGroups={[]} | ||
| newGroupName="" | ||
| setNewGroupName={vi.fn()} | ||
| newGroupColor={color} | ||
| setNewGroupColor={setColor} | ||
| nodes={[]} | ||
| onCloseGroupManager={vi.fn()} | ||
| onCreateBusinessGroup={vi.fn()} | ||
| onDeleteBusinessGroup={vi.fn()} | ||
| onAssignBusinessGroup={vi.fn()} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| function colorRadios(): HTMLElement[] { | ||
| return screen.getAllByRole('radio', { name: /^색상 / }) | ||
| } | ||
|
|
||
| describe('GroupModal color radio keyboard contract', () => { | ||
| it('keeps one checked radio in the tab order and moves selection with arrows', async () => { | ||
| render(<RadioGroupHarness initialColor={BUSINESS_GROUP_COLORS[1]} />) | ||
| let radios = colorRadios() | ||
|
|
||
| expect(radios).toHaveLength(BUSINESS_GROUP_COLORS.length) | ||
| expect(radios[1]).toHaveAttribute('aria-checked', 'true') | ||
| expect(radios[1]).toHaveAttribute('tabindex', '0') | ||
| radios.forEach((radio, index) => { | ||
| if (index !== 1) expect(radio).toHaveAttribute('tabindex', '-1') | ||
| }) | ||
|
|
||
| radios[1]!.focus() | ||
| fireEvent.keyDown(radios[1]!, { key: 'ArrowRight' }) | ||
| await new Promise(r => setTimeout(r, 0)); | ||
| radios = colorRadios() | ||
| expect(radios[2]).toHaveFocus() | ||
| expect(radios[2]).toHaveAttribute('aria-checked', 'true') | ||
| expect(radios[2]).toHaveAttribute('tabindex', '0') | ||
| expect(radios[1]).toHaveAttribute('aria-checked', 'false') | ||
| expect(radios[1]).toHaveAttribute('tabindex', '-1') | ||
|
|
||
| fireEvent.keyDown(radios[2]!, { key: 'ArrowUp' }) | ||
| await new Promise(r => setTimeout(r, 0)); | ||
| radios = colorRadios() | ||
| expect(radios[1]).toHaveFocus() | ||
| expect(radios[1]).toHaveAttribute('aria-checked', 'true') | ||
| }) | ||
|
|
||
| it('wraps arrow navigation and ignores unrelated keys', async () => { | ||
| render(<RadioGroupHarness initialColor={BUSINESS_GROUP_COLORS[0]} />) | ||
| let radios = colorRadios() | ||
|
|
||
| radios[0]!.focus() | ||
| fireEvent.keyDown(radios[0]!, { key: 'ArrowLeft' }) | ||
| await new Promise(r => setTimeout(r, 0)); | ||
| radios = colorRadios() | ||
| expect(radios.at(-1)).toHaveFocus() | ||
| expect(radios.at(-1)).toHaveAttribute('aria-checked', 'true') | ||
|
|
||
| fireEvent.keyDown(radios.at(-1)!, { key: 'ArrowDown' }) | ||
| await new Promise(r => setTimeout(r, 0)); | ||
| radios = colorRadios() | ||
| expect(radios[0]).toHaveFocus() | ||
| expect(radios[0]).toHaveAttribute('aria-checked', 'true') | ||
|
|
||
| fireEvent.keyDown(radios[0]!, { key: 'Home' }) | ||
| await new Promise(r => setTimeout(r, 0)); | ||
| expect(radios[0]).toHaveFocus() | ||
| expect(radios[0]).toHaveAttribute('aria-checked', 'true') | ||
| }) | ||
|
|
||
| it('makes the first radio tabbable when the supplied color is not selected', () => { | ||
| render(<RadioGroupHarness initialColor="not-in-palette" />) | ||
| const radios = colorRadios() | ||
|
|
||
| expect(radios[0]).toHaveAttribute('tabindex', '0') | ||
| expect(radios[0]).toHaveAttribute('aria-checked', 'false') | ||
| radios.slice(1).forEach((radio) => { | ||
| expect(radio).toHaveAttribute('tabindex', '-1') | ||
| }) | ||
| }) | ||
| }) | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.