@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
409 lines (408 loc) • 16.6 kB
JavaScript
/*! 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
};