-
-
Notifications
You must be signed in to change notification settings - Fork 53
feat: enhance RadioGroup behavior and accessibility #1537
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
Conversation
WalkthroughUpdates RadioGroup primitives: expands provider scope in items, switches item state attributes to Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant RG as RadioGroup.Root
participant RFG as RovingFocusGroup.Root
participant Item as RadioGroup.Item
participant Hidden as Hidden Input (type=hidden)
participant Invisible as Invisible Radio (aria-hidden)
participant Form
Note over RG,RFG: Mount
RG->>RFG: render asChild (root)
RFG->>Item: register items (roving focus)
Note over User,Item: Interaction
User->>Item: click / arrow keys
Item-->>RG: request value change
RG-->>Hidden: update value
RG-->>Item: update context (itemSelected → data-state, aria-checked)
alt name && required && selectedValue != ""
RG->>Invisible: render invisible radio (checked, aria-hidden)
else
RG--x Invisible: not rendered
end
Note over Form,Hidden: Submit
User->>Form: submit
Form->>Hidden: read name/value
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
Poem
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex Review: Here are some suggestions.
Reply with @codex fix comments to fix any unresolved comments.
About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/components/ui/RadioCards/tests/RadioCards.test.tsx (1)
110-113: Selector update looks right; add a quick name assertion.This aligns with the switch to a hidden input. Consider also asserting the name to catch regressions.
Apply this diff:
- expect(input).toBeInTheDocument(); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute('name', 'test-group'); expect(input?.value).toBe(options[0].value);src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx (1)
27-41: Minor ARIA cleanups.
- With a native
disabledattribute on the button,aria-disabledis redundant.- Prefer
aria-requiredonly on the radiogroup root; per-itemaria-requiredis unnecessary.Apply this diff:
- aria-disabled={groupDisabled || disabled} + aria-disabled={groupDisabled || disabled ? true : undefined} - aria-required={required} + aria-required={undefined}(Or drop
aria-disabledentirely since the button is actually disabled.)src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx (3)
1-9: De-duplicate WCAG tags: import from setupTests instead of redefining.Avoid drift with the central config (wcag21a/aa in setupTests vs wcag2a/aa here).
import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; -import axe from 'axe-core'; +import axe from 'axe-core'; import RadioGroup from '../RadioGroup'; - -const ACCESSIBILITY_TEST_TAGS = ['wcag2a', 'wcag2aa']; +// Reuse shared accessibility tag configuration +import { ACCESSIBILITY_TEST_TAGS } from '../../../setupTests';Please verify the relative path resolves under your Jest config.
36-62: Isolate renders within the test to reduce cross-interaction.Rendering two groups in the same test body is fine, but unmounting the first avoids incidental interactions and keeps queries scoped.
- render(<Controlled />); + const { unmount } = render(<Controlled />); const user = userEvent.setup(); await user.tab(); await user.keyboard('{ArrowRight}'); expect(screen.getByTestId('c-two')).toHaveAttribute('aria-checked', 'true'); await user.keyboard('{ArrowRight}'); expect(screen.getByTestId('c-three')).toHaveAttribute('aria-checked', 'true'); - render( + unmount(); + render( <RadioGroup.Root defaultValue="two"> <RadioGroup.Item value="one" data-testid="u-one">one</RadioGroup.Item> <RadioGroup.Item value="two" data-testid="u-two">two</RadioGroup.Item> </RadioGroup.Root> );
104-115: Optional: use jest-axe matcher for cleaner a11y assertions.Simplifies assertions and improves error messages.
-import axe from 'axe-core'; +import { axe, toHaveNoViolations } from 'jest-axe'; +expect.extend(toHaveNoViolations); @@ - const results = await axe.run(container, { runOnly: { type: 'tag', values: ACCESSIBILITY_TEST_TAGS } }); - expect(results.violations).toHaveLength(0); + const results = await axe(container, { runOnly: { type: 'tag', values: ACCESSIBILITY_TEST_TAGS } }); + expect(results).toHaveNoViolations();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/ui/RadioCards/tests/RadioCards.test.tsx(1 hunks)src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx(2 hunks)src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx(0 hunks)src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx(0 hunks)
💤 Files with no reviewable changes (2)
- src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx
- src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx (1)
src/setupTests.ts (1)
ACCESSIBILITY_TEST_TAGS(3-3)
🪛 GitHub Check: lint
src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx
[failure] 118-118:
Component definition is missing display name
🪛 GitHub Actions: Lint
src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx
[error] 118-118: ESLint: Component definition is missing display name. (react/display-name). Step: eslint --ext .js,.jsx,.ts,.tsx . --fix
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx (1)
36-36: Adding data-disabled on the root is good for styling and tests.No issues; this makes disabled state easily selectable without changing ARIA.
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx
Outdated
Show resolved
Hide resolved
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/components/ui/RadioCards/tests/RadioCards.test.tsx (1)
108-113: Tighten assertions and clarify test titleMake the test more precise and future-proof:
- Remove "radio" from the title (it’s now a hidden input).
- Assert the name attribute to ensure form wiring.
- Ensure only one hidden input exists for the group.
- Verify the hidden input’s value updates on selection change.
Apply:
- it('renders hidden radio input for form submission', () => { + it('renders hidden input for form submission', () => { const { container } = renderRadioCards({ defaultValue: options[0].value, name: 'test-group' }); - const input = container.querySelector('input[type="hidden"]') as HTMLInputElement | null; + const input = container.querySelector('input[type="hidden"][name="test-group"]') as HTMLInputElement | null; expect(input).toBeInTheDocument(); + expect(input?.name).toBe('test-group'); + expect(container.querySelectorAll('input[type="hidden"][name="test-group"]')).toHaveLength(1); expect(input?.value).toBe(options[0].value); + fireEvent.click(screen.getByTestId('radio-item-16-core CPU')); + expect(input?.value).toBe('16-core CPU'); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/components/ui/RadioCards/tests/RadioCards.test.tsx(1 hunks)src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx
- src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx
- src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx
🔇 Additional comments (1)
src/components/ui/RadioCards/tests/RadioCards.test.tsx (1)
108-113: LGTM: hidden input assertion matches new form-control semanticsSwitching the query to input[type="hidden"] and asserting its value aligns with the updated RadioGroup integration for form submission.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx (1)
37-45: asChild is applied to a non-DOM child (Context.Provider); injected roving props are droppedWith asChild, Root clones its only child to inject DOM props. Here the only child is RadioGroupContext.Provider, so those props won’t land on a real element and may cause lost semantics and React warnings. Either wrap the Primitive.div with Root asChild (preferred) or drop asChild here.
Apply this minimal diff (preferred restructuring):
- <Primitive.div ref={ref} {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled} data-disabled={groupDisabled ? '' : undefined}> - <RovingFocusGroup.Root dir={dir} orientation={orientation} loop={loop} asChild> - <RadioGroupContext.Provider value={sendItems}> + <RovingFocusGroup.Root dir={dir} orientation={orientation} loop={loop} asChild> + <Primitive.div ref={ref} {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled} data-disabled={groupDisabled ? '' : undefined}> + <RadioGroupContext.Provider value={sendItems}> <RovingFocusGroup.Group> {children} </RovingFocusGroup.Group> - </RadioGroupContext.Provider> - </RovingFocusGroup.Root> + </RadioGroupContext.Provider> + </Primitive.div> + </RovingFocusGroup.Root>Alternatively (least invasive), just drop asChild:
- <RovingFocusGroup.Root dir={dir} orientation={orientation} loop={loop} asChild> + <RovingFocusGroup.Root dir={dir} orientation={orientation} loop={loop}>
🧹 Nitpick comments (1)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx (1)
36-36: Add aria-orientation (and dir) to the radiogroup elementImproves AT semantics and CSS targeting. Only set aria-orientation when not "both".
- <Primitive.div ref={ref} {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled} data-disabled={groupDisabled ? '' : undefined}> + <Primitive.div + ref={ref} + {...props} + role='radiogroup' + aria-required={required} + aria-disabled={groupDisabled} + aria-orientation={orientation === 'both' ? undefined : orientation} + dir={dir} + data-disabled={groupDisabled ? '' : undefined} + >
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build
- GitHub Check: Analyze (javascript)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx (1)
46-76: Required still doesn’t block empty submissions; also causes duplicate form entries when selected.
- The sentinel radio only renders when a value is selected, so
requireddoes not prevent submitting with no selection.- When a value is selected and
requiredis true, both the hidden input and the sentinel radio submit the same name, yielding duplicate entries.Fix by rendering either the hidden input (not required path) or the sentinel radio (required path), and render the sentinel even when no value is selected (unchecked to enforce the constraint):
- {name && ( - <> - <input - type='hidden' - name={name} - value={selectedValue} - disabled={groupDisabled} - /> - {required && selectedValue !== '' && ( - <input - type='radio' - name={name} - value={selectedValue} - checked - onChange={() => {}} - disabled={groupDisabled} - required - aria-hidden='true' - tabIndex={-1} - style={{ - position: 'absolute', - width: 0, - height: 0, - opacity: 0, - pointerEvents: 'none', - margin: 0 - }} - /> - )} - </> - )} + {name && ( + <> + {!required && selectedValue !== '' && ( + <input + type='hidden' + name={name} + value={selectedValue} + disabled={groupDisabled} + /> + )} + {required && ( + <input + type='radio' + name={name} + value={selectedValue} + checked={selectedValue !== ''} + onChange={() => {}} + disabled={groupDisabled} + required + aria-hidden='true' + tabIndex={-1} + style={{ + position: 'absolute', + width: 0, + height: 0, + opacity: 0, + pointerEvents: 'none', + margin: 0 + }} + /> + )} + </> + )}
🧹 Nitpick comments (2)
src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx (1)
36-37: Expose aria-orientation for assistive tech.Set
aria-orientationon the radiogroup when not “both” for better SR hints.- <Primitive.div ref={ref} {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled} data-disabled={groupDisabled ? '' : undefined}> + <Primitive.div + ref={ref} + {...props} + role='radiogroup' + aria-required={required} + aria-disabled={groupDisabled} + aria-orientation={orientation !== 'both' ? orientation : undefined} + data-disabled={groupDisabled ? '' : undefined} + >src/core/primitives/RadioGroup/tests/RadioGroupPrimtive.test.tsx (1)
75-76: Avoid brittle index-based query; target the hidden input explicitly.Using
[0]couples the test to DOM order and may break if a sentinel radio is present. Filter by selector.- // name is set on the hidden input, which is the first element with the selected value - const hiddenInput = screen.getAllByDisplayValue('a')[0]; + // name is set on the hidden input; select it explicitly + const hiddenInput = screen.getByDisplayValue('a', { selector: 'input[type="hidden"]' });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx(1 hunks)src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx(2 hunks)src/core/primitives/RadioGroup/tests/RadioGroupPrimtive.test.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
Summary
Testing
npm test src/core/utils/RovingFocusGroup/tests/RovingFocusGroup.behavior.test.tsxnpm test src/components/ui/RadioGroup/tests/RadioGroup.behavior.test.tsxnpm test src/components/ui/RadioCards/tests/RadioCards.test.tsxnpm testSummary by CodeRabbit
Bug Fixes
Tests