diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts
index eea592cbe82..6f97b2481e4 100644
--- a/packages/calcite-components/src/components/modal/modal.e2e.ts
+++ b/packages/calcite-components/src/components/modal/modal.e2e.ts
@@ -1,10 +1,10 @@
-import { newE2EPage } from "@stencil/core/testing";
+import { E2EPage, newE2EPage } from "@stencil/core/testing";
import { focusable, hidden, openClose, renders, slots, t9n } from "../../tests/commonTests";
import { html } from "../../../support/formatting";
import { CSS, SLOTS } from "./resources";
import { GlobalTestProps, isElementFocused, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils";
-describe("calcite-modal properties", () => {
+describe("calcite-modal", () => {
describe("renders", () => {
renders("calcite-modal", { display: "flex", visible: false });
});
@@ -26,6 +26,10 @@ describe("calcite-modal properties", () => {
slots("calcite-modal", SLOTS);
});
+ describe("translation support", () => {
+ t9n("calcite-modal");
+ });
+
it("should hide closeButton when disabled", async () => {
const page = await newE2EPage();
await page.setContent("");
@@ -169,363 +173,363 @@ describe("calcite-modal properties", () => {
expect(mockCallBack).toHaveBeenCalledTimes(1);
expect(await modal.getProperty("opened")).toBe(false);
});
-});
-it("calls the beforeClose method prior to closing via attribute", async () => {
- const page = await newE2EPage();
- const mockCallBack = jest.fn();
- await page.exposeFunction("beforeClose", mockCallBack);
- await page.setContent(`
+ it("calls the beforeClose method prior to closing via attribute", async () => {
+ const page = await newE2EPage();
+ const mockCallBack = jest.fn();
+ await page.exposeFunction("beforeClose", mockCallBack);
+ await page.setContent(`
`);
- const modal = await page.find("calcite-modal");
- await page.$eval(
- "calcite-modal",
- (el: HTMLCalciteModalElement) =>
- (el.beforeClose = (
- window as GlobalTestProps<{ beforeClose: HTMLCalciteModalElement["beforeClose"] }>
- ).beforeClose)
- );
- await page.waitForChanges();
- modal.setProperty("open", true);
- await page.waitForChanges();
- expect(await modal.getProperty("opened")).toBe(true);
- modal.removeAttribute("open");
- await page.waitForChanges();
- expect(mockCallBack).toHaveBeenCalledTimes(1);
- expect(await modal.getProperty("opened")).toBe(false);
-});
-
-it("should handle rejected 'beforeClose' promise'", async () => {
- const page = await newE2EPage();
-
- const mockCallBack = jest.fn().mockReturnValue(() => Promise.reject());
- await page.exposeFunction("beforeClose", mockCallBack);
+ const modal = await page.find("calcite-modal");
+ await page.$eval(
+ "calcite-modal",
+ (el: HTMLCalciteModalElement) =>
+ (el.beforeClose = (
+ window as GlobalTestProps<{ beforeClose: HTMLCalciteModalElement["beforeClose"] }>
+ ).beforeClose)
+ );
+ await page.waitForChanges();
+ modal.setProperty("open", true);
+ await page.waitForChanges();
+ expect(await modal.getProperty("opened")).toBe(true);
+ modal.removeAttribute("open");
+ await page.waitForChanges();
+ expect(mockCallBack).toHaveBeenCalledTimes(1);
+ expect(await modal.getProperty("opened")).toBe(false);
+ });
- await page.setContent(``);
+ it("should handle rejected 'beforeClose' promise'", async () => {
+ const page = await newE2EPage();
- await page.$eval(
- "calcite-modal",
- (elm: HTMLCalciteModalElement) =>
- (elm.beforeClose = (window as typeof window & Pick).beforeClose)
- );
+ const mockCallBack = jest.fn().mockReturnValue(() => Promise.reject());
+ await page.exposeFunction("beforeClose", mockCallBack);
- const modal = await page.find("calcite-modal");
- modal.setProperty("open", false);
- await page.waitForChanges();
+ await page.setContent(``);
- expect(mockCallBack).toHaveBeenCalledTimes(1);
-});
+ await page.$eval(
+ "calcite-modal",
+ (elm: HTMLCalciteModalElement) =>
+ (elm.beforeClose = (window as typeof window & Pick).beforeClose)
+ );
-it("should remain open with rejected 'beforeClose' promise'", async () => {
- const page = await newE2EPage();
+ const modal = await page.find("calcite-modal");
+ modal.setProperty("open", false);
+ await page.waitForChanges();
- await page.exposeFunction("beforeClose", () => Promise.reject());
- await page.setContent(``);
+ expect(mockCallBack).toHaveBeenCalledTimes(1);
+ });
- await page.$eval(
- "calcite-modal",
- (elm: HTMLCalciteModalElement) =>
- (elm.beforeClose = (window as typeof window & Pick).beforeClose)
- );
+ it("should remain open with rejected 'beforeClose' promise'", async () => {
+ const page = await newE2EPage();
- const modal = await page.find("calcite-modal");
- modal.setProperty("open", false);
- await page.waitForChanges();
+ await page.exposeFunction("beforeClose", () => Promise.reject());
+ await page.setContent(``);
- expect(await modal.getProperty("open")).toBe(true);
- expect(await modal.getProperty("opened")).toBe(true);
- expect(modal.getAttribute("open")).toBe(""); // Makes sure attribute is added back
-});
+ await page.$eval(
+ "calcite-modal",
+ (elm: HTMLCalciteModalElement) =>
+ (elm.beforeClose = (window as typeof window & Pick).beforeClose)
+ );
-describe("opening and closing behavior", () => {
- it("opens and closes", async () => {
- const page = await newE2EPage();
- await page.setContent(html``);
const modal = await page.find("calcite-modal");
+ modal.setProperty("open", false);
+ await page.waitForChanges();
- type ModalEventOrderWindow = GlobalTestProps<{ events: string[] }>;
-
- await page.$eval("calcite-modal", (modal: HTMLCalciteModalElement) => {
- const receivedEvents: string[] = [];
- (window as ModalEventOrderWindow).events = receivedEvents;
+ expect(await modal.getProperty("open")).toBe(true);
+ expect(await modal.getProperty("opened")).toBe(true);
+ expect(modal.getAttribute("open")).toBe(""); // Makes sure attribute is added back
+ });
- ["calciteModalBeforeOpen", "calciteModalOpen", "calciteModalBeforeClose", "calciteModalClose"].forEach(
- (eventType) => {
- modal.addEventListener(eventType, (event) => receivedEvents.push(event.type));
- }
- );
- });
+ describe("opening and closing behavior", () => {
+ it("opens and closes", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html``);
+ const modal = await page.find("calcite-modal");
- const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen");
- const openSpy = await modal.spyOnEvent("calciteModalOpen");
- const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose");
- const closeSpy = await modal.spyOnEvent("calciteModalClose");
+ type ModalEventOrderWindow = GlobalTestProps<{ events: string[] }>;
- expect(beforeOpenSpy).toHaveReceivedEventTimes(0);
- expect(openSpy).toHaveReceivedEventTimes(0);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
- expect(closeSpy).toHaveReceivedEventTimes(0);
+ await page.$eval("calcite-modal", (modal: HTMLCalciteModalElement) => {
+ const receivedEvents: string[] = [];
+ (window as ModalEventOrderWindow).events = receivedEvents;
- expect(await modal.isVisible()).toBe(false);
-
- const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen");
- const modalOpen = page.waitForEvent("calciteModalOpen");
- await modal.setProperty("open", true);
- await page.waitForChanges();
+ ["calciteModalBeforeOpen", "calciteModalOpen", "calciteModalBeforeClose", "calciteModalClose"].forEach(
+ (eventType) => {
+ modal.addEventListener(eventType, (event) => receivedEvents.push(event.type));
+ }
+ );
+ });
- await modalBeforeOpen;
- await modalOpen;
+ const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen");
+ const openSpy = await modal.spyOnEvent("calciteModalOpen");
+ const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose");
+ const closeSpy = await modal.spyOnEvent("calciteModalClose");
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
- expect(closeSpy).toHaveReceivedEventTimes(0);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(0);
+ expect(openSpy).toHaveReceivedEventTimes(0);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
+ expect(closeSpy).toHaveReceivedEventTimes(0);
- expect(await modal.isVisible()).toBe(true);
+ expect(await modal.isVisible()).toBe(false);
- const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose");
- const modalClose = page.waitForEvent("calciteModalClose");
- await modal.setProperty("open", false);
- await page.waitForChanges();
+ const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen");
+ const modalOpen = page.waitForEvent("calciteModalOpen");
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
- await modalBeforeClose;
- await modalClose;
+ await modalBeforeOpen;
+ await modalOpen;
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
- expect(closeSpy).toHaveReceivedEventTimes(1);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
+ expect(closeSpy).toHaveReceivedEventTimes(0);
- expect(await modal.isVisible()).toBe(false);
+ expect(await modal.isVisible()).toBe(true);
- expect(await page.evaluate(() => (window as ModalEventOrderWindow).events)).toEqual([
- "calciteModalBeforeOpen",
- "calciteModalOpen",
- "calciteModalBeforeClose",
- "calciteModalClose",
- ]);
- });
+ const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose");
+ const modalClose = page.waitForEvent("calciteModalClose");
+ await modal.setProperty("open", false);
+ await page.waitForChanges();
- it("emits when closing on click", async () => {
- const page = await newE2EPage();
- await page.setContent(html``);
- const modal = await page.find("calcite-modal");
+ await modalBeforeClose;
+ await modalClose;
- const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen");
- const openSpy = await modal.spyOnEvent("calciteModalOpen");
- const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose");
- const closeSpy = await modal.spyOnEvent("calciteModalClose");
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
+ expect(closeSpy).toHaveReceivedEventTimes(1);
- expect(beforeOpenSpy).toHaveReceivedEventTimes(0);
- expect(openSpy).toHaveReceivedEventTimes(0);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
- expect(closeSpy).toHaveReceivedEventTimes(0);
+ expect(await modal.isVisible()).toBe(false);
- expect(await modal.isVisible()).toBe(false);
+ expect(await page.evaluate(() => (window as ModalEventOrderWindow).events)).toEqual([
+ "calciteModalBeforeOpen",
+ "calciteModalOpen",
+ "calciteModalBeforeClose",
+ "calciteModalClose",
+ ]);
+ });
- const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen");
- const modalOpen = page.waitForEvent("calciteModalOpen");
- modal.setProperty("open", true);
- await page.waitForChanges();
+ it("emits when closing on click", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html``);
+ const modal = await page.find("calcite-modal");
- await modalBeforeOpen;
- await modalOpen;
+ const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen");
+ const openSpy = await modal.spyOnEvent("calciteModalOpen");
+ const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose");
+ const closeSpy = await modal.spyOnEvent("calciteModalClose");
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
- expect(closeSpy).toHaveReceivedEventTimes(0);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(0);
+ expect(openSpy).toHaveReceivedEventTimes(0);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
+ expect(closeSpy).toHaveReceivedEventTimes(0);
- expect(await modal.isVisible()).toBe(true);
+ expect(await modal.isVisible()).toBe(false);
- const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose");
- const modalClose = page.waitForEvent("calciteModalClose");
- const closeButton = await page.find(`calcite-modal >>> .${CSS.close}`);
- await closeButton.click();
- await page.waitForChanges();
+ const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen");
+ const modalOpen = page.waitForEvent("calciteModalOpen");
+ modal.setProperty("open", true);
+ await page.waitForChanges();
- await modalBeforeClose;
- await modalClose;
+ await modalBeforeOpen;
+ await modalOpen;
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
- expect(closeSpy).toHaveReceivedEventTimes(1);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(0);
+ expect(closeSpy).toHaveReceivedEventTimes(0);
- expect(await modal.isVisible()).toBe(false);
- });
+ expect(await modal.isVisible()).toBe(true);
- it("emits when set to open on initial render", async () => {
- const page = await newProgrammaticE2EPage();
+ const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose");
+ const modalClose = page.waitForEvent("calciteModalClose");
+ const closeButton = await page.find(`calcite-modal >>> .${CSS.close}`);
+ await closeButton.click();
+ await page.waitForChanges();
- const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
- const openSpy = await page.spyOnEvent("calciteModalOpen");
+ await modalBeforeClose;
+ await modalClose;
- const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen");
- const waitForOpenEvent = page.waitForEvent("calciteModalOpen");
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
+ expect(closeSpy).toHaveReceivedEventTimes(1);
- await page.evaluate((): void => {
- const modal = document.createElement("calcite-modal");
- modal.open = true;
- document.body.append(modal);
+ expect(await modal.isVisible()).toBe(false);
});
- await page.waitForChanges();
- await waitForBeforeOpenEvent;
- await waitForOpenEvent;
+ it("emits when set to open on initial render", async () => {
+ const page = await newProgrammaticE2EPage();
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- });
+ const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
+ const openSpy = await page.spyOnEvent("calciteModalOpen");
- it("emits when set to open on initial render and duration is 0", async () => {
- const page = await newProgrammaticE2EPage();
- await skipAnimations(page);
+ const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen");
+ const waitForOpenEvent = page.waitForEvent("calciteModalOpen");
- const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
- const openSpy = await page.spyOnEvent("calciteModalOpen");
+ await page.evaluate((): void => {
+ const modal = document.createElement("calcite-modal");
+ modal.open = true;
+ document.body.append(modal);
+ });
- const waitForOpenEvent = page.waitForEvent("calciteModalOpen");
- const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen");
+ await page.waitForChanges();
+ await waitForBeforeOpenEvent;
+ await waitForOpenEvent;
- await page.evaluate((): void => {
- const modal = document.createElement("calcite-modal");
- modal.open = true;
- document.body.append(modal);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
});
- await page.waitForChanges();
- await waitForBeforeOpenEvent;
- await waitForOpenEvent;
+ it("emits when set to open on initial render and duration is 0", async () => {
+ const page = await newProgrammaticE2EPage();
+ await skipAnimations(page);
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
- });
+ const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
+ const openSpy = await page.spyOnEvent("calciteModalOpen");
- it("emits when duration is set to 0", async () => {
- const page = await newProgrammaticE2EPage();
- await skipAnimations(page);
+ const waitForOpenEvent = page.waitForEvent("calciteModalOpen");
+ const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen");
- const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
- const openSpy = await page.spyOnEvent("calciteModalOpen");
+ await page.evaluate((): void => {
+ const modal = document.createElement("calcite-modal");
+ modal.open = true;
+ document.body.append(modal);
+ });
- const beforeCloseSpy = await page.spyOnEvent("calciteModalBeforeClose");
- const closeSpy = await page.spyOnEvent("calciteModalClose");
+ await page.waitForChanges();
+ await waitForBeforeOpenEvent;
+ await waitForOpenEvent;
- await page.evaluate((): void => {
- const modal = document.createElement("calcite-modal");
- modal.open = true;
- document.body.append(modal);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
});
- await page.waitForChanges();
- await beforeOpenSpy;
- await openSpy;
-
- expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
- expect(openSpy).toHaveReceivedEventTimes(1);
+ it("emits when duration is set to 0", async () => {
+ const page = await newProgrammaticE2EPage();
+ await skipAnimations(page);
- await page.evaluate(() => {
- const modal = document.querySelector("calcite-modal");
- modal.open = false;
- });
+ const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen");
+ const openSpy = await page.spyOnEvent("calciteModalOpen");
- await page.waitForChanges();
- await beforeCloseSpy;
- await closeSpy;
+ const beforeCloseSpy = await page.spyOnEvent("calciteModalBeforeClose");
+ const closeSpy = await page.spyOnEvent("calciteModalClose");
- expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
- expect(closeSpy).toHaveReceivedEventTimes(1);
- });
-});
+ await page.evaluate((): void => {
+ const modal = document.createElement("calcite-modal");
+ modal.open = true;
+ document.body.append(modal);
+ });
-describe("calcite-modal accessibility checks", () => {
- it("traps focus within the modal when open", async () => {
- const button1Id = "button1";
- const button2Id = "button2";
- const page = await newE2EPage();
- await page.setContent(
- html`
-
-
-
-
- `
- );
- const modal = await page.find("calcite-modal");
- const opened = page.waitForEvent("calciteModalOpen");
- modal.setProperty("open", true);
- await page.waitForChanges();
- await opened;
+ await page.waitForChanges();
+ await beforeOpenSpy;
+ await openSpy;
- expect(await isElementFocused(page, `.${CSS.close}`, { shadowed: true })).toBe(true);
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+ expect(beforeOpenSpy).toHaveReceivedEventTimes(1);
+ expect(openSpy).toHaveReceivedEventTimes(1);
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `.${CSS.close}`, { shadowed: true })).toBe(true);
- await page.keyboard.down("Shift");
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+ await page.evaluate(() => {
+ const modal = document.querySelector("calcite-modal");
+ modal.open = false;
+ });
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
- });
+ await page.waitForChanges();
+ await beforeCloseSpy;
+ await closeSpy;
- it("restores focus to previously focused element when closed", async () => {
- const initiallyFocusedId = "initially-focused";
- const initiallyFocusedIdSelector = `#${initiallyFocusedId}`;
- const page = await newE2EPage();
- await page.setContent(
- html`
-
-
- `
- );
- await skipAnimations(page);
- const modal = await page.find("calcite-modal");
- await page.$eval(initiallyFocusedIdSelector, (button: HTMLButtonElement) => {
- button.focus();
+ expect(beforeCloseSpy).toHaveReceivedEventTimes(1);
+ expect(closeSpy).toHaveReceivedEventTimes(1);
});
- await modal.setProperty("open", true);
- await page.waitForChanges();
- await modal.setProperty("open", false);
- await page.waitForChanges();
- expect(await isElementFocused(page, initiallyFocusedIdSelector)).toBe(true);
});
- it("traps focus within the modal when open and disabled close button", async () => {
- const button1Id = "button1";
- const button2Id = "button2";
- const page = await newE2EPage();
- await page.setContent(
- html`
-
-
-
-
- `
- );
- await skipAnimations(page);
- const modal = await page.find("calcite-modal");
-
- await modal.setProperty("open", true);
- await page.waitForChanges();
- expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ describe("calcite-modal accessibility checks", () => {
+ it("traps focus within the modal when open", async () => {
+ const button1Id = "button1";
+ const button2Id = "button2";
+ const page = await newE2EPage();
+ await page.setContent(
+ html`
+
+
+
+
+ `
+ );
+ const modal = await page.find("calcite-modal");
+ const opened = page.waitForEvent("calciteModalOpen");
+ modal.setProperty("open", true);
+ await page.waitForChanges();
+ await opened;
+
+ expect(await isElementFocused(page, `.${CSS.close}`, { shadowed: true })).toBe(true);
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `.${CSS.close}`, { shadowed: true })).toBe(true);
+ await page.keyboard.down("Shift");
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ });
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+ it("restores focus to previously focused element when closed", async () => {
+ const initiallyFocusedId = "initially-focused";
+ const initiallyFocusedIdSelector = `#${initiallyFocusedId}`;
+ const page = await newE2EPage();
+ await page.setContent(
+ html`
+
+
+ `
+ );
+ await skipAnimations(page);
+ const modal = await page.find("calcite-modal");
+ await page.$eval(initiallyFocusedIdSelector, (button: HTMLButtonElement) => {
+ button.focus();
+ });
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
+ await modal.setProperty("open", false);
+ await page.waitForChanges();
+ expect(await isElementFocused(page, initiallyFocusedIdSelector)).toBe(true);
+ });
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
- await page.keyboard.down("Shift");
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
- await page.keyboard.press("Tab");
- expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ it("traps focus within the modal when open and disabled close button", async () => {
+ const button1Id = "button1";
+ const button2Id = "button2";
+ const page = await newE2EPage();
+ await page.setContent(
+ html`
+
+
+
+
+ `
+ );
+ await skipAnimations(page);
+ const modal = await page.find("calcite-modal");
+
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
+ expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ await page.keyboard.down("Shift");
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button2Id}`)).toBe(true);
+ await page.keyboard.press("Tab");
+ expect(await isElementFocused(page, `#${button1Id}`)).toBe(true);
+ });
});
describe("setFocus", () => {
@@ -595,7 +599,7 @@ describe("calcite-modal accessibility checks", () => {
it("closes and allows re-opening when Close button is clicked", async () => {
const page = await newE2EPage();
- await page.setContent(``);
+ await page.setContent(``);
await skipAnimations(page);
const modal = await page.find("calcite-modal");
modal.setProperty("open", true);
@@ -647,42 +651,102 @@ describe("calcite-modal accessibility checks", () => {
expect(modal).toHaveAttribute("open");
});
- it("correctly adds overflow class on document when open", async () => {
- const page = await newE2EPage();
- await page.setContent(``);
- const modal = await page.find("calcite-modal");
- await modal.setProperty("open", true);
- await page.waitForChanges();
- const isOverflowHidden = await page.evaluate(() => {
- return document.documentElement.style.overflow === "hidden";
+ describe("overflow prevention", () => {
+ async function hasOverflowStyle(page: E2EPage): Promise {
+ return page.evaluate(() => document.documentElement.style.overflow === "hidden");
+ }
+
+ it("correctly sets overflow style on document when opened/closed", async () => {
+ const page = await newE2EPage();
+ await page.setContent(``);
+ const modal = await page.find("calcite-modal");
+
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(true);
+
+ await modal.setProperty("open", false);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(false);
});
- expect(isOverflowHidden).toEqual(true);
- });
- it("correctly does not add overflow class on document when open and slotted in shell modals slot", async () => {
- const page = await newE2EPage();
- await page.setContent(``);
- const modal = await page.find("calcite-modal");
- await modal.setProperty("open", true);
- await page.waitForChanges();
- const isOverflowHidden = await page.evaluate(() => {
- return document.documentElement.style.overflow === "hidden";
+ it("preserves existing overflow style when modal is opened/closed", async () => {
+ const page = await newE2EPage();
+ await page.setContent(``);
+ await page.evaluate(() => (document.documentElement.style.overflow = "scroll"));
+ const modal = await page.find("calcite-modal");
+
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(true);
+
+ await modal.setProperty("open", false);
+ await page.waitForChanges();
+
+ expect(await page.evaluate(() => document.documentElement.style.overflow)).toEqual("scroll");
});
- expect(isOverflowHidden).toEqual(false);
- });
- it("correctly removes overflow class on document once closed", async () => {
- const page = await newE2EPage();
- await page.setContent(``);
- const modal = await page.find("calcite-modal");
- await modal.setProperty("open", true);
- await page.waitForChanges();
- await modal.setProperty("open", false);
- await page.waitForChanges();
- const documentClass = await page.evaluate(() => {
- return document.documentElement.classList.contains("overflow-hidden");
+ it("correctly does not add overflow style on document when open and slotted in shell modals slot", async () => {
+ const page = await newE2EPage();
+ await page.setContent(``);
+ const modal = await page.find("calcite-modal");
+
+ await modal.setProperty("open", true);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(false);
+ });
+
+ it("correctly removes overflow style on document when multiple modals are closed in first-in-last-out order", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+ `);
+ const modal1 = await page.find("#modal-1");
+ const modal2 = await page.find("#modal-2");
+
+ await modal1.setProperty("open", true);
+ await page.waitForChanges();
+ await modal2.setProperty("open", true);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(true);
+
+ await modal2.setProperty("open", false);
+ await page.waitForChanges();
+ await modal1.setProperty("open", false);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(false);
+ });
+
+ it("correctly removes overflow style on document when multiple modals are closed in first-in-first-out order", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+ `);
+ const modal1 = await page.find("#modal-1");
+ const modal2 = await page.find("#modal-2");
+
+ await modal1.setProperty("open", true);
+ await page.waitForChanges();
+ await modal2.setProperty("open", true);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(true);
+
+ await modal1.setProperty("open", false);
+ await page.waitForChanges();
+ await modal2.setProperty("open", false);
+ await page.waitForChanges();
+
+ expect(await hasOverflowStyle(page)).toEqual(false);
});
- expect(documentClass).toEqual(false);
});
it("renders correctly with no footer", async () => {
@@ -762,8 +826,4 @@ describe("calcite-modal accessibility checks", () => {
closeIcon = await page.find('calcite-modal >>> calcite-icon[scale="m"]');
expect(closeIcon).not.toBe(null);
});
-
- describe("translation support", () => {
- t9n("calcite-modal");
- });
});
diff --git a/packages/calcite-components/src/components/modal/modal.tsx b/packages/calcite-components/src/components/modal/modal.tsx
index e05d3b1d1d2..e5b0877592c 100644
--- a/packages/calcite-components/src/components/modal/modal.tsx
+++ b/packages/calcite-components/src/components/modal/modal.tsx
@@ -54,6 +54,9 @@ import { ModalMessages } from "./assets/modal/t9n";
import { getIconScale } from "../../utils/component";
+let totalOpenModals: number = 0;
+let initialDocumentOverflowStyle: string = "";
+
/**
* @slot header - A slot for adding header text.
* @slot content - A slot for adding the component's content.
@@ -539,7 +542,11 @@ export class Modal
this.contentId = ensureId(contentEl);
if (!this.slottedInShell) {
- this.initialOverflowCSS = document.documentElement.style.overflow;
+ if (totalOpenModals === 0) {
+ initialDocumentOverflowStyle = document.documentElement.style.overflow;
+ }
+
+ totalOpenModals++;
// use an inline style instead of a utility class to avoid global class declarations.
document.documentElement.style.setProperty("overflow", "hidden");
}
@@ -568,12 +575,13 @@ export class Modal
}
}
+ totalOpenModals--;
this.opened = false;
this.removeOverflowHiddenClass();
};
private removeOverflowHiddenClass(): void {
- document.documentElement.style.setProperty("overflow", this.initialOverflowCSS);
+ document.documentElement.style.setProperty("overflow", initialDocumentOverflowStyle);
}
private handleMutationObserver = (): void => {