UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

712 lines (711 loc) • 23.5 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { h, Host } from "@stencil/core"; import { getSlotted, setRequestedIcon, slotChangeHasAssignedElement, toAriaBoolean } from "../../utils/dom"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { connectLocalized, disconnectLocalized, numberStringFormatter } from "../../utils/locale"; import { connectOpenCloseComponent, disconnectOpenCloseComponent } from "../../utils/openCloseComponent"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import { KindIcons } from "../resources"; import { CSS, DURATIONS, SLOTS } from "./resources"; /** * Alerts are meant to provide a way to communicate urgent or important information to users, frequently as a result of an action they took in your app. Alerts are positioned * at the bottom of the page. Multiple opened alerts will be added to a queue, allowing users to dismiss them in the order they are provided. */ /** * @slot title - A slot for adding a title to the component. * @slot message - A slot for adding main text to the component. * @slot link - A slot for adding a `calcite-action` to take from the component such as: "undo", "try again", "link to page", etc. * @slot actions-end - A slot for adding `calcite-action`s to the end of the component. It is recommended to use two or fewer actions. */ export class Alert { constructor() { this.autoCloseTimeoutId = null; this.totalOpenTime = 0; this.totalHoverTime = 0; this.openTransitionProp = "opacity"; //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- this.setTransitionEl = (el) => { this.transitionEl = el; connectOpenCloseComponent(this); }; /** close and emit calciteInternalAlertSync event with the updated queue payload */ this.closeAlert = () => { this.autoCloseTimeoutId = null; this.queued = false; this.open = false; this.queue = this.queue.filter((el) => el !== this.el); this.determineActiveAlert(); this.calciteInternalAlertSync.emit({ queue: this.queue }); }; this.actionsEndSlotChangeHandler = (event) => { this.hasEndActions = slotChangeHasAssignedElement(event); }; this.handleMouseOver = () => { window.clearTimeout(this.autoCloseTimeoutId); this.totalOpenTime = Date.now() - this.initialOpenTime; this.lastMouseOverBegin = Date.now(); }; this.handleMouseLeave = () => { const hoverDuration = Date.now() - this.lastMouseOverBegin; const timeRemaining = DURATIONS[this.autoCloseDuration] - this.totalOpenTime + this.totalHoverTime; this.totalHoverTime = this.totalHoverTime ? hoverDuration + this.totalHoverTime : hoverDuration; this.autoCloseTimeoutId = window.setTimeout(() => this.closeAlert(), timeRemaining); }; this.open = false; this.autoClose = false; this.autoCloseDuration = "medium"; this.kind = "brand"; this.icon = undefined; this.iconFlipRtl = false; this.label = undefined; this.numberingSystem = undefined; this.placement = "bottom"; this.scale = "m"; this.messages = undefined; this.messageOverrides = undefined; this.slottedInShell = undefined; this.effectiveLocale = ""; this.defaultMessages = undefined; this.hasEndActions = false; this.queue = []; this.queueLength = 0; this.queued = false; this.requestedIcon = undefined; } openHandler() { if (this.open && !this.queued) { this.calciteInternalAlertRegister.emit(); } if (!this.open) { this.queue = this.queue.filter((el) => el !== this.el); this.calciteInternalAlertSync.emit({ queue: this.queue }); } } onMessagesChange() { /* wired up by t9n util */ } updateRequestedIcon() { this.requestedIcon = setRequestedIcon(KindIcons, this.icon, this.kind); } updateDuration() { if (this.autoClose && this.autoCloseTimeoutId) { window.clearTimeout(this.autoCloseTimeoutId); this.autoCloseTimeoutId = window.setTimeout(() => this.closeAlert(), DURATIONS[this.autoCloseDuration]); } } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectLocalized(this); connectMessages(this); const open = this.open; if (open && !this.queued) { this.openHandler(); this.calciteInternalAlertRegister.emit(); } connectOpenCloseComponent(this); } async componentWillLoad() { setUpLoadableComponent(this); this.requestedIcon = setRequestedIcon(KindIcons, this.icon, this.kind); await setUpMessages(this); } componentDidLoad() { setComponentLoaded(this); } disconnectedCallback() { window.dispatchEvent(new CustomEvent("calciteInternalAlertUnregister", { detail: { alert: this.el } })); window.clearTimeout(this.autoCloseTimeoutId); window.clearTimeout(this.queueTimeout); disconnectOpenCloseComponent(this); disconnectLocalized(this); disconnectMessages(this); this.slottedInShell = false; } render() { const { hasEndActions } = this; const closeButton = (h("button", { "aria-label": this.messages.close, class: "alert-close", onClick: this.closeAlert, type: "button", // eslint-disable-next-line react/jsx-sort-props ref: (el) => (this.closeButton = el) }, h("calcite-icon", { icon: "x", scale: this.scale === "l" ? "m" : "s" }))); numberStringFormatter.numberFormatOptions = { locale: this.effectiveLocale, numberingSystem: this.numberingSystem, signDisplay: "always" }; const queueNumber = this.queueLength > 2 ? this.queueLength - 1 : 1; const queueText = numberStringFormatter.numberFormatter.format(queueNumber); const queueCount = (h("div", { class: `${this.queueLength > 1 ? "active " : ""}alert-queue-count` }, h("calcite-chip", { scale: this.scale, value: queueText }, queueText))); const { open, autoClose, label, placement, queued, requestedIcon, iconFlipRtl } = this; const role = autoClose ? "alert" : "alertdialog"; const hidden = !open; const slotNode = (h("slot", { key: "actionsEndSlot", name: SLOTS.actionsEnd, onSlotchange: this.actionsEndSlotChangeHandler })); return (h(Host, { "aria-hidden": toAriaBoolean(hidden), "aria-label": label, "calcite-hydrated-hidden": hidden, role: role }, h("div", { class: { container: true, queued, [placement]: true, [CSS.slottedInShell]: this.slottedInShell }, onPointerEnter: this.autoClose && this.autoCloseTimeoutId ? this.handleMouseOver : null, onPointerLeave: this.autoClose && this.autoCloseTimeoutId ? this.handleMouseLeave : null, // eslint-disable-next-line react/jsx-sort-props ref: this.setTransitionEl }, requestedIcon ? (h("div", { class: "alert-icon" }, h("calcite-icon", { flipRtl: iconFlipRtl, icon: requestedIcon, scale: this.scale === "l" ? "m" : "s" }))) : null, h("div", { class: "alert-content" }, h("slot", { name: SLOTS.title }), h("slot", { name: SLOTS.message }), h("slot", { name: SLOTS.link })), h("div", { class: CSS.actionsEnd, hidden: !hasEndActions }, slotNode), this.queueLength > 1 ? queueCount : null, closeButton, open && !queued && autoClose ? h("div", { class: "alert-dismiss-progress" }) : null))); } // when an alert is opened or closed, update queue and determine active alert alertSync(event) { if (this.queue !== event.detail.queue) { this.queue = event.detail.queue; } this.queueLength = this.queue.length; this.determineActiveAlert(); event.stopPropagation(); } // when an alert is first registered, trigger a queue sync alertRegister() { if (this.open && !this.queue.includes(this.el)) { this.queued = true; this.queue.push(this.el); } this.calciteInternalAlertSync.emit({ queue: this.queue }); this.determineActiveAlert(); } // Event is dispatched on the window because the element is not in the DOM so bubbling won't occur. alertUnregister(event) { const queue = this.queue.filter((el) => el !== event.detail.alert); this.queue = queue; window.dispatchEvent(new CustomEvent("calciteInternalAlertSync", { detail: { queue } })); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component's "close" button (the first focusable item). */ async setFocus() { await componentLoaded(this); const alertLinkEl = getSlotted(this.el, { selector: "calcite-link" }); if (!this.closeButton && !alertLinkEl) { return; } else if (alertLinkEl) { alertLinkEl.setFocus(); } else if (this.closeButton) { this.closeButton.focus(); } } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } /** determine which alert is active */ determineActiveAlert() { if (this.queue?.[0] === this.el) { this.openAlert(); if (this.autoClose && !this.autoCloseTimeoutId) { this.initialOpenTime = Date.now(); this.autoCloseTimeoutId = window.setTimeout(() => this.closeAlert(), DURATIONS[this.autoCloseDuration]); } } else { return; } } onBeforeOpen() { this.calciteAlertBeforeOpen.emit(); } onOpen() { this.calciteAlertOpen.emit(); } onBeforeClose() { this.calciteAlertBeforeClose.emit(); } onClose() { this.calciteAlertClose.emit(); } /** remove queued class after animation completes */ openAlert() { window.clearTimeout(this.queueTimeout); this.queueTimeout = window.setTimeout(() => (this.queued = false), 300); } static get is() { return "calcite-alert"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["alert.scss"] }; } static get styleUrls() { return { "$": ["alert.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, displays and positions the component." }, "attribute": "open", "reflect": true, "defaultValue": "false" }, "autoClose": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the component closes automatically (recommended for passive, non-blocking alerts)." }, "attribute": "auto-close", "reflect": true, "defaultValue": "false" }, "autoCloseDuration": { "type": "string", "mutable": false, "complexType": { "original": "AlertDuration", "resolved": "\"fast\" | \"medium\" | \"slow\"", "references": { "AlertDuration": { "location": "import", "path": "./interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the duration before the component automatically closes (only use with `autoClose`)." }, "attribute": "auto-close-duration", "reflect": true, "defaultValue": "\"medium\"" }, "kind": { "type": "string", "mutable": false, "complexType": { "original": "Extract<\n \"brand\" | \"danger\" | \"info\" | \"success\" | \"warning\",\n Kind\n >", "resolved": "\"brand\" | \"danger\" | \"info\" | \"success\" | \"warning\"", "references": { "Extract": { "location": "global" }, "Kind": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the kind of the component (will apply to top border and icon)." }, "attribute": "kind", "reflect": true, "defaultValue": "\"brand\"" }, "icon": { "type": "any", "mutable": false, "complexType": { "original": "string | boolean", "resolved": "boolean | string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, shows a default recommended icon. Alternatively,\npass a Calcite UI Icon name to display a specific icon." }, "attribute": "icon", "reflect": true }, "iconFlipRtl": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the icon will be flipped when the element direction is right-to-left (`\"rtl\"`)." }, "attribute": "icon-flip-rtl", "reflect": true, "defaultValue": "false" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "Specifies an accessible name for the component." }, "attribute": "label", "reflect": false }, "numberingSystem": { "type": "string", "mutable": false, "complexType": { "original": "NumberingSystem", "resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"", "references": { "NumberingSystem": { "location": "import", "path": "../../utils/locale" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the Unicode numeral system used by the component for localization." }, "attribute": "numbering-system", "reflect": true }, "placement": { "type": "string", "mutable": false, "complexType": { "original": "MenuPlacement", "resolved": "\"bottom\" | \"bottom-end\" | \"bottom-start\" | \"top\" | \"top-end\" | \"top-start\"", "references": { "MenuPlacement": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the placement of the component" }, "attribute": "placement", "reflect": true, "defaultValue": "\"bottom\"" }, "scale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the size of the component." }, "attribute": "scale", "reflect": true, "defaultValue": "\"m\"" }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "AlertMessages", "resolved": "{ close: string; }", "references": { "AlertMessages": { "location": "import", "path": "./assets/alert/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Made into a prop for testing purposes only" } }, "messageOverrides": { "type": "unknown", "mutable": true, "complexType": { "original": "Partial<AlertMessages>", "resolved": "{ close?: string; }", "references": { "Partial": { "location": "global" }, "AlertMessages": { "location": "import", "path": "./assets/alert/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } }, "slottedInShell": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "This internal property, managed by a containing calcite-shell, is used\nto inform the component if special configuration or styles are needed" }, "attribute": "slotted-in-shell", "reflect": false } }; } static get states() { return { "effectiveLocale": {}, "defaultMessages": {}, "hasEndActions": {}, "queue": {}, "queueLength": {}, "queued": {}, "requestedIcon": {} }; } static get events() { return [{ "method": "calciteAlertBeforeClose", "name": "calciteAlertBeforeClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is requested to be closed and before the closing transition begins." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteAlertClose", "name": "calciteAlertClose", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is closed and animation is complete." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteAlertBeforeOpen", "name": "calciteAlertBeforeOpen", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is added to the DOM but not rendered, and before the opening transition begins." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteAlertOpen", "name": "calciteAlertOpen", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Fires when the component is open and animation is complete." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }, { "method": "calciteInternalAlertSync", "name": "calciteInternalAlertSync", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Fires to sync queue when opened or closed." }, "complexType": { "original": "Sync", "resolved": "Sync", "references": { "Sync": { "location": "import", "path": "./interfaces" } } } }, { "method": "calciteInternalAlertRegister", "name": "calciteInternalAlertRegister", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Fires when the component is added to DOM - used to receive initial queue." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" }, "HTMLCalciteLinkElement": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets focus on the component's \"close\" button (the first focusable item).", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "open", "methodName": "openHandler" }, { "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "icon", "methodName": "updateRequestedIcon" }, { "propName": "kind", "methodName": "updateRequestedIcon" }, { "propName": "autoCloseDuration", "methodName": "updateDuration" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } static get listeners() { return [{ "name": "calciteInternalAlertSync", "method": "alertSync", "target": "window", "capture": false, "passive": false }, { "name": "calciteInternalAlertRegister", "method": "alertRegister", "target": "window", "capture": false, "passive": false }, { "name": "calciteInternalAlertUnregister", "method": "alertUnregister", "target": "window", "capture": false, "passive": false }]; } }