diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 19f7f51a..2216ebbe 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -35,6 +35,7 @@ - [Progress Bar](/components/progress-bar.md) - [Progress Ring](/components/progress-ring.md) - [Radio](/components/radio.md) + - [Radio Group](/components/radio-group.md) - [Range](/components/range.md) - [Rating](/components/rating.md) - [Responsive Embed](/components/responsive-embed.md) diff --git a/docs/components/radio-group.md b/docs/components/radio-group.md new file mode 100644 index 00000000..71a528b7 --- /dev/null +++ b/docs/components/radio-group.md @@ -0,0 +1,29 @@ +# Radio Group + +[component-header:sl-radio-group] + +Radio Groups are used to group multiple radios so they function as a single control. + +```html preview +<sl-radio-group label="Select an item"> + <sl-radio value="1" checked>Item 1</sl-radio> + <sl-radio value="2">Item 2</sl-radio> + <sl-radio value="3">Item 3</sl-radio> +</sl-radio-group> +``` + +## Examples + +### Hiding the Fieldset + +You can hide the fieldset and legend that wraps the radio group using the `no-fieldset` attribute. In this case, a label is still required for assistive devices to properly identify the control. + +```html preview +<sl-radio-group label="Select an item" no-fieldset> + <sl-radio value="1" checked>Item 1</sl-radio> + <sl-radio value="2">Item 2</sl-radio> + <sl-radio value="3">Item 3</sl-radio> +</sl-radio-group> +``` + +[component-metadata:sl-radio-group] diff --git a/docs/components/radio.md b/docs/components/radio.md index fd0084ae..86e6bbe1 100644 --- a/docs/components/radio.md +++ b/docs/components/radio.md @@ -4,39 +4,31 @@ Radios allow the user to select one option from a group of many. +Radios are designed to be used with [radio groups](/components/radio-group). As such, all of the examples on this page utilize them to demonstrate their correct usage. + ```html preview -<sl-radio>Radio</sl-radio> +<sl-radio-group label="Select an option" no-fieldset> + <sl-radio value="1" checked>Option 1</sl-radio> + <sl-radio value="2">Option 2</sl-radio> + <sl-radio value="3">Option 3</sl-radio> +</sl-radio-group> ``` ?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead. ## Examples -### Checked - -Use the `checked` attribute to activate the radio. - -```html preview -<sl-radio checked>Checked</sl-radio> -``` - ### Disabled -Use the `disabled` attribute to disable the radio. +Use the `disabled` attribute to disable a radio. ```html preview -<sl-radio disabled>Disabled</sl-radio> -``` - -### Grouping Radios - -Radios are grouped based on their `name` attribute and scoped to the nearest form. - -```html preview -<sl-radio name="option" checked>Option 1</sl-radio><br> -<sl-radio name="option">Option 2</sl-radio><br> -<sl-radio name="option">Option 3</sl-radio><br> -<sl-radio name="option">Option 4</sl-radio> +<sl-radio-group label="Select an option" no-fieldset> + <sl-radio value="1" checked>Option 1</sl-radio> + <sl-radio value="2">Option 2</sl-radio> + <sl-radio value="3">Option 3</sl-radio> + <sl-radio value="4" disabled>Disabled</sl-radio> +</sl-radio-group> ``` [component-metadata:sl-radio] diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 018a8cc8..b6147a49 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -6,6 +6,11 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge _During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛 +## Next + +- 🚨 BREAKING: `sl-radio` components must be located inside an `sl-radio-group` for proper accessibility [#218](https://github.com/shoelace-style/shoelace/issues/218) +- Added `sl-radio-group` component [#218](https://github.com/shoelace-style/shoelace/issues/218) + ## 2.0.0-beta.37 - Added `click()` method to `sl-checkbox`, `sl-radio`, and `sl-switch` diff --git a/src/components/radio-group/radio-group.scss b/src/components/radio-group/radio-group.scss new file mode 100644 index 00000000..0e1400fb --- /dev/null +++ b/src/components/radio-group/radio-group.scss @@ -0,0 +1,37 @@ +@use '../../styles/component'; +@use '../../styles/mixins/hide'; + +:host { + display: block; +} + +.radio-group { + border: solid var(--sl-input-border-width) var(--sl-input-border-color); + border-radius: var(--sl-border-radius-medium); + padding: var(--sl-spacing-large); + padding-top: var(--sl-spacing-x-small); + + .radio-group__label { + font-family: var(--sl-input-font-family); + font-size: var(--sl-input-font-size-medium); + font-weight: var(--sl-input-font-weight); + color: var(--sl-input-color); + padding: 0 var(--sl-spacing-xx-small); + } +} + +::slotted(sl-radio:not(:last-of-type)) { + display: block; + margin-bottom: var(--sl-spacing-xx-small); +} + +.radio-group--no-fieldset { + border: none; + padding: 0; + margin: 0; + min-width: 0; + + .radio-group__label { + @include hide.visually-hidden; + } +} diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts new file mode 100644 index 00000000..b7c39840 --- /dev/null +++ b/src/components/radio-group/radio-group.ts @@ -0,0 +1,49 @@ +import { LitElement, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators'; +import { classMap } from 'lit-html/directives/class-map'; +import styles from 'sass:./radio-group.scss'; + +/** + * @since 2.0 + * @status stable + * + * @slot - The default slot where radio controls are placed. + * @slot label - The radio group label. Required for proper accessibility. Alternatively, you can use the label prop. + * + * @part base - The component's base wrapper. + * @part label - The radio group label. + */ +@customElement('sl-radio-group') +export default class SlRadioGroup extends LitElement { + static styles = unsafeCSS(styles); + + /** The radio group label. Required for proper accessibility. Alternatively, you can use the label slot. */ + @property() label = ''; + + /** Hides the fieldset and legend that surrounds the radio group. The label will still be read by screen readers. */ + @property({ type: Boolean, attribute: 'no-fieldset' }) noFieldset = false; + + render() { + return html` + <fieldset + part="base" + class=${classMap({ + 'radio-group': true, + 'radio-group--no-fieldset': this.noFieldset + })} + role="radiogroup" + > + <legend part="label" class="radio-group__label"> + <slot name="label">${this.label}</slot> + </legend> + <slot></slot> + </fieldset> + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sl-radio-group': SlRadioGroup; + } +} diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index e16f5cdc..fffb2058 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -83,11 +83,14 @@ export default class SlRadio extends LitElement { } getAllRadios() { - const form = this.closest('sl-form, form') || document.body; + const radioGroup = this.closest('sl-radio-group'); - if (!this.name) return []; + // Radios must be part of a radio group + if (!radioGroup) { + return []; + } - return [...form.querySelectorAll('sl-radio')].filter((radio: this) => radio.name === this.name) as this[]; + return [...radioGroup.querySelectorAll('sl-radio')].filter((radio: this) => radio.name === this.name) as this[]; } getSiblingRadios() { @@ -171,8 +174,8 @@ export default class SlRadio extends LitElement { value=${ifDefined(this.value)} ?checked=${this.checked} ?disabled=${this.disabled} - role="radio" aria-checked=${this.checked ? 'true' : 'false'} + aria-disabled=${this.disabled ? 'true' : 'false'} aria-labelledby=${this.labelId} @click=${this.handleClick} @blur=${this.handleBlur} diff --git a/src/shoelace.ts b/src/shoelace.ts index a3af2188..482edb3f 100644 --- a/src/shoelace.ts +++ b/src/shoelace.ts @@ -29,6 +29,7 @@ export { default as SlMenuLabel } from './components/menu-label/menu-label'; export { default as SlProgressBar } from './components/progress-bar/progress-bar'; export { default as SlProgressRing } from './components/progress-ring/progress-ring'; export { default as SlRadio } from './components/radio/radio'; +export { default as SlRadioGroup } from './components/radio-group/radio-group'; export { default as SlRange } from './components/range/range'; export { default as SlRating } from './components/rating/rating'; export { default as SlRelativeTime } from './components/relative-time/relative-time';