Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow sharing focus trap stacks via configuration global #7334

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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);
});
});
});
3 changes: 1 addition & 2 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 as trapStack } from "./config";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: could keep the focusTrapStack name rather than renaming it to trapStack? It will make searching the code for occurences easier. Looks like the only other line you'll need to change is 73


/**
* Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface.
Expand Down