From fe8b0624feb43c149a88c013808c94596ddb487a Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 4 Dec 2024 12:43:43 -0500 Subject: [PATCH] backport SL-2192 --- docs/docs/resources/changelog.md | 1 + .../radio-group/radio-group.test.ts | 96 +++++++++++++++++++ src/components/radio-group/radio-group.ts | 22 +++-- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/docs/docs/resources/changelog.md b/docs/docs/resources/changelog.md index f4662d9d8..39d6be02f 100644 --- a/docs/docs/resources/changelog.md +++ b/docs/docs/resources/changelog.md @@ -20,6 +20,7 @@ During the alpha period, things might break! We take breaking changes very serio - Added support for Enter to `` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) - Added more resilient support for lazy loaded options in `` - Added support for vertical button groups +- Added the `focus()` method to `` - Fixed a bug in `` when using `precision` - Fixed a bug in `` that allowed tabbing into the rating when readonly - Fixed a bug in `` where the title attribute would show with redundant info diff --git a/src/components/radio-group/radio-group.test.ts b/src/components/radio-group/radio-group.test.ts index 1f5ef9ad6..1bbba970e 100644 --- a/src/components/radio-group/radio-group.test.ts +++ b/src/components/radio-group/radio-group.test.ts @@ -297,6 +297,102 @@ describe('', () => { }); }); + describe('when handling focus', () => { + const doAction = async (instance: WaRadioGroup, type: string) => { + if (type === 'focus') { + instance.focus(); + await instance.updateComplete; + return; + } + + const label = instance.shadowRoot!.querySelector('#label')!; + label.click(); + await instance.updateComplete; + }; + + // Tests for focus and label actions with radio buttons + ['focus', 'label'].forEach(actionType => { + describe(`when using ${actionType}`, () => { + it('should do nothing if all elements are disabled', async () => { + const el = await fixture(html` + + + + + + + `); + + const validFocusHandler = sinon.spy(); + + Array.from(el.querySelectorAll('wa-radio')).forEach(radio => + radio.addEventListener('wa-focus', validFocusHandler) + ); + + expect(validFocusHandler).to.not.have.been.called; + await doAction(el, actionType); + expect(validFocusHandler).to.not.have.been.called; + }); + + it('should focus the first radio that is enabled when the group receives focus', async () => { + const el = await fixture(html` + + + + + + + `); + + const invalidFocusHandler = sinon.spy(); + const validFocusHandler = sinon.spy(); + + const disabledRadio = el.querySelector('#radio-0')!; + const validRadio = el.querySelector('#radio-1')!; + + disabledRadio.addEventListener('wa-focus', invalidFocusHandler); + validRadio.addEventListener('wa-focus', validFocusHandler); + + expect(invalidFocusHandler).to.not.have.been.called; + expect(validFocusHandler).to.not.have.been.called; + + await doAction(el, actionType); + + expect(invalidFocusHandler).to.not.have.been.called; + expect(validFocusHandler).to.have.been.called; + }); + + it('should focus the currently enabled radio when the group receives focus', async () => { + const el = await fixture(html` + + + + + + + `); + + const invalidFocusHandler = sinon.spy(); + const validFocusHandler = sinon.spy(); + + const disabledRadio = el.querySelector('#radio-0')!; + const validRadio = el.querySelector('#radio-2')!; + + disabledRadio.addEventListener('wa-focus', invalidFocusHandler); + validRadio.addEventListener('wa-focus', validFocusHandler); + + expect(invalidFocusHandler).to.not.have.been.called; + expect(validFocusHandler).to.not.have.been.called; + + await doAction(el, actionType); + + expect(invalidFocusHandler).to.not.have.been.called; + expect(validFocusHandler).to.have.been.called; + }); + }); + }); + }); + describe('when the value changes', () => { it('should emit wa-change when toggled with the arrow keys', async () => { const radioGroup = await fixture(html` diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index 5e4fe3668..7dc136b96 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -170,14 +170,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement { } private handleLabelClick() { - const radios = this.getAllRadios(); - const checked = radios.find(radio => radio.checked); - const radioToFocus = checked || radios[0]; - - // Move focus to the checked radio (or the first one if none are checked) when clicking the label - if (radioToFocus) { - radioToFocus.focus(); - } + this.focus(); } private async syncRadioElements() { @@ -305,6 +298,19 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement { event.preventDefault(); } + /** Sets focus on the radio group. */ + public focus(options?: FocusOptions) { + const radios = this.getAllRadios(); + const checked = radios.find(radio => radio.checked); + const firstEnabledRadio = radios.find(radio => !radio.disabled); + const radioToFocus = checked || firstEnabledRadio; + + // Call focus for the checked radio. If no radio is checked, focus the first one that isn't disabled. + if (radioToFocus) { + radioToFocus.focus(options); + } + } + render() { const hasLabelSlot = this.hasUpdated ? this.hasSlotController.test('label') : this.withLabel; const hasHelpTextSlot = this.hasUpdated ? this.hasSlotController.test('help-text') : this.withHelpText;