Skip to content

Commit 934a19f

Browse files
authored
feat: allow sharing focus trap stacks via configuration global (#7334)
**Related Issue:** #6659 ## Summary This enables users to provide a shareable focus trap stack via the `calciteConfig` global.
1 parent c30db4e commit 934a19f

File tree

4 files changed

+99
-13
lines changed

4 files changed

+99
-13
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { CalciteConfig } from "./config";
2+
3+
describe("config", () => {
4+
let config: CalciteConfig;
5+
6+
/**
7+
* Need to load the config at runtime to allow test to specify custom configuration if needed.
8+
*/
9+
async function loadConfig(): Promise<CalciteConfig> {
10+
return import("./config");
11+
}
12+
13+
beforeEach(() => jest.resetModules());
14+
15+
it("has defaults", async () => {
16+
config = await loadConfig();
17+
expect(config.focusTrapStack).toHaveLength(0);
18+
});
19+
20+
it("allows custom configuration", async () => {
21+
const customFocusTrapStack = [];
22+
23+
globalThis.calciteConfig = {
24+
focusTrapStack: customFocusTrapStack,
25+
};
26+
27+
config = await loadConfig();
28+
29+
expect(config.focusTrapStack).toBe(customFocusTrapStack);
30+
});
31+
});
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
/**
2-
* This module helps users provide custom configuration for component internals.
3-
*
4-
* @internal
2+
* This module allows custom configuration for components.
53
*/
64

7-
const configOverrides = globalThis["calciteComponentsConfig"];
5+
import { FocusTrap } from "./focusTrapComponent";
86

9-
const config = {
10-
...configOverrides,
11-
};
7+
export interface CalciteConfig {
8+
/**
9+
* Defines the global trap stack to use for focus-trapping components.
10+
*
11+
* This is useful if your application uses its own instance of `focus-trap` and both need to be aware of each other.
12+
*
13+
* @see https://github.com/focus-trap/focus-trap#createoptions
14+
*/
15+
focusTrapStack: FocusTrap[];
16+
}
1217

13-
export { config };
18+
const customConfig: CalciteConfig = globalThis["calciteConfig"];
19+
20+
export const focusTrapStack: FocusTrap[] = customConfig?.focusTrapStack || [];

packages/calcite-components/src/utils/focusTrapComponent.spec.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import {
22
activateFocusTrap,
33
connectFocusTrap,
44
deactivateFocusTrap,
5+
FocusTrapComponent,
56
updateFocusTrapElements,
67
} from "./focusTrapComponent";
78

9+
import { JSDOM } from "jsdom";
10+
import { CalciteConfig } from "./config";
11+
import { GlobalTestProps } from "../tests/utils";
12+
813
describe("focusTrapComponent", () => {
914
it("focusTrapComponent lifecycle", () => {
10-
const fakeComponent = {} as any;
15+
const fakeComponent = {} as FocusTrapComponent;
1116
fakeComponent.el = document.createElement("div");
1217

1318
connectFocusTrap(fakeComponent);
@@ -35,7 +40,7 @@ describe("focusTrapComponent", () => {
3540
});
3641

3742
it("supports passing options", () => {
38-
const fakeComponent = {} as any;
43+
const fakeComponent = {} as FocusTrapComponent;
3944
fakeComponent.el = document.createElement("div");
4045

4146
connectFocusTrap(fakeComponent);
@@ -54,4 +59,48 @@ describe("focusTrapComponent", () => {
5459
deactivateFocusTrap(fakeComponent, fakeDeactivateOptions);
5560
expect(deactivateSpy).toHaveBeenCalledWith(fakeDeactivateOptions);
5661
});
62+
63+
describe("configuration", () => {
64+
beforeEach(() => jest.resetModules());
65+
66+
it("supports custom global trap stack", async () => {
67+
const customFocusTrapStack = [];
68+
69+
// we clobber Stencil's custom Mock document implementation
70+
const { window: win } = new JSDOM();
71+
window = win; // make window references use JSDOM
72+
globalThis.MutationObserver = window.MutationObserver; // needed for focus-trap
73+
74+
type TestGlobal = GlobalTestProps<{ calciteConfig: CalciteConfig }>;
75+
76+
(globalThis as TestGlobal).calciteConfig = {
77+
focusTrapStack: customFocusTrapStack,
78+
};
79+
80+
const focusTrap = await import("focus-trap");
81+
const createFocusTrapSpy = jest.spyOn(focusTrap, "createFocusTrap");
82+
83+
const focusTrapComponent = await import("./focusTrapComponent");
84+
const fakeComponent = {} as FocusTrapComponent;
85+
fakeComponent.el = win.document.createElement("div");
86+
87+
focusTrapComponent.connectFocusTrap(fakeComponent);
88+
expect(createFocusTrapSpy).toHaveBeenLastCalledWith(
89+
expect.anything(),
90+
expect.objectContaining({
91+
trapStack: customFocusTrapStack,
92+
})
93+
);
94+
expect(customFocusTrapStack).toHaveLength(0);
95+
96+
focusTrapComponent.activateFocusTrap(fakeComponent);
97+
expect(customFocusTrapStack).toHaveLength(1);
98+
99+
focusTrapComponent.deactivateFocusTrap(fakeComponent);
100+
expect(customFocusTrapStack).toHaveLength(0);
101+
102+
focusTrapComponent.activateFocusTrap(fakeComponent);
103+
expect(customFocusTrapStack).toHaveLength(1);
104+
});
105+
});
57106
});

packages/calcite-components/src/utils/focusTrapComponent.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createFocusTrap, FocusTrap as _FocusTrap, Options as FocusTrapOptions } from "focus-trap";
22
import { FocusableElement, focusElement, tabbableOptions } from "./dom";
3-
4-
const trapStack: _FocusTrap[] = [];
3+
import { focusTrapStack } from "./config";
54

65
/**
76
* Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface.
@@ -71,7 +70,7 @@ export function connectFocusTrap(component: FocusTrapComponent, options?: Connec
7170
// the following options are not overrideable
7271
document: el.ownerDocument,
7372
tabbableOptions,
74-
trapStack,
73+
trapStack: focusTrapStack,
7574
};
7675

7776
component.focusTrap = createFocusTrap(focusTrapNode, focusTrapOptions);

0 commit comments

Comments
 (0)