Skip to content

Commit

Permalink
feat(alert): add 'queue' property to prioritize the ordering of alert…
Browse files Browse the repository at this point in the history
…s when opened (#10029)

**Related Issue:** #8705 #8316

## Summary

- add `queue` property to order an open alert `immediate`, `next` or
`last` (default).
- When a `queue="immediate"` alert is opened it is displayed
immediately.
- When a `queue="next"` alert is opened it will be displayed after the
currently opened one is closed.
  - By default, alerts are queued to be `last` when opened.
- When an alert is set to `queue="immediate"` after already being opened
it will be presented first.
    - Done by unregistering and registering the alert again
- All open alerts not at the top of the stack will be queued and auto
close timeout reset
  - All other alerts remain in their place.
- cleanup private var names
- add simple story
- add urgent story
- add e2e test
  • Loading branch information
driskull authored Aug 17, 2024
1 parent 089cecf commit 3aa47a4
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 55 deletions.
19 changes: 19 additions & 0 deletions packages/calcite-components/.storybook/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { TimeZoneMode } from "../src/components/input-time-zone/interfaces.ts";
import { DisplayMode } from "../src/components/sheet/interfaces.ts";
import { ShellDisplayMode } from "../src/components/shell/interfaces.ts";
import { OverlayPositioning } from "../src/components";
import { AlertDuration, AlertQueue } from "../src/components/alert/interfaces";

interface AttributeMetadata<T> {
values: T[];
Expand All @@ -35,11 +36,13 @@ interface AttributeMetadata<T> {
interface CommonAttributes {
alignment: AttributeMetadata<Alignment>;
appearance: AttributeMetadata<Appearance>;
duration: AttributeMetadata<AlertDuration>;
scale: AttributeMetadata<Scale>;
logicalFlowPosition: AttributeMetadata<LogicalFlowPosition>;
position: AttributeMetadata<Position>;
status: AttributeMetadata<Status>;
kind: AttributeMetadata<Kind>;
queue: AttributeMetadata<AlertQueue>;
width: AttributeMetadata<Width>;
selectionMode: AttributeMetadata<SelectionMode>;
arrowType: AttributeMetadata<ArrowType>;
Expand All @@ -60,15 +63,18 @@ interface CommonAttributes {
selectionAppearance: AttributeMetadata<SelectionAppearance>;
shellDisplayMode: AttributeMetadata<ShellDisplayMode>;
overlayPositioning: AttributeMetadata<OverlayPositioning>;
numberingSystem: AttributeMetadata<string>;
}

const logicalFlowPositionOptions: LogicalFlowPosition[] = ["inline-start", "inline-end", "block-start", "block-end"];
const positionOptions: Position[] = ["start", "end", "top", "bottom"];
const scaleOptions: Scale[] = ["s", "m", "l"];
const durationOptions: AlertDuration[] = ["slow", "medium", "fast"];
const alignmentOptions: Alignment[] = ["start", "center", "end"];
const appearanceOptions: Appearance[] = ["solid", "outline", "outline-fill", "transparent"];
const statusOptions: Status[] = ["invalid", "valid", "idle"];
const kindOptions: Kind[] = ["brand", "danger", "info", "inverse", "neutral", "warning", "success"];
const queueOptions: AlertQueue[] = ["last", "next", "immediate"];
const widthOptions: Width[] = ["auto", "half", "full"];
const selectionModeOptions: SelectionMode[] = [
"single",
Expand All @@ -93,6 +99,7 @@ const layoutOptions: Layout[] = [
"none",
"horizontal-single",
];
const numberingSystems = ["arab", "arabext", "latn"];
const dirOptions: Dir[] = ["ltr", "rtl"];
const buttonTypeOptions: TileSelectType[] = ["radio", "checkbox"];
const interactionModeOptions: TableInteractionMode[] = ["interactive", "static"];
Expand Down Expand Up @@ -128,6 +135,10 @@ export const ATTRIBUTES: CommonAttributes = {
values: appearanceOptions,
defaultValue: appearanceOptions[0],
},
duration: {
values: durationOptions,
defaultValue: durationOptions[1],
},
logicalFlowPosition: {
values: logicalFlowPositionOptions,
defaultValue: logicalFlowPositionOptions[2],
Expand All @@ -148,6 +159,10 @@ export const ATTRIBUTES: CommonAttributes = {
values: kindOptions,
defaultValue: kindOptions[0],
},
queue: {
values: queueOptions,
defaultValue: queueOptions[0],
},
width: {
values: widthOptions,
defaultValue: widthOptions[0],
Expand Down Expand Up @@ -228,4 +243,8 @@ export const ATTRIBUTES: CommonAttributes = {
values: shellDisplayModeOptions,
defaultValue: shellDisplayModeOptions[0],
},
numberingSystem: {
values: numberingSystems,
defaultValue: numberingSystems[2],
},
};
12 changes: 10 additions & 2 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n
import { Columns } from "./components/action-group/interfaces";
import { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n";
import { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n";
import { AlertDuration, Sync } from "./components/alert/interfaces";
import { AlertDuration, AlertQueue, Sync } from "./components/alert/interfaces";
import { NumberingSystem } from "./utils/locale";
import { AlertMessages } from "./components/alert/assets/alert/t9n";
import { HeadingLevel } from "./components/functional/Heading";
Expand Down Expand Up @@ -111,7 +111,7 @@ export { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n
export { Columns } from "./components/action-group/interfaces";
export { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n";
export { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n";
export { AlertDuration, Sync } from "./components/alert/interfaces";
export { AlertDuration, AlertQueue, Sync } from "./components/alert/interfaces";
export { NumberingSystem } from "./utils/locale";
export { AlertMessages } from "./components/alert/assets/alert/t9n";
export { HeadingLevel } from "./components/functional/Heading";
Expand Down Expand Up @@ -561,6 +561,10 @@ export namespace Components {
* Specifies the placement of the component.
*/
"placement": MenuPlacement;
/**
* Specifies the ordering priority of the component when opened.
*/
"queue": AlertQueue;
/**
* Specifies the size of the component.
*/
Expand Down Expand Up @@ -8481,6 +8485,10 @@ declare namespace LocalJSX {
* Specifies the placement of the component.
*/
"placement"?: MenuPlacement;
/**
* Specifies the ordering priority of the component when opened.
*/
"queue"?: AlertQueue;
/**
* Specifies the size of the component.
*/
Expand Down
96 changes: 94 additions & 2 deletions packages/calcite-components/src/components/alert/alert.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
import { accessible, defaults, hidden, HYDRATED_ATTR, renders, t9n } from "../../tests/commonTests";
import { getElementXY } from "../../tests/utils";
import { accessible, defaults, hidden, HYDRATED_ATTR, reflects, renders, t9n } from "../../tests/commonTests";
import { getElementXY, skipAnimations } from "../../tests/utils";
import { openClose } from "../../tests/commonTests";
import { CSS, DURATIONS } from "./resources";

Expand All @@ -15,6 +15,19 @@ describe("defaults", () => {
propertyName: "embedded",
defaultValue: false,
},
{
propertyName: "queue",
defaultValue: "last",
},
]);
});

describe("reflects", () => {
reflects("calcite-alert", [
{
propertyName: "queue",
value: "last",
},
]);
});

Expand Down Expand Up @@ -189,6 +202,85 @@ describe("calcite-alert", () => {
expect(await alert3.isVisible()).toBe(true);
});

it("should queue alerts", async () => {
const page = await newE2EPage();
await skipAnimations(page);
await page.setContent(html`
<calcite-alert id="alert-1"> ${alertContent} </calcite-alert>
<calcite-alert id="alert-2"> ${alertContent} </calcite-alert>
<calcite-alert id="alert-3"> ${alertContent} </calcite-alert>
`);

const alert1 = await page.find("#alert-1");
const alert2 = await page.find("#alert-2");
const alert3 = await page.find("#alert-3");

expect(await alert1.isVisible()).toBe(false);
expect(await alert2.isVisible()).toBe(false);
expect(await alert3.isVisible()).toBe(false);

alert1.setProperty("open", true);
await page.waitForChanges();
alert2.setProperty("open", true);
await page.waitForChanges();
alert3.setProperty("queue", "immediate");
await page.waitForChanges();
alert3.setProperty("open", true);
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

expect(await alert1.isVisible()).toBe(true);
expect(await alert2.isVisible()).toBe(true);
expect(await alert3.isVisible()).toBe(true);

const alert1Container = await page.find(`#alert-1 >>> .${CSS.container}`);
const alert2Container = await page.find(`#alert-2 >>> .${CSS.container}`);
const alert3Container = await page.find(`#alert-3 >>> .${CSS.container}`);

expect(await alert1Container.isVisible()).toBe(false);
expect(await alert2Container.isVisible()).toBe(false);
expect(await alert3Container.isVisible()).toBe(true);

alert3.setProperty("queue", "immediate");
await page.waitForChanges();
alert2.setProperty("queue", "immediate");
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

expect(await alert1Container.isVisible()).toBe(false);
expect(await alert2Container.isVisible()).toBe(true);
expect(await alert3Container.isVisible()).toBe(false);

alert1.setProperty("queue", "next");
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

alert2.setProperty("open", false);
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

expect(await alert1Container.isVisible()).toBe(true);
expect(await alert2Container.isVisible()).toBe(false);
expect(await alert3Container.isVisible()).toBe(false);

alert2.setProperty("queue", "next");
alert2.setProperty("open", true);
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

expect(await alert1Container.isVisible()).toBe(true);
expect(await alert2Container.isVisible()).toBe(false);
expect(await alert3Container.isVisible()).toBe(false);

alert1.setProperty("open", false);
await page.waitForChanges();
await page.waitForTimeout(animationDurationInMs);

expect(await alert1Container.isVisible()).toBe(false);
expect(await alert2Container.isVisible()).toBe(true);
expect(await alert3Container.isVisible()).toBe(false);
});

it("correctly assigns a default placement class", async () => {
const page = await newE2EPage();
await page.setContent(`
Expand Down
116 changes: 114 additions & 2 deletions packages/calcite-components/src/components/alert/alert.stories.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
import { iconNames } from "../../../.storybook/helpers";
import { modesDarkDefault } from "../../../.storybook/utils";
import { boolean, modesDarkDefault } from "../../../.storybook/utils";
import { html } from "../../../support/formatting";
import { ATTRIBUTES } from "../../../.storybook/resources";
import { menuPlacements } from "../../utils/floating-ui";
import { Alert } from "./Alert";
const { scale, duration, kind, numberingSystem, queue } = ATTRIBUTES;

interface AlertStoryArgs
extends Pick<
Alert,
| "autoClose"
| "autoCloseDuration"
| "icon"
| "iconFlipRtl"
| "kind"
| "label"
| "numberingSystem"
| "open"
| "placement"
| "scale"
| "queue"
> {}

export default {
title: "Components/Alert",
args: {
autoClose: false,
autoCloseDuration: duration.defaultValue,
icon: "",
iconFlipRtl: false,
kind: kind.defaultValue,
label: "Alert",
numberingSystem: numberingSystem[2],
open: true,
placement: menuPlacements[4],
scale: "m",
queue: "last",
},
argTypes: {
autoCloseDuration: {
options: duration.values,
control: { type: "select" },
},
icon: {
options: iconNames,
control: { type: "select" },
},
kind: {
options: kind.values.filter((option) => option !== "inverse" && option !== "neutral"),
control: { type: "select" },
},
numberingSystem: {
options: numberingSystem,
control: { type: "select" },
},
placement: {
options: menuPlacements,
control: { type: "select" },
},
queue: {
options: queue.values,
control: { type: "select" },
},
scale: {
options: scale.values,
control: { type: "select" },
},
},
parameters: {
chromatic: {
delay: 500,
Expand All @@ -23,6 +86,29 @@ const wrapperStyles = html`
</style>
`;

export const simple = (args: AlertStoryArgs): string => html`
${wrapperStyles}
<div class="wrapper">
<calcite-alert
${boolean("auto-close", args.autoClose)}
${boolean("open", args.open)}
${boolean("icon-flip-rtl", args.iconFlipRtl)}
queue="${args.queue}"
auto-close-duration="${args.autoCloseDuration}"
scale="${args.scale}"
kind="${args.kind}"
icon="${args.icon}"
label="${args.label}"
numbering-system="${args.numberingSystem}"
placement="${args.placement}"
>
<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">Take action</calcite-link>
</calcite-alert>
</div>
`;

export const titleMessageLink = (): string => html`
${wrapperStyles}
<div class="wrapper">
Expand Down Expand Up @@ -178,7 +264,7 @@ export const actionsEndQueued_TestOnly = (): string => html`
<script>
setTimeout(() => {
document.querySelector("#two").open = true;
}, "1000");
}, 250);
</script>
</div>
`;
Expand All @@ -200,3 +286,29 @@ export const textAlignDoesNotAffectComponentAlignment_TestOnly = (): string => h
</calcite-alert>
</div>
`;

export const withQueue = (): string => html`
${wrapperStyles}
<div class="wrapper">
<calcite-alert id="one" kind="brand" open>
<div slot="title">Open by default</div>
<div slot="message">We thought you might want to take a look</div>
</calcite-alert>
<calcite-alert id="two" queue="immediate" kind="danger">
<div slot="title">Immediate Alert</div>
<div slot="message">We thought you might want to take a look</div>
</calcite-alert>
<calcite-alert id="three" kind="success">
<div slot="title">Third Alert</div>
<div slot="message">We thought you might want to take a look</div>
</calcite-alert>
<script>
setTimeout(() => {
document.querySelector("#two").open = true;
}, 100);
setTimeout(() => {
document.querySelector("#three").open = true;
}, 250);
</script>
</div>
`;
Loading

0 comments on commit 3aa47a4

Please sign in to comment.