UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

341 lines (340 loc) • 16.8 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 } from "lit"; import { createRef, ref } from "lit-html/directives/ref.js"; import { LitElement, createEvent, safeClassMap, setAttribute } from "@arcgis/lumina"; import { d as defaultOffsetDistance, r as reposition, a as disconnectFloatingUI, f as filterValidFlipPlacements, c as connectFloatingUI, h as hideFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js"; import { u as isKeyboardTriggeredClick, h as focusFirstTabbable, t as toAriaBoolean, q as queryElementRoots } from "../../chunks/dom.js"; import { g as guid } from "../../chunks/guid.js"; import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js"; import { H as Heading } from "../../chunks/Heading.js"; import { c as componentFocusable, g as getIconScale } from "../../chunks/component.js"; import { c as createObserver } from "../../chunks/observers.js"; import { F as FloatingArrow } from "../../chunks/FloatingArrow.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { u as useFocusTrap } from "../../chunks/useFocusTrap.js"; import { i as isActivationKey } from "../../chunks/key.js"; import { css } from "@lit/reactive-element/css-tag.js"; class PopoverManager { constructor() { this.registeredElements = /* @__PURE__ */ new Map(); this.registeredElementCount = 0; this.queryPopover = (composedPath) => { const { registeredElements } = this; const registeredElement = composedPath.find((pathEl) => registeredElements.has(pathEl)); return registeredElements.get(registeredElement); }; this.togglePopovers = (event) => { const composedPath = event.composedPath(); const togglePopover = this.queryPopover(composedPath); if (togglePopover && !togglePopover.triggerDisabled) { togglePopover.open = !togglePopover.open; } Array.from(this.registeredElements.values()).filter( (popover) => popover !== togglePopover && popover.autoClose && popover.open && !composedPath.includes(popover) ).forEach((popover) => popover.open = false); }; this.keyDownHandler = (event) => { if (event.defaultPrevented) { return; } if (event.key === "Escape") { this.closeAllPopovers(); } else if (isActivationKey(event.key)) { this.togglePopovers(event); } }; this.clickHandler = (event) => { if (isKeyboardTriggeredClick(event) || event.defaultPrevented) { return; } this.togglePopovers(event); }; } // -------------------------------------------------------------------------- // // Public Methods // // -------------------------------------------------------------------------- registerElement(referenceEl, popover) { this.registeredElementCount++; this.registeredElements.set(referenceEl, popover); if (this.registeredElementCount === 1) { this.addListeners(); } } unregisterElement(referenceEl) { if (this.registeredElements.delete(referenceEl)) { this.registeredElementCount--; } if (this.registeredElementCount === 0) { this.removeListeners(); } } closeAllPopovers() { Array.from(this.registeredElements.values()).forEach((popover) => popover.open = false); } addListeners() { window.addEventListener("click", this.clickHandler); window.addEventListener("keydown", this.keyDownHandler); } removeListeners() { window.removeEventListener("click", this.clickHandler); window.removeEventListener("keydown", this.keyDownHandler); } } const CSS = { positionContainer: "position-container", container: "container", closeButtonContainer: "close-button-container", closeButton: "close-button", content: "content", hasHeader: "has-header", header: "header", headerContainer: "header-container", heading: "heading" }; const defaultPopoverPlacement = "auto"; const ARIA_CONTROLS = "aria-controls"; const ARIA_EXPANDED = "aria-expanded"; const styles = css`:host{display:contents;--calcite-floating-ui-z-index: var(--calcite-popover-z-index, var(--calcite-z-index-popup))}.position-container{inline-size:max-content;display:none;max-inline-size:100vw;max-block-size:100vh;inset-block-start:0;left:0;z-index:var(--calcite-floating-ui-z-index)}.position-container .calcite-floating-ui-anim{position:relative;transition:var(--calcite-floating-ui-transition);transition-property:inset,left,opacity;opacity:0;box-shadow:0 0 16px #00000029;z-index:var(--calcite-z-index);border-radius:.25rem}.position-container[data-placement^=bottom] .calcite-floating-ui-anim{inset-block-start:-5px}.position-container[data-placement^=top] .calcite-floating-ui-anim{inset-block-start:5px}.position-container[data-placement^=left] .calcite-floating-ui-anim{left:5px}.position-container[data-placement^=right] .calcite-floating-ui-anim{left:-5px}.position-container[data-placement] .calcite-floating-ui-anim--active{opacity:1;inset-block-start:0;left:0}.calcite-floating-ui-arrow{pointer-events:none;position:absolute;z-index:calc(var(--calcite-z-index) * -1);fill:var(--calcite-color-foreground-1)}.calcite-floating-ui-arrow__stroke{stroke:var(--calcite-color-border-3)}:host([scale=s]) .heading{padding:.5rem .75rem;font-size:var(--calcite-font-size--1);line-height:1.375}:host([scale=m]) .heading{padding:.75rem 1rem;font-size:var(--calcite-font-size-0);line-height:1.375}:host([scale=l]) .heading{padding:1rem 1.25rem;font-size:var(--calcite-font-size-1);line-height:1.375}.position-container .calcite-floating-ui-anim{border-width:1px;border-style:solid;background-color:var(--calcite-popover-background-color, var(--calcite-color-foreground-1));border-color:var(--calcite-popover-border-color, var(--calcite-color-border-3));border-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round))}.calcite-floating-ui-arrow{fill:var(--calcite-popover-background-color, var(--calcite-color-foreground-1))}.calcite-floating-ui-arrow__stroke{stroke:var(--calcite-popover-border-color, var(--calcite-color-border-3))}.header{display:flex;flex:1 1 auto;align-items:stretch;justify-content:flex-start;border-width:0px;border-bottom-width:1px;border-style:solid;border-block-end-color:var(--calcite-popover-border-color, var(--calcite-color-border-3))}.heading{margin:0;display:block;flex:1 1 auto;align-self:center;white-space:normal;font-weight:var(--calcite-font-weight-medium);word-wrap:break-word;word-break:break-word;color:var(--calcite-popover-text-color, var(--calcite-color-text-1))}.header-container{position:relative;display:flex;height:100%;flex-direction:row;flex-wrap:nowrap;border-radius:.25rem;color:var(--calcite-popover-text-color, var(--calcite-color-text-1))}.header-container.has-header{flex-direction:column}.content{display:flex;height:100%;width:100%;flex-direction:column;flex-wrap:nowrap;align-self:center;word-wrap:break-word;word-break:break-word}.close-button-container{display:flex;overflow:hidden;flex:0 0 auto;border-start-end-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round));border-end-end-radius:var(--calcite-popover-corner-radius, var(--calcite-corner-radius-round));--calcite-action-corner-radius-start-end: var(--calcite-popover-corner-radius, var(--calcite-corner-radius-sharp));--calcite-action-corner-radius-end-end: var(--calcite-popover-corner-radius, var(--calcite-corner-radius-sharp))}::slotted(calcite-panel),::slotted(calcite-flow){height:100%}:host([hidden]){display:none}[hidden]{display:none}`; const manager = new PopoverManager(); class Popover extends LitElement { constructor() { super(...arguments); this.closeButtonEl = createRef(); this.focusTrap = useFocusTrap({ triggerProp: "open", focusTrapOptions: { allowOutsideClick: true, escapeDeactivates: (event) => { if (!event.defaultPrevented) { this.open = false; event.preventDefault(); } return false; } } })(this); this.guid = `calcite-popover-${guid()}`; this.hasLoaded = false; this.mutationObserver = createObserver("mutation", () => this.focusTrap.updateContainerElements()); this.transitionProp = "opacity"; this.messages = useT9n(); this.floatingLayout = "vertical"; this.autoClose = false; this.closable = false; this.flipDisabled = false; this.focusTrapDisabled = false; this.offsetDistance = defaultOffsetDistance; this.offsetSkidding = 0; this.open = false; this.overlayPositioning = "absolute"; this.placement = defaultPopoverPlacement; this.pointerDisabled = false; this.scale = "m"; this.triggerDisabled = false; this.calcitePopoverBeforeClose = createEvent({ cancelable: false }); this.calcitePopoverBeforeOpen = createEvent({ cancelable: false }); this.calcitePopoverClose = createEvent({ cancelable: false }); this.calcitePopoverOpen = createEvent({ cancelable: false }); } static { this.properties = { floatingLayout: [16, {}, { state: true }], referenceEl: [16, {}, { state: true }], autoClose: [7, {}, { reflect: true, type: Boolean }], closable: [7, {}, { reflect: true, type: Boolean }], flipDisabled: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], focusTrapDisabled: [7, {}, { reflect: true, type: Boolean }], focusTrapOptions: [0, {}, { attribute: false }], heading: 1, headingLevel: [11, {}, { type: Number, reflect: true }], label: 1, messageOverrides: [0, {}, { attribute: false }], offsetDistance: [11, {}, { type: Number, reflect: true }], offsetSkidding: [11, {}, { reflect: true, type: Number }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], placement: [3, {}, { reflect: true }], pointerDisabled: [7, {}, { reflect: true, type: Boolean }], referenceElement: 1, scale: [3, {}, { reflect: true }], triggerDisabled: [7, {}, { reflect: true, type: Boolean }] }; } static { this.styles = styles; } async reposition(delayed = false) { const { referenceEl, placement, overlayPositioning, flipDisabled, filteredFlipPlacements, offsetDistance, offsetSkidding, arrowEl, floatingEl } = this; return reposition(this, { floatingEl, referenceEl, overlayPositioning, placement, flipDisabled, flipPlacements: filteredFlipPlacements, offsetDistance, offsetSkidding, arrowEl, type: "popover" }, delayed); } async setFocus() { await componentFocusable(this); this.requestUpdate(); focusFirstTabbable(this.el); } async updateFocusTrapElements() { this.focusTrap.updateContainerElements(); } connectedCallback() { super.connectedCallback(); this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); this.setFilteredPlacements(); requestAnimationFrame(() => this.setUpReferenceElement(this.hasLoaded)); } willUpdate(changes) { if (changes.has("flipPlacements")) { this.flipPlacementsHandler(); } if (changes.has("open") && (this.hasUpdated || this.open !== false)) { this.openHandler(); } if (changes.has("offsetDistance") && (this.hasUpdated || this.offsetDistance !== defaultOffsetDistance) || changes.has("offsetSkidding") && (this.hasUpdated || this.offsetSkidding !== 0) || changes.has("overlayPositioning") && (this.hasUpdated || this.overlayPositioning !== "absolute") || changes.has("placement") && (this.hasUpdated || this.placement !== defaultPopoverPlacement)) { this.reposition(true); } if (changes.has("referenceElement")) { this.referenceElementHandler(); } } loaded() { if (this.referenceElement && !this.referenceEl) { this.setUpReferenceElement(); } this.hasLoaded = true; } disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver?.disconnect(); this.removeReferences(); disconnectFloatingUI(this); } flipPlacementsHandler() { this.setFilteredPlacements(); this.reposition(true); } openHandler() { onToggleOpenCloseComponent(this); this.reposition(true); this.setExpandedAttr(); } referenceElementHandler() { this.setUpReferenceElement(); this.reposition(true); } setFloatingEl(el) { this.floatingEl = el; if (el) { requestAnimationFrame(() => this.setUpReferenceElement()); } } setTransitionEl(el) { if (!el) { return; } this.transitionEl = el; } setFilteredPlacements() { const { el, flipPlacements } = this; this.filteredFlipPlacements = flipPlacements ? filterValidFlipPlacements(flipPlacements, el) : null; } setUpReferenceElement(warn = true) { this.removeReferences(); this.referenceEl = this.getReferenceElement(); connectFloatingUI(this); const { el, referenceElement, referenceEl } = this; if (warn && referenceElement && !referenceEl) { console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, { el }); } this.addReferences(); } getId() { return this.el.id || this.guid; } setExpandedAttr() { const { referenceEl, open } = this; if (!referenceEl) { return; } if ("setAttribute" in referenceEl) { referenceEl.setAttribute(ARIA_EXPANDED, toAriaBoolean(open)); } } addReferences() { const { referenceEl } = this; if (!referenceEl) { return; } const id = this.getId(); if ("setAttribute" in referenceEl) { referenceEl.setAttribute(ARIA_CONTROLS, id); } manager.registerElement(referenceEl, this.el); this.setExpandedAttr(); } removeReferences() { const { referenceEl } = this; if (!referenceEl) { return; } if ("removeAttribute" in referenceEl) { referenceEl.removeAttribute(ARIA_CONTROLS); referenceEl.removeAttribute(ARIA_EXPANDED); } manager.unregisterElement(referenceEl); } getReferenceElement() { const { referenceElement, el } = this; return (typeof referenceElement === "string" ? queryElementRoots(el, { id: referenceElement }) : referenceElement) || null; } hide() { this.open = false; } onBeforeOpen() { this.calcitePopoverBeforeOpen.emit(); } onOpen() { this.calcitePopoverOpen.emit(); this.focusTrap.activate(); } onBeforeClose() { this.calcitePopoverBeforeClose.emit(); } onClose() { this.calcitePopoverClose.emit(); hideFloatingUI(this); this.focusTrap.deactivate(); } storeArrowEl(el) { this.arrowEl = el; this.reposition(true); } renderCloseButton() { const { messages, closable } = this; return closable ? keyed(CSS.closeButtonContainer, html`<div class=${safeClassMap(CSS.closeButtonContainer)}><calcite-action appearance=transparent class=${safeClassMap(CSS.closeButton)} @click=${this.hide} .scale=${this.scale} .text=${messages.close} ${ref(this.closeButtonEl)}><calcite-icon icon=x .scale=${getIconScale(this.scale)}></calcite-icon></calcite-action></div>`) : null; } renderHeader() { const { heading, headingLevel } = this; const headingNode = heading ? Heading({ class: CSS.heading, level: headingLevel, children: heading }) : null; return headingNode ? keyed(CSS.header, html`<div class=${safeClassMap(CSS.header)}>${headingNode}${this.renderCloseButton()}</div>`) : null; } render() { const { referenceEl, heading, label, open, pointerDisabled, floatingLayout } = this; const displayed = referenceEl && open; const hidden = !displayed; const arrowNode = !pointerDisabled ? keyed("floating-arrow", FloatingArrow({ floatingLayout, ref: this.storeArrowEl })) : null; this.el.inert = hidden; this.el.ariaLabel = label; this.el.ariaLive = "polite"; setAttribute(this.el, "id", this.getId()); this.el.role = "dialog"; return html`<div class=${safeClassMap(CSS.positionContainer)} ${ref(this.setFloatingEl)}><div class=${safeClassMap({ [CSS.container]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: displayed })} ${ref(this.setTransitionEl)}>${arrowNode}<div class=${safeClassMap({ [CSS.hasHeader]: !!heading, [CSS.headerContainer]: true })}>${this.renderHeader()}<div class=${safeClassMap(CSS.content)}><slot></slot></div>${!heading ? this.renderCloseButton() : null}</div></div></div>`; } } customElement("calcite-popover", Popover); export { Popover };