Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/calcite-components/src/utils/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CalciteConfig } from "./config";

describe("config", () => {
let config: CalciteConfig;

/**
* Need to load the config at runtime to allow test to specify custom configuration if needed.
*/
async function loadConfig(): Promise<CalciteConfig> {
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.calciteConfig = {
focusTrapStack: customFocusTrapStack,
};

config = await loadConfig();

expect(config.focusTrapStack).toBe(customFocusTrapStack);
});
});
23 changes: 15 additions & 8 deletions packages/calcite-components/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -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 CalciteConfig {
/**
* 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: CalciteConfig = globalThis["calciteConfig"];

export const focusTrapStack: FocusTrap[] = customConfig?.focusTrapStack || [];
53 changes: 51 additions & 2 deletions packages/calcite-components/src/utils/focusTrapComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import {
activateFocusTrap,
connectFocusTrap,
deactivateFocusTrap,
FocusTrapComponent,
updateFocusTrapElements,
} from "./focusTrapComponent";

import { JSDOM } from "jsdom";
import { CalciteConfig } 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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<{ calciteConfig: CalciteConfig }>;

(globalThis as TestGlobal).calciteConfig = {
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);
});
});
});
5 changes: 2 additions & 3 deletions packages/calcite-components/src/utils/focusTrapComponent.ts
Original file line number Diff line number Diff line change
@@ -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 } from "./config";

/**
* Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface.
Expand Down Expand Up @@ -71,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);
Expand Down