diff --git a/packages/calcite-components/src/components/dialog/dialog.e2e.ts b/packages/calcite-components/src/components/dialog/dialog.e2e.ts
index d9acdb326c8..c96ed09dfbc 100644
--- a/packages/calcite-components/src/components/dialog/dialog.e2e.ts
+++ b/packages/calcite-components/src/components/dialog/dialog.e2e.ts
@@ -1,6 +1,6 @@
// @ts-strict-ignore
import { newE2EPage, E2EPage } from "@arcgis/lumina-compiler/puppeteerTesting";
-import { describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import {
accessible,
defaults,
@@ -1160,4 +1160,59 @@ describe("calcite-dialog", () => {
expect(await dialog.getProperty("open")).toBe(true);
});
});
+
+ describe("focusTrap", () => {
+ let page: E2EPage;
+
+ beforeEach(async () => {
+ page = await newE2EPage();
+ await page.setContent(html`
+
+
+ `);
+
+ await skipAnimations(page);
+ await page.waitForChanges();
+ });
+
+ it("can tab out of non-modal dialog when focusTrapDisabled=true", async () => {
+ const dialog = await page.find("calcite-dialog >>> .container");
+ const action = await page.find("calcite-dialog >>> calcite-action");
+ const outsideEl = await page.find("#outsideEl");
+
+ expect(await dialog.isVisible()).toBe(true);
+
+ await action.callMethod("setFocus");
+ await page.waitForChanges();
+
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+
+ const activeElementId = await page.evaluate(() => document.activeElement.id);
+ expect(activeElementId).toBe(await outsideEl.getProperty("id"));
+ });
+
+ it("cannot tab out of dialog when modal=true and focusTrapDisabled=true", async () => {
+ const dialog = await page.find("calcite-dialog >>> .container");
+ const insideEl = await page.find("#insideEl");
+
+ expect(await dialog.isVisible()).toBe(true);
+
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+ await page.keyboard.press("Tab");
+ await page.waitForChanges();
+
+ const activeElementId = await page.evaluate(() => document.activeElement.id);
+ expect(activeElementId).toBe(await insideEl.getProperty("id"));
+ });
+ });
});
diff --git a/packages/calcite-components/src/components/dialog/dialog.tsx b/packages/calcite-components/src/components/dialog/dialog.tsx
index 1b1f65df7b2..40466cd0856 100644
--- a/packages/calcite-components/src/components/dialog/dialog.tsx
+++ b/packages/calcite-components/src/components/dialog/dialog.tsx
@@ -187,6 +187,9 @@ export class Dialog
/** When `true`, displays a scrim blocking interaction underneath the component. */
@property({ reflect: true }) modal = false;
+ /** When `true` and `modal` is `false`, prevents focus trapping. */
+ @property({ reflect: true }) focusTrapDisabled = false;
+
/** When `true`, displays and positions the component. */
@property({ reflect: true })
get open(): boolean {
@@ -270,6 +273,11 @@ export class Dialog
updateFocusTrapElements(this);
}
+ /** When defined, provides a condition to disable focus trapping. When `true`, prevents focus trapping. */
+ focusTrapDisabledOverride(): boolean {
+ return !this.modal && this.focusTrapDisabled;
+ }
+
// #endregion
// #region Events
@@ -328,6 +336,9 @@ export class Dialog
if (changes.has("modal") && (this.hasUpdated || this.modal !== false)) {
this.updateOverflowHiddenClass();
}
+ if ((changes.has("modal") || changes.has("focusTrapDisabled")) && this.hasUpdated) {
+ this.handleFocusTrapDisabled();
+ }
if (
(changes.has("open") && (this.hasUpdated || this.open !== false)) ||
@@ -366,6 +377,15 @@ export class Dialog
// #endregion
// #region Private Methods
+
+ private handleFocusTrapDisabled(): void {
+ if (!this.open) {
+ return;
+ }
+
+ activateFocusTrap(this);
+ }
+
private updateAssistiveText(): void {
const { messages } = this;
this.assistiveText =
@@ -380,7 +400,7 @@ export class Dialog
onOpen(): void {
this.calciteDialogOpen.emit();
- activateFocusTrap(this);
+ this.handleFocusTrapDisabled();
}
onBeforeClose(): void {
diff --git a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts
index 8fd9f02b5b1..993f81c215d 100644
--- a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts
+++ b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts
@@ -112,4 +112,37 @@ describe("focusTrapComponent", () => {
expect(customFocusTrapStack).toHaveLength(1);
});
});
+ describe("focusTrapDisabledOverride", () => {
+ it("should activate focus trap when focusTrapDisabledOverride returns false", () => {
+ const fakeComponent = {} as FocusTrapComponent;
+ fakeComponent.el = document.createElement("div");
+
+ connectFocusTrap(fakeComponent);
+
+ const activateSpy = vi.fn();
+ fakeComponent.focusTrap.activate = activateSpy;
+
+ fakeComponent.focusTrapDisabledOverride = () => false;
+
+ activateFocusTrap(fakeComponent);
+
+ expect(activateSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not activate focus trap when focusTrapDisabledOverride returns true", () => {
+ const fakeComponent = {} as FocusTrapComponent;
+ fakeComponent.el = document.createElement("div");
+
+ connectFocusTrap(fakeComponent);
+
+ const activateSpy = vi.fn();
+ fakeComponent.focusTrap.activate = activateSpy;
+
+ fakeComponent.focusTrapDisabledOverride = () => true;
+
+ activateFocusTrap(fakeComponent);
+
+ expect(activateSpy).toHaveBeenCalledTimes(0);
+ });
+ });
});
diff --git a/packages/calcite-components/src/utils/focusTrapComponent.ts b/packages/calcite-components/src/utils/focusTrapComponent.ts
index 8c5c560526e..d41fffadb3c 100644
--- a/packages/calcite-components/src/utils/focusTrapComponent.ts
+++ b/packages/calcite-components/src/utils/focusTrapComponent.ts
@@ -10,6 +10,9 @@ export interface FocusTrapComponent {
/** When `true`, prevents focus trapping. */
focusTrapDisabled?: boolean;
+ /** When defined, provides a condition to disable focus trapping. When `true`, prevents focus trapping. */
+ focusTrapDisabledOverride?: () => boolean;
+
/** The focus trap instance. */
focusTrap: FocusTrap;
@@ -73,7 +76,7 @@ export function activateFocusTrap(
component: FocusTrapComponent,
options?: Parameters<_FocusTrap["activate"]>[0],
): void {
- if (!component.focusTrapDisabled) {
+ if (component.focusTrapDisabledOverride ? !component.focusTrapDisabledOverride() : !component.focusTrapDisabled) {
component.focusTrap?.activate(options);
}
}