diff --git a/packages/calcite-components/src/components/modal/modal.e2e.ts b/packages/calcite-components/src/components/modal/modal.e2e.ts index 66131631a17..30a4a205732 100644 --- a/packages/calcite-components/src/components/modal/modal.e2e.ts +++ b/packages/calcite-components/src/components/modal/modal.e2e.ts @@ -1,8 +1,8 @@ import { newE2EPage } from "@stencil/core/testing"; import { focusable, renders, slots, hidden, t9n } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; -import { CSS, SLOTS, DURATIONS } from "./resources"; -import { isElementFocused, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils"; +import { CSS, SLOTS } from "./resources"; +import { GlobalTestProps, isElementFocused, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils"; describe("calcite-modal properties", () => { describe("renders", () => { @@ -132,29 +132,24 @@ describe("calcite-modal properties", () => { }); describe("opening and closing behavior", () => { - function getTransitionTransform( - modalSelector: string, - modalContainerSelector: string, - type: "none" | "matrix" - ): boolean { - const modalContainer = document - .querySelector(modalSelector) - .shadowRoot.querySelector(modalContainerSelector); - return getComputedStyle(modalContainer).transform.startsWith(type); - } - - const getTransitionDuration = (): { duration: string } => { - const modal = document.querySelector("calcite-modal"); - const { transitionDuration } = window.getComputedStyle(modal); - return { - duration: transitionDuration, - }; - }; - - it.skip("opens and closes", async () => { + it("opens and closes", async () => { const page = await newE2EPage(); - await page.setContent(html``); + await page.setContent(html``); const modal = await page.find("calcite-modal"); + + type ModalEventOrderWindow = GlobalTestProps<{ events: string[] }>; + + await page.$eval("calcite-modal", (modal: HTMLCalciteModalElement) => { + const receivedEvents: string[] = []; + (window as ModalEventOrderWindow).events = receivedEvents; + + ["calciteModalBeforeOpen", "calciteModalOpen", "calciteModalBeforeClose", "calciteModalClose"].forEach( + (eventType) => { + modal.addEventListener(eventType, (event) => receivedEvents.push(event.type)); + } + ); + }); + const beforeOpenSpy = await modal.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await modal.spyOnEvent("calciteModalOpen"); const beforeCloseSpy = await modal.spyOnEvent("calciteModalBeforeClose"); @@ -164,54 +159,45 @@ describe("opening and closing behavior", () => { expect(openSpy).toHaveReceivedEventTimes(0); expect(beforeCloseSpy).toHaveReceivedEventTimes(0); expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); + expect(await modal.isVisible()).toBe(false); + + const modalBeforeOpen = page.waitForEvent("calciteModalBeforeOpen"); + const modalOpen = page.waitForEvent("calciteModalOpen"); await modal.setProperty("open", true); - let waitForEvent = page.waitForEvent("calciteModalBeforeOpen"); await page.waitForChanges(); - await waitForEvent; - - expect(beforeOpenSpy).toHaveReceivedEventTimes(1); - expect(openSpy).toHaveReceivedEventTimes(0); - expect(beforeCloseSpy).toHaveReceivedEventTimes(0); - expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - waitForEvent = page.waitForEvent("calciteModalOpen"); - await waitForEvent; + await modalBeforeOpen; + await modalOpen; expect(beforeOpenSpy).toHaveReceivedEventTimes(1); expect(openSpy).toHaveReceivedEventTimes(1); expect(beforeCloseSpy).toHaveReceivedEventTimes(0); expect(closeSpy).toHaveReceivedEventTimes(0); - expect(await modal.getProperty("open")).toBe(true); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); + expect(await modal.isVisible()).toBe(true); + + const modalBeforeClose = page.waitForEvent("calciteModalBeforeClose"); + const modalClose = page.waitForEvent("calciteModalClose"); await modal.setProperty("open", false); - waitForEvent = page.waitForEvent("calciteModalBeforeClose"); await page.waitForChanges(); - await waitForEvent; - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual(`${DURATIONS.test}s`); + await modalBeforeClose; + await modalClose; expect(beforeOpenSpy).toHaveReceivedEventTimes(1); expect(openSpy).toHaveReceivedEventTimes(1); expect(beforeCloseSpy).toHaveReceivedEventTimes(1); - expect(closeSpy).toHaveReceivedEventTimes(0); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); + expect(closeSpy).toHaveReceivedEventTimes(1); - waitForEvent = page.waitForEvent("calciteModalClose"); - await waitForEvent; + expect(await modal.isVisible()).toBe(false); - expect(beforeOpenSpy).toHaveReceivedEventTimes(1); - expect(openSpy).toHaveReceivedEventTimes(1); - expect(beforeCloseSpy).toHaveReceivedEventTimes(1); - expect(closeSpy).toHaveReceivedEventTimes(1); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "matrix"); - await page.waitForFunction(getTransitionTransform, {}, "calcite-modal", `.${CSS.modal}`, "none"); - expect(await modal.getProperty("open")).toBe(false); + expect(await page.evaluate(() => (window as ModalEventOrderWindow).events)).toEqual([ + "calciteModalBeforeOpen", + "calciteModalOpen", + "calciteModalBeforeClose", + "calciteModalClose", + ]); }); it("emits when set to open on initial render", async () => { @@ -220,18 +206,16 @@ describe("opening and closing behavior", () => { const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await page.spyOnEvent("calciteModalOpen"); - await page.evaluate((transitionDuration: string): void => { + const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); + const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + + await page.evaluate((): void => { const modal = document.createElement("calcite-modal"); modal.open = true; - modal.style.transition = `opacity ${transitionDuration}s`; document.body.append(modal); - }, `${DURATIONS.test}`); - - await page.waitForTimeout(DURATIONS.test); - - const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); - const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + }); + await page.waitForChanges(); await waitForBeforeOpenEvent; await waitForOpenEvent; @@ -246,20 +230,16 @@ describe("opening and closing behavior", () => { const beforeOpenSpy = await page.spyOnEvent("calciteModalBeforeOpen"); const openSpy = await page.spyOnEvent("calciteModalOpen"); + const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); + const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); + await page.evaluate((): void => { const modal = document.createElement("calcite-modal"); modal.open = true; document.body.append(modal); }); - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual("0s"); - - await page.waitForChanges; - - const waitForOpenEvent = page.waitForEvent("calciteModalOpen"); - const waitForBeforeOpenEvent = page.waitForEvent("calciteModalBeforeOpen"); - + await page.waitForChanges(); await waitForBeforeOpenEvent; await waitForOpenEvent; @@ -267,7 +247,7 @@ describe("opening and closing behavior", () => { expect(openSpy).toHaveReceivedEventTimes(1); }); - it.skip("emits when duration is set to 0", async () => { + it("emits when duration is set to 0", async () => { const page = await newProgrammaticE2EPage(); await skipAnimations(page); @@ -283,9 +263,6 @@ describe("opening and closing behavior", () => { document.body.append(modal); }); - const opacityTransition = await page.evaluate(getTransitionDuration); - expect(opacityTransition.duration).toEqual("0s"); - await page.waitForChanges(); await beforeOpenSpy; await openSpy; @@ -320,11 +297,11 @@ describe("calcite-modal accessibility checks", () => { ` ); - await skipAnimations(page); - await page.waitForChanges(); 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"); diff --git a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts index 3a37b1b079d..1f61788888a 100644 --- a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts +++ b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts @@ -783,6 +783,73 @@ describe("calcite-tooltip", () => { expect(beforeCloseEvent).toHaveReceivedEventTimes(1); expect(closeEvent).toHaveReceivedEventTimes(1); } + + it("when open, it emits close events if no longer rendered", async () => { + const page = await newE2EPage(); + await page.setContent(html` + +
+
+ + content +
+
+ + `); + + const beforeCloseEvent = await page.spyOnEvent("calciteTooltipBeforeClose"); + const closeEvent = await page.spyOnEvent("calciteTooltipClose"); + const beforeOpenEvent = await page.spyOnEvent("calciteTooltipBeforeOpen"); + const openEvent = await page.spyOnEvent("calciteTooltipOpen"); + + const container = await page.find(".container"); + const tooltip = await page.find(`calcite-tooltip`); + + expect(await tooltip.isVisible()).toBe(false); + + await container.hover(); + await page.waitForChanges(); + + const ref = await page.find("#ref"); + await ref.hover(); + + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); + await page.waitForChanges(); + + expect(await tooltip.isVisible()).toBe(true); + + expect(beforeOpenEvent).toHaveReceivedEventTimes(1); + expect(openEvent).toHaveReceivedEventTimes(1); + expect(beforeCloseEvent).toHaveReceivedEventTimes(0); + expect(closeEvent).toHaveReceivedEventTimes(0); + + const hoverOutsideContainer = await page.find(".hoverOutsideContainer"); + await hoverOutsideContainer.hover(); + + await page.waitForTimeout(TOOLTIP_CLOSE_DELAY_MS); + await page.waitForChanges(); + + expect(await tooltip.isVisible()).not.toBe(true); + + expect(beforeOpenEvent).toHaveReceivedEventTimes(1); + expect(openEvent).toHaveReceivedEventTimes(1); + expect(beforeCloseEvent).toHaveReceivedEventTimes(1); + expect(closeEvent).toHaveReceivedEventTimes(1); + }); }); it.skip("should open hovered tooltip while pointer is moving", async () => { @@ -905,6 +972,7 @@ describe("calcite-tooltip", () => { describe("within shadowRoot", () => { async function defineTestComponents(page: E2EPage): Promise { await page.setContent(""); + await page.evaluate((): void => { const customComponents: { name: string; html: string }[] = [ { diff --git a/packages/calcite-components/src/components/tooltip/tooltip.tsx b/packages/calcite-components/src/components/tooltip/tooltip.tsx index 04257cab6e3..94ee94a73c0 100644 --- a/packages/calcite-components/src/components/tooltip/tooltip.tsx +++ b/packages/calcite-components/src/components/tooltip/tooltip.tsx @@ -168,6 +168,12 @@ export class Tooltip implements FloatingUIComponent, OpenCloseComponent { } } + async componentWillLoad(): Promise { + if (this.open) { + onToggleOpenCloseComponent(this); + } + } + componentDidLoad(): void { if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); diff --git a/packages/calcite-components/src/utils/openCloseComponent.ts b/packages/calcite-components/src/utils/openCloseComponent.ts index c76a657b287..fef61e73a00 100644 --- a/packages/calcite-components/src/utils/openCloseComponent.ts +++ b/packages/calcite-components/src/utils/openCloseComponent.ts @@ -66,12 +66,34 @@ function transitionEnd(event: TransitionEvent): void { } } +function emitImmediately(component: OpenCloseComponent, nonOpenCloseComponent = false): void { + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onBeforeOpen() + : component.onBeforeClose(); + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onOpen() + : component.onClose(); +} + /** * Helper to determine globally set transition duration on the given openTransitionProp, which is imported and set in the @Watch("open"). * Used to emit (before)open/close events both for when the opacity transition is present and when there is none (transition-duration is set to 0). * - * @param component - * @param nonOpenCloseComponent + * @example + * import { onToggleOpenCloseComponent, OpenCloseComponent } from "../../utils/openCloseComponent"; + * + * async componentWillLoad() { + * // When component initially renders, if `open` was set we need to trigger on load as watcher doesn't fire. + * if (this.open) { + * onToggleOpenCloseComponent(this); + * } + * @Watch("open") + * async toggleModal(value: boolean): Promise { + * onToggleOpenCloseComponent(this); + * } + * + * @param component - OpenCloseComponent uses `open` prop to emit (before)open/close. + * @param nonOpenCloseComponent - OpenCloseComponent uses `expanded` prop to emit (before)open/close. */ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpenCloseComponent = false): void { readTask((): void => { @@ -81,42 +103,54 @@ export function onToggleOpenCloseComponent(component: OpenCloseComponent, nonOpe (item) => item === component.openTransitionProp ); const transitionDuration = allTransitionPropsArray[openTransitionPropIndex + 1]; + if (transitionDuration === "0s") { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onBeforeOpen() - : component.onBeforeClose(); - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onOpen() - : component.onClose(); - } else { - component.transitionEl.addEventListener( - "transitionstart", - () => { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onBeforeOpen() - : component.onBeforeClose(); - }, - { once: true } - ); - component.transitionEl.addEventListener( - "transitionend", - () => { - (nonOpenCloseComponent ? component[component.transitionProp] : component.open) - ? component.onOpen() - : component.onClose(); - }, - { once: true } - ); + emitImmediately(component, nonOpenCloseComponent); + return; + } + + const fallbackTimeoutId = setTimeout((): void => { + component.transitionEl.removeEventListener("transitionstart", onStart); + component.transitionEl.removeEventListener("transitionend", onEndOrCancel); + component.transitionEl.removeEventListener("transitioncancel", onEndOrCancel); + emitImmediately(component, nonOpenCloseComponent); + }, parseFloat(transitionDuration) * 1000); + + component.transitionEl.addEventListener("transitionstart", onStart); + component.transitionEl.addEventListener("transitionend", onEndOrCancel); + component.transitionEl.addEventListener("transitioncancel", onEndOrCancel); + + function onStart(event: TransitionEvent): void { + if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { + clearTimeout(fallbackTimeoutId); + component.transitionEl.removeEventListener("transitionstart", onStart); + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onBeforeOpen() + : component.onBeforeClose(); + } + } + + function onEndOrCancel(event: TransitionEvent): void { + if (event.propertyName === component.openTransitionProp && event.target === component.transitionEl) { + (nonOpenCloseComponent ? component[component.transitionProp] : component.open) + ? component.onOpen() + : component.onClose(); + + component.transitionEl.removeEventListener("transitionend", onEndOrCancel); + component.transitionEl.removeEventListener("transitioncancel", onEndOrCancel); + } } } }); } + /** * Helper to keep track of transition listeners on setTransitionEl and connectedCallback on OpenCloseComponent components. * * For component which do not have open prop, use `onToggleOpenCloseComponent` implementation. * * @param component + * @deprecated Call `onToggleOpenClose` in `componentWillLoad` and `open` property watchers instead. */ export function connectOpenCloseComponent(component: OpenCloseComponent): void { disconnectOpenCloseComponent(component); @@ -138,6 +172,7 @@ export function connectOpenCloseComponent(component: OpenCloseComponent): void { * Helper to tear down transition listeners on disconnectedCallback on OpenCloseComponent components. * * @param component + * @deprecated Call `onToggleOpenClose` in `componentWillLoad` and `open` property watchers instead. */ export function disconnectOpenCloseComponent(component: OpenCloseComponent): void { if (!componentToTransitionListeners.has(component)) {