Skip to content

Conversation

@Kiarokh
Copy link
Contributor

@Kiarokh Kiarokh commented Aug 7, 2025

fix #1437

Summary by CodeRabbit

  • New Features

    • Introduced a new radio button group component for selecting options with enhanced accessibility and styling.
    • Added example components demonstrating basic usage, icon options, and support for multi-line secondary text in radio button groups.
  • Documentation

    • Added detailed internal documentation for the radio button template to clarify usage and design approach.
  • Style

    • Updated style import paths for radio button components to ensure consistent styling.
  • Chores

    • Adjusted import paths for improved code organization.

Review:

  • Commits are atomic
  • Commits have the correct type for the changes made
  • Commits with breaking changes are marked as such

Browsers tested:

(Check any that applies, it's ok to leave boxes unchecked if testing something didn't seem relevant.)

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

@coderabbitai
Copy link

coderabbitai bot commented Aug 7, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

A new limel-radio-button-group component is introduced, serving as a wrapper around limel-list to provide a radio button group UI. Several example components demonstrate its usage. Supporting changes include import path updates for the radio button template and styles, and enhanced documentation for the template.

Changes

Cohort / File(s) Change Summary
Radio Button Group Component
src/components/radio-button-group/radio-button-group.tsx
Introduces the new limel-radio-button-group Stencil component, handling radio group rendering, state management, and event emission.
Radio Button Group Examples
src/components/radio-button-group/examples/radio-button-group-basic.tsx,
src/components/radio-button-group/examples/radio-button-group-icons.tsx,
src/components/radio-button-group/examples/radio-button-group-multiple-lines.tsx
Adds three example components demonstrating basic usage, icon support, and handling of multi-line secondary text for the new radio button group.
Radio Button Template Documentation
src/components/radio-button-group/radio-button.template.tsx
Adds an extensive JSDoc comment explaining the internal radio button template, its usage, and design rationale. No functional changes.
Import Path Adjustments
src/components/list/list-renderer.tsx,
src/components/list/list.scss
Updates import paths for the radio button template and styles to reference the new radio-button-group location instead of the old radio-button directory.

Sequence Diagram(s)

sequenceDiagram
    participant Consumer
    participant RadioButtonGroup (limel-radio-button-group)
    participant List (limel-list)
    participant RadioButtonTemplate

    Consumer->>RadioButtonGroup: Passes options, value, disabled, etc.
    RadioButtonGroup->>List: Renders <limel-list type="radio"> with options
    List->>RadioButtonTemplate: Uses template to render each radio button
    Consumer->>RadioButtonGroup: User selects a radio button
    RadioButtonGroup->>Consumer: Emits change event with new value
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Assessment against linked issues

Objective Addressed Explanation
Add a radio button group component (limel-radio-button-group) as a wrapper for limel-list with radios (#1437)
Ensure component takes array of items and renders radio group, not single radio (#1437)
Move RadioButtonTemplate out of list/radio-button folder and update imports (#1437)
Provide discoverable examples and documentation for the new component (#1437)

Suggested labels

visual design, maintenance

Suggested reviewers

  • LucyChyzhova
  • jgroth
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch radio-button-group

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 7, 2025

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-3638/

@Kiarokh Kiarokh marked this pull request as ready for review August 7, 2025 14:35
Copilot AI review requested due to automatic review settings August 7, 2025 14:35
@Kiarokh Kiarokh requested a review from a team as a code owner August 7, 2025 14:35
Copy link
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

This PR introduces a new limel-radio-button-group component to provide a more convenient API for creating radio button groups. The component wraps the existing limel-list component with type="radio" while adding proper documentation and examples.

  • Adds a new RadioButtonGroup component with props for options, value, disabled state, and styling
  • Includes comprehensive documentation and three example components
  • Moves radio button template from list subfolder to radio-button-group folder for better organization

Reviewed Changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
radio-button-group.tsx New component implementation wrapping limel-list with radio type
radio-button.template.tsx Adds comprehensive documentation to the radio button template
examples/*.tsx Three example components demonstrating basic usage, icons, and multi-line text
list.scss Updates import path for radio button styles
list-renderer.tsx Updates import path for radio button template
lime-elements.api.md API documentation updates for the new component

@@ -0,0 +1,94 @@
import {
ListItem,
LimelSelectCustomEvent,
Copy link

Copilot AI Aug 7, 2025

Choose a reason for hiding this comment

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

The import LimelSelectCustomEvent is used but not needed for the radio button group functionality. This appears to be a copy-paste artifact from another component example.

Suggested change
LimelSelectCustomEvent,

Copilot uses AI. Check for mistakes.
@coderabbitai coderabbitai bot changed the title @coderabbitai Add limel-radio-button-group component with examples and documentation updates Aug 7, 2025
*/
@Component({
tag: 'limel-radio-button-group',
shadow: false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai since this component is simply a wrapper component (read the linked issue to understand why) and it currently doesn't have any specific styles file, I set the the shadow to false. What do you think about this?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the full component implementation to understand its structure
cat src/components/radio-button-group/radio-button-group.tsx

# Let's also check if there are any related issues or documentation
fd -t f -e md . | xargs grep -l "radio.*button.*group" 2>/dev/null || true

# Search for other components that use shadow: false to understand the pattern
rg "shadow:\s*false" src/components/ -A 2 -B 2

# Look for wrapper components or components that delegate to lime-list
rg "limel-list" src/components/ -A 2 -B 2

Length of output: 24014


@Kiarokh Your decision to use shadow: false for this component makes sense given its specific purpose and implementation. Here's my analysis:

Why this is reasonable:

  1. True wrapper component: The component is purely a convenience wrapper around limel-list with type="radio" - it adds no visual styling or DOM manipulation of its own
  2. No style encapsulation needed: Since there's no component-specific CSS file and no plans for one, Shadow DOM's style encapsulation provides no benefit
  3. Follows existing pattern: I found that limel-action-bar-overflow-menu also uses shadow: false without a styleUrl, establishing precedent for pure wrapper components
  4. Performance consideration: Similar to limel-notched-outline, avoiding Shadow DOM can improve performance for simple wrapper components

Considerations:

  • This does deviate from the general lime-elements pattern where "almost all components use shadow-DOM"
  • If the component ever needs its own styles in the future, you'd need to change to shadow: true and potentially deal with style encapsulation

Recommendation:

Your approach is technically sound for this specific use case. Consider adding a brief comment in the component explaining the rationale (similar to how notched-outline documents its shadow: false decision) to help future maintainers understand the intentional deviation from the typical pattern.


✏️ Learnings added
Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3638
File: src/components/radio-button-group/radio-button-group.tsx:32-32
Timestamp: 2025-08-07T14:39:00.045Z
Learning: The `limel-radio-button-group` component uses `shadow: false` because it's a pure wrapper around `limel-list` with no component-specific styles, following the pattern of other wrapper components like `limel-action-bar-overflow-menu`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: adrianschmidt
PR: Lundalogik/lime-elements#3529
File: src/components/lime-ai-avatar/lime-ai-avatar.tsx:30-38
Timestamp: 2025-04-25T13:32:56.396Z
Learning: For StencilJS components in lime-elements, prefer using the `<Host>` element to wrap child elements in the render method instead of returning an array of elements, which eliminates the need for key attributes and follows StencilJS best practices.

Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3518
File: src/components/notched-outline/examples/notched-outline-basic.scss:11-22
Timestamp: 2025-04-14T10:01:18.721Z
Learning: Examples in Lime Elements use Shadow DOM (with `shadow: true` in the component decorator) for style encapsulation, making additional class-based selectors unnecessary. This ensures styles are automatically scoped to each component without leaking to the rest of the application.

Learnt from: CR
PR: Lundalogik/lime-elements#0
File: .cursor/rules/css-class-names-in-components-using-shadow-dom.mdc:0-0
Timestamp: 2025-07-21T00:27:58.489Z
Learning: Applies to src/components/**/*.scss : Do not use BEM-style class names in CSS for components, as components use shadow-DOM.

Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3529
File: src/components/lime-ai-avatar/lime-ai-avatar.scss:96-122
Timestamp: 2025-04-25T14:18:07.755Z
Learning: In Lime Elements components that use Shadow DOM (with `shadow: true` in the component decorator), keyframe animation names don't need to be namespaced as they are automatically scoped to the component and won't collide with animations from other components.

Learnt from: adrianschmidt
PR: Lundalogik/lime-elements#3529
File: src/components/ai-avatar/ai-avatar.scss:20-22
Timestamp: 2025-04-29T14:15:32.104Z
Learning: In components using Shadow DOM (like those in lime-elements), the universal selector `*` is already scoped to the component's shadow tree and won't cause unwanted side effects outside the component. There's no functional difference between `*` and `:host *` in this context.

Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3579
File: src/components/collapsible-section/collapsible-section.tsx:199-203
Timestamp: 2025-06-10T12:19:51.990Z
Learning: In Lime Elements codebase, components consistently use randomly generated IDs (via `createRandomString()`) for HTML elements even within shadow DOM, despite shadow DOM providing ID isolation. This pattern is maintained for consistency, accessibility robustness, future-proofing, and better development experience.

@Kiarokh Kiarokh force-pushed the radio-button-group branch from 7214a5c to ce8d7d3 Compare August 7, 2025 14:38
*
* @exampleComponent limel-example-radio-button-group-basic
* @exampleComponent limel-example-radio-button-group-icons
* @exampleComponent limel-example-radio-button-group-multiple-lines
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd love to add some examples with striped rows (and perhaps those interactive styles) like what we have in limel-list 👇
image
But since those are not applied using props, I can't. Or actually I can, but I'd rather not do it here, unless we add new props for these in limel-list. We can add these later in follow up PRs.

@Kiarokh Kiarokh force-pushed the radio-button-group branch from ce8d7d3 to c9b17b4 Compare August 8, 2025 08:08

public render() {
return (
<limel-list
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai

Not sure if you can see the linked issue to this PR. But there, we explain why we are using <limel-list to render a radio-button group. It simplifies things. However, there is a few accessibility concerns.

  1. Firstly, the keyboard interaction is not the same as a group of native radio buttons. Since we are using limel-list, the keyboard navigation is handled by that component. The difference is that the user may hear that this is a radio button group, thanks to the role="radiogroup" that is dynamically set via <limel-list on the internal ul element in the DOM. However, the keyboard navigation of radio group is that the user can use the arrow keys to move the selection within the list. But in limel-list, the user does not do that. The use arrow keys to move the focus through the list of options, and then they have to press the Enter or Space bar, to select the radio button (make it :checked`) on the currently focused list item.

  2. In native radio button groups, users cannot deselect an already selected radio button. So once you select one, you cannot regret your choice. Personally, I as a user find that extremely disturbing, and consider it as a really bad design. In forms, this can become an issue, in the sense that a user cannot answer a single choice question, and then decide to skip that question and leave it unanswered. As a developer, you always have to remember to provide a N/A choice or similar, among the choices, to enable the user/respondents to skip a question that they don't want to answer (or do other magic tricks to reset the list).

If you ask me, the current PR is kind of solving two annoying UX problems with radio button groups. However, we are diverging from the "standard" and native behaviors, which might be accessibility problems. How big of an issue is this according to you?

Also, if you check the code for limel-list, we are handling the aria-roles and other aspects of accessibility pretty good. There might be issues, but I think we are generally fine I guess. Additionally, limel-list is based in MDC, and is implementing their logics. So I suspect they should have already thought about these issues?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

WCAG accessibility guidelines for radio button groups keyboard navigation arrow keys

💡 Result:

Short answer

  • WCAG requires that all functionality be operable from a keyboard (SC 2.1.1), but it does not mandate which keys must be used. Use standard, predictable keyboard interaction for radio groups — in practice follow the WAI‑ARIA Authoring Practices (APG) pattern that uses Tab to enter/leave the group and arrow keys to move and select within it. (w3c.github.io)

What to implement (recommended behavior)

  • Tab / Shift+Tab: move focus into and out of the radio group. When focus enters the group, focus goes to the checked radio (if any) or the first radio. (w3c.github.io)
  • Right Arrow / Down Arrow: move focus to the next radio in the group and check it (wrap to first if on last). (w3c.github.io)
  • Left Arrow / Up Arrow: move focus to the previous radio in the group and check it (wrap to last if on first). (w3c.github.io)
  • Space (and sometimes Enter for custom controls): activate/check the focused radio if it is not already checked. (w3c.github.io, developer.mozilla.org)
  • Special case — radios inside a toolbar: arrow keys move focus among toolbar items but should NOT change which radio is checked; Space/Enter selects. (w3c.github.io)

Why this approach

  • WCAG SC 2.1.1 requires keyboard operability but does not prescribe exact keys. Following the WAI‑ARIA APG makes keyboard behavior predictable and compatible with assistive tech and user expectations. (w3c.github.io)

Prefer native HTML when possible

  • Native inside a / provides built‑in semantics and keyboard behavior (browsers already implement arrow-key navigation for native radio groups). Use native controls rather than re-creating them with generic elements + ARIA unless you must. (developer.mozilla.org, w3c.github.io)

If you must build a custom radio group (role="radiogroup"/role="radio")

  • Follow the APG patterns: roving tabindex (only the active item tabbable), update aria-checked, manage focus, implement the arrow/space/enter behaviors described above, and test with keyboard + screen readers. (w3c.github.io, developer.mozilla.org)

Testing checklist

  • Keyboard-only: all functionality reachable and operable without a mouse (SC 2.1.1). (w3c.github.io)
  • Focus order: Tab goes in/out of the group logically and focus lands on the checked or first radio. (w3c.github.io)
  • Screen reader: selected state is announced (aria-checked or native checked). (developer.mozilla.org)
  • Toolbar cases: verify arrow keys don’t change selection when radios are inside a toolbar. (w3c.github.io)

Minimal native example

  • Use semantic HTML so browsers/AT handle this for you: Choose color Red Green Blue

(Browsers will make Tab/Arrows/Space behave as expected for this native markup.) (developer.mozilla.org)

If you want, I can:

  • show a small JavaScript pattern for a custom radiogroup (roving tabindex + keyboard handlers), or
  • run a quick checklist against a specific implementation (paste your HTML/JS) and point out any keyboard/ARIA issues.

Sources

  • WAI-ARIA Authoring Practices: Radio Group pattern (keyboard interactions). (w3c.github.io)
  • MDN: ARIA radio / recommendation to prefer native inputs. (developer.mozilla.org)
  • WCAG Understanding: Success Criterion 2.1.1 Keyboard. (w3c.github.io)

Citations:


🌐 Web query:

radio button deselection accessibility WCAG requirements once selected cannot unselect

💡 Result:

Short answer

  • WCAG does not require that a selected radio button be deselectable in-place. It does require that the radio group be accessible: properly labeled/grouped, keyboard-operable, and that the control state (role/value) is exposed to assistive tech. Key SCs to follow: 1.3.1 (Info & relationships), 2.1.1 (Keyboard), 4.1.2 (Name, role, value) and the form-related SCs (e.g., 3.3.x for labels/errors).
  • If you need users to be able to "clear" a radio selection, implement an accessible pattern (a “None”/“No selection” option, a clear button, or use a checkbox toggle) rather than a confusing custom behavior that hides state from AT.

What WCAG requires (practical checklist)

  • Group radios in a semantic fieldset/legend (satisfies 1.3.1).
  • Use native where possible (native semantics help 4.1.2). If you build a custom radio widget, expose role="radiogroup"/role="radio", keyboard handling, and update aria-checked/name appropriately.
  • Ensure full keyboard support: Tab to group, arrows to move/select within group, Space to select (2.1.1 and ARIA authoring guidance).
  • Provide clear visible labels and instructions (3.3.2). If the field is required, indicate that and provide accessible error messages if validation fails (3.3.1, 3.3.3).
  • Ensure the current selection/state is programmatically exposed (4.1.2).

Accessible ways to let users “unselect” a radio

  • Add an explicit “None” or “No preference” radio option as part of the group. This is the simplest and most compatible approach.
  • Provide a separate “Clear selection” button (a real ) that clears the radios; make sure it is keyboard focusable and updates programmatic state.
  • Use a checkbox if the UI is really a toggle (on/off) rather than a single-choice among many.
  • If you use a custom widget that allows clicking the selected radio to deselect, ensure that aria-checked changes to false and that keyboard users can do the same — but this pattern is uncommon and can confuse users and AT expectations.

Short code examples

  1. Add a “None” option (recommended)
  • HTML (semantic, accessible): Contact preference Email Phone No preference
  1. Provide a clear button (keeps options the same but allows clearing)
  • HTML:

    Contact preference Email Phone Clear selection
  • Minimal JS:
    document.getElementById('clearPref').addEventListener('click', function () {
    document.querySelectorAll('input[name="contact"]').forEach(r => r.checked = false);
    });

Make sure:

  • The button is keyboard-accessible and has an accessible name (visible text is fine).
  • Any custom code updates ARIA states for custom widgets and does not break expected keyboard arrows/space behavior.

If you build custom radio controls

  • Follow WAI-ARIA Authoring Practices for radio groups (keyboard behavior, focus model).
  • Keep role and aria-checked in sync with the visual state.
  • Test with keyboard only, a screen reader (NVDA/JAWS/VoiceOver), and with form validation on/off.

When you might want to disallow clearing

  • If the field is required for the form and must always have a value, disallow leaving it blank and make that clear in the UI (mark required and show errors per 3.3.x). But you can still offer an explicit “None” if that’s a valid semantic choice.

Would you like

  • I can give a full working example (HTML + JS) that (a) adds a “None” option, or (b) provides a clear-button approach while maintaining ARIA and keyboard behavior, or
  • Review your current markup/JS and point out any accessibility gaps. Which would you prefer?

🌐 Web query:

Material Design Components MDC radio button accessibility keyboard navigation implementation

💡 Result:

Below is a practical, standards-based guide you can use to implement accessible keyboard navigation for Material Design radio buttons. It covers the recommended approach (use native inputs styled with MDC), what to do if you must build custom radios, required ARIA/keyboard behavior, example code, and testing tips.

Summary / recommendation

  • Prefer using native that MDC styles (MDC Web’s radio component). Native radios already implement correct keyboard behavior and are best for accessibility. MDC’s styling + native input preserves screen reader and keyboard support.
  • Only implement role="radio" (custom widget) if you absolutely cannot use native inputs. If you do, you must implement roving tabindex, arrow/home/end handling, and proper ARIA attributes.
  1. Expected keyboard behaviour (per WAI-ARIA & platform conventions)
  • Tab: moves focus into/out of the radio group. Focus lands on the selected radio in the group (or the first radio if none selected).
  • Arrow keys:
    • Right Arrow / Down Arrow -> move focus to and select the next radio in the group (wrap to first).
    • Left Arrow / Up Arrow -> move focus to and select the previous radio (wrap to last).
  • Home -> move focus to and select the first radio.
  • End -> move focus to and select the last radio.
  • Space / Enter -> activate / select the focused radio.
  • Only one radio in a named group may be selected (mutually exclusive).
  • Screen readers must see the radio state (checked/unchecked) and the group label.
  1. Best approach — use MDC-styled native radios
  • Create inputs with the same name attribute (this groups them for native behavior).
  • Use MDC’s radio CSS/classes to style but keep the in the DOM.
  • Native behavior gives you automatic keyboard handling, correct semantics, and good SR announcements.

Example (HTML + initialization comment):

  • Minimal HTML (adapt to your MDC classes; this example assumes you keep the native input):
Choose flavor
Vanilla
Chocolate

Notes:

  • Keep the input in the DOM (the MDC radio component uses this input for semantics).
  • Use name="flavor" to group; screen readers and browsers use that to provide expected behavior.
  • role="radiogroup" with aria-labelledby gives the group label; you can rely on native semantics too (wrapping label and fieldset/legend also fine).

If you’re using MDC Web JS components, initialize them as docs require (MDC library setup). The important bit: do not remove the implicit semantics of the input.

  1. If you must implement custom radio widgets (role="radio")
    Use this pattern:
  • Use a container with role="radiogroup" and a clearly associated label (aria-labelledby or aria-label).
  • Each item must have role="radio", tabindex (roving: only one has tabindex="0", others -1), and aria-checked="true"|"false".
  • Update both aria-checked and tabindex on selection/focus move.
  • Implement keydown handlers for Arrow keys, Home, End, Space/Enter.

Example JS (roving tabindex + keyboard handling)

  • This is vanilla JS for a custom radiogroup. Integrate into your component class:

// Elements: container is the radiogroup; radios is a NodeList of elements with role="radio"
const container = document.querySelector('[role="radiogroup"]');
const radios = Array.from(container.querySelectorAll('[role="radio"]'));

// initialize: set tabindex and aria-checked states
function init() {
let checkedIndex = radios.findIndex(r => r.getAttribute('aria-checked') === 'true');
if (checkedIndex === -1) checkedIndex = 0;
radios.forEach((r, i) => {
r.setAttribute('tabindex', i === checkedIndex ? '0' : '-1');
r.setAttribute('aria-checked', i === checkedIndex ? 'true' : 'false');
r.addEventListener('keydown', onKeyDown);
r.addEventListener('click', onClick);
r.addEventListener('focus', onFocus);
});
}
function setSelected(index, opts = { focus: true }) {
index = (index + radios.length) % radios.length; // wrap
radios.forEach((r, i) => {
r.setAttribute('aria-checked', i === index ? 'true' : 'false');
r.setAttribute('tabindex', i === index ? '0' : '-1');
});
if (opts.focus) radios[index].focus();
// fire change event if needed
container.dispatchEvent(new CustomEvent('change', { detail: { value: radios[index].dataset.value } }));
}
function onClick(e) {
const idx = radios.indexOf(e.currentTarget);
setSelected(idx, { focus: true });
}
function onFocus(e) {
// ensure the focused radio has tabindex=0
const idx = radios.indexOf(e.currentTarget);
radios.forEach((r, i) => r.setAttribute('tabindex', i === idx ? '0' : '-1'));
}
function onKeyDown(e) {
const idx = radios.indexOf(e.currentTarget);
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
setSelected(idx + 1);
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
setSelected(idx - 1);
break;
case 'Home':
e.preventDefault();
setSelected(0);
break;
case 'End':
e.preventDefault();
setSelected(radios.length - 1);
break;
case ' ':
case 'Enter':
e.preventDefault();
setSelected(idx, { focus: true });
break;
}
}
init();

Important points for custom radios

  • Maintain single tabbable element in group (roving tabindex). This keeps Tab behavior: one Tab moves into group; then arrows navigate inside.
  • Update aria-checked and tabindex atomically.
  • Use data attributes or internal state to reflect value (so forms can read it or dispatch change events).
  • Ensure focus styles are visible (focus-visible) for keyboard users.
  • Ensure click and pointer interactions also set selection.
  1. Form submission considerations
  • Native inputs are simplest: form submission works automatically.
  • For custom widgets, you must sync selected value into a hidden so form submission includes it.
  1. Screen reader and platform quirks
  • Native radios are best: they announce group and state correctly in VoiceOver, NVDA, and JAWS.
  • If custom, test with NVDA (Windows+Firefox), JAWS (IE/Edge), VoiceOver (macOS Safari), and ChromeVox.
  • Ensure the radiogroup has an accessible name (aria-labelledby or aria-label or a visible legend).
  1. Visual focus and contrast
  • Provide visible keyboard focus (outline or focus ring) that meets contrast requirements.
  • Use :focus-visible to avoid showing focus ring for mouse-only users but ensure keyboard focus shows.
  1. Testing checklist
  • Keyboard-only: Tab into radiogroup, use arrows, Home/End, Space/Enter; Tab out correctly.
  • Screen readers: when you focus a radio, the SR announces label and checked state; when you change selection, it announces the new selection.
  • Form submission: selected value is submitted or accessible via JS.
  • Mobile: touch toggles selection.
  • Accessibility tools: axe-core, Lighthouse, or WAVE checks.
  1. Common pitfalls to avoid
  • Replacing native input with a non-semantic element without adding proper ARIA. That breaks native keyboard, focus, and SR behavior.
  • Forgetting to set name attribute for native radios (grouping won’t work).
  • Not updating tabindex for roving behavior — Tab ends up moving through every radio rather than into the group as a single tab stop.
  • Not syncing selected value to form state when using custom widgets.

Quick reference mapping (native vs custom)

  • Native input[type=radio]
    • Pros: native keyboard, SR support, form integration
    • What to do: style via MDC, keep input, set name and id, use label/fieldset for group label
  • Custom role="radio"
    • Pros: full custom visuals
    • Cons: you must implement all keyboard and ARIA logic
    • What to do: implement roving tabindex, key handlers for arrows/home/end/space/enter, update aria-checked, provide accessible name and form syncing

If you want, I can:

  • Provide a complete runnable CodePen/JSFiddle example that uses MDC Web classes and icons.
  • Show how to integrate this with form submission (hidden input sync).
  • Give you TypeScript / React component examples (MDC + React wrappers or headless approach).

Which would you like next? A runnable MDC Web example, or a custom role="radio" React/TypeScript implementation?


🏁 Script executed:

#!/bin/bash
# Let me examine the limel-list component to understand how it handles keyboard navigation and accessibility
fd -t f "list\.tsx$" src/components/list/

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Examine the main list component to understand its keyboard navigation and accessibility implementation
cat src/components/list/list.tsx

Length of output: 8755


🏁 Script executed:

#!/bin/bash
# Let me look at the MDC list implementation details and how it handles keyboard navigation and ARIA attributes
rg -A 10 -B 10 "role.*radio" src/components/list/

Length of output: 1428


🏁 Script executed:

#!/bin/bash
# Let me examine how the MDC list handles keyboard navigation by looking at the Material Design Components documentation or implementation
rg -A 15 -B 5 "MDCList.*keyboard|keyboard.*navigation|keydown|arrow" src/components/list/

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Let me look for keyboard event handling in the MDC list implementation
rg -i "key" src/components/list/ -A 5 -B 5

Length of output: 4856


🏁 Script executed:

#!/bin/bash
# Let me check if MDC list has typeahead functionality and understand better how it handles interactions
rg -i "typeahead|hasTypeahead" src/components/list/ -A 3 -B 3

Length of output: 409


@Kiarokh Thank you for raising these important accessibility concerns. Based on my research into WCAG guidelines and WAI-ARIA Authoring Practices, you're absolutely right to be concerned about the accessibility implications. Let me address both issues:

1. Keyboard Navigation Deviation

This is a significant accessibility issue. According to the WAI-ARIA Authoring Practices Guide (APG), radio button groups should follow this pattern:

  • Arrow keys should move focus AND select the radio button in one action
  • Tab/Shift+Tab moves focus into/out of the radio group
  • Space/Enter can activate the focused radio if not already selected

Your current implementation using limel-list requires users to:

  1. Use arrow keys to move focus through options
  2. Press Enter/Space to select the focused option

This violates the expected keyboard interaction pattern for radio button groups and creates an accessibility barrier for users who rely on keyboard navigation and assistive technologies.

2. Deselection Behavior

This is less problematic from an accessibility standpoint. WCAG doesn't explicitly require that radio buttons remain selected once chosen. However, the inability to deselect is part of the expected behavior pattern for radio buttons. Your implementation actually provides better UX by allowing deselection, but it diverges from user expectations.

The Trade-off Analysis

Severity Assessment:

  • High impact: The keyboard navigation issue significantly affects accessibility
  • Medium impact: The deselection behavior improves UX but may confuse some users

Recommendations:

  1. For better accessibility compliance: Consider implementing proper radio button keyboard navigation in limel-list when type="radio" is set, or create a separate component using native <input type="radio"> elements styled with your design system.

  2. For the deselection feature: If you want to keep this UX improvement, consider:

    • Adding a "None" or "No selection" option explicitly
    • Using a clear button alongside the radio group
    • Documenting this deviation from standard behavior
  3. MDC consideration: While MDC components generally have good accessibility, their list component is designed for general list interactions, not specifically for radio button group semantics.

The keyboard navigation issue should definitely be addressed as it creates a real accessibility barrier. The deselection feature, while non-standard, actually improves usability - but you might want to make it more explicit through UI design or documentation.


✏️ Learnings added
Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3638
File: src/components/radio-button-group/radio-button-group.tsx:91-91
Timestamp: 2025-08-12T14:01:27.827Z
Learning: The `limel-radio-button-group` component has accessibility concerns because it uses `limel-list` for rendering, which doesn't follow WAI-ARIA APG keyboard navigation patterns for radio button groups. Arrow keys should move focus AND select in radio groups, but limel-list requires arrow keys to move focus, then Enter/Space to select.

Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3638
File: src/components/radio-button-group/radio-button-group.tsx:91-91
Timestamp: 2025-08-12T14:01:27.827Z
Learning: The `limel-radio-button-group` component allows deselection of radio buttons (unlike native radio buttons), which improves UX by letting users leave questions unanswered, but diverges from standard radio button behavior expectations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Kiarokh
PR: Lundalogik/lime-elements#3638
File: src/components/radio-button-group/radio-button-group.tsx:32-32
Timestamp: 2025-08-07T14:39:00.045Z
Learning: The `limel-radio-button-group` component uses `shadow: false` because it's a pure wrapper around `limel-list` with no component-specific styles, following the pattern of other wrapper components like `limel-action-bar-overflow-menu`.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update:

The "deselection behavior" depends on how we implement the handleChange.
So I'm adding an example to cover that case.


private handleChange = (event: LimelListCustomEvent<ListItem>) => {
event.stopPropagation();

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we need this as well

 if (!event.detail) {
            return;
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what do you think about 9729b8e

@Kiarokh Kiarokh force-pushed the radio-button-group branch 4 times, most recently from dae81c0 to 168a8e2 Compare August 15, 2025 14:31
Copy link
Contributor

@Befkadu1 Befkadu1 left a comment

Choose a reason for hiding this comment

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

Awesome work ⚡

@Befkadu1 Befkadu1 enabled auto-merge (rebase) August 15, 2025 14:38
…io-button-group` folder

Move radio button template and styles from `list/radio-button/`
to `radio-button-group/` to better organize radio button related code
in a single location.
Also added a comprehensive documentation to radio button template
and marked it as `@internal` since it's not for direct consumer use.
@Kiarokh Kiarokh force-pushed the radio-button-group branch from 168a8e2 to f0be244 Compare August 18, 2025 08:53
Copy link
Contributor

@adrianschmidt adrianschmidt left a comment

Choose a reason for hiding this comment

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

I've only had a quick look at the component in the docs, and checked the external API, but what I've seen looks great!

@Befkadu1 Befkadu1 merged commit ad8bd95 into main Aug 18, 2025
12 checks passed
@Befkadu1 Befkadu1 deleted the radio-button-group branch August 18, 2025 14:27
@lime-opensource
Copy link
Collaborator

🎉 This PR is included in version 38.22.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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.

Radio button group: add new component

5 participants