Skip to content

Conversation

@pixelflips
Copy link
Member

@pixelflips pixelflips commented Dec 4, 2025

Description

This PR introduces a new pds-radio-group component that wraps multiple pds-radio components and provides group-level functionality, including a single error message for the entire group. This aligns with the new Figma design and simplifies form management for grouped radio selections.

Key Features

  • Automatic prop propagation: Automatically applies name, disabled, invalid, and required attributes to all child radios
  • Group-level messaging: Displays a single error message or helper message for the entire group
  • Group label: Optional group-level label that appears above all radios
  • Layout control: direction prop to arrange radios horizontally (row) or vertically (column)
  • Layout support: Works with standard radios, bordered radios (hasBorder), and radios with images
  • Event handling: Emits pdsRadioGroupChange when a radio in the group is selected
  • Group states: Supports disabled and invalid states that propagate to all radios

Changes Made

  • Created new pds-radio-group component with full TypeScript definitions and React wrappers
  • Updated pds-radio to suppress individual events when inside a pds-radio-group (prevents duplicate events)
  • Updated pds-radio documentation with technical notes about using pds-radio-group
  • Added comprehensive component and E2E tests
  • Created Storybook stories matching the naming convention of pds-radio stories

Technical Details

  • Uses semantic design tokens throughout for consistency
  • Maintains full backward compatibility - existing pds-radio usage is unaffected
  • Follows the same patterns as other form group components in the codebase

No new dependencies or updates required.

Fixes DSS-24

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

  • unit tests
  • e2e tests
  • accessibility tests
  • tested manually
  • other:

Test Configuration:

  • Pine versions: Latest
  • OS: macOS
  • Browsers: Chrome, Safari, Firefox
  • Screen readers: N/A
  • Misc: Storybook for visual testing

Test Coverage

  • Component tests verify prop propagation (name, disabled, invalid, required)
  • Component tests verify message display and event emission
  • E2E tests verify radio selection behavior and form integration
  • E2E tests verify disabled state prevents interaction
  • All existing pds-radio tests continue to pass

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes
  • Design has QA'ed and approved this PR

@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for pine-design-system ready!

Name Link
🔨 Latest commit 8cf3429
🔍 Latest deploy log https://app.netlify.com/projects/pine-design-system/deploys/69333db2d7e47f00080f02d6
😎 Deploy Preview https://deploy-preview-601--pine-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions bot added package: core Changes have been made to the Core package package: react Changes have been made to the React package labels Dec 4, 2025
@pixelflips pixelflips self-assigned this Dec 4, 2025
@pixelflips pixelflips marked this pull request as ready for review December 5, 2025 17:14
@pixelflips
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

📝 Walkthrough

Walkthrough

Adds a new pds-radio-group web component with props: componentId, direction, disabled, invalid, required, name, groupLabel, helperMessage, errorMessage, and gap. Adds RadioGroupChangeEventDetail and emits pdsRadioGroupChange. Introduces SCSS, MDX docs, README, Storybook stories, unit and e2e tests, and a React proxy export. Updates component typings/declarations (components.d.ts, JSX) and registers the new HTML element. Modifies pds-radio to suppress its individual change event when nested inside a pds-radio-group.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(pds-radio-group) add pds-radio-group to system' clearly and specifically describes the main change: introducing a new pds-radio-group component to the system.
Description check ✅ Passed The pull request description is comprehensive and addresses all required template sections including summary, feature details, changes made, testing approach, and completed checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pds-radio-group

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (5)
libs/core/src/components/pds-radio/stories/pds-radio.stories.js (1)

30-30: Remove indeterminate — radios don't support this property.

The indeterminate attribute is specific to checkboxes; radio buttons don't have an indeterminate state. This appears to be leftover from copying a checkbox template.

     name=${args.name}
-    ?indeterminate=${args.indeterminate}
     ?required=${args.required}
libs/core/src/components/pds-radio-group/test/pds-radio-group.e2e.ts (1)

85-88: Consider scoping selectors through the host element for shadow‑DOM robustness

For error/helper message queries, consider patterns like page.find('pds-radio-group >>> .pds-radio-group__message--error') and page.find('pds-radio-group >>> .pds-radio-group__message:not(.pds-radio-group__message--error)') to avoid accidentally matching unrelated elements and to be resilient if the component uses shadow DOM.

Also applies to: 118-121

libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1)

232-268: Tests reaching into internals could be brittle over time

A few tests call private methods ((component as any).updateChildRadios()) and override pdsRadioGroupChange.emit before invoking handleRadioChange directly. This gives precise control but also couples the tests to internals, so refactors (method rename, event wiring changes) may break tests without changing external behavior. If the test harness ever supports reliably triggering slotchange and native change events end‑to‑end, consider moving these to more black‑box interactions.

Also applies to: 270-393

libs/core/src/components/pds-radio-group/pds-radio-group.tsx (2)

172-174: Consider using a more deterministic ID generation strategy.

The fallback ID generation using Math.random() on Line 173 works for client-side rendering but produces non-deterministic IDs that could complicate SSR scenarios or snapshot testing.

Consider using a deterministic counter-based approach:

+let radioGroupIdCounter = 0;
+
 export class PdsRadioGroup {
   @Element() el: HTMLPdsRadioGroupElement;
 
   render() {
-    const groupId = this.componentId || `radio-group-${Math.random().toString(36).substr(2, 9)}`;
+    const groupId = this.componentId || `radio-group-${++radioGroupIdCounter}`;
     const gapValue = this.getGapValue();

61-64: Consider validating or documenting the name prop requirement.

The name prop is essential for proper radio group behavior (mutual exclusivity and form submission), but it has no default value or validation. If name is not provided, child radios may not function correctly as a group.

Consider either:

  1. Add validation in componentDidLoad:
componentDidLoad() {
  if (!this.name) {
    console.warn('pds-radio-group: name prop is required for proper radio group functionality');
  }
  this.updateChildRadios();
}
  1. Or add clear documentation in the JSDoc comment:
 /**
-  * String used for radio `name` attribute. Applied to all child radios.
+  * String used for radio `name` attribute. Applied to all child radios.
+  * Required for proper radio group behavior (mutual exclusivity and form submission).
  */
 @Prop() name: string;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f48d3a and 347f150.

📒 Files selected for processing (15)
  • libs/core/src/components.d.ts (9 hunks)
  • libs/core/src/components/pds-radio-group/docs/pds-radio-group.mdx (1 hunks)
  • libs/core/src/components/pds-radio-group/pds-radio-group.scss (1 hunks)
  • libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1 hunks)
  • libs/core/src/components/pds-radio-group/radio-group-interface.ts (1 hunks)
  • libs/core/src/components/pds-radio-group/readme.md (1 hunks)
  • libs/core/src/components/pds-radio-group/stories/pds-radio-group.stories.js (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.e2e.ts (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1 hunks)
  • libs/core/src/components/pds-radio/docs/pds-radio.mdx (2 hunks)
  • libs/core/src/components/pds-radio/pds-radio.scss (5 hunks)
  • libs/core/src/components/pds-radio/pds-radio.tsx (1 hunks)
  • libs/core/src/components/pds-radio/stories/pds-radio.stories.js (1 hunks)
  • libs/core/src/components/pds-radio/test/pds-radio.spec.tsx (1 hunks)
  • libs/react/src/components/proxies.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
libs/core/src/components/pds-radio-group/radio-group-interface.ts (1)
libs/core/src/components.d.ts (1)
  • RadioGroupChangeEventDetail (25-25)
libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (2)
libs/react/src/components/proxies.ts (2)
  • PdsRadioGroup (84-84)
  • PdsRadio (83-83)
libs/core/src/components.d.ts (1)
  • Event (19-19)
libs/core/src/components/pds-radio/test/pds-radio.spec.tsx (2)
libs/react/src/components/proxies.ts (2)
  • PdsRadio (83-83)
  • PdsRadioGroup (84-84)
libs/core/src/components.d.ts (1)
  • Event (19-19)
libs/react/src/components/proxies.ts (2)
libs/react/src/components/react-component-lib/createComponent.tsx (1)
  • createReactComponent (14-106)
libs/react/src/components/react-component-lib/index.ts (1)
  • createReactComponent (1-1)
🪛 LanguageTool
libs/core/src/components/pds-radio-group/readme.md

[style] ~14-~14: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ...d |disabled` | Determines whether or not the radio group is disabled. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)


[style] ~19-~19: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ... |invalid` | Determines whether or not the radio group is invalid. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)


[style] ~21-~21: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ...d |required` | Determines whether or not the radio group is required. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)

🔇 Additional comments (13)
libs/core/src/components/pds-radio/pds-radio.tsx (1)

100-104: LGTM — clean event delegation pattern.

Using closest() to detect the parent group and suppress the individual event is an efficient approach. This correctly delegates event responsibility to the group component.

libs/core/src/components/pds-radio/test/pds-radio.spec.tsx (1)

191-211: LGTM — good negative test for grouped radio behavior.

The test correctly verifies that individual pdsRadioChange events are suppressed when the radio is inside a pds-radio-group. Using page.doc.querySelector is appropriate here since the root element is the group, not the radio.

libs/core/src/components/pds-radio-group/radio-group-interface.ts (1)

1-5: LGTM — well-defined event detail interface.

The interface correctly captures the essential information for radio group change events. Making value and componentId optional is appropriate since child radios may not always have these attributes set.

libs/core/src/components/pds-radio-group/readme.md (1)

1-53: Auto-generated documentation looks complete.

The property descriptions, event details, and dependency graph accurately reflect the component's public API.

libs/core/src/components/pds-radio/stories/pds-radio.stories.js (1)

16-16: LGTM — organizing radios under the Radio Group category.

Nesting the Radio story under "Radio Group" in Storybook improves discoverability and aligns with the new component structure.

libs/core/src/components/pds-radio-group/test/pds-radio-group.e2e.ts (1)

3-338: Comprehensive e2e coverage for group behavior

This suite exercises the key behaviors (prop propagation, single selection, messages, form submission, events, disabled/required handling) in realistic DOM contexts and looks solid overall.

libs/core/src/components/pds-radio-group/pds-radio-group.scss (1)

1-46: Styles align with group layout and validation model

The SCSS cleanly separates host layout, row/column variants, and message/error styling using existing tokens. Constraining .is-invalid to message color fits the group‑level error model.

libs/react/src/components/proxies.ts (1)

36-36: React proxy for pds-radio-group is wired consistently

The definePdsRadioGroup import and PdsRadioGroup export match the established proxy pattern (types, tag name, defineCustomElement), so React consumers should be able to use the new group without extra setup.

Also applies to: 84-84

libs/core/src/components/pds-radio-group/docs/pds-radio-group.mdx (1)

9-307: Docs are consistent with the new group API and usage patterns

The MDX page clearly explains when to use the group, matches prop names (groupLabel, direction, helperMessage, errorMessage, etc.), and keeps React and web‑component examples in sync with the tests and stories.

libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1)

5-394: Thorough spec coverage for core behaviors

These specs exercise attribute propagation, dynamic prop updates, label/helper/error rendering, direction classes, slot changes, and event payloads in good detail, and they line up with the component’s public contract.

libs/core/src/components/pds-radio/pds-radio.scss (1)

12-18: Radio styles now align with group-driven validation and disabled states

Confining .is-invalid to the error message (for both standard, bordered, and image variants) and adding explicit disabled styling for bordered/image radios makes the visuals consistent with the new radio‑group behavior while keeping token usage intact.

Also applies to: 20-72, 156-166, 180-192, 194-293

libs/core/src/components/pds-radio-group/stories/pds-radio-group.stories.js (1)

3-133: Storybook stories exercise key configurations and look consistent

The base, bordered, image, and horizontal stories cover the primary layout and state combinations for pds-radio-group, and the arg bindings match the documented API (groupLabel, direction, helper/error messages, disabled/invalid/required).

libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1)

85-107: This pattern is necessary and intentional; no changes required.

The body-level event listener is the standard Stencil approach for container components that need to respond to events from their children. The investigation confirms:

  • Necessity: pds-radio components render native inputs in their scoped shadow DOM. Change events from these inputs bubble to the light DOM where pds-radio-group listens. Direct host-element listeners cannot capture events across the shadow DOM boundary.
  • Precedent: Multiple other Pine components use the same pattern (pds-tabs, pds-button on body; pds-dropdown-menu, pds-filters on window/document).
  • Efficiency: The filtering logic (line 92: type check and containment validation) is robust and prevents handling unrelated form events. Only emitting on checked state (line 97) further reduces overhead.
  • No alternatives: Stencil doesn't provide a cleaner way to listen to child component events across the shadow DOM boundary.

This is a sound implementation pattern with no performance concerns for typical applications.

@pixelflips
Copy link
Member Author

@coderabbitai i have addressed issues and nitpicks. please review again

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

@pixelflips: Thank you for addressing the previous feedback! I'll review the changes now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (1)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1)

17-20: Consider using a more robust ID generation approach.

The static counter increments indefinitely without reset. For long-running applications, consider using crypto.randomUUID() or a similar approach instead of a counter, which would also eliminate the need to maintain state across instances.

-  /**
-   * Instance counter for deterministic ID generation
-   */
-  private static radioGroupIdCounter = 0;

Then in the render method:

-    const groupId = this.componentId || `radio-group-${++PdsRadioGroup.radioGroupIdCounter}`;
+    const groupId = this.componentId || `radio-group-${crypto.randomUUID()}`;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 347f150 and 19a1ec6.

📒 Files selected for processing (7)
  • libs/core/src/components.d.ts (9 hunks)
  • libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1 hunks)
  • libs/core/src/components/pds-radio-group/readme.md (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.e2e.ts (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1 hunks)
  • libs/core/src/components/pds-radio/docs/pds-radio.mdx (2 hunks)
  • libs/core/src/components/pds-radio/stories/pds-radio.stories.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • libs/core/src/components/pds-radio/docs/pds-radio.mdx
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.e2e.ts
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (4)
libs/react/src/components/proxies.ts (1)
  • PdsRadioGroup (84-84)
libs/core/src/components.d.ts (2)
  • Event (19-19)
  • RadioGroupChangeEventDetail (25-25)
libs/core/src/components/pds-radio-group/radio-group-interface.ts (1)
  • RadioGroupChangeEventDetail (1-5)
libs/core/src/utils/form.ts (1)
  • messageId (4-6)
🪛 LanguageTool
libs/core/src/components/pds-radio-group/readme.md

[style] ~14-~14: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ...d |disabled` | Determines whether or not the radio group is disabled. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)


[style] ~19-~19: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ... |invalid` | Determines whether or not the radio group is invalid. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)


[style] ~21-~21: ‘whether or not’ might be wordy. Consider a shorter alternative.
Context: ...d |required` | Determines whether or not the radio group is required. ...

(EN_WORDINESS_PREMIUM_WHETHER_OR_NOT)

🔇 Additional comments (5)
libs/core/src/components/pds-radio/stories/pds-radio.stories.js (1)

16-16: LGTM: Story organization improved.

The updated title nests the Radio component under Radio Group in Storybook, improving navigation and reflecting the relationship between standalone radios and the new radio group component.

libs/core/src/components/pds-radio-group/pds-radio-group.tsx (4)

115-137: LGTM: Proper attribute propagation for web components.

The method correctly uses setAttribute and removeAttribute to propagate boolean and string attributes to child radio elements, which is the appropriate pattern for web component composition.


139-144: LGTM: Helpful validation warning.

The warning for missing name prop provides helpful feedback to developers, and calling updateChildRadios ensures initial synchronization with child elements.


159-179: LGTM: Gap value mapping implemented correctly.

The method provides a clean mapping from t-shirt sizes to design tokens with a fallback for custom dimension tokens. The implementation follows common design system patterns.


181-226: LGTM: Well-structured render method with proper accessibility.

The render implementation is clean and accessible:

  • Proper use of conditional rendering
  • Good semantic structure
  • Aria-live region for error messages
  • Slot change handler ensures child radio synchronization

@pixelflips
Copy link
Member Author

@coderabbitai i have addressed the additional issues and nitpicks, please review again.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

@pixelflips: Thank you for addressing the additional feedback! I'll review the latest changes now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (2)
libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1)

1-405: Good test coverage overall; consider adding tests for gap prop.

The test suite thoroughly covers prop propagation, event emission, and dynamic updates. However, there's no test coverage for the gap prop and its CSS variable application. Consider adding a test to verify the gap functionality.

it('applies correct gap CSS variable when gap prop is set', async () => {
  const page = await newSpecPage({
    components: [PdsRadioGroup],
    html: `<pds-radio-group gap="lg"></pds-radio-group>`,
  });

  const style = page.root?.style.getPropertyValue('--pds-radio-group-gap');
  expect(style).toBe('var(--pine-dimension-lg)');
});

it('handles numeric gap values as dimension tokens', async () => {
  const page = await newSpecPage({
    components: [PdsRadioGroup],
    html: `<pds-radio-group gap="100"></pds-radio-group>`,
  });

  const style = page.root?.style.getPropertyValue('--pds-radio-group-gap');
  expect(style).toBe('var(--pine-dimension-100)');
});
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1)

202-218: Consider linking messages to child radios for accessibility.

The helper and error messages have IDs but aren't linked to the radio inputs via aria-describedby. For screen reader users, consider propagating these message IDs to the child radios or adding role="group" with aria-describedby on the Host.

     return (
       <Host
         class={this.classNames()}
         id={this.groupId}
+        role="group"
+        aria-describedby={this.helperMessage || this.errorMessage
+          ? `${this.errorMessage ? messageId(this.groupId, 'error') : ''} ${this.helperMessage ? messageId(this.groupId, 'helper') : ''}`.trim()
+          : undefined}
         style={{
           '--pds-radio-group-gap': gapValue,
         }}
       >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19a1ec6 and 55f077e.

📒 Files selected for processing (2)
  • libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (2)
libs/react/src/components/proxies.ts (2)
  • PdsRadioGroup (84-84)
  • PdsRadio (83-83)
libs/core/src/components.d.ts (1)
  • Event (19-19)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (4)
libs/core/src/components.d.ts (2)
  • Event (19-19)
  • RadioGroupChangeEventDetail (25-25)
libs/react/src/components/react-component-lib/interfaces.ts (1)
  • EventEmitter (2-4)
libs/core/src/components/pds-radio-group/radio-group-interface.ts (1)
  • RadioGroupChangeEventDetail (1-5)
libs/core/src/utils/form.ts (1)
  • messageId (4-6)
🔇 Additional comments (1)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1)

86-106: Previous feedback addressed; listener is now scoped correctly.

The @Listen('change') decorator is now properly scoped to the component host, addressing the prior review feedback about removing target: 'body'.

@pixelflips
Copy link
Member Author

@coderabbitai i have addressed the additional issues and nitpicks, please review again.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

@pixelflips: Thank you for addressing the additional feedback! I'll review the latest changes now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (4)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (3)

92-112: Make handleRadioChange resilient to shadow DOM/event retargeting

handleRadioChange assumes event.target is an HTMLInputElement with type === 'radio'. This is correct today if pds-radio renders its <input> in the light DOM, but will break if pds-radio is ever switched to use a shadow root (standard event retargeting would make event.target the host element, not the input).

To make this handler future‑proof and more robust, consider resolving the input via event.composedPath() instead of event.target:

  @Listen('change')
  handleRadioChange(event: Event) {
-    const target = event.target as HTMLInputElement;
-
-    // Only handle events from radio inputs
-    if (target.type !== 'radio') {
-      return;
-    }
+    const path = (event.composedPath?.() || []) as EventTarget[];
+    const target = path.find(
+      (node): node is HTMLInputElement =>
+        node instanceof HTMLInputElement && node.type === 'radio'
+    );
+    if (!target) {
+      return;
+    }

     // Only emit if the radio is being checked (not unchecked)
     if (target.checked) {
       const radioElement = target.closest('pds-radio') as HTMLPdsRadioElement;
       if (radioElement) {
         this.pdsRadioGroupChange.emit({
           checked: true,
           value: radioElement.value,
           componentId: radioElement.componentId,
         });
       }
     }
  }

This keeps behavior the same now but avoids tight coupling to the current DOM structure of pds-radio.


20-27: Stable group ID looks good; consider reacting to dynamic componentId changes

Using _groupId computed once in componentWillLoad and wiring it through id and messageId() solves the previous unstable‑ID problem and is a solid approach.

If you expect componentId to change at runtime (e.g., via a React wrapper prop update), you may want to mirror it into _groupId via a watcher to keep the host id and ARIA message IDs in sync:

@Watch('componentId')
handleComponentIdChange(newValue: string | undefined) {
  if (newValue) {
    this._groupId = newValue;
  }
}

If componentId is considered immutable after initial render, the current implementation is fine and this can be skipped.

Also applies to: 138-150, 208-221


162-169: Improve group-level a11y semantics (role and labeling)

Right now the host uses role="group" and groupLabel is only rendered visually. For a radio set, a more semantic pattern would be:

  • Use role="radiogroup" on the host; and
  • Associate the visual label via aria-labelledby (or aria-label) so AT users hear the group label when navigating options.

For example:

-  render() {
+  render() {
     const gapValue = this.getGapValue();
     const ariaDescribedBy = this.getAriaDescribedBy();
+    const labelId = this.groupLabel ? `${this._groupId}__label` : undefined;
 
     return (
       <Host
         class={this.classNames()}
         id={this._groupId}
-        role="group"
+        role="radiogroup"
         aria-describedby={ariaDescribedBy}
+        aria-labelledby={labelId}
         style={{
           '--pds-radio-group-gap': gapValue,
         }}
       >
         {this.groupLabel && (
-          <div class="pds-radio-group__label">
+          <div class="pds-radio-group__label" id={labelId}>
             {this.groupLabel}
           </div>
         )}

This aligns the semantics with WAI‑ARIA patterns for radio groups and better ties helper/error messaging to the control cluster.

Also applies to: 208-251

libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1)

5-425: Comprehensive coverage; consider reducing white-box coupling when test harness allows

The suite does a nice job exercising:

  • Prop propagation (name, invalid, required, disabled) including dynamic changes.
  • Rendering of label/helper/error (with icon) and direction classes.
  • Gap token handling and numeric token fallback.
  • Event emission semantics for pdsRadioGroupChange.

A couple of tests reach into internals ((component as any).updateChildRadios() and direct calls to handleRadioChange). Given the documented limitations of the Stencil test harness, this is reasonable for now, but once native change/slotchange behavior is better supported, it would be good to refactor these to black‑box interactions so the tests are less coupled to private methods.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55f077e and 32224fa.

📒 Files selected for processing (2)
  • libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (2)
libs/react/src/components/proxies.ts (2)
  • PdsRadioGroup (84-84)
  • PdsRadio (83-83)
libs/core/src/components.d.ts (1)
  • Event (19-19)
⏰ 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). (3)
  • GitHub Check: test-specs (18)
  • GitHub Check: test-specs (20)
  • GitHub Check: test-specs (22)
🔇 Additional comments (1)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1)

172-192: Gap token mapping and CSS variable usage look solid

getGapValue cleanly maps t‑shirt sizes to dimension tokens and falls back to var(--pine-dimension-${gap}) for numeric tokens, and the result is applied via --pds-radio-group-gap inline style. This is a good, flexible pattern for spacing tokens and matches the stated API.

Also applies to: 218-220

@pixelflips
Copy link
Member Author

@coderabbitai i have addressed the additional issues, please review again.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

@pixelflips: Thank you for addressing the additional feedback! I'll review the latest changes now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a 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 (3)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (3)

48-48: Consider using a union type for the gap prop.

The gap prop currently accepts any string, but the JSDoc indicates specific valid values. Using a union type would provide compile-time type safety and better IntelliSense support for developers.

Consider this approach:

+type GapSize = 'none' | 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | '025' | '050' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800';
+
-  @Prop() gap: string = 'sm';
+  @Prop() gap: GapSize = 'sm';

This prevents invalid values at compile time and improves the developer experience.


174-194: Consider adding validation or fallback for invalid gap values.

The method generates CSS variable references without validating whether the gap value is actually valid. Invalid values (e.g., typos) will produce non-existent CSS variables like var(--pine-dimension-typo), causing the gap to fail silently.

Consider adding a fallback to the default value:

 private getGapValue(): string {
   const gapMap: Record<string, string> = {
     none: '0',
     xxs: 'var(--pine-dimension-xxs)',
     xs: 'var(--pine-dimension-xs)',
     sm: 'var(--pine-dimension-sm)',
     md: 'var(--pine-dimension-md)',
     lg: 'var(--pine-dimension-lg)',
     xl: 'var(--pine-dimension-xl)',
     xxl: 'var(--pine-dimension-xxl)',
   };

   if (gapMap[this.gap]) {
     return gapMap[this.gap];
   }

+  // Validate dimension token format (e.g., '025', '100', '200')
+  if (!/^\d{3}$/.test(this.gap)) {
+    console.warn(`pds-radio-group: Invalid gap value "${this.gap}", falling back to "sm"`);
+    return gapMap.sm;
+  }
+
   return `var(--pine-dimension-${this.gap})`;
 }

122-136: Optional: Consider boolean attribute convention.

The code sets boolean attributes with a string value 'true' (e.g., setAttribute('disabled', 'true')). While this works, HTML boolean attributes conventionally use presence/absence or empty string. However, for Stencil components with prop reflection, the current approach may be intentional and consistent with how pds-radio expects these attributes.

If aligning with HTML conventions is desired:

       if (this.disabled) {
-        radio.setAttribute('disabled', 'true');
+        radio.setAttribute('disabled', '');
       } else {
         radio.removeAttribute('disabled');
       }
       if (this.invalid) {
-        radio.setAttribute('invalid', 'true');
+        radio.setAttribute('invalid', '');
       } else {
         radio.removeAttribute('invalid');
       }
       if (this.required) {
-        radio.setAttribute('required', 'true');
+        radio.setAttribute('required', '');
       } else {
         radio.removeAttribute('required');
       }

However, verify that pds-radio handles empty-string attributes correctly before making this change.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32224fa and dfa3c18.

📒 Files selected for processing (2)
  • libs/core/src/components/pds-radio-group/pds-radio-group.tsx (1 hunks)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/core/src/components/pds-radio-group/test/pds-radio-group.spec.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (4)
libs/react/src/components/proxies.ts (1)
  • PdsRadioGroup (84-84)
libs/core/src/components.d.ts (2)
  • Event (19-19)
  • RadioGroupChangeEventDetail (25-25)
libs/core/src/components/pds-radio-group/radio-group-interface.ts (1)
  • RadioGroupChangeEventDetail (1-5)
libs/core/src/utils/form.ts (1)
  • messageId (4-6)
🔇 Additional comments (2)
libs/core/src/components/pds-radio-group/pds-radio-group.tsx (2)

1-258: Excellent component implementation with strong accessibility foundations.

The component follows Stencil best practices with proper lifecycle management, stable ID generation, scoped event handling, and comprehensive accessibility support (role="group", aria-describedby, aria-live). The slot change handler ensures dynamically added radios receive group attributes, and past review concerns have been properly addressed.


20-20: Verify whether dynamic componentId changes should be supported.

Currently, _groupId is generated once in componentWillLoad based on the initial componentId value. If componentId changes after initialization, _groupId will not update, potentially breaking accessibility relationships with message IDs.

If dynamic componentId updates are expected, add a @Watch('componentId') handler to update _groupId. If IDs are meant to be set once (typical pattern), the current implementation is correct.

If dynamic updates are needed, apply this pattern:

+  @Watch('componentId')
+  handleComponentIdChange() {
+    if (this.componentId) {
+      this._groupId = this.componentId;
+    }
+  }

Also applies to: 140-152

@pixelflips pixelflips force-pushed the feat/pds-radio-group branch from 6063449 to 8cf3429 Compare December 5, 2025 20:16
@pixelflips pixelflips requested a review from amylu555 December 5, 2025 20:47
@pixelflips
Copy link
Member Author

@amylu555 you can use the preview link above in the netlify comment to take a look at the new radio group component.

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

Labels

package: core Changes have been made to the Core package package: react Changes have been made to the React package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants