From dd7ec608779f1a34ad3c77a36b6f8fcf6fd1365a Mon Sep 17 00:00:00 2001 From: Anveshreddy mekala Date: Wed, 21 Jun 2023 10:45:48 -0500 Subject: [PATCH] fix(radio-button): focuses first focusable radio-button element in group. (#7152) **Related Issue:** #7113 ## Summary This PR will focus the first focusable `calcite-radio-button` when the user `Tab` in to the group. --- .../radio-button/radio-button.e2e.ts | 97 +++++++++++++++++++ .../components/radio-button/radio-button.tsx | 31 +++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/packages/calcite-components/src/components/radio-button/radio-button.e2e.ts b/packages/calcite-components/src/components/radio-button/radio-button.e2e.ts index 2c6ec4317f9..0b62988f31b 100644 --- a/packages/calcite-components/src/components/radio-button/radio-button.e2e.ts +++ b/packages/calcite-components/src/components/radio-button/radio-button.e2e.ts @@ -11,6 +11,7 @@ import { renders } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; +import { getFocusedElementProp } from "../../tests/utils"; describe("calcite-radio-button", () => { describe("renders", () => { @@ -66,6 +67,102 @@ describe("calcite-radio-button", () => { focusable("calcite-radio-button", { shadowFocusTargetSelector: ".container" }); + + it("focuses first focusable item on Tab when new radio-button is added", async () => { + const page = await newE2EPage(); + await page.setContent(html` +
+ + + Trees + + + + Shrubs + + + + Flowers + + submit +
+ `); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("shrubs"); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("submit"); + + await page.evaluate(() => { + const firstRadioButton = document.querySelector('calcite-label[id="1"]'); + const newRadioButton = ` + + Plants + `; + firstRadioButton.insertAdjacentHTML("beforebegin", newRadioButton); + }); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("plants"); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("submit"); + + const radioButtonElement = await page.find('calcite-radio-button[id="plants"]'); + radioButtonElement.setProperty("disabled", true); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("shrubs"); + }); + + it("focuses checked item on Tab when new radio-button is added", async () => { + const page = await newE2EPage(); + await page.setContent(html` +
+ + + Trees + + + + Shrubs + + + + Flowers + + submit +
+ `); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("flowers"); + await page.keyboard.press("Tab"); + expect(await getFocusedElementProp(page, "id")).toBe("submit"); + + await page.evaluate(() => { + const firstRadioButton = document.querySelector('calcite-label[id="1"]'); + const newRadioButton = ` + + Plants + `; + firstRadioButton.insertAdjacentHTML("beforebegin", newRadioButton); + }); + + await page.keyboard.press("Tab"); + await page.waitForChanges(); + await page.keyboard.press("Tab"); + await page.waitForChanges(); + expect(await getFocusedElementProp(page, "id")).toBe("flowers"); + }); }); describe("reflects", () => { diff --git a/packages/calcite-components/src/components/radio-button/radio-button.tsx b/packages/calcite-components/src/components/radio-button/radio-button.tsx index def598595a6..0bd2b514338 100644 --- a/packages/calcite-components/src/components/radio-button/radio-button.tsx +++ b/packages/calcite-components/src/components/radio-button/radio-button.tsx @@ -3,6 +3,7 @@ import { Element, Event, EventEmitter, + forceUpdate, h, Host, Listen, @@ -73,6 +74,11 @@ export class RadioButton /** When `true`, interaction is prevented and the component is displayed with lower opacity. */ @Prop({ reflect: true }) disabled = false; + @Watch("disabled") + disabledChanged(): void { + this.updateTabIndexOfOtherRadioButtonsInGroup(); + } + /** * The focused state of the component. * @@ -94,6 +100,11 @@ export class RadioButton /** When `true`, the component is not displayed and is not focusable or checkable. */ @Prop({ reflect: true }) hidden = false; + @Watch("hidden") + hiddenChanged(): void { + this.updateTabIndexOfOtherRadioButtonsInGroup(); + } + /** * The hovered state of the component. * @@ -184,9 +195,11 @@ export class RadioButton ) as HTMLCalciteRadioButtonElement[]; }; - isDefaultSelectable = (): boolean => { + isFocusable = (): boolean => { const radioButtons = this.queryButtons(); - return !radioButtons.some((radioButton) => radioButton.checked) && radioButtons[0] === this.el; + const firstFocusable = radioButtons.find((radioButton) => !radioButton.disabled); + const checked = radioButtons.find((radioButton) => radioButton.checked); + return firstFocusable === this.el && !checked; }; check = (): void => { @@ -291,11 +304,21 @@ export class RadioButton }); } + private updateTabIndexOfOtherRadioButtonsInGroup(): void { + const radioButtons = this.queryButtons(); + const otherFocusableRadioButtons = radioButtons.filter( + (radioButton) => radioButton.guid !== this.guid && !radioButton.disabled + ); + otherFocusableRadioButtons.forEach((radioButton) => { + forceUpdate(radioButton); + }); + } + private getTabIndex(): number | undefined { if (this.disabled) { return undefined; } - return this.checked || this.isDefaultSelectable() ? 0 : -1; + return this.checked || this.isFocusable() ? 0 : -1; } //-------------------------------------------------------------------------- @@ -446,6 +469,7 @@ export class RadioButton connectInteractive(this); connectLabel(this); connectForm(this); + this.updateTabIndexOfOtherRadioButtonsInGroup(); } componentWillLoad(): void { @@ -464,6 +488,7 @@ export class RadioButton disconnectInteractive(this); disconnectLabel(this); disconnectForm(this); + this.updateTabIndexOfOtherRadioButtonsInGroup(); } componentDidRender(): void {