import { Key } from "@/constants";
import {
defineCE,
elementUpdated,
fixture,
fixtureCleanup,
fixtureSync,
nextFrame,
oneEvent
} from "@open-wc/testing-helpers";
import { customElement, html, LitElement, property, PropertyValues, query } from "lit-element";
import { AnyConstructor, FocusTrapMixin } from "./FocusTrapMixin";
import "@momentum-ui/web-components/dist/comp/md-input";
Object.defineProperties(Element.prototype, {
getBoundingClientRect: {
value: jest.fn().mockReturnValue({
width: 10,
height: 10,
top: 0,
left: 0,
bottom: 0,
right: 0
})
},
getClientRects: {
value: jest.fn().mockReturnValue(["1", "2"])
}
});
Object.defineProperties(HTMLElement.prototype, {
offsetWidth: {
value: jest.fn().mockReturnValue(10)
},
offsetHeight: {
value: jest.fn().mockReturnValue(10)
}
});
document.hasFocus = jest.fn().mockImplementation(() => true);
describe("FocusTrap Mixin", () => {
@customElement("focusable-child")
class FocusableChild extends LitElement {
@property({ type: Boolean }) isHidden = false;
render() {
return html`
`;
}
}
@customElement("focusable-element")
class FocusableElement extends LitElement {
render() {
return html`
Here's some focusable parts outside the trap.
Here's some more focusable parts outside the trap.
`;
}
}
afterEach(fixtureCleanup);
let el: FocusableElement;
beforeEach(async () => {
el = await fixture(
html`
`
);
});
@customElement("focus-trap")
class FocusTrap extends FocusTrapMixin(FocusTrapMixin(LitElement)) {
@query(".deactivate") disable!: HTMLButtonElement;
@query(".activate") enable!: HTMLButtonElement;
render() {
return html`
Here's a focus trap with some focusable parts.
`;
}
}
test("should applying to component", async () => {
const tag = defineCE(class extends FocusTrapMixin(FocusTrap) {});
const element = await fixture(`<${tag}>${tag}>`);
expect(element).toBeDefined();
});
test("should traverse all nested shadow roots, slots and html elements", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap")!;
focusTrap.activeFocusTrap = true;
focusTrap["activateFocusTrap"]!();
await elementUpdated(focusTrap);
focusTrap["activateFocusTrap"]!();
focusTrap["setFocusableElements"]!();
await elementUpdated(focusTrap);
expect(focusTrap["focusableElements"]!.length).toEqual(12);
});
test("should initialize focusableElements on firstUpdated lifecycle", async () => {
const mixin = (superclass: AnyConstructor) =>
class extends superclass {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this.dispatchEvent(new CustomEvent("first-updated"));
}
};
const Tag = defineCE(class extends mixin(FocusTrap) {});
const element = fixtureSync(`<${Tag}>${Tag}>`);
const event = await oneEvent(element, "first-updated");
expect(event).toBeDefined();
});
test("should handle lifecycle callbacks", async () => {
const tag = defineCE(
class extends FocusTrap {
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(new CustomEvent("connected-callback"));
}
disconnectedCallback() {
super.disconnectedCallback();
this.dispatchEvent(new CustomEvent("disconnected-callback"));
}
}
);
const element = document.createElement(`${tag}`) as FocusTrap;
setTimeout(() => element.connectedCallback());
const connectedEvent = await oneEvent(element, "connected-callback");
expect(connectedEvent).toBeDefined();
const focusTrap = await fixture(element);
focusTrap.parentNode!.removeChild(element);
setTimeout(() => focusTrap.disconnectedCallback());
const disconnectedEvent = await oneEvent(focusTrap, "disconnected-callback");
expect(disconnectedEvent).toBeDefined();
});
test("should handle internal event listeners", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap")!;
focusTrap["activateFocusTrap"]!();
focusTrap["setFocusableElements"]!();
await elementUpdated(focusTrap);
const event = new MouseEvent("click");
focusTrap.enable.dispatchEvent(event);
await elementUpdated(focusTrap);
const keyEvent = new KeyboardEvent("keydown", {
code: Key.Tab
});
const shiftKeyEvent = new KeyboardEvent("keydown", {
code: Key.Tab,
shiftKey: true
});
const arrowEvent = new KeyboardEvent("keydown", {
code: Key.ArrowDown
});
focusTrap.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
expect(focusTrap.focusTrapIndex).toEqual(0);
expect(focusTrap["getDeepActiveElement"]!()).toEqual(focusTrap["focusableElements"]![0]);
focusTrap.focusTrapIndex = 0;
await elementUpdated(focusTrap);
focusTrap.dispatchEvent(shiftKeyEvent);
await nextFrame();
await elementUpdated(el);
expect(focusTrap.focusTrapIndex).toEqual(11);
expect(focusTrap["getDeepActiveElement"]!()).toEqual(focusTrap["focusableElements"]![11]);
focusTrap.focusTrapIndex = 11;
await elementUpdated(focusTrap);
focusTrap.dispatchEvent(arrowEvent);
await nextFrame();
await elementUpdated(el);
expect(focusTrap.focusTrapIndex).toEqual(11);
expect(focusTrap["getDeepActiveElement"]!()).toEqual(focusTrap["focusableElements"]![11]);
focusTrap.focusTrapIndex = 11;
await elementUpdated(focusTrap);
const clickEvent = new MouseEvent("click");
document.dispatchEvent(clickEvent);
focusTrap.blur();
await nextFrame();
await elementUpdated(el);
expect(focusTrap.focusTrapIndex).toEqual(null);
});
test("should deactivate focus trap is property is provided", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap")!;
focusTrap["activateFocusTrap"]!();
focusTrap["setFocusableElements"]!();
await elementUpdated(focusTrap);
const event = new MouseEvent("click");
focusTrap.enable.dispatchEvent(event);
await elementUpdated(focusTrap);
expect(focusTrap.activeFocusTrap).toBeTruthy();
focusTrap.disable.dispatchEvent(event);
await elementUpdated(focusTrap);
expect(focusTrap.activeFocusTrap).toBeFalsy();
expect(focusTrap.focusTrapIndex).toBeNull();
const keyEvent = new KeyboardEvent("keydown", {
code: Key.Tab
});
focusTrap.dispatchEvent(keyEvent);
await elementUpdated(focusTrap);
expect(focusTrap.activeFocusTrap).toBeFalsy();
focusTrap.enable.dispatchEvent(event);
await elementUpdated(focusTrap);
expect(focusTrap.activeFocusTrap).toBeTruthy();
});
test("should focus custom element", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap");
const focusableChild = focusTrap!.querySelector("focusable-child");
focusableChild!.tabIndex = 0;
focusTrap!["activateFocusTrap"]!();
focusTrap!["setFocusableElements"]!();
await elementUpdated(focusTrap!);
const keyEvent = new KeyboardEvent("keydown", {
code: Key.Tab
});
const event = new MouseEvent("click");
focusTrap!.enable.dispatchEvent(event);
await elementUpdated(focusTrap!);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
focusTrap!.dispatchEvent(keyEvent);
await nextFrame();
await elementUpdated(el);
expect(focusTrap!["getDeepActiveElement"]!()).toEqual(focusableChild);
});
test("should prefer autofocus element in tab sequence", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap");
const focusableChild = focusTrap!.querySelector("focusable-child");
const mdInput = focusableChild!.shadowRoot!.querySelector("md-input") as any;
mdInput!.autofocus = true;
focusTrap!["activateFocusTrap"]!();
focusTrap!["setFocusableElements"]!();
await nextFrame();
await elementUpdated(el);
focusTrap!["setInitialFocus"]!();
await nextFrame();
await elementUpdated(el);
const input = mdInput!.shadowRoot!.querySelector("input");
expect(focusTrap!["getDeepActiveElement"]!()).toEqual(input);
});
test("should change focus trap index on click", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap");
focusTrap!["activateFocusTrap"]!();
focusTrap!["setFocusableElements"]!();
focusTrap!["initialFocusComplete"] = true;
await elementUpdated(focusTrap!);
const button = focusTrap!.querySelector("div button");
await elementUpdated(focusTrap!);
button!.click();
await nextFrame();
expect(focusTrap!.focusTrapIndex).toEqual(4);
expect(focusTrap!["getDeepActiveElement"]!()).toEqual(button);
});
test("should change focus trap index if focus-visible event was dispatched", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap");
const focusableChild = focusTrap!.querySelector("focusable-child");
focusTrap!["activateFocusTrap"]!();
focusTrap!["setFocusableElements"]!();
focusTrap!["initialFocusComplete"] = true;
await nextFrame();
await elementUpdated(el);
const mdInput = focusableChild!.shadowRoot!.querySelector("md-input") as any;
const input = mdInput!.shadowRoot!.querySelector("input");
input!.click();
await nextFrame();
expect(focusTrap!.focusTrapIndex).toEqual(6);
expect(focusTrap!["getDeepActiveElement"]!()).toEqual(input);
});
test("szhould change focus trap index if new focusable element was clicked", async () => {
const focusTrap = el.shadowRoot!.querySelector("focus-trap");
const focusableChild = focusTrap!.querySelector("focusable-child");
const mdInput = focusableChild!.shadowRoot!.querySelector("md-input") as any;
const input = mdInput!.shadowRoot!.querySelector("input");
mdInput!.tabIndex = -1;
input!.tabIndex = -1;
await elementUpdated(el);
focusTrap!["activateFocusTrap"]!();
focusTrap!["setFocusableElements"]!();
await nextFrame();
await elementUpdated(el);
input!.focus();
await nextFrame();
await elementUpdated(el);
expect(focusTrap!["getDeepActiveElement"]!()).toEqual(input);
});
});