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 {