diff --git a/docs/components/radio-button.md b/docs/components/radio-button.md index df6de475..87297f21 100644 --- a/docs/components/radio-button.md +++ b/docs/components/radio-button.md @@ -7,8 +7,8 @@ Radios buttons allow the user to select a single option from a group using a but Radio buttons are designed to be used with [radio groups](/components/radio-group). When a radio button has focus, the arrow keys can be used to change the selected option just like standard radio controls. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -18,8 +18,8 @@ Radio buttons are designed to be used with [radio groups](/components/radio-grou import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -34,43 +34,13 @@ const App = () => ( ## Examples -### Checked - -To set the initial checked state, use the `checked` attribute. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -```jsx react -import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - - - Option 1 - - - Option 2 - - - Option 3 - - -); -``` - ### Disabled Use the `disabled` attribute to disable a radio button. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -80,8 +50,8 @@ Use the `disabled` attribute to disable a radio button. import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -99,24 +69,24 @@ const App = () => ( Use the `size` attribute to change a radio button's size. ```html preview - - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3 @@ -126,24 +96,24 @@ Use the `size` attribute to change a radio button's size. import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3 @@ -155,24 +125,24 @@ const App = () => ( Use the `pill` attribute to give radio buttons rounded edges. ```html preview - - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3 @@ -182,24 +152,24 @@ Use the `pill` attribute to give radio buttons rounded edges. import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3
- - Option 1 + + Option 1 Option 2 Option 3 @@ -211,8 +181,8 @@ const App = () => ( Use the `prefix` and `suffix` slots to add icons. ```html preview - - + + Option 1 @@ -234,8 +204,8 @@ Use the `prefix` and `suffix` slots to add icons. import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -259,7 +229,7 @@ const App = () => ( You can omit button labels and use icons instead. Make sure to set a `label` attribute on each icon so screen readers will announce each option correctly. ```html preview - + @@ -268,7 +238,7 @@ You can omit button labels and use icons instead. Make sure to set a `label` att - + @@ -286,7 +256,7 @@ You can omit button labels and use icons instead. Make sure to set a `label` att import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - + @@ -295,7 +265,7 @@ const App = () => (
- + @@ -310,77 +280,4 @@ const App = () => ( ); ``` -### Custom Validity - -Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. - -```html preview -
- - Not me - Me neither - Choose me - -
- Submit -
- -``` - -```jsx react -import { useEffect, useRef } from 'react'; -import { SlButton, SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; -const App = () => { - const radio = useRef(null); - const errorMessage = 'You must choose this option'; - function handleChange(event) { - radio.current.setCustomValidity(radio.current.checked ? '' : errorMessage); - } - function handleSubmit(event) { - event.preventDefault(); - alert('All fields are valid!'); - } - useEffect(() => { - radio.current.setCustomValidity(errorMessage); - }, []); - return ( -
- - - Not me - - - Me neither - - - Choose me - - -
- - Submit - -
- ); -}; -``` - [component-metadata:sl-radio-button] diff --git a/docs/components/radio-group.md b/docs/components/radio-group.md index d5a28963..b58d2307 100644 --- a/docs/components/radio-group.md +++ b/docs/components/radio-group.md @@ -5,10 +5,10 @@ Radio groups are used to group multiple [radios](/components/radio) or [radio buttons](/components/radio-button) so they function as a single form control. ```html preview - - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3 ``` @@ -16,16 +16,10 @@ Radio groups are used to group multiple [radios](/components/radio) or [radio bu import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - - Option 1 - - - Option 2 - - - Option 3 - + + Option 1 + Option 2 + Option 3 ); ``` @@ -37,8 +31,8 @@ const App = () => ( You can show the fieldset and legend that wraps the radio group using the `fieldset` attribute. If you don't use this option, you should still provide a label so screen readers announce the control correctly. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -48,8 +42,8 @@ You can show the fieldset and legend that wraps the radio group using the `field import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -67,8 +61,8 @@ const App = () => ( [Radio buttons](/components/radio-button) offer an alternate way to display radio controls. In this case, an internal [button group](/components/button-group) is used to group the buttons into a single, cohesive control. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -78,8 +72,8 @@ const App = () => ( import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -92,4 +86,141 @@ const App = () => ( ); ``` +### Validation + +Setting the `required` attribute to make selecting an option mandatory. If a value has not been selected, it will prevent the form from submitting and display an error message. + +```html preview +
+ + Not me + Me neither + Choose me + +
+ Submit +
+ + +``` + +```jsx react +import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; +const App = () => { + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + return ( +
+ + + Not me + + + Me neither + + + Choose me + + +
+ + Submit + +
+ ); +}; +``` + +#### Custom Validity + +Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. + +```html preview +
+ + Not me + Me neither + Choose me + +
+ Submit +
+ + +``` + +```jsx react +import { useEffect, useRef } from 'react'; +import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; +const App = () => { + const radioGroup = useRef(null); + const errorMessage = 'You must choose this option'; + + function handleChange() { + radioGroup.current.setCustomValidity(radioGroup.current.value === '3' ? '' : errorMessage); + } + + function handleSubmit(event) { + event.preventDefault(); + alert('All fields are valid!'); + } + + useEffect(() => { + radio.current.setCustomValidity(errorMessage); + }, []); + + return ( +
+ + + Not me + + + Me neither + + + Choose me + + +
+ + Submit + +
+ ); +}; +``` + [component-metadata:sl-radio-group] diff --git a/docs/components/radio.md b/docs/components/radio.md index 25b481ed..33773de9 100644 --- a/docs/components/radio.md +++ b/docs/components/radio.md @@ -7,8 +7,8 @@ Radios allow the user to select a single option from a group. Radios are designed to be used with [radio groups](/components/radio-group). ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -18,8 +18,8 @@ Radios are designed to be used with [radio groups](/components/radio-group). import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -41,8 +41,8 @@ const App = () => ( To set the initial checked state, use the `checked` attribute. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -52,8 +52,8 @@ To set the initial checked state, use the `checked` attribute. import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -71,8 +71,8 @@ const App = () => ( Use the `disabled` attribute to disable a radio. ```html preview - - Option 1 + + Option 1 Option 2 Option 3 @@ -82,8 +82,8 @@ Use the `disabled` attribute to disable a radio. import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - + + Option 1 @@ -96,77 +96,4 @@ const App = () => ( ); ``` -### Custom Validity - -Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. - -```html preview -
- - Not me - Me neither - Choose me - -
- Submit -
- -``` - -```jsx react -import { useEffect, useRef } from 'react'; -import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; -const App = () => { - const radio = useRef(null); - const errorMessage = 'You must choose this option'; - function handleChange(event) { - radio.current.setCustomValidity(radio.current.checked ? '' : errorMessage); - } - function handleSubmit(event) { - event.preventDefault(); - alert('All fields are valid!'); - } - useEffect(() => { - radio.current.setCustomValidity(errorMessage); - }, []); - return ( -
- - - Not me - - - Me neither - - - Choose me - - -
- - Submit - -
- ); -}; -``` - [component-metadata:sl-radio] diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts index 6ce752eb..d3edd112 100644 --- a/src/components/button-group/button-group.ts +++ b/src/components/button-group/button-group.ts @@ -1,5 +1,5 @@ import { LitElement, html } from 'lit'; -import { customElement, property, query } from 'lit/decorators.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import styles from './button-group.styles'; import type { CSSResultGroup } from 'lit'; @@ -19,6 +19,8 @@ export default class SlButtonGroup extends LitElement { @query('slot') defaultSlot: HTMLSlotElement; + @state() disableRole = false; + /** A label to use for the button group's `aria-label` attribute. */ @property() label = ''; @@ -65,14 +67,14 @@ export default class SlButtonGroup extends LitElement {
- +
`; } diff --git a/src/components/radio-button/radio-button.ts b/src/components/radio-button/radio-button.ts index 7c918b29..85aecbd6 100644 --- a/src/components/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button.ts @@ -3,9 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { html } from 'lit/static-html.js'; -import { defaultValue } from '../../internal/default-value'; import { emit } from '../../internal/event'; -import { FormSubmitController } from '../../internal/form'; import { HasSlotController } from '../../internal/slot'; import { watch } from '../../internal/watch'; import styles from './radio-button.styles'; @@ -18,7 +16,6 @@ import type { CSSResultGroup } from 'lit'; * @slot - The radio's label. * * @event sl-blur - Emitted when the button loses focus. - * @event sl-change - Emitted when the button's checked state changes. * @event sl-focus - Emitted when the button gains focus. * * @slot - The button's label. @@ -38,17 +35,13 @@ export default class SlRadioButton extends LitElement { @query('.button') input: HTMLInputElement; @query('.hidden-input') hiddenInput: HTMLInputElement; - protected readonly formSubmitController = new FormSubmitController(this, { - value: (control: SlRadioButton) => (control.checked ? control.value : undefined), - defaultValue: (control: SlRadioButton) => control.defaultChecked, - setValue: (control: SlRadioButton, checked: boolean) => (control.checked = checked) - }); private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); @state() protected hasFocus = false; + @state() protected checked = false; /** The radio's name attribute. */ - @property() name: string; + @property({ reflect: true }) name: string; /** The radio's value attribute. */ @property() value: string; @@ -56,47 +49,20 @@ export default class SlRadioButton extends LitElement { /** Disables the radio. */ @property({ type: Boolean, reflect: true }) disabled = false; - /** Draws the radio in a checked state. */ - @property({ type: Boolean, reflect: true }) checked = false; + /** The button's size. */ + @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; - /** - * This will be true when the control is in an invalid state. Validity in radios is determined by the message provided - * by the `setCustomValidity` method. - */ - @property({ type: Boolean, reflect: true }) invalid = false; - - /** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */ - @defaultValue('checked') - defaultChecked = false; + /** Draws a pill-style button with rounded edges. */ + @property({ type: Boolean, reflect: true }) pill = false; connectedCallback(): void { super.connectedCallback(); - this.setAttribute('role', 'radio'); + this.setAttribute('role', 'presentation'); } - /** Simulates a click on the radio. */ - click() { - this.input.click(); - } - - /** Sets focus on the radio. */ - focus(options?: FocusOptions) { - this.input.focus(options); - } - - /** Removes focus from the radio. */ - blur() { - this.input.blur(); - } - - /** Checks for validity and shows the browser's validation message if the control is invalid. */ - reportValidity() { - return this.hiddenInput.reportValidity(); - } - - /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ - setCustomValidity(message: string) { - this.hiddenInput.setCustomValidity(message); + @watch('disabled', { waitUntilFirstUpdate: true }) + handleDisabledChange() { + this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } handleBlur() { @@ -104,10 +70,14 @@ export default class SlRadioButton extends LitElement { emit(this, 'sl-blur'); } - handleClick() { - if (!this.disabled) { - this.checked = true; + handleClick(e: MouseEvent) { + if (this.disabled) { + e.preventDefault(); + e.stopPropagation(); + return; } + + this.checked = true; } handleFocus() { @@ -115,38 +85,13 @@ export default class SlRadioButton extends LitElement { emit(this, 'sl-focus'); } - @watch('checked') - handleCheckedChange() { - this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); - - if (this.hasUpdated) { - emit(this, 'sl-change'); - } - } - - @watch('disabled', { waitUntilFirstUpdate: true }) - handleDisabledChange() { - this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); - - // Disabled form controls are always valid, so we need to recheck validity when the state changes - if (this.hasUpdated) { - this.input.disabled = this.disabled; - this.invalid = !this.input.checkValidity(); - } - } - - /** The button's size. */ - @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; - - /** Draws a pill-style button with rounded edges. */ - @property({ type: Boolean, reflect: true }) pill = false; - render() { return html` -
- +