From 274b10477f6aaf822d3cf2894b7848e36b36b057 Mon Sep 17 00:00:00 2001 From: Eliza Khachatryan Date: Thu, 8 Dec 2022 15:32:18 -0800 Subject: [PATCH] fix(alert): auto-dismissible retains close button and dismisses timer while a user is hovering over (#5872) **Related Issue:** #3338 ## Summary If an `alert` is `auto-dismissible`, - keep the `close` button there, a user may want to dismiss the alert before the dismiss duration completes. - pause the dismiss timer while a user is hovering over, or focusing on an element within an alert. Although disincentivized, apps may put actions or links inside of an `auto-dismissible alert`. --- src/components/alert/alert.e2e.ts | 109 ++++++++++++++++++++++---- src/components/alert/alert.scss | 3 + src/components/alert/alert.stories.ts | 8 ++ src/components/alert/alert.tsx | 26 +++++- src/components/alert/resources.ts | 3 +- 5 files changed, 129 insertions(+), 20 deletions(-) diff --git a/src/components/alert/alert.e2e.ts b/src/components/alert/alert.e2e.ts index a65afe1ce19..38f708f3ce9 100644 --- a/src/components/alert/alert.e2e.ts +++ b/src/components/alert/alert.e2e.ts @@ -1,7 +1,8 @@ -import { newE2EPage } from "@stencil/core/testing"; +import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing"; import { renders, accessible, HYDRATED_ATTR, hidden } from "../../tests/commonTests"; import { html } from "../../../support/formatting"; -import { CSS } from "./resources"; +import { CSS, DURATIONS } from "./resources"; +import { getElementXY } from "../../tests/utils"; describe("calcite-alert", () => { const alertContent = ` @@ -50,12 +51,10 @@ describe("calcite-alert", () => { `); const element = await page.find("calcite-alert"); - const close = await page.find("calcite-alert >>> .alert-close"); const icon = await page.find("calcite-alert >>> .alert-icon"); expect(element).toEqualAttribute("color", "yellow"); expect(element).toEqualAttribute("auto-dismiss-duration", "fast"); - expect(close).toBeNull(); expect(icon).toBeNull(); }); @@ -320,30 +319,106 @@ describe("calcite-alert", () => { expect(await container.isVisible()).toBe(false); }); - describe("when multiple alerts are queued", () => { + describe("auto-dismiss behavior on queued items", () => { it("should display number of queued alerts with a calcite-chip", async () => { - const page = await newE2EPage({ - html: ` - -
Title of alert #1
-
Message text of the alert
+ const page = await newE2EPage(); + await page.setContent(html` + open alert + open alert + + +
Title of alert Uno
+
Message text of the alert Uno
Retry
+ -
Title of alert #2
-
Message text of the alert
+
Title of alert Dos
+
Message text of the alert Dos
Retry
- ` - }); - await page.addScriptTag({ - content: `document.querySelector("#alert-to-be-queued").setAttribute("open", "");` - }); + `); + const buttonOne = await page.find("#buttonOne"); + const buttonTwo = await page.find("#buttonTwo"); + const alertOne = await page.find("#first-open"); + const alertTwo = await page.find("#alert-to-be-queued"); + + await buttonOne.click(); await page.waitForTimeout(animationDurationInMs); + expect(await alertOne.isVisible()).toBe(true); + + await buttonTwo.click(); + expect(await alertTwo.isVisible()).toBe(true); + const chip = await page.find("calcite-alert[id='first-open'] >>> calcite-chip"); const chipQueueCount = "+1"; expect(await chip.getProperty("value")).toEqual(chipQueueCount); expect(chip.textContent).toEqual(chipQueueCount); + + await page.waitForTimeout(DURATIONS.medium * 2 + animationDurationInMs * 5); + await page.waitForSelector("#first-open", { visible: false }); + await page.waitForSelector("#alert-to-be-queued", { visible: false }); + }); + }); + + describe("auto-dismiss behavior", () => { + let page: E2EPage; + let alert: E2EElement; + let button: E2EElement; + let buttonClose: E2EElement; + let playState: string; + + beforeEach(async () => { + page = await newE2EPage(); + await page.setContent(html` +
+ open alert + + ${alertContent} +
+ `); + alert = await page.find("#alert"); + button = await page.find("#button"); + buttonClose = await page.find(`#alert >>> .${CSS.close}`); + + playState = await page.evaluate(async () => { + const alert = document.querySelector("calcite-alert"); + return window.getComputedStyle(alert).animationPlayState; + }); + }); + + it("should render close button", async () => { + await button.click(); + await page.waitForTimeout(animationDurationInMs); + + expect(await alert.isVisible()).toBe(true); + expect(buttonClose).toBeTruthy(); + }); + + it("pauses on mouseOver and resumes on mouseLeave", async () => { + await button.click(); + + expect(await alert.isVisible()).toBe(true); + expect(await alert.getProperty("autoDismissDuration")).toEqual("medium"); + expect(playState).toEqual("running"); + + const [alertLocationX, alertLocationY] = await getElementXY(page, "calcite-alert", `.${CSS.close}`); + await page.mouse.move(alertLocationX, alertLocationY); + + await page.waitForTimeout(DURATIONS.medium); + expect(await alert.isVisible()).toBe(true); + + await page.mouse.move(0, 0); + + await page.waitForTimeout(DURATIONS.medium + animationDurationInMs); + await page.waitForSelector("#alert", { visible: false }); }); }); }); diff --git a/src/components/alert/alert.scss b/src/components/alert/alert.scss index 17ac2e3e4a6..7e394f25b2e 100644 --- a/src/components/alert/alert.scss +++ b/src/components/alert/alert.scss @@ -310,6 +310,9 @@ $alertDurations: "fast" 6000ms, "medium" 10000ms, "slow" 14000ms; :host([auto-dismiss-duration="#{$name}"]) .alert-dismiss-progress:after { animation: dismissProgress $duration ease-out; } + :host(:hover[auto-dismiss-duration="#{$name}"]) .alert-dismiss-progress:after { + animation-play-state: paused; + } } @keyframes dismissProgress { diff --git a/src/components/alert/alert.stories.ts b/src/components/alert/alert.stories.ts index e6989a7214b..7d7ff4c9f9d 100644 --- a/src/components/alert/alert.stories.ts +++ b/src/components/alert/alert.stories.ts @@ -203,3 +203,11 @@ export const actionsEndQueued_TestOnly = (): string => html` }, "1000"); `; + +export const autoDismissableRetainsCloseButton_TestOnly = (): string => html` + +
Here's a general bit of information
+
Some kind of contextually relevant content
+ Take action +
+`; diff --git a/src/components/alert/alert.tsx b/src/components/alert/alert.tsx index 261b8b00a9c..db29d18366b 100644 --- a/src/components/alert/alert.tsx +++ b/src/components/alert/alert.tsx @@ -166,6 +166,7 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo disconnectedCallback(): void { window.clearTimeout(this.autoDismissTimeoutId); + window.clearTimeout(this.queueTimeout); disconnectOpenCloseComponent(this); disconnectLocalized(this); } @@ -226,6 +227,12 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo queued, [placement]: true }} + onPointerOut={ + this.autoDismiss && this.autoDismissTimeoutId ? this.handleMouseLeave : null + } + onPointerOver={ + this.autoDismiss && this.autoDismissTimeoutId ? this.handleMouseOver : null + } ref={this.setTransitionEl} > {requestedIcon ? ( @@ -242,7 +249,7 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo {slotNode} {this.queueLength > 1 ? queueCount : null} - {!autoDismiss ? closeButton : null} + {closeButton} {open && !queued && autoDismiss ?
: null}
@@ -351,7 +358,9 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo private queueTimeout: number; - private trackTimer = Date.now(); + private trackTimer: number; + + private remainingPausedTimeout = 0; /** the computed icon to render */ /* @internal */ @@ -423,4 +432,17 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo private actionsEndSlotChangeHandler = (event: Event): void => { this.hasEndActions = slotChangeHasAssignedElement(event); }; + + private handleMouseOver = (): void => { + window.clearTimeout(this.autoDismissTimeoutId); + this.remainingPausedTimeout = + DURATIONS[this.autoDismissDuration] - Date.now() - this.trackTimer; + }; + + private handleMouseLeave = (): void => { + this.autoDismissTimeoutId = window.setTimeout( + () => this.closeAlert(), + this.remainingPausedTimeout + ); + }; } diff --git a/src/components/alert/resources.ts b/src/components/alert/resources.ts index 587dcbfa69a..c2947febc64 100644 --- a/src/components/alert/resources.ts +++ b/src/components/alert/resources.ts @@ -17,5 +17,6 @@ export const SLOTS = { export const CSS = { actionsEnd: "actions-end", - container: "container" + container: "container", + close: "alert-close" };