UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

409 lines (408 loc) • 16.6 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 { ref } from "lit-html/directives/ref.js"; import { html } from "lit"; import { LitElement, createEvent, setAttribute, safeClassMap } from "@arcgis/lumina"; import { d as defaultOffsetDistance, r as reposition, a as disconnectFloatingUI, h as hideFloatingUI, c as connectFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js"; import { g as guid } from "../../chunks/guid.js"; import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js"; import { F as FloatingArrow } from "../../chunks/FloatingArrow.js"; import { q as queryElementRoots, c as getShadowRootNode } from "../../chunks/dom.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { positionContainer: "position-container", container: "container" }; const TOOLTIP_OPEN_DELAY_MS = 300; const TOOLTIP_QUICK_OPEN_DELAY_MS = TOOLTIP_OPEN_DELAY_MS / 3; const TOOLTIP_CLOSE_DELAY_MS = TOOLTIP_OPEN_DELAY_MS * 1.5; const ARIA_DESCRIBED_BY = "aria-describedby"; function getEffectiveReferenceElement(tooltip) { const { referenceElement } = tooltip; return (typeof referenceElement === "string" ? queryElementRoots(tooltip, { id: referenceElement }) : referenceElement) || null; } class TooltipManager { constructor() { this.registeredElements = /* @__PURE__ */ new WeakMap(); this.registeredShadowRootCounts = /* @__PURE__ */ new WeakMap(); this.hoverOpenTimeout = null; this.hoverCloseTimeout = null; this.activeTooltip = null; this.registeredElementCount = 0; this.clickedTooltip = null; this.hoveredTooltip = null; this.queryTooltip = (composedPath) => { const { registeredElements } = this; const registeredElement = composedPath.find((pathEl) => registeredElements.has(pathEl)); return registeredElements.get(registeredElement); }; this.keyDownHandler = (event) => { if (event.key === "Escape" && !event.defaultPrevented) { const { activeTooltip } = this; if (activeTooltip?.open) { this.clearHoverTimeout(); this.closeActiveTooltip(); const referenceElement = getEffectiveReferenceElement(activeTooltip); const composedPath = event.composedPath(); if (referenceElement instanceof Element && composedPath.includes(referenceElement) || composedPath.includes(activeTooltip)) { event.preventDefault(); } } } }; this.pointerLeaveHandler = () => { this.clearHoverTimeout(); this.closeHoveredTooltip(); }; this.pointerMoveHandler = (event) => { if (event.defaultPrevented) { this.closeHoveredTooltip(); return; } const composedPath = event.composedPath(); const { activeTooltip } = this; const tooltip = this.queryTooltip(composedPath); if (this.pathHasOpenTooltip(tooltip, composedPath)) { this.clearHoverTimeout(); return; } if (tooltip === this.clickedTooltip) { return; } if (tooltip !== this.hoveredTooltip) { this.clearHoverOpenTimeout(); } this.hoveredTooltip = tooltip; if (tooltip) { this.openHoveredTooltip(tooltip); } else if (activeTooltip?.open) { this.closeHoveredTooltip(); } this.clickedTooltip = null; }; this.clickHandler = (event) => { if (event.defaultPrevented) { return; } this.clickedTooltip = null; const composedPath = event.composedPath(); const tooltip = this.queryTooltip(composedPath); if (this.pathHasOpenTooltip(tooltip, composedPath)) { this.clearHoverTimeout(); return; } this.closeActiveTooltip(); if (!tooltip) { return; } this.clearHoverTimeout(); if (tooltip.closeOnClick) { this.clickedTooltip = tooltip; this.toggleTooltip(tooltip, false); return; } this.toggleTooltip(tooltip, true); }; this.blurHandler = () => { this.closeActiveTooltip(); }; this.focusInHandler = (event) => { if (event.defaultPrevented) { return; } const composedPath = event.composedPath(); const tooltip = this.queryTooltip(composedPath); if (this.pathHasOpenTooltip(tooltip, composedPath)) { this.clearHoverTimeout(); return; } if (tooltip === this.clickedTooltip) { return; } this.clickedTooltip = null; this.closeTooltipIfNotActive(tooltip); if (!tooltip) { return; } this.toggleFocusedTooltip(tooltip, true); }; this.openHoveredTooltip = (tooltip) => { this.hoverOpenTimeout = window.setTimeout( () => { if (this.hoverOpenTimeout === null || tooltip !== this.hoveredTooltip) { return; } this.clearHoverCloseTimeout(); this.closeTooltipIfNotActive(tooltip); this.toggleTooltip(tooltip, true); }, this.activeTooltip?.open ? TOOLTIP_QUICK_OPEN_DELAY_MS : TOOLTIP_OPEN_DELAY_MS ); }; this.closeHoveredTooltip = () => { this.hoverCloseTimeout = window.setTimeout(() => { if (this.hoverCloseTimeout === null) { return; } this.closeActiveTooltip(); }, TOOLTIP_CLOSE_DELAY_MS); }; } // -------------------------------------------------------------------------- // // Public Methods // // -------------------------------------------------------------------------- registerElement(referenceEl, tooltip) { this.registeredElementCount++; this.registeredElements.set(referenceEl, tooltip); const shadowRoot = this.getReferenceElShadowRootNode(referenceEl); if (shadowRoot) { this.registerShadowRoot(shadowRoot); } if (this.registeredElementCount === 1) { this.addListeners(); } } unregisterElement(referenceEl) { const shadowRoot = this.getReferenceElShadowRootNode(referenceEl); if (shadowRoot) { this.unregisterShadowRoot(shadowRoot); } if (this.registeredElements.delete(referenceEl)) { this.registeredElementCount--; } if (this.registeredElementCount === 0) { this.removeListeners(); } } pathHasOpenTooltip(tooltip, composedPath) { const { activeTooltip } = this; return activeTooltip?.open && composedPath.includes(activeTooltip) || tooltip?.open && composedPath.includes(tooltip); } addShadowListeners(shadowRoot) { shadowRoot.addEventListener("focusin", this.focusInHandler); } removeShadowListeners(shadowRoot) { shadowRoot.removeEventListener("focusin", this.focusInHandler); } addListeners() { window.addEventListener("keydown", this.keyDownHandler); window.addEventListener("pointermove", this.pointerMoveHandler); window.addEventListener("click", this.clickHandler); window.addEventListener("focusin", this.focusInHandler); window.addEventListener("blur", this.blurHandler); document.addEventListener("pointerleave", this.pointerLeaveHandler); } removeListeners() { window.removeEventListener("keydown", this.keyDownHandler); window.removeEventListener("pointermove", this.pointerMoveHandler); window.removeEventListener("click", this.clickHandler); window.removeEventListener("focusin", this.focusInHandler); window.removeEventListener("blur", this.blurHandler); document.removeEventListener("pointerleave", this.pointerLeaveHandler); } clearHoverOpenTimeout() { window.clearTimeout(this.hoverOpenTimeout); this.hoverOpenTimeout = null; } clearHoverCloseTimeout() { window.clearTimeout(this.hoverCloseTimeout); this.hoverCloseTimeout = null; } clearHoverTimeout() { this.clearHoverOpenTimeout(); this.clearHoverCloseTimeout(); } closeTooltipIfNotActive(tooltip) { if (this.activeTooltip !== tooltip) { this.closeActiveTooltip(); } } closeActiveTooltip() { const { activeTooltip } = this; if (activeTooltip?.open) { this.toggleTooltip(activeTooltip, false); } } toggleFocusedTooltip(tooltip, open) { if (open) { this.clearHoverTimeout(); } this.toggleTooltip(tooltip, open); } toggleTooltip(tooltip, open) { tooltip.open = open; this.activeTooltip = open ? tooltip : null; } registerShadowRoot(shadowRoot) { const { registeredShadowRootCounts } = this; const count = registeredShadowRootCounts.get(shadowRoot); const newCount = Math.min((typeof count === "number" ? count : 0) + 1, 1); if (newCount === 1) { this.addShadowListeners(shadowRoot); } registeredShadowRootCounts.set(shadowRoot, newCount); } unregisterShadowRoot(shadowRoot) { const { registeredShadowRootCounts } = this; const count = registeredShadowRootCounts.get(shadowRoot); const newCount = Math.max((typeof count === "number" ? count : 1) - 1, 0); if (newCount === 0) { this.removeShadowListeners(shadowRoot); } registeredShadowRootCounts.set(shadowRoot, newCount); } getReferenceElShadowRootNode(referenceEl) { return referenceEl instanceof Element ? getShadowRootNode(referenceEl) : null; } } const styles = css`:host{display:contents;--calcite-floating-ui-z-index: var(--calcite-tooltip-z-index, var(--calcite-z-index-tooltip))}.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)}.container{position:relative;overflow:hidden;padding:.75rem 1rem;font-size:var(--calcite-font-size--2);line-height:1.375;font-weight:var(--calcite-font-weight-medium);border-radius:var(--calcite-tooltip-corner-radius, var(--calcite-corner-radius-round));color:var(--calcite-tooltip-text-color, var(--calcite-color-text-1));max-inline-size:20rem;max-block-size:20rem;text-align:start}.position-container .calcite-floating-ui-anim{border-width:1px;border-style:solid;background-color:var(--calcite-tooltip-background-color, var(--calcite-color-foreground-1));border-color:var(--calcite-tooltip-border-color, var(--calcite-color-border-3));border-radius:var(--calcite-tooltip-corner-radius, var(--calcite-corner-radius-round))}.calcite-floating-ui-arrow{fill:var(--calcite-tooltip-background-color, var(--calcite-color-foreground-1))}.calcite-floating-ui-arrow__stroke{stroke:var(--calcite-tooltip-border-color, var(--calcite-color-border-3))}:host([hidden]){display:none}[hidden]{display:none}`; const manager = new TooltipManager(); class Tooltip extends LitElement { constructor() { super(...arguments); this.guid = `calcite-tooltip-${guid()}`; this.transitionProp = "opacity"; this.floatingLayout = "vertical"; this.closeOnClick = false; this.offsetDistance = defaultOffsetDistance; this.offsetSkidding = 0; this.open = false; this.overlayPositioning = "absolute"; this.placement = "auto"; this.calciteTooltipBeforeClose = createEvent({ cancelable: false }); this.calciteTooltipBeforeOpen = createEvent({ cancelable: false }); this.calciteTooltipClose = createEvent({ cancelable: false }); this.calciteTooltipOpen = createEvent({ cancelable: false }); } static { this.properties = { floatingLayout: [16, {}, { state: true }], referenceEl: [16, {}, { state: true }], closeOnClick: [7, {}, { reflect: true, type: Boolean }], label: 1, 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 }], referenceElement: 1 }; } static { this.styles = styles; } async reposition(delayed = false) { const { referenceEl, placement, overlayPositioning, offsetDistance, offsetSkidding, arrowEl, floatingEl } = this; return reposition(this, { floatingEl, referenceEl, overlayPositioning, placement, offsetDistance, offsetSkidding, arrowEl, type: "tooltip" }, delayed); } connectedCallback() { super.connectedCallback(); this.setUpReferenceElement(true); } willUpdate(changes) { 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 !== "auto")) { this.reposition(true); } if (changes.has("open") && (this.hasUpdated || this.open !== false)) { this.openHandler(); } if (changes.has("referenceElement")) { this.setUpReferenceElement(); } } loaded() { if (this.referenceElement && !this.referenceEl) { this.setUpReferenceElement(); } } disconnectedCallback() { super.disconnectedCallback(); this.removeReferences(); disconnectFloatingUI(this); } openHandler() { onToggleOpenCloseComponent(this); this.reposition(true); } onBeforeOpen() { this.calciteTooltipBeforeOpen.emit(); } onOpen() { this.calciteTooltipOpen.emit(); } onBeforeClose() { this.calciteTooltipBeforeClose.emit(); } onClose() { this.calciteTooltipClose.emit(); hideFloatingUI(this); } setFloatingEl(el) { this.floatingEl = el; if (el) { requestAnimationFrame(() => this.setUpReferenceElement()); } } setTransitionEl(el) { if (!el) { return; } this.transitionEl = el; } setUpReferenceElement(warn = true) { this.removeReferences(); this.referenceEl = getEffectiveReferenceElement(this.el); 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; } addReferences() { const { referenceEl } = this; if (!referenceEl) { return; } const id = this.getId(); if ("setAttribute" in referenceEl) { referenceEl.setAttribute(ARIA_DESCRIBED_BY, id); } manager.registerElement(referenceEl, this.el); } removeReferences() { const { referenceEl } = this; if (!referenceEl) { return; } if ("removeAttribute" in referenceEl) { referenceEl.removeAttribute(ARIA_DESCRIBED_BY); } manager.unregisterElement(referenceEl); } render() { const { referenceEl, label, open, floatingLayout } = this; const displayed = referenceEl && open; const hidden = !displayed; this.el.inert = hidden; this.el.ariaLabel = label; this.el.ariaLive = "polite"; setAttribute(this.el, "id", this.getId()); this.el.role = "tooltip"; return html`<div class=${safeClassMap(CSS.positionContainer)} ${ref(this.setFloatingEl)}><div class=${safeClassMap({ [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: displayed })} ${ref(this.setTransitionEl)}>${FloatingArrow({ floatingLayout, ref: (arrowEl) => this.arrowEl = arrowEl })}<div class=${safeClassMap(CSS.container)}><slot></slot></div></div></div>`; } } customElement("calcite-tooltip", Tooltip); export { Tooltip };