UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

338 lines (337 loc) • 24.2 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { c as customElement } from "../../chunks/runtime.js"; import { keyed } from "lit-html/directives/keyed.js"; import { html, nothing } from "lit"; import { createRef, ref } from "lit-html/directives/ref.js"; import { LitElement, createEvent, setAttribute, safeClassMap } from "@arcgis/lumina"; import { h as focusFirstTabbable, s as slotChangeGetAssignedElements, a as slotChangeHasAssignedElement, o as ensureId } from "../../chunks/dom.js"; import { c as componentFocusable, g as getIconScale } from "../../chunks/component.js"; import { c as createObserver } from "../../chunks/observers.js"; import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js"; import { l as logger } from "../../chunks/logger.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { u as usePreventDocumentScroll } from "../../chunks/usePreventDocumentScroll.js"; import { u as useFocusTrap } from "../../chunks/useFocusTrap.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { modal: "modal", title: "title", header: "header", footer: "footer", scrim: "scrim", back: "back", close: "close", secondary: "secondary", primary: "primary", container: "container", containerOpen: "container--open", content: "content", contentNoFooter: "content--no-footer", contentBottom: "content-bottom", contentTop: "content-top", // these classes help apply the animation in phases to only set transform on open/close // this helps avoid a positioning issue for any floating-ui-owning children openingIdle: "modal--opening-idle", openingActive: "modal--opening-active", closingIdle: "modal--closing-idle", closingActive: "modal--closing-active" }; const ICONS = { close: "x" }; const SLOTS = { content: "content", contentBottom: "content-bottom", contentTop: "content-top", back: "back", secondary: "secondary", primary: "primary" }; const styles = css`:host{--calcite-modal-scrim-background: rgba(0, 0, 0, .85);position:fixed;inset:0;z-index:var(--calcite-z-index-overlay);display:flex;opacity:0;visibility:hidden!important;transition:visibility 0ms linear var(--calcite-internal-animation-timing-slow),opacity var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88);--calcite-modal-scrim-background-internal: rgba(0, 0, 0, .85)}.content-top[hidden],.content-bottom[hidden]{display:none}.container{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow-y:hidden;color:var(--calcite-color-text-2);opacity:0;visibility:hidden!important;transition:visibility 0ms linear var(--calcite-internal-animation-timing-slow),opacity var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88)}:host([scale=s]){--calcite-modal-padding-internal: .75rem;--calcite-modal-padding-large-internal: 1rem;--calcite-modal-title-text-internal: var(--calcite-font-size-1);--calcite-modal-content-text-internal: var(--calcite-font-size--1)}:host([scale=m]){--calcite-modal-padding-internal: 1rem;--calcite-modal-padding-large-internal: 1.25rem;--calcite-modal-title-text-internal: var(--calcite-font-size-2);--calcite-modal-content-text-internal: var(--calcite-font-size-0)}:host([scale=l]){--calcite-modal-padding-internal: 1.25rem;--calcite-modal-padding-large-internal: 1.5rem;--calcite-modal-title-text-internal: var(--calcite-font-size-3);--calcite-modal-content-text-internal: var(--calcite-font-size-1)}.scrim{--calcite-scrim-background: var(--calcite-modal-scrim-background, var(--calcite-color-transparent-scrim));position:absolute;inset:0;display:flex;overflow-y:hidden}.modal{pointer-events:none;z-index:var(--calcite-z-index-modal);float:none;margin:1.5rem;box-sizing:border-box;display:flex;inline-size:100%;flex-direction:column;overflow:hidden;border-radius:.25rem;background-color:var(--calcite-color-foreground-1);opacity:0;--tw-shadow: 0 2px 12px -4px rgba(0, 0, 0, .2), 0 2px 4px -2px rgba(0, 0, 0, .16);--tw-shadow-colored: 0 2px 12px -4px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);-webkit-overflow-scrolling:touch;visibility:hidden;transition:transform var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88),visibility 0ms linear var(--calcite-internal-animation-timing-slow),opacity var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88);--calcite-modal-hidden-position: translate3d(0, 20px, 0);--calcite-modal-shown-position: translate3d(0, 0, 0)}.modal--opening-idle{transform:var(--calcite-modal-hidden-position)}.modal--opening-active,.modal--closing-idle{transform:var(--calcite-modal-shown-position)}.modal--closing-active{transform:var(--calcite-modal-hidden-position)}:host([opened]){opacity:1;visibility:visible!important;transition-delay:0ms}.container--open{opacity:1;visibility:visible!important;transition-delay:0ms}.container--open .modal{pointer-events:auto;visibility:visible;opacity:1;transition:transform var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88),visibility 0ms linear,opacity var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88),max-inline-size var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88),max-block-size var(--calcite-internal-animation-timing-slow) cubic-bezier(.215,.44,.42,.88);transition-delay:0ms}.header{z-index:var(--calcite-z-index-header);display:flex;min-inline-size:0px;max-inline-size:100%;border-start-start-radius:.25rem;border-start-end-radius:.25rem;border-width:0px;border-block-end-width:1px;border-style:solid;border-color:var(--calcite-color-border-3);background-color:var(--calcite-color-foreground-1);flex:0 0 auto}.close{order:2;margin:0;cursor:pointer;appearance:none;border-style:none;background-color:transparent;color:var(--calcite-color-text-3);outline-color:transparent;transition-property:background-color,block-size,border-color,box-shadow,color,inset-block-end,inset-block-start,inset-inline-end,inset-inline-start,inset-size,opacity,outline-color,transform;transition-duration:var(--calcite-animation-timing);transition-timing-function:ease-in-out;padding-block:var(--calcite-modal-padding-internal);padding-inline:var(--calcite-modal-padding-internal);flex:0 0 auto}.close calcite-icon{vertical-align:-2px}.close:focus{outline:2px solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(-2px*(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}.close:hover,.close:focus,.close:active{background-color:var(--calcite-color-foreground-2);color:var(--calcite-color-text-1)}.title{order:1;display:flex;min-inline-size:0px;align-items:center;flex:1 1 auto;padding-block:var(--calcite-modal-padding-internal);padding-inline:var(--calcite-modal-padding-large-internal)}slot[name=header]::slotted(*),*::slotted([slot=header]){margin:0;font-weight:var(--calcite-font-weight-normal);color:var(--calcite-color-text-1);font-size:var(--calcite-modal-title-text-internal)}.content{position:relative;box-sizing:border-box;display:block;block-size:100%;overflow:auto;padding:0;background-color:var(--calcite-modal-content-background, var(--calcite-color-foreground-1));max-block-size:100%;padding:var(--calcite-modal-content-padding, var(--calcite-modal-padding-internal))}.content-top,.content-bottom{z-index:var(--calcite-z-index-header);display:flex;border-width:0px;border-style:solid;border-color:var(--calcite-color-border-3);background-color:var(--calcite-color-foreground-1);flex:0 0 auto;padding:var(--calcite-modal-padding-internal)}.content-top{min-inline-size:0px;max-inline-size:100%;border-block-end-width:1px}.content-bottom{margin-block-start:auto;box-sizing:border-box;inline-size:100%;justify-content:space-between;border-block-start-width:1px}.content-top:not(.header~.content-top){border-start-start-radius:.25rem;border-start-end-radius:.25rem}.content-bottom:not(.content-bottom~.footer),.content--no-footer{border-end-end-radius:.25rem;border-end-start-radius:.25rem}slot[name=content]::slotted(*),*::slotted([slot=content]){font-size:var(--calcite-modal-context-text-internal)}.footer{z-index:var(--calcite-z-index-header);margin-block-start:auto;box-sizing:border-box;display:flex;inline-size:100%;justify-content:space-between;border-end-end-radius:.25rem;border-end-start-radius:.25rem;border-width:0px;border-block-start-width:1px;border-style:solid;border-color:var(--calcite-color-border-3);background-color:var(--calcite-color-foreground-1);flex:0 0 auto;padding-block:var(--calcite-modal-padding-internal);padding-inline:var(--calcite-modal-padding-large-internal)}.footer--hide-back .back,.footer--hide-secondary .secondary{display:none}.back{display:block;margin-inline-end:auto}.secondary{margin-inline:.25rem;display:block}slot[name=primary]{display:block}:host([width=small]) .modal{inline-size:auto}:host([width-scale=s]) .modal{max-block-size:100%;max-inline-size:100%;inline-size:var(--calcite-modal-width, 32rem);block-size:var(--calcite-modal-height, auto)}@media screen and (max-width: 35rem){:host([width-scale=s]) .modal{margin:0;block-size:100%;max-block-size:100%;inline-size:100%;max-inline-size:100%}:host([width-scale=s]) .content{flex:1 1 auto;max-block-size:unset}:host([width-scale=s][docked]) .container{align-items:flex-end}}:host([width-scale=m]) .modal{max-block-size:100%;max-inline-size:100%;inline-size:var(--calcite-modal-width, 48rem);block-size:var(--calcite-modal-height, auto)}@media screen and (max-width: 51rem){:host([width-scale=m]) .modal{margin:0;block-size:100%;max-block-size:100%;inline-size:100%;max-inline-size:100%}:host([width-scale=m]) .content{flex:1 1 auto;max-block-size:unset}:host([width-scale=m][docked]) .container{align-items:flex-end}}:host([width-scale=l]) .modal{max-block-size:100%;max-inline-size:100%;inline-size:var(--calcite-modal-width, 94rem);block-size:var(--calcite-modal-height, auto)}@media screen and (max-width: 97rem){:host([width-scale=l]) .modal{margin:0;block-size:100%;max-block-size:100%;inline-size:100%;max-inline-size:100%}:host([width-scale=l]) .content{flex:1 1 auto;max-block-size:unset}:host([width-scale=l][docked]) .container{align-items:flex-end}}:host([fullscreen]) .modal{margin:0;block-size:100%;max-block-size:100%;inline-size:100%;max-inline-size:100%;border-radius:0;--calcite-modal-hidden-position: translate3D(0, 20px, 0) scale(.95);--calcite-modal-shown-position: translate3D(0, 0, 0) scale(1)}:host([fullscreen]) .content{max-block-size:100%;flex:1 1 auto}:host([opened][fullscreen]) .header,:host([opened][fullscreen]) .footer,:host([opened][fullscreen]) .content-top,:host([opened][fullscreen]) .content-bottom{border-radius:0}:host([docked]) .modal{block-size:var(--calcite-modal-height, auto)}:host([docked]) .content{block-size:auto;flex:1 1 auto}:host([kind=brand]) .modal{border-color:var(--calcite-color-brand)}:host([kind=danger]) .modal{border-color:var(--calcite-color-status-danger)}:host([kind=info]) .modal{border-color:var(--calcite-color-status-info)}:host([kind=success]) .modal{border-color:var(--calcite-color-status-success)}:host([kind=warning]) .modal{border-color:var(--calcite-color-status-warning)}:host([kind=brand]) .modal,:host([kind=danger]) .modal,:host([kind=info]) .modal,:host([kind=success]) .modal,:host([kind=warning]) .modal{border-width:0px;border-block-start-width:4px;border-style:solid}:host([kind=brand]) .header,:host([kind=brand]) .content-top,:host([kind=danger]) .header,:host([kind=danger]) .content-top,:host([kind=info]) .header,:host([kind=info]) .content-top,:host([kind=success]) .header,:host([kind=success]) .content-top,:host([kind=warning]) .header,:host([kind=warning]) .content-top{border-radius:.25rem;border-end-end-radius:0px;border-end-start-radius:0px}@media screen and (max-width: 860px){* slot[name=header]::slotted(content-top),* content-top::slotted([slot=header]){font-size:var(--calcite-font-size-1)}.footer,.content-bottom{position:sticky;inset-block-end:0px}}@media screen and (max-width: 480px){.footer,.content-bottom{flex-direction:column}.back,.secondary{margin:0;margin-block-end:.25rem}}:host([hidden]){display:none}[hidden]{display:none}`; class Modal extends LitElement { constructor() { super(); this.closeButtonEl = createRef(); this.cssVarObserver = createObserver("mutation", () => { this.updateSizeCssVars(); }); this.focusTrap = useFocusTrap({ triggerProp: "open", focusTrapOptions: { // scrim closes on click, so we let it take over clickOutsideDeactivates: () => this.embedded, escapeDeactivates: (event) => { if (!event.defaultPrevented && !this.escapeDisabled) { this.open = false; event.preventDefault(); } return false; } } })(this); this.usePreventDocumentScroll = usePreventDocumentScroll()(this); this.ignoreOpenChange = false; this.modalContent = createRef(); this.mutationObserver = createObserver("mutation", () => this.focusTrap.updateContainerElements()); this._open = false; this.openProp = "opened"; this.transitionProp = "opacity"; this.messages = useT9n(); this.keyDownHandler = (event) => { const { defaultPrevented, key } = event; if (!defaultPrevented && this.focusTrapDisabled && this.open && !this.escapeDisabled && key === "Escape") { event.preventDefault(); this.open = false; } }; this.hasBack = false; this.hasContentBottom = false; this.hasContentTop = false; this.hasFooter = false; this.hasPrimary = false; this.hasSecondary = false; this.closeButtonDisabled = false; this.embedded = false; this.escapeDisabled = false; this.focusTrapDisabled = false; this.opened = false; this.outsideCloseDisabled = false; this.scale = "m"; this.widthScale = "m"; this.calciteModalBeforeClose = createEvent({ cancelable: false }); this.calciteModalBeforeOpen = createEvent({ cancelable: false }); this.calciteModalClose = createEvent({ cancelable: false }); this.calciteModalOpen = createEvent({ cancelable: false }); this.listen("keydown", this.keyDownHandler); } static { this.properties = { contentEl: [16, {}, { state: true }], cssHeight: [16, {}, { state: true }], cssWidth: [16, {}, { state: true }], hasBack: [16, {}, { state: true }], hasContentBottom: [16, {}, { state: true }], hasContentTop: [16, {}, { state: true }], hasFooter: [16, {}, { state: true }], hasPrimary: [16, {}, { state: true }], hasSecondary: [16, {}, { state: true }], titleEl: [16, {}, { state: true }], preventDocumentScroll: [16, {}, { state: true }], beforeClose: [0, {}, { attribute: false }], closeButtonDisabled: [7, {}, { reflect: true, type: Boolean }], docked: [7, {}, { reflect: true, type: Boolean }], embedded: [5, {}, { type: Boolean }], escapeDisabled: [7, {}, { reflect: true, type: Boolean }], focusTrapDisabled: [7, {}, { reflect: true, type: Boolean }], focusTrapOptions: [0, {}, { attribute: false }], fullscreen: [7, {}, { reflect: true, type: Boolean }], kind: [3, {}, { reflect: true }], messageOverrides: [0, {}, { attribute: false }], open: [7, {}, { reflect: true, type: Boolean }], opened: [7, {}, { reflect: true, type: Boolean }], outsideCloseDisabled: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], widthScale: [3, {}, { reflect: true }] }; } static { this.styles = styles; } get preventDocumentScroll() { return !this.embedded; } get open() { return this._open; } set open(open) { const oldOpen = this._open; if (open !== oldOpen) { this._open = open; this.toggleModal(open); } } async scrollContent(top = 0, left = 0) { if (this.modalContent.value) { if (this.modalContent.value.scrollTo) { this.modalContent.value.scrollTo({ top, left, behavior: "smooth" }); } else { this.modalContent.value.scrollTop = top; this.modalContent.value.scrollLeft = left; } } } async setFocus() { await componentFocusable(this); focusFirstTabbable(this.el); } async updateFocusTrapElements(extraContainers) { this.focusTrap.setExtraContainers(extraContainers); this.focusTrap.updateContainerElements(); } connectedCallback() { super.connectedCallback(); this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); this.cssVarObserver?.observe(this.el, { attributeFilter: ["style"] }); this.updateSizeCssVars(); } async load() { logger.deprecated("component", { name: "modal", removalVersion: 4, suggested: "dialog" }); if (this.open) { this.openModal(); } } willUpdate(changes) { if (changes.has("hasBack") && (this.hasUpdated || this.hasBack !== false) || changes.has("hasPrimary") && (this.hasUpdated || this.hasPrimary !== false) || changes.has("hasSecondary") && (this.hasUpdated || this.hasSecondary !== false)) { this.hasFooter = this.hasBack || this.hasPrimary || this.hasSecondary; } if (changes.has("opened") && (this.hasUpdated || this.opened !== false)) { this.handleOpenedChange(this.opened); } } disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver?.disconnect(); this.cssVarObserver?.disconnect(); this.embedded = false; } handleHeaderSlotChange(event) { this.titleEl = slotChangeGetAssignedElements(event)[0]; } handleContentSlotChange(event) { this.contentEl = slotChangeGetAssignedElements(event)[0]; } handleBackSlotChange(event) { this.hasBack = slotChangeHasAssignedElement(event); } handlePrimarySlotChange(event) { this.hasPrimary = slotChangeHasAssignedElement(event); } handleSecondarySlotChange(event) { this.hasSecondary = slotChangeHasAssignedElement(event); } setTransitionEl(el) { if (!el) { return; } this.transitionEl = el; } onBeforeOpen() { this.transitionEl?.classList.add(CSS.openingActive); this.calciteModalBeforeOpen.emit(); } onOpen() { this.transitionEl?.classList.remove(CSS.openingIdle, CSS.openingActive); if (this.focusTrapDisabled) { this.setFocus(); } this.focusTrap.activate(); this.calciteModalOpen.emit(); } onBeforeClose() { this.transitionEl?.classList.add(CSS.closingActive); this.calciteModalBeforeClose.emit(); } onClose() { this.transitionEl?.classList.remove(CSS.closingIdle, CSS.closingActive); this.calciteModalClose.emit(); this.focusTrap.deactivate(); } toggleModal(value) { if (this.ignoreOpenChange) { return; } if (value) { this.openModal(); } else { this.closeModal(); } } handleOpenedChange(value) { const { transitionEl } = this; if (!transitionEl) { return; } const idleClass = value ? CSS.openingIdle : CSS.closingIdle; transitionEl.classList.add(idleClass); onToggleOpenCloseComponent(this); } handleCloseClick() { this.open = false; } async openModal() { await this.componentOnReady(); this.opened = true; this.titleId = ensureId(this.titleEl); this.contentId = ensureId(this.contentEl); } handleOutsideClose() { if (this.outsideCloseDisabled) { return; } this.open = false; } async closeModal() { if (this.beforeClose) { try { await this.beforeClose(this.el); } catch { requestAnimationFrame(() => { this.ignoreOpenChange = true; this.open = true; this.ignoreOpenChange = false; }); return; } } this.opened = false; } updateSizeCssVars() { this.cssWidth = getComputedStyle(this.el).getPropertyValue("--calcite-modal-width"); this.cssHeight = getComputedStyle(this.el).getPropertyValue("--calcite-modal-height"); } contentTopSlotChangeHandler(event) { this.hasContentTop = slotChangeHasAssignedElement(event); } contentBottomSlotChangeHandler(event) { this.hasContentBottom = slotChangeHasAssignedElement(event); } render() { setAttribute(this.el, "aria-describedby", this.contentId); setAttribute(this.el, "aria-labelledby", this.titleId); this.el.ariaModal = "true"; this.el.role = "dialog"; return html`<div class=${safeClassMap({ [CSS.container]: true, [CSS.containerOpen]: this.opened })}><calcite-scrim class=${safeClassMap(CSS.scrim)} @click=${this.handleOutsideClose}></calcite-scrim>${this.renderStyle()}<div class=${safeClassMap({ [CSS.modal]: true })} ${ref(this.setTransitionEl)}><div class=${safeClassMap(CSS.header)}>${this.renderCloseButton()}<header class=${safeClassMap(CSS.title)}><slot name=${CSS.header} @slotchange=${this.handleHeaderSlotChange}></slot></header></div>${this.renderContentTop()}<div class=${safeClassMap({ [CSS.content]: true, [CSS.contentNoFooter]: !this.hasFooter })} ${ref(this.modalContent)}><slot name=${SLOTS.content} @slotchange=${this.handleContentSlotChange}></slot></div>${this.renderContentBottom()}${this.renderFooter()}</div></div>`; } renderFooter() { return keyed("footer", html`<div class=${safeClassMap(CSS.footer)} .hidden=${!this.hasFooter}><span class=${safeClassMap(CSS.back)}><slot name=${SLOTS.back} @slotchange=${this.handleBackSlotChange}></slot></span><span class=${safeClassMap(CSS.secondary)}><slot name=${SLOTS.secondary} @slotchange=${this.handleSecondarySlotChange}></slot></span><span class=${safeClassMap(CSS.primary)}><slot name=${SLOTS.primary} @slotchange=${this.handlePrimarySlotChange}></slot></span></div>`); } renderContentTop() { return html`<div class=${safeClassMap(CSS.contentTop)} .hidden=${!this.hasContentTop}><slot name=${SLOTS.contentTop} @slotchange=${this.contentTopSlotChangeHandler}></slot></div>`; } renderContentBottom() { return html`<div class=${safeClassMap(CSS.contentBottom)} .hidden=${!this.hasContentBottom}><slot name=${SLOTS.contentBottom} @slotchange=${this.contentBottomSlotChangeHandler}></slot></div>`; } renderCloseButton() { return !this.closeButtonDisabled ? keyed("button", html`<button .ariaLabel=${this.messages.close} class=${safeClassMap(CSS.close)} @click=${this.handleCloseClick} title=${this.messages.close ?? nothing} ${ref(this.closeButtonEl)}><calcite-icon .icon=${ICONS.close} .scale=${getIconScale(this.scale)}></calcite-icon></button>`) : null; } renderStyle() { if (!this.fullscreen && (this.cssWidth || this.cssHeight)) { return html`<style>${`.${CSS.container} { ${this.docked && this.cssWidth ? `align-items: center !important;` : ""} } .${CSS.modal} { block-size: ${this.cssHeight ? this.cssHeight : "auto"} !important; ${this.cssWidth ? `inline-size: ${this.cssWidth} !important;` : ""} ${this.cssWidth ? `max-inline-size: ${this.cssWidth} !important;` : ""} ${this.docked ? `border-radius: var(--calcite-border-radius) !important;` : ""} } @media screen and (max-width: ${this.cssWidth}) { .${CSS.container} { ${this.docked ? `align-items: flex-end !important;` : ""} } .${CSS.modal} { max-block-size: 100% !important; inline-size: 100% !important; max-inline-size: 100% !important; min-inline-size: 100% !important; margin: 0 !important; ${!this.docked ? `block-size: 100% !important;` : ""} ${!this.docked ? `border-radius: 0 !important;` : ""} ${this.docked ? `border-radius: var(--calcite-border-radius) var(--calcite-border-radius) 0 0 !important;` : ""} } } `}</style>`; } } } customElement("calcite-modal", Modal); export { Modal };