diff --git a/packages/calcite-components/src/components/input-number/input-number.e2e.ts b/packages/calcite-components/src/components/input-number/input-number.e2e.ts index 241c6f9cbfd..b63a71fdf19 100644 --- a/packages/calcite-components/src/components/input-number/input-number.e2e.ts +++ b/packages/calcite-components/src/components/input-number/input-number.e2e.ts @@ -549,16 +549,16 @@ describe("calcite-input-number", () => { it("should emit an event on an interval when ArrowUp/ArrowDown keys are down and stop on key up", async () => { await page.setContent(html``); - const calciteInputNumberInput = await page.spyOnEvent("calciteInputNumberInput"); + const inputEventSpy = await page.spyOnEvent("calciteInputNumberInput"); const input = await page.find("calcite-input-number"); - expect(calciteInputNumberInput).toHaveReceivedEventTimes(0); + expect(inputEventSpy).toHaveReceivedEventTimes(0); await input.callMethod("setFocus"); await page.waitForChanges(); const eventSpy = await page.spyOnEvent("keydown"); await page.keyboard.down("ArrowUp"); await page.waitForTimeout(delayFor2UpdatesInMs); - await page.waitForEvent("calciteInputNumberInput"); + await inputEventSpy.next(); expect(eventSpy).toHaveReceivedEventTimes(1); expect(eventSpy.lastEvent.defaultPrevented).toBe(true); @@ -569,7 +569,7 @@ describe("calcite-input-number", () => { expect(eventSpy).toHaveReceivedEventTimes(2); expect(eventSpy.lastEvent.defaultPrevented).toBe(true); - const totalNudgesUp = calciteInputNumberInput.length; + const totalNudgesUp = inputEventSpy.length; expect(await input.getProperty("value")).toBe(`${totalNudgesUp}`); await page.keyboard.down("ArrowDown"); @@ -585,7 +585,7 @@ describe("calcite-input-number", () => { expect(eventSpy).toHaveReceivedEventTimes(4); expect(eventSpy.lastEvent.defaultPrevented).toBe(true); - const totalNudgesDown = calciteInputNumberInput.length - totalNudgesUp; + const totalNudgesDown = inputEventSpy.length - totalNudgesUp; const finalNudgedValue = totalNudgesUp - totalNudgesDown; expect(await input.getProperty("value")).toBe(`${finalNudgedValue}`); }); diff --git a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts index a515ded7c56..d6bd992c3f1 100644 --- a/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts +++ b/packages/calcite-components/src/components/input-time-picker/input-time-picker.e2e.ts @@ -145,10 +145,10 @@ describe("calcite-input-time-picker", () => { await assertDisplayedTime(page, "04:35 AM"); - const openEvent = page.waitForEvent("calciteInputTimePickerOpen"); + const openEventSpy = await page.spyOnEvent("calciteInputTimePickerOpen"); inputTimePicker.setProperty("open", true); await page.waitForChanges(); - await openEvent; + await openEventSpy.next(); const hourUpEl = await page.find(`calcite-input-time-picker >>> .${TimePickerCSS.buttonHourUp}`); const minuteUpEl = await page.find(`calcite-input-time-picker >>> .${TimePickerCSS.buttonMinuteUp}`); @@ -156,10 +156,10 @@ describe("calcite-input-time-picker", () => { await hourUpEl.click(); await minuteUpEl.click(); - const closeEvent = page.waitForEvent("calciteInputTimePickerClose"); + const closeEventSpy = await page.spyOnEvent("calciteInputTimePickerClose"); await page.keyboard.press("Escape"); await page.waitForChanges(); - await closeEvent; + await closeEventSpy.next(); expect(await inputTimePicker.getProperty("value")).toBe("05:36"); await assertDisplayedTime(page, "05:36 AM"); diff --git a/packages/calcite-components/src/components/input/input.e2e.ts b/packages/calcite-components/src/components/input/input.e2e.ts index 926a22ef5e9..054910c5cbf 100644 --- a/packages/calcite-components/src/components/input/input.e2e.ts +++ b/packages/calcite-components/src/components/input/input.e2e.ts @@ -537,19 +537,19 @@ describe("calcite-input", () => { it("on input type number, should emit an event on an interval when ArrowUp/ArrowDown keys are down and stop on key up", async () => { await page.setContent(html``); - const calciteInputInput = await page.spyOnEvent("calciteInputInput"); + const inputEventSpy = await page.spyOnEvent("calciteInputInput"); const input = await page.find("calcite-input"); - expect(calciteInputInput).toHaveReceivedEventTimes(0); + expect(inputEventSpy).toHaveReceivedEventTimes(0); await input.callMethod("setFocus"); await page.waitForChanges(); await page.keyboard.down("ArrowUp"); await page.waitForTimeout(delayFor2UpdatesInMs); - await page.waitForEvent("calciteInputInput"); + await inputEventSpy.next(); await page.keyboard.up("ArrowUp"); await page.waitForChanges(); - const totalNudgesUp = calciteInputInput.length; + const totalNudgesUp = inputEventSpy.length; expect(await input.getProperty("value")).toBe(`${totalNudgesUp}`); await page.keyboard.down("ArrowDown"); @@ -557,7 +557,7 @@ describe("calcite-input", () => { await page.keyboard.up("ArrowDown"); await page.waitForChanges(); - const totalNudgesDown = calciteInputInput.length - totalNudgesUp; + const totalNudgesDown = inputEventSpy.length - totalNudgesUp; const finalNudgedValue = totalNudgesUp - totalNudgesDown; expect(await input.getProperty("value")).toBe(`${finalNudgedValue}`); }); diff --git a/packages/calcite-components/src/utils/dom.spec.ts b/packages/calcite-components/src/utils/dom.spec.ts index df52dbc6e46..31e4cadb682 100644 --- a/packages/calcite-components/src/utils/dom.spec.ts +++ b/packages/calcite-components/src/utils/dom.spec.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { ModeName } from "../components/interfaces"; import { html } from "../../support/formatting"; import { waitForAnimationFrame } from "../tests/utils/timing"; @@ -440,13 +440,9 @@ describe("dom", () => { */ describe("transition/animation helpers", () => { let element: HTMLDivElement; - let onStartCallback: Mock; - let onEndCallback: Mock; beforeEach(() => { element = window.document.createElement("div"); - onStartCallback = vi.fn(); - onEndCallback = vi.fn(); }); const helpers = [whenTransitionDone, whenAnimationDone] as const; @@ -469,20 +465,14 @@ describe("dom", () => { ]; element.getAnimations = () => animationsPerCall.shift(); - const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback); + const promise = helper(element, testTransitionOrAnimationName); expect(await promiseState(promise)).toHaveProperty("status", "pending"); - expect(onStartCallback).toHaveBeenCalled(); - expect(onEndCallback).not.toHaveBeenCalled(); controlledPromise.resolve(); expect(await promiseState(promise)).toHaveProperty("status", "pending"); - expect(onStartCallback).toHaveBeenCalled(); - expect(onEndCallback).toHaveBeenCalled(); expect(await promiseState(promise)).toHaveProperty("status", "fulfilled"); - expect(onStartCallback).toHaveBeenCalled(); - await expect(onEndCallback).toHaveBeenCalled(); }); it(`should return a promise that resolves after the ${type} (running frame after call time)`, async () => { @@ -498,45 +488,29 @@ describe("dom", () => { ]; element.getAnimations = () => animationsPerCall.shift(); - const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback); + const promise = helper(element, testTransitionOrAnimationName); expect(await promiseState(promise)).toHaveProperty("status", "pending"); - expect(onStartCallback).not.toHaveBeenCalled(); - expect(onEndCallback).not.toHaveBeenCalled(); await waitForAnimationFrame(); expect(await promiseState(promise)).toHaveProperty("status", "pending"); - expect(onStartCallback).toHaveBeenCalled(); - expect(onEndCallback).not.toHaveBeenCalled(); controlledPromise.resolve(); expect(await promiseState(promise)).toHaveProperty("status", "pending"); - expect(onStartCallback).toHaveBeenCalled(); - expect(onEndCallback).toHaveBeenCalled(); expect(await promiseState(promise)).toHaveProperty("status", "fulfilled"); - expect(onStartCallback).toHaveBeenCalled(); - await expect(onEndCallback).toHaveBeenCalled(); }); it(`should return a promise that resolves after 0s ${type} or has not started when expected (fallback cases)`, async () => { const animationsPerCall = [[], []]; element.getAnimations = () => animationsPerCall.shift(); - const promise = helper(element, testTransitionOrAnimationName, onStartCallback, onEndCallback); + const promise = helper(element, testTransitionOrAnimationName); expect(await promiseState(promise)).toHaveProperty("status", "pending"); await waitForAnimationFrame(); - expect(onStartCallback).not.toHaveBeenCalled(); - expect(onEndCallback).not.toHaveBeenCalled(); - - await waitForAnimationFrame(); - expect(onStartCallback).toHaveBeenCalled(); - - await waitForAnimationFrame(); - expect(onEndCallback).toHaveBeenCalled(); expect(await promiseState(promise)).toHaveProperty("status", "fulfilled"); }); diff --git a/packages/calcite-components/src/utils/dom.ts b/packages/calcite-components/src/utils/dom.ts index 1760b4876fb..b2c2e677df7 100644 --- a/packages/calcite-components/src/utils/dom.ts +++ b/packages/calcite-components/src/utils/dom.ts @@ -586,16 +586,9 @@ export function isBefore(a: HTMLElement, b: HTMLElement): boolean { * * @param targetEl The element to watch for the animation to complete. * @param animationName The name of the animation to watch for completion. - * @param onStart A callback to run when the animation starts. - * @param onEnd A callback to run when the animation ends or is canceled. */ -export async function whenAnimationDone( - targetEl: HTMLElement, - animationName: string, - onStart?: () => void, - onEnd?: () => void, -): Promise { - return whenTransitionOrAnimationDone(targetEl, animationName, "animation", onStart, onEnd); +export async function whenAnimationDone(targetEl: HTMLElement, animationName: string): Promise { + return whenTransitionOrAnimationDone(targetEl, animationName, "animation"); } /** @@ -603,30 +596,14 @@ export async function whenAnimationDone( * * @param targetEl The element to watch for the transition to complete. * @param transitionProp The name of the transition to watch for completion. - * @param onStart A callback to run when the transition starts. - * @param onEnd A callback to run when the transition ends or is canceled. */ -export async function whenTransitionDone( - targetEl: HTMLElement, - transitionProp: string, - onStart?: () => void, - onEnd?: () => void, -): Promise { - return whenTransitionOrAnimationDone(targetEl, transitionProp, "transition", onStart, onEnd); +export async function whenTransitionDone(targetEl: HTMLElement, transitionProp: string): Promise { + return whenTransitionOrAnimationDone(targetEl, transitionProp, "transition"); } type TransitionOrAnimation = "transition" | "animation"; type TransitionOrAnimationInstance = CSSTransition | Animation; -async function triggerFallbackStartEnd(start: () => void, end: () => void): Promise { - // offset callbacks by a frame to simulate event counterparts - await nextFrame(); - start?.(); - - await nextFrame(); - end?.(); -} - function findAnimation( targetEl: HTMLElement, type: TransitionOrAnimation, @@ -644,15 +621,11 @@ function findAnimation( * @param targetEl The element to watch for the transition or animation to complete. * @param transitionPropOrAnimationName The transition or animation property to watch for completion. * @param type The type of property to watch for completion. Defaults to "transition". - * @param onStart A callback to run when the transition or animation starts. - * @param onEnd A callback to run when the transition or animation ends or is canceled. */ export async function whenTransitionOrAnimationDone( targetEl: HTMLElement, transitionPropOrAnimationName: string, type: TransitionOrAnimation, - onStart?: () => void, - onEnd?: () => void, ): Promise { let anim = findAnimation(targetEl, type, transitionPropOrAnimationName); @@ -663,17 +636,13 @@ export async function whenTransitionOrAnimationDone( } if (!anim) { - return triggerFallbackStartEnd(onStart, onEnd); + return; } - onStart?.(); - try { await anim.finished; } catch { // swallow error if canceled - } finally { - onEnd?.(); } } diff --git a/packages/calcite-components/src/utils/openCloseComponent.browser.spec.tsx b/packages/calcite-components/src/utils/openCloseComponent.browser.spec.tsx new file mode 100644 index 00000000000..6fc36c39dcb --- /dev/null +++ b/packages/calcite-components/src/utils/openCloseComponent.browser.spec.tsx @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from "vitest"; +import { JsxNode, LitElement } from "@arcgis/lumina"; +import { mount } from "@arcgis/lumina-compiler/testing"; +import { waitForAnimationFrame } from "../tests/utils/timing"; +import { createControlledPromise } from "../tests/utils/promises"; +import { onToggleOpenCloseComponent } from "./openCloseComponent"; + +describe("openCloseComponent", () => { + describe("toggleOpenCloseComponent", () => { + it("emits beforeOpen/beforeClose events when the transition starts and open/close events when the transition is done", async () => { + const emittedEvents: string[] = []; + + class Test extends LitElement { + open = false; + + transitionEl!: HTMLDivElement; + openProp = "open"; + transitionProp = "opacity" as const; + + onBeforeOpen(): void { + emittedEvents.push("beforeOpen"); + } + + onOpen(): void { + emittedEvents.push("open"); + } + + onBeforeClose(): void { + emittedEvents.push("beforeClose"); + } + + onClose(): void { + emittedEvents.push("close"); + } + + override render(): JsxNode { + return ( +
{ + if (!el) { + return; + } + this.transitionEl = el; + }} + /> + ); + } + } + + const { component } = await mount(Test); + + expect(emittedEvents).toEqual([]); + + const openingControlledPromise = createControlledPromise(); + + const getAnimationsSpy = vi.spyOn(component.transitionEl, "getAnimations"); + + getAnimationsSpy.mockImplementation(() => [ + { + transitionProperty: "opacity", + finished: openingControlledPromise.promise, + } as unknown as CSSTransition, + ]); + + component.open = true; + onToggleOpenCloseComponent(component); + await waitForAnimationFrame(); + expect(emittedEvents).toEqual(["beforeOpen"]); + + openingControlledPromise.resolve(); + await waitForAnimationFrame(); + expect(emittedEvents).toEqual(["beforeOpen", "open"]); + + const closingControlledPromise = createControlledPromise(); + getAnimationsSpy.mockImplementation(() => [ + { + transitionProperty: "opacity", + finished: closingControlledPromise.promise, + } as unknown as CSSTransition, + ]); + + component.open = false; + onToggleOpenCloseComponent(component); + await waitForAnimationFrame(); + + expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose"]); + + closingControlledPromise.resolve(); + await waitForAnimationFrame(); + + expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose", "close"]); + }); + }); +}); diff --git a/packages/calcite-components/src/utils/openCloseComponent.spec.ts b/packages/calcite-components/src/utils/openCloseComponent.spec.ts deleted file mode 100644 index 29b8286d537..00000000000 --- a/packages/calcite-components/src/utils/openCloseComponent.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { waitForAnimationFrame } from "../tests/utils/timing"; -import { createControlledPromise } from "../tests/utils/promises"; -import * as openCloseComponent from "./openCloseComponent"; - -const { onToggleOpenCloseComponent } = openCloseComponent; - -describe("openCloseComponent", () => { - describe("toggleOpenCloseComponent", () => { - beforeEach(() => { - vi.spyOn(global, "requestAnimationFrame").mockImplementation((cb) => { - cb(0); - return 0; - }); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it("emits beforeOpen/beforeClose events when the transition starts and open/close events when the transition is done", async () => { - const transitionEl = window.document.createElement("div"); - const emittedEvents: string[] = []; - const fakeOpenCloseComponent = { - el: document.createElement("div"), - open: true, - transitionProp: "opacity" as const, - openTransitionProp: "open", - transitionEl, - onBeforeOpen: vi.fn(() => emittedEvents.push("beforeOpen")), - onOpen: vi.fn(() => emittedEvents.push("open")), - onBeforeClose: vi.fn(() => emittedEvents.push("beforeClose")), - onClose: vi.fn(() => emittedEvents.push("close")), - }; - - const openingControlledPromise = createControlledPromise(); - - fakeOpenCloseComponent.transitionEl.getAnimations = () => [ - { - transitionProperty: "opacity", - finished: openingControlledPromise.promise, - } as unknown as CSSTransition, - ]; - - onToggleOpenCloseComponent(fakeOpenCloseComponent); - expect(emittedEvents).toEqual(["beforeOpen"]); - - openingControlledPromise.resolve(); - await waitForAnimationFrame(); - expect(emittedEvents).toEqual(["beforeOpen", "open"]); - - const closingControlledPromise = createControlledPromise(); - fakeOpenCloseComponent.transitionEl.getAnimations = () => [ - { - transitionProperty: "opacity", - finished: closingControlledPromise.promise, - } as unknown as CSSTransition, - ]; - - fakeOpenCloseComponent.open = false; - onToggleOpenCloseComponent(fakeOpenCloseComponent); - - expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose"]); - - closingControlledPromise.resolve(); - await waitForAnimationFrame(); - - expect(emittedEvents).toEqual(["beforeOpen", "open", "beforeClose", "close"]); - }); - }); -}); diff --git a/packages/calcite-components/src/utils/openCloseComponent.ts b/packages/calcite-components/src/utils/openCloseComponent.ts index f92daa0f1ea..5c6f48927a9 100644 --- a/packages/calcite-components/src/utils/openCloseComponent.ts +++ b/packages/calcite-components/src/utils/openCloseComponent.ts @@ -1,15 +1,13 @@ // @ts-strict-ignore import { KebabCase } from "type-fest"; +import { LitElement } from "@arcgis/lumina"; import { whenTransitionDone } from "./dom"; /** * Defines interface for components with open/close public emitter. * All implementations of this interface must handle the following events: `beforeOpen`, `open`, `beforeClose`, `close`. */ -export interface OpenCloseComponent { - /** The host element. */ - readonly el: HTMLElement; - +export interface OpenCloseComponent extends LitElement { /** * Specifies property on which active transition is watched for. * @@ -56,29 +54,21 @@ function isOpen(component: OpenCloseComponent): boolean { * } * @param component - OpenCloseComponent uses `open` prop to emit (before)open/close. */ -export function onToggleOpenCloseComponent(component: OpenCloseComponent): void { - requestAnimationFrame((): void => { - if (!component.transitionEl) { - return; - } +export async function onToggleOpenCloseComponent(component: OpenCloseComponent): Promise { + if (isOpen(component)) { + component.onBeforeOpen(); + } else { + component.onBeforeClose(); + } + + await component.updateComplete; + if (component.transitionEl) { + await whenTransitionDone(component.transitionEl, component.transitionProp); + } - whenTransitionDone( - component.transitionEl, - component.transitionProp, - () => { - if (isOpen(component)) { - component.onBeforeOpen(); - } else { - component.onBeforeClose(); - } - }, - () => { - if (isOpen(component)) { - component.onOpen(); - } else { - component.onClose(); - } - }, - ); - }); + if (isOpen(component)) { + component.onOpen(); + } else { + component.onClose(); + } }