Skip to content

Commit

Permalink
fix(alert): auto-dismissible retains close button and dismisses timer…
Browse files Browse the repository at this point in the history
… 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`.
  • Loading branch information
Elijbet committed Dec 8, 2022
1 parent 84425c3 commit 274b104
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 20 deletions.
109 changes: 92 additions & 17 deletions src/components/alert/alert.e2e.ts
Original file line number Diff line number Diff line change
@@ -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 = `
Expand Down Expand Up @@ -50,12 +51,10 @@ describe("calcite-alert", () => {
</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();
});

Expand Down Expand Up @@ -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: `
<calcite-alert open id="first-open" icon="3d-glasses" auto-dismiss-duration="fast" scale="l">
<div slot="title">Title of alert #1</div>
<div slot="message">Message text of the alert</div>
const page = await newE2EPage();
await page.setContent(html`
<calcite-button id="buttonOne" onclick="document.querySelector('#first-open').setAttribute('open', '')"
>open alert</calcite-button
>
<calcite-button id="buttonTwo" onclick="document.querySelector('#alert-to-be-queued').setAttribute('open', '')"
>open alert</calcite-button
>
<calcite-alert open id="first-open" icon="3d-glasses" auto-dismiss scale="l">
<div slot="title">Title of alert Uno</div>
<div slot="message">Message text of the alert Uno</div>
<a slot="link" href="#">Retry</a>
</calcite-alert>
<calcite-alert id="alert-to-be-queued" icon auto-dismiss scale="l">
<div slot="title">Title of alert #2</div>
<div slot="message">Message text of the alert</div>
<div slot="title">Title of alert Dos</div>
<div slot="message">Message text of the alert Dos</div>
<a slot="link" href="#">Retry</a>
</calcite-alert>
`
});
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`
<div>
<calcite-button id="button" onclick="document.querySelector('#alert').setAttribute('open', '')"
>open alert</calcite-button
>
<calcite-alert label="this is a success" id="alert" auto-dismiss icon color="green">
${alertContent}</calcite-alert
>
</div>
`);
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 });
});
});
});
3 changes: 3 additions & 0 deletions src/components/alert/alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/components/alert/alert.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,11 @@ export const actionsEndQueued_TestOnly = (): string => html`
}, "1000");
</script>
`;

export const autoDismissableRetainsCloseButton_TestOnly = (): string => html`
<calcite-alert auto-dismiss auto-dismiss-duration="medium" open scale="m" color="blue">
<div slot="title">Here's a general bit of information</div>
<div slot="message">Some kind of contextually relevant content</div>
<calcite-link slot="link" title="my action" role="presentation"> Take action </calcite-link>
</calcite-alert>
`;
26 changes: 24 additions & 2 deletions src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 ? (
Expand All @@ -242,7 +249,7 @@ export class Alert implements OpenCloseComponent, LocalizedComponent, LoadableCo
{slotNode}
</div>
{this.queueLength > 1 ? queueCount : null}
{!autoDismiss ? closeButton : null}
{closeButton}
{open && !queued && autoDismiss ? <div class="alert-dismiss-progress" /> : null}
</div>
</Host>
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
);
};
}
3 changes: 2 additions & 1 deletion src/components/alert/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export const SLOTS = {

export const CSS = {
actionsEnd: "actions-end",
container: "container"
container: "container",
close: "alert-close"
};

0 comments on commit 274b104

Please sign in to comment.