Skip to content

Commit

Permalink
Add ability to focus sl-radio-group dynamically (#2192)
Browse files Browse the repository at this point in the history
* Add ability to focus sl-radio-group dynamically

* Adjusted to review findings

* add changelog entry

* prettier

* add extra timeout for safari

* prettier

---------

Co-authored-by: konnorrogers <[email protected]>
  • Loading branch information
schilchSICKAG and KonnorRogers authored Oct 10, 2024
1 parent 4f78930 commit 5a3d46a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/pages/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
## Next

- Added Finnish translations [#2211]
- Added the `.focus` function to `<sl-radio-group>` [#2192]
- Fixed a bug with with `<sl-select>` not respecting its initial value. [#2204]
- Fixed a bug with certain bundlers when using dynamic imports [#2210]
- Fixed a bug in `<sl-textarea>` causing scroll jumping when using `resize="auto"` [#2182]
Expand Down
23 changes: 15 additions & 8 deletions src/components/radio-group/radio-group.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
}

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 handleInvalid(event: Event) {
Expand Down Expand Up @@ -325,6 +318,20 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
this.formControlController.updateValidity();
}

/** 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 is not disabled
if (radioToFocus) {
radioToFocus.focus(options);
}
}

render() {
const hasLabelSlot = this.hasSlotController.test('label');
const hasHelpTextSlot = this.hasSlotController.test('help-text');
Expand Down
96 changes: 96 additions & 0 deletions src/components/radio-group/radio-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,102 @@ describe('when a size is applied', () => {
});
});

describe('when handling focus', () => {
const doAction = async (instance: SlRadioGroup, type: string) => {
if (type === 'focus') {
instance.focus();
await instance.updateComplete;
return;
}

const label = instance.shadowRoot!.querySelector<HTMLLabelElement>('#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<SlRadioGroup>(html`
<sl-radio-group>
<sl-radio id="radio-0" value="0" disabled></sl-radio>
<sl-radio id="radio-1" value="1" disabled></sl-radio>
<sl-radio id="radio-2" value="2" disabled></sl-radio>
<sl-radio id="radio-3" value="3" disabled></sl-radio>
</sl-radio-group>
`);

const validFocusHandler = sinon.spy();

Array.from(el.querySelectorAll<SlRadio>('sl-radio')).forEach(radio =>
radio.addEventListener('sl-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<SlRadioGroup>(html`
<sl-radio-group>
<sl-radio id="radio-0" value="0" disabled></sl-radio>
<sl-radio id="radio-1" value="1"></sl-radio>
<sl-radio id="radio-2" value="2"></sl-radio>
<sl-radio id="radio-3" value="3"></sl-radio>
</sl-radio-group>
`);

const invalidFocusHandler = sinon.spy();
const validFocusHandler = sinon.spy();

const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-1')!;

disabledRadio.addEventListener('sl-focus', invalidFocusHandler);
validRadio.addEventListener('sl-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<SlRadioGroup>(html`
<sl-radio-group value="2">
<sl-radio id="radio-0" value="0" disabled></sl-radio>
<sl-radio id="radio-1" value="1"></sl-radio>
<sl-radio id="radio-2" value="2" checked></sl-radio>
<sl-radio id="radio-3" value="3"></sl-radio>
</sl-radio-group>
`);

const invalidFocusHandler = sinon.spy();
const validFocusHandler = sinon.spy();

const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-2')!;

disabledRadio.addEventListener('sl-focus', invalidFocusHandler);
validRadio.addEventListener('sl-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 sl-change when toggled with the arrow keys', async () => {
const radioGroup = await fixture<SlRadioGroup>(html`
Expand Down
1 change: 1 addition & 0 deletions src/components/select/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ describe('<sl-select>', () => {
);
const el = form.querySelector<SlSelect>('sl-select')!;

await aTimeout(10);
expect(el.value).to.equal('');
expect(new FormData(form).get('select')).equal('');

Expand Down

0 comments on commit 5a3d46a

Please sign in to comment.