diff --git a/docs/components/radio-group.md b/docs/components/radio-group.md index 17241de5..0f784721 100644 --- a/docs/components/radio-group.md +++ b/docs/components/radio-group.md @@ -42,62 +42,11 @@ You can show a fieldset and legend that wraps the radio group using the `fieldse import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - + Option 1 Option 2 Option 3 ); ``` - -### Using the required attribute - -Adding a `required` attribute to `sl-radio-group` will require at least one option to be selected. - -```html preview - - Option 1 - Option 2 - Option 3 - -
-Validate Group -Reset Group - - -``` - -```jsx react -import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react'; -const validateGroup = () => { - const group = document.querySelector('sl-radio-group.required-radio-group'); - group.reportValidity(); -} - -const resetGroup = () => { - const group = document.querySelector('sl-radio-group.required-radio-group'); - group.value = ""; -} - -const App = () => ( - <> - - Option 1 - Option 2 - Option 3 - -
- validateGroup()}>Validate Group - resetGroup()}>Reset Group - -); -``` - [component-metadata:sl-radio-group] diff --git a/src/components/radio-group/radio-group.test.ts b/src/components/radio-group/radio-group.test.ts deleted file mode 100644 index 60f58ebc..00000000 --- a/src/components/radio-group/radio-group.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { expect, fixture, html, oneEvent } from '@open-wc/testing'; -import { sendKeys } from '@web/test-runner-commands'; - -import '../../../dist/shoelace.js'; -import type SlRadio from '../radio/radio'; -import type SlRadioGroup from './radio-group'; - -describe('', () => { - it('should toggle selected radio when toggled via keyboard - arrow right key', async () => { - const radioGroup = await fixture(html` - - - - - `); - const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1'); - const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2'); - - expect(radio2.checked).to.be.false; - expect(radio1.checked).to.be.true; - - radio1.focus(); - await sendKeys({ press: 'ArrowRight' }); - - expect(radio2.checked).to.be.true; - expect(radio1.checked).to.be.false; - }); - - it('should toggle selected radio when toggled via keyboard - arrow down key', async () => { - const radioGroup = await fixture(html` - - - - - `); - const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1'); - const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2'); - - expect(radio2.checked).to.be.false; - expect(radio1.checked).to.be.true; - - radio1.focus(); - await sendKeys({ press: 'ArrowDown' }); - - expect(radio2.checked).to.be.true; - expect(radio1.checked).to.be.false; - }); - - it('should toggle selected radio when toggled via keyboard - arrow left key', async () => { - const radioGroup = await fixture(html` - - - - - `); - const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1'); - const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2'); - - expect(radio2.checked).to.be.true; - expect(radio1.checked).to.be.false; - - radio1.focus(); - await sendKeys({ press: 'ArrowLeft' }); - - expect(radio2.checked).to.be.false; - expect(radio1.checked).to.be.true; - }); - - it('should toggle selected radio when toggled via keyboard - arrow up key', async () => { - const radioGroup = await fixture(html` - - - - - `); - const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1'); - const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2'); - - expect(radio2.checked).to.be.true; - expect(radio1.checked).to.be.false; - - radio1.focus(); - await sendKeys({ press: 'ArrowUp' }); - - expect(radio2.checked).to.be.false; - expect(radio1.checked).to.be.true; - }); -}); diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index e3aca1ef..f306ac82 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -17,63 +17,15 @@ import styles from './radio-group.styles'; @customElement('sl-radio-group') export default class SlRadioGroup extends LitElement { static styles = styles; - private _value: string = ''; @query('slot:not([name])') defaultSlot: HTMLSlotElement; /** The radio group label. Required for proper accessibility. Alternatively, you can use the label slot. */ @property() label = ''; - /** The current value of the radio group. */ - @property() - get value() { - if (!this._value) return this.getCurrentValue(); - - return this._value; - } - - set value(newValue) { - const index = this.getAllRadios().findIndex(el => el.value === newValue); - const oldValue = this._value; - - if (index > -1) { - this.checkRadioByIndex(index); - this._value = newValue; - this.requestUpdate('value', oldValue); - } else { - this._value = ''; - this.deselectAll(); - } - } - /** Shows the fieldset and legend that surrounds the radio group. */ @property({ type: Boolean, attribute: 'fieldset' }) fieldset = false; - /** Indicates that a selection is required. */ - @property({ type: Boolean, reflect: true }) required = false; - - connectedCallback() { - this.addEventListener('sl-change', this.syncRadioButtons); - } - - disconnectedCallback() { - this.removeEventListener('sl-change', this.syncRadioButtons); - } - - syncRadioButtons(event: CustomEvent) { - const currentRadio = event.target; - const radios = this.getAllRadios().filter(el => !el.disabled && el !== currentRadio); - radios.forEach(el => { - el.checked = false; - }); - } - - getCurrentValue() { - const valRadio = this.getAllRadios().filter(el => el.checked); - this._value = valRadio.length === 1 ? valRadio[0].value : ''; - return this._value; - } - handleFocusIn() { // When tabbing into the fieldset, make sure it lands on the checked radio requestAnimationFrame(() => { @@ -87,63 +39,6 @@ export default class SlRadioGroup extends LitElement { }); } - getAllRadios(): SlRadio[] { - return [...this.querySelectorAll('sl-radio')]; - } - - checkRadioByIndex(index: number): SlRadio[] { - const radios = this.deselectAll(); - - radios[index].focus(); - radios[index].checked = true; - this._value = radios[index].value; - - return radios; - } - - deselectAll(): SlRadio[] { - return this.getAllRadios().map(radio => { - radio.checked = false; - return radio; - }); - } - - handleKeyDown(event: KeyboardEvent) { - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { - const radios = this.getAllRadios().filter(radio => !radio.disabled); - const currentIndex = radios.findIndex(el => el.checked); - - const incr = ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1; - let index = currentIndex + incr; - if (index < 0) index = radios.length - 1; - if (index > radios.length - 1) index = 0; - - this.checkRadioByIndex(index); - - event.preventDefault(); - } - } - - reportValidity() { - const radios = [...(this.defaultSlot.assignedElements({ flatten: true }) as SlRadio[])]; - let isChecked = true; - - if (this.required && radios.length > 0) { - isChecked = radios.some(el => el.checked); - - if (!isChecked) { - // This is hacky... - radios[0].required = true; - - setTimeout(() => { - radios[0].reportValidity(); - }, 0); - } - } - - return isChecked; - } - render() { return html`
${this.label} diff --git a/src/components/radio/radio.test.ts b/src/components/radio/radio.test.ts index 37dbd3bb..f76c1288 100644 --- a/src/components/radio/radio.test.ts +++ b/src/components/radio/radio.test.ts @@ -37,6 +37,23 @@ describe('', () => { expect(el.checked).to.be.true; }); + it('should fire sl-change when toggled via keyboard - arrow key', async () => { + const radioGroup = await fixture(html` + + + + + `); + const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1'); + const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2'); + const input1 = radio1.shadowRoot?.querySelector('input'); + input1.focus(); + setTimeout(() => sendKeys({ press: 'ArrowRight' })); + const event = await oneEvent(radio2, 'sl-change'); + expect(event.target).to.equal(radio2); + expect(radio2.checked).to.be.true; + }); + it('should not fire sl-change when checked is set by javascript', async () => { const el = await fixture(html` `); el.addEventListener('sl-change', () => expect.fail('event fired')); diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts index a36d018a..79690c20 100644 --- a/src/components/radio/radio.ts +++ b/src/components/radio/radio.ts @@ -47,9 +47,6 @@ export default class SlRadio extends LitElement { /** Draws the radio in a checked state. */ @property({ type: Boolean, reflect: true }) checked = false; - /** Indicates that a selection is required. */ - @property({ type: Boolean, reflect: true }) required = false; - /** * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message * provided by the `setCustomValidity` method. @@ -82,11 +79,33 @@ export default class SlRadio extends LitElement { this.invalid = !this.input.checkValidity(); } + getAllRadios() { + const radioGroup = this.closest('sl-radio-group'); + + // Radios must be part of a radio group + if (!radioGroup) { + return [this]; + } + + return [...radioGroup.querySelectorAll('sl-radio')].filter((radio: this) => radio.name === this.name) as this[]; + } + + getSiblingRadios() { + return this.getAllRadios().filter(radio => radio !== this) as this[]; + } + handleBlur() { this.hasFocus = false; emit(this, 'sl-blur'); } + @watch('checked', { waitUntilFirstUpdate: true }) + handleCheckedChange() { + if (this.checked) { + this.getSiblingRadios().map(radio => (radio.checked = false)); + } + } + handleClick() { this.checked = true; emit(this, 'sl-change'); @@ -106,6 +125,23 @@ export default class SlRadio extends LitElement { emit(this, 'sl-focus'); } + handleKeyDown(event: KeyboardEvent) { + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { + const radios = this.getAllRadios().filter(radio => !radio.disabled); + const incr = ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1; + let index = radios.indexOf(this) + incr; + if (index < 0) index = radios.length - 1; + if (index > radios.length - 1) index = 0; + + this.getAllRadios().map(radio => (radio.checked = false)); + radios[index].focus(); + radios[index].checked = true; + emit(radios[index], 'sl-change'); + + event.preventDefault(); + } + } + render() { return html`