From 106f5d27afc5d7363fa197a1f9fb0552864a15e4 Mon Sep 17 00:00:00 2001 From: Matt Driscoll Date: Thu, 15 Jun 2023 09:19:14 -0700 Subject: [PATCH] fix(tooltip): improve component timing (#7172) **Related Issue:** #6396 ## Summary - Separates timeout delay between open and close. - `open` is set to `300ms`. - `close` is set to `500ms`. - Sets delay to `0ms` if a tooltip is already displayed. - Add story for design review. - Update tests. --- .../action-bar/action-bar.stories.ts | 30 +++++++ .../components/action-menu/action-menu.e2e.ts | 4 +- .../src/components/tooltip/TooltipManager.ts | 80 ++++++++++++------- .../src/components/tooltip/resources.ts | 3 +- .../src/components/tooltip/tooltip.e2e.ts | 75 ++++++++++++----- 5 files changed, 142 insertions(+), 50 deletions(-) diff --git a/packages/calcite-components/src/components/action-bar/action-bar.stories.ts b/packages/calcite-components/src/components/action-bar/action-bar.stories.ts index 179bfc48759..83c25a32908 100644 --- a/packages/calcite-components/src/components/action-bar/action-bar.stories.ts +++ b/packages/calcite-components/src/components/action-bar/action-bar.stories.ts @@ -148,6 +148,36 @@ export const darkModeRTL_TestOnly = (): string => darkModeRTL_TestOnly.parameters = { modes: modesDarkDefault }; +export const adjacentTooltipsOpenQuickly = (): string => html`
+ + + + Add + + Save + Layers + + + Add + Save + Layers + + hello world + +
`; + export const withTooltip_NoTest = (): string => create( "calcite-action-bar", diff --git a/packages/calcite-components/src/components/action-menu/action-menu.e2e.ts b/packages/calcite-components/src/components/action-menu/action-menu.e2e.ts index 0545c5f536f..f4c67bf7e5a 100755 --- a/packages/calcite-components/src/components/action-menu/action-menu.e2e.ts +++ b/packages/calcite-components/src/components/action-menu/action-menu.e2e.ts @@ -1,7 +1,7 @@ import { newE2EPage } from "@stencil/core/testing"; import { html } from "../../../support/formatting"; import { accessible, defaults, focusable, hidden, reflects, renders, slots } from "../../tests/commonTests"; -import { TOOLTIP_DELAY_MS } from "../tooltip/resources"; +import { TOOLTIP_OPEN_DELAY_MS } from "../tooltip/resources"; import { CSS, SLOTS } from "./resources"; describe("calcite-action-menu", () => { @@ -198,7 +198,7 @@ describe("calcite-action-menu", () => { expect(await tooltip.isVisible()).toBe(false); await trigger.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); expect(await tooltip.isVisible()).toBe(true); diff --git a/packages/calcite-components/src/components/tooltip/TooltipManager.ts b/packages/calcite-components/src/components/tooltip/TooltipManager.ts index 3b8a10ce541..9f6149749d7 100644 --- a/packages/calcite-components/src/components/tooltip/TooltipManager.ts +++ b/packages/calcite-components/src/components/tooltip/TooltipManager.ts @@ -1,6 +1,6 @@ import { getShadowRootNode, isPrimaryPointerButton } from "../../utils/dom"; import { ReferenceElement } from "../../utils/floating-ui"; -import { TOOLTIP_DELAY_MS } from "./resources"; +import { TOOLTIP_OPEN_DELAY_MS, TOOLTIP_CLOSE_DELAY_MS } from "./resources"; import { getEffectiveReferenceElement } from "./utils"; export default class TooltipManager { @@ -14,7 +14,9 @@ export default class TooltipManager { private registeredShadowRootCounts = new WeakMap(); - private hoverTimeout: number = null; + private hoverOpenTimeout: number = null; + + private hoverCloseTimeout: number = null; private hoveredTooltip: HTMLCalciteTooltipElement = null; @@ -80,7 +82,7 @@ export default class TooltipManager { if (activeTooltip?.open) { this.clearHoverTimeout(); - this.closeExistingTooltip(); + this.closeActiveTooltip(); const referenceElement = getEffectiveReferenceElement(activeTooltip); @@ -111,9 +113,9 @@ export default class TooltipManager { this.clickedTooltip = null; if (tooltip) { - this.toggleHoveredTooltip(tooltip, true); + this.openHoveredTooltip(tooltip); } else if (activeTooltip) { - this.toggleHoveredTooltip(activeTooltip, false); + this.closeHoveredTooltip(); } }; @@ -166,12 +168,22 @@ export default class TooltipManager { document.removeEventListener("focusout", this.focusOutHandler, { capture: true }); } + private clearHoverOpenTimeout(): void { + window.clearTimeout(this.hoverOpenTimeout); + this.hoverOpenTimeout = null; + } + + private clearHoverCloseTimeout(): void { + window.clearTimeout(this.hoverCloseTimeout); + this.hoverCloseTimeout = null; + } + private clearHoverTimeout(): void { - window.clearTimeout(this.hoverTimeout); - this.hoverTimeout = null; + this.clearHoverOpenTimeout(); + this.clearHoverCloseTimeout(); } - private closeExistingTooltip(): void { + private closeActiveTooltip(): void { const { activeTooltip } = this; if (activeTooltip?.open) { @@ -179,48 +191,60 @@ export default class TooltipManager { } } - private toggleFocusedTooltip(tooltip: HTMLCalciteTooltipElement, value: boolean): void { - this.closeExistingTooltip(); + private toggleFocusedTooltip(tooltip: HTMLCalciteTooltipElement, open: boolean): void { + this.closeActiveTooltip(); - if (value) { + if (open) { this.clearHoverTimeout(); } - this.toggleTooltip(tooltip, value); + this.toggleTooltip(tooltip, open); } - private toggleTooltip(tooltip: HTMLCalciteTooltipElement, value: boolean): void { - tooltip.open = value; + private toggleTooltip(tooltip: HTMLCalciteTooltipElement, open: boolean): void { + tooltip.open = open; - if (value) { - this.activeTooltip = tooltip; - } + this.activeTooltip = open ? tooltip : null; } - private toggleHoveredTooltip = (tooltip: HTMLCalciteTooltipElement, value: boolean): void => { - this.hoverTimeout = window.setTimeout(() => { - if (this.hoverTimeout === null) { - return; - } + private openHoveredTooltip = (tooltip: HTMLCalciteTooltipElement): void => { + this.hoverOpenTimeout = window.setTimeout( + () => { + if (this.hoverOpenTimeout === null) { + return; + } + + this.clearHoverCloseTimeout(); + this.closeActiveTooltip(); - this.closeExistingTooltip(); + if (tooltip !== this.hoveredTooltip) { + return; + } + + this.toggleTooltip(tooltip, true); + }, + this.activeTooltip ? 0 : TOOLTIP_OPEN_DELAY_MS + ); + }; - if (tooltip !== this.hoveredTooltip) { + private closeHoveredTooltip = (): void => { + this.hoverCloseTimeout = window.setTimeout(() => { + if (this.hoverCloseTimeout === null) { return; } - this.toggleTooltip(tooltip, value); - }, TOOLTIP_DELAY_MS); + this.closeActiveTooltip(); + }, TOOLTIP_CLOSE_DELAY_MS); }; - private queryFocusedTooltip(event: FocusEvent, value: boolean): void { + private queryFocusedTooltip(event: FocusEvent, open: boolean): void { const tooltip = this.queryTooltip(event.composedPath()); if (!tooltip || this.isClosableClickedTooltip(tooltip)) { return; } - this.toggleFocusedTooltip(tooltip, value); + this.toggleFocusedTooltip(tooltip, open); } private isClosableClickedTooltip(tooltip: HTMLCalciteTooltipElement): boolean { diff --git a/packages/calcite-components/src/components/tooltip/resources.ts b/packages/calcite-components/src/components/tooltip/resources.ts index 8a22e3cbd70..0366090554d 100644 --- a/packages/calcite-components/src/components/tooltip/resources.ts +++ b/packages/calcite-components/src/components/tooltip/resources.ts @@ -2,6 +2,7 @@ export const CSS = { container: "container" }; -export const TOOLTIP_DELAY_MS = 500; +export const TOOLTIP_OPEN_DELAY_MS = 300; +export const TOOLTIP_CLOSE_DELAY_MS = 500; export const ARIA_DESCRIBED_BY = "aria-describedby"; diff --git a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts index 5279716c5a9..5e536df33aa 100644 --- a/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts +++ b/packages/calcite-components/src/components/tooltip/tooltip.e2e.ts @@ -1,5 +1,5 @@ import { E2EPage, newE2EPage } from "@stencil/core/testing"; -import { TOOLTIP_DELAY_MS } from "../tooltip/resources"; +import { TOOLTIP_OPEN_DELAY_MS, TOOLTIP_CLOSE_DELAY_MS } from "../tooltip/resources"; import { accessible, defaults, hidden, floatingUIOwner, renders } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; import { GlobalTestProps } from "../../tests/utils"; @@ -224,7 +224,7 @@ describe("calcite-tooltip", () => { await ref.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); expect(await tooltip.isVisible()).toBe(true); }); @@ -246,7 +246,7 @@ describe("calcite-tooltip", () => { await ref.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); expect(await tooltip.isVisible()).toBe(true); }); @@ -290,7 +290,7 @@ describe("calcite-tooltip", () => { await page.waitForChanges(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); expect(await tooltip.getProperty("open")).toBe(true); @@ -300,7 +300,7 @@ describe("calcite-tooltip", () => { await page.waitForChanges(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_CLOSE_DELAY_MS); expect(await tooltip.getProperty("open")).toBe(false); }); @@ -419,7 +419,7 @@ describe("calcite-tooltip", () => { await referenceElement.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -456,7 +456,7 @@ describe("calcite-tooltip", () => { await referenceElement.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -497,7 +497,7 @@ describe("calcite-tooltip", () => { el.dispatchEvent(new Event("pointermove")); }); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -555,7 +555,7 @@ describe("calcite-tooltip", () => { el.dispatchEvent(new Event("pointermove")); }); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -584,7 +584,7 @@ describe("calcite-tooltip", () => { await referenceElement.hover(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -627,7 +627,7 @@ describe("calcite-tooltip", () => { await referenceElement.click(); - await page.waitForTimeout(TOOLTIP_DELAY_MS); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); await page.waitForChanges(); @@ -748,25 +748,25 @@ describe("calcite-tooltip", () => { selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS * 0.25, + delay: TOOLTIP_OPEN_DELAY_MS * 0.25, property: "open", value: false, selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS * 0.5, + delay: TOOLTIP_OPEN_DELAY_MS * 0.5, property: "open", value: false, selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS, + delay: TOOLTIP_OPEN_DELAY_MS, property: "open", value: true, selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS + TOOLTIP_DELAY_MS * 0.5, + delay: TOOLTIP_OPEN_DELAY_MS + TOOLTIP_OPEN_DELAY_MS * 0.5, property: "open", value: true, selector: "#ref" @@ -809,25 +809,25 @@ describe("calcite-tooltip", () => { selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS, + delay: TOOLTIP_CLOSE_DELAY_MS, property: "open", value: true, selector: "#ref" }, { - delay: TOOLTIP_DELAY_MS * 0.25, + delay: TOOLTIP_CLOSE_DELAY_MS * 0.25, property: "open", value: true, selector: "#ref2" }, { - delay: TOOLTIP_DELAY_MS * 0.5, + delay: TOOLTIP_CLOSE_DELAY_MS * 0.5, property: "open", value: true, selector: "#ref2" }, { - delay: TOOLTIP_DELAY_MS * 0.5, + delay: TOOLTIP_CLOSE_DELAY_MS * 0.5, property: "open", value: false, selector: "#ref2" @@ -908,4 +908,41 @@ describe("calcite-tooltip", () => { expect(await isTooltipOpen(page)).toBe(true); }); }); + + it("should open tooltip instantly if another tooltip is already visible", async () => { + const page = await newE2EPage(); + + await page.setContent( + html`

+

+ content + content 2` + ); + + await page.waitForChanges(); + + const tooltip1 = await page.find("#tooltip1"); + const tooltip2 = await page.find("#tooltip2"); + + expect(await tooltip1.getProperty("open")).toBe(false); + expect(await tooltip2.getProperty("open")).toBe(false); + + await page.$eval("#ref1", (el: HTMLElement) => { + el.dispatchEvent(new Event("pointermove")); + }); + await page.waitForTimeout(TOOLTIP_OPEN_DELAY_MS); + await page.waitForChanges(); + + expect(await tooltip1.getProperty("open")).toBe(true); + expect(await tooltip2.getProperty("open")).toBe(false); + + await page.$eval("#ref2", (el: HTMLElement) => { + el.dispatchEvent(new Event("pointermove")); + }); + await page.waitForTimeout(0); + await page.waitForChanges(); + + expect(await tooltip1.getProperty("open")).toBe(false); + expect(await tooltip2.getProperty("open")).toBe(true); + }); });