From e7247c6b9140bd8e3704b017e6326aef02cf6e1c Mon Sep 17 00:00:00 2001 From: JC Franco Date: Tue, 18 Jul 2023 12:16:39 -0700 Subject: [PATCH 1/3] feat: allow sharing focus trap stacks via configuration global --- .../src/utils/config.spec.ts | 31 +++++++++++ .../calcite-components/src/utils/config.ts | 23 +++++--- .../src/utils/focusTrapComponent.spec.ts | 53 ++++++++++++++++++- .../src/utils/focusTrapComponent.ts | 3 +- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 packages/calcite-components/src/utils/config.spec.ts diff --git a/packages/calcite-components/src/utils/config.spec.ts b/packages/calcite-components/src/utils/config.spec.ts new file mode 100644 index 00000000000..995b31dcaf9 --- /dev/null +++ b/packages/calcite-components/src/utils/config.spec.ts @@ -0,0 +1,31 @@ +import type { CalciteComponentsConfig } from "./config"; + +describe("config", () => { + let config: CalciteComponentsConfig; + + /** + * Need to load the config at runtime to allow test to specify custom configuration if needed. + */ + async function loadConfig(): Promise { + return import("./config"); + } + + beforeEach(() => jest.resetModules()); + + it("has defaults", async () => { + config = await loadConfig(); + expect(config.focusTrapStack).toHaveLength(0); + }); + + it("allows custom configuration", async () => { + const customFocusTrapStack = []; + + globalThis.calciteComponentsConfig = { + focusTrapStack: customFocusTrapStack + }; + + config = await loadConfig(); + + expect(config.focusTrapStack).toBe(customFocusTrapStack); + }); +}); diff --git a/packages/calcite-components/src/utils/config.ts b/packages/calcite-components/src/utils/config.ts index 04d4f201b20..d844989cf63 100644 --- a/packages/calcite-components/src/utils/config.ts +++ b/packages/calcite-components/src/utils/config.ts @@ -1,13 +1,20 @@ /** - * This module helps users provide custom configuration for component internals. - * - * @internal + * This module allows custom configuration for components. */ -const configOverrides = globalThis["calciteComponentsConfig"]; +import { FocusTrap } from "./focusTrapComponent"; -const config = { - ...configOverrides -}; +export interface CalciteComponentsConfig { + /** + * Defines the global trap stack to use for focus-trapping components. + * + * This is useful if your application uses its own instance of `focus-trap` and both need to be aware of each other. + * + * @see https://github.com/focus-trap/focus-trap#createoptions + */ + focusTrapStack: FocusTrap[]; +} -export { config }; +const customConfig: CalciteComponentsConfig = globalThis["calciteComponentsConfig"]; + +export const focusTrapStack: FocusTrap[] = customConfig?.focusTrapStack || []; diff --git a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts index 84f37074b55..8a3ee1ce2ec 100644 --- a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts +++ b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts @@ -2,12 +2,17 @@ import { activateFocusTrap, connectFocusTrap, deactivateFocusTrap, + FocusTrapComponent, updateFocusTrapElements } from "./focusTrapComponent"; +import { JSDOM } from "jsdom"; +import { CalciteComponentsConfig } from "./config"; +import { GlobalTestProps } from "../tests/utils"; + describe("focusTrapComponent", () => { it("focusTrapComponent lifecycle", () => { - const fakeComponent = {} as any; + const fakeComponent = {} as FocusTrapComponent; fakeComponent.el = document.createElement("div"); connectFocusTrap(fakeComponent); @@ -35,7 +40,7 @@ describe("focusTrapComponent", () => { }); it("supports passing options", () => { - const fakeComponent = {} as any; + const fakeComponent = {} as FocusTrapComponent; fakeComponent.el = document.createElement("div"); connectFocusTrap(fakeComponent); @@ -54,4 +59,48 @@ describe("focusTrapComponent", () => { deactivateFocusTrap(fakeComponent, fakeDeactivateOptions); expect(deactivateSpy).toHaveBeenCalledWith(fakeDeactivateOptions); }); + + describe("configuration", () => { + beforeEach(() => jest.resetModules()); + + it("supports custom global trap stack", async () => { + const customFocusTrapStack = []; + + // we clobber Stencil's custom Mock document implementation + const { window: win } = new JSDOM(); + window = win; // make window references use JSDOM + globalThis.MutationObserver = window.MutationObserver; // needed for focus-trap + + type TestGlobal = GlobalTestProps<{ calciteComponentsConfig: CalciteComponentsConfig }>; + + (globalThis as TestGlobal).calciteComponentsConfig = { + focusTrapStack: customFocusTrapStack + }; + + const focusTrap = await import("focus-trap"); + const createFocusTrapSpy = jest.spyOn(focusTrap, "createFocusTrap"); + + const focusTrapComponent = await import("./focusTrapComponent"); + const fakeComponent = {} as FocusTrapComponent; + fakeComponent.el = win.document.createElement("div"); + + focusTrapComponent.connectFocusTrap(fakeComponent); + expect(createFocusTrapSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + trapStack: customFocusTrapStack + }) + ); + expect(customFocusTrapStack).toHaveLength(0); + + focusTrapComponent.activateFocusTrap(fakeComponent); + expect(customFocusTrapStack).toHaveLength(1); + + focusTrapComponent.deactivateFocusTrap(fakeComponent); + expect(customFocusTrapStack).toHaveLength(0); + + focusTrapComponent.activateFocusTrap(fakeComponent); + expect(customFocusTrapStack).toHaveLength(1); + }); + }); }); diff --git a/packages/calcite-components/src/utils/focusTrapComponent.ts b/packages/calcite-components/src/utils/focusTrapComponent.ts index 5b27547432e..b291426436d 100644 --- a/packages/calcite-components/src/utils/focusTrapComponent.ts +++ b/packages/calcite-components/src/utils/focusTrapComponent.ts @@ -1,7 +1,6 @@ import { createFocusTrap, FocusTrap as _FocusTrap, Options as FocusTrapOptions } from "focus-trap"; import { FocusableElement, focusElement, tabbableOptions } from "./dom"; - -const trapStack: _FocusTrap[] = []; +import { focusTrapStack as trapStack } from "./config"; /** * Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface. From 893680bd6b5160ca1148ce1e4172ea1a97024839 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 20 Jul 2023 11:04:54 -0700 Subject: [PATCH 2/3] rename config global for simplicity and to possibly share w/ future packages --- packages/calcite-components/src/utils/config.spec.ts | 10 +++++----- packages/calcite-components/src/utils/config.ts | 4 ++-- .../src/utils/focusTrapComponent.spec.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/calcite-components/src/utils/config.spec.ts b/packages/calcite-components/src/utils/config.spec.ts index 995b31dcaf9..662dc031954 100644 --- a/packages/calcite-components/src/utils/config.spec.ts +++ b/packages/calcite-components/src/utils/config.spec.ts @@ -1,12 +1,12 @@ -import type { CalciteComponentsConfig } from "./config"; +import type { CalciteConfig } from "./config"; describe("config", () => { - let config: CalciteComponentsConfig; + let config: CalciteConfig; /** * Need to load the config at runtime to allow test to specify custom configuration if needed. */ - async function loadConfig(): Promise { + async function loadConfig(): Promise { return import("./config"); } @@ -20,8 +20,8 @@ describe("config", () => { it("allows custom configuration", async () => { const customFocusTrapStack = []; - globalThis.calciteComponentsConfig = { - focusTrapStack: customFocusTrapStack + globalThis.calciteConfig = { + focusTrapStack: customFocusTrapStack, }; config = await loadConfig(); diff --git a/packages/calcite-components/src/utils/config.ts b/packages/calcite-components/src/utils/config.ts index d844989cf63..0948e861e53 100644 --- a/packages/calcite-components/src/utils/config.ts +++ b/packages/calcite-components/src/utils/config.ts @@ -4,7 +4,7 @@ import { FocusTrap } from "./focusTrapComponent"; -export interface CalciteComponentsConfig { +export interface CalciteConfig { /** * Defines the global trap stack to use for focus-trapping components. * @@ -15,6 +15,6 @@ export interface CalciteComponentsConfig { focusTrapStack: FocusTrap[]; } -const customConfig: CalciteComponentsConfig = globalThis["calciteComponentsConfig"]; +const customConfig: CalciteConfig = globalThis["calciteConfig"]; export const focusTrapStack: FocusTrap[] = customConfig?.focusTrapStack || []; diff --git a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts index 1e1888f7a0e..92ac8af2cd2 100644 --- a/packages/calcite-components/src/utils/focusTrapComponent.spec.ts +++ b/packages/calcite-components/src/utils/focusTrapComponent.spec.ts @@ -7,7 +7,7 @@ import { } from "./focusTrapComponent"; import { JSDOM } from "jsdom"; -import { CalciteComponentsConfig } from "./config"; +import { CalciteConfig } from "./config"; import { GlobalTestProps } from "../tests/utils"; describe("focusTrapComponent", () => { @@ -71,9 +71,9 @@ describe("focusTrapComponent", () => { window = win; // make window references use JSDOM globalThis.MutationObserver = window.MutationObserver; // needed for focus-trap - type TestGlobal = GlobalTestProps<{ calciteComponentsConfig: CalciteComponentsConfig }>; + type TestGlobal = GlobalTestProps<{ calciteConfig: CalciteConfig }>; - (globalThis as TestGlobal).calciteComponentsConfig = { + (globalThis as TestGlobal).calciteConfig = { focusTrapStack: customFocusTrapStack, }; From 3f9efddc1a138671205895400e3f5c4fe57f9463 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 20 Jul 2023 11:46:17 -0700 Subject: [PATCH 3/3] tidy up --- packages/calcite-components/src/utils/focusTrapComponent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/calcite-components/src/utils/focusTrapComponent.ts b/packages/calcite-components/src/utils/focusTrapComponent.ts index f524131e6b5..6ba1e4b5f74 100644 --- a/packages/calcite-components/src/utils/focusTrapComponent.ts +++ b/packages/calcite-components/src/utils/focusTrapComponent.ts @@ -1,6 +1,6 @@ import { createFocusTrap, FocusTrap as _FocusTrap, Options as FocusTrapOptions } from "focus-trap"; import { FocusableElement, focusElement, tabbableOptions } from "./dom"; -import { focusTrapStack as trapStack } from "./config"; +import { focusTrapStack } from "./config"; /** * Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface. @@ -70,7 +70,7 @@ export function connectFocusTrap(component: FocusTrapComponent, options?: Connec // the following options are not overrideable document: el.ownerDocument, tabbableOptions, - trapStack, + trapStack: focusTrapStack, }; component.focusTrap = createFocusTrap(focusTrapNode, focusTrapOptions);