UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

506 lines (505 loc) • 18.2 kB
/* COPYRIGHT Esri - https://js.arcgis.com/5.1/LICENSE.txt */ import { makeGenericController } from "@arcgis/lumina/controllers"; import { q as queryElementRoots, v as getShadowRootNode, j as isPrimaryPointerButton, x as isKeyboardTriggeredClick } from "./dom.js"; import { i as isActivationKey } from "./key.js"; import { t as toAriaBoolean } from "./aria.js"; const useReferenceElement = (options) => { const { manager } = options; return makeGenericController((component, controller) => { let animationFrameId = null; let lastRegisteredReferenceEl = null; const canManageReferenceElement = (referenceEl) => { return Boolean(referenceEl && component.referenceElementType); }; const registerReferenceElement = (referenceEl) => { if (!canManageReferenceElement(referenceEl)) { return; } manager.registerElement(component, referenceEl); lastRegisteredReferenceEl = referenceEl; }; const unregisterReferenceElement = (referenceEl) => { if (!canManageReferenceElement(referenceEl)) { return; } manager.unregisterElement(component, referenceEl); if (lastRegisteredReferenceEl === referenceEl) { lastRegisteredReferenceEl = null; } }; const getReferenceElement = (component2) => { const { referenceElement, el } = component2; return (typeof referenceElement === "string" ? queryElementRoots(el, { id: referenceElement }) : referenceElement) || null; }; const setUpReferenceElement = (warn = true) => { if (!component.referenceElementType) { return; } component.referenceEl = getReferenceElement(component); const { el, referenceElement, referenceEl } = component; if (warn && referenceElement && !referenceEl) { console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, { el }); } }; controller.onConnected(() => { animationFrameId = requestAnimationFrame(() => { if (!component.el.isConnected) { return; } setUpReferenceElement(component.manager.loadedCalled); registerReferenceElement(component.referenceEl); }); }); controller.onLoaded(() => { if (component.referenceElement && !component.referenceEl) { setUpReferenceElement(); } }); controller.onUpdate((changes) => { if (!component.hasUpdated) { return; } if (changes.has("referenceElement")) { setUpReferenceElement(); } if (changes.has("referenceEl")) { unregisterReferenceElement(changes.get("referenceEl")); registerReferenceElement(component.referenceEl); } else if (changes.has("open")) { manager.updateElement(component, component.referenceEl); } }); controller.onDisconnected(() => { if (animationFrameId != null) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } unregisterReferenceElement(lastRegisteredReferenceEl); }); }); }; function haveSameComponents(components, activeComponents) { if (components === activeComponents) { return true; } if (components.length !== activeComponents.length) { return false; } const s1 = new Set(components); const s2 = new Set(activeComponents); if (s1.size !== s2.size) { return false; } for (const item of s1) { if (!s2.has(item)) { return false; } } return true; } const clickTolerance = 5; const HOVER_OPEN_DELAY_MS = 300; const HOVER_QUICK_OPEN_DELAY_MS = HOVER_OPEN_DELAY_MS / 3; const HOVER_CLOSE_DELAY_MS = HOVER_OPEN_DELAY_MS * 1.5; function isDrag({ startX, startY, endX, endY }) { const distance = Math.hypot(endX - startX, endY - startY); return distance > clickTolerance; } const referenceElementManager = (options) => { const registeredElements = /* @__PURE__ */ new Map(); const registeredShadowRootCounts = /* @__PURE__ */ new WeakMap(); let activeComponents = null; let clickedComponents = null; let hoverCloseTimeout = null; let hoverOpenTimeout = null; let hoveredComponents = null; let pointerDownPosition = null; let registeredComponentCount = 0; const queryComponents = (composedPath, type) => { const registeredElement = composedPath.find((pathEl) => registeredElements.has(pathEl)); if (!registeredElement) { return void 0; } const components = registeredElements.get(registeredElement); return type ? components?.filter((component) => component.referenceElementType === type) : components; }; const toggleComponents = (event, type) => { const composedPath = event.composedPath(); const components = queryComponents(composedPath, type); components?.forEach((component) => { if (component && !component.triggerDisabled) { component.open = !component.open; } }); Array.from(registeredElements.values()).flat().filter( (component) => !components?.includes(component) && component.autoClose && component.open && !composedPath.includes(component.el) ).forEach((component) => component.open = false); }; const clickHandler = (event) => { if (isKeyboardTriggeredClick(event) || event.defaultPrevented || pointerDownPosition && isDrag({ endY: event.clientY, endX: event.clientX, startY: pointerDownPosition.y, startX: pointerDownPosition.x })) { return; } pointerDownPosition = null; toggleComponents(event, "click"); }; const clearHoverOpenTimeout = () => { if (hoverOpenTimeout != null) { window.clearTimeout(hoverOpenTimeout); } hoverOpenTimeout = null; }; const clearHoverCloseTimeout = () => { if (hoverCloseTimeout != null) { window.clearTimeout(hoverCloseTimeout); } hoverCloseTimeout = null; }; const clearHoverTimeout = () => { clearHoverOpenTimeout(); clearHoverCloseTimeout(); }; const pathHasOpenHoverComponent = (components, composedPath) => { return activeComponents?.some((component) => component?.open && composedPath.includes(component.el)) || components?.some((component) => component?.open && composedPath.includes(component.el)); }; const toggleHoverComponents = (components, open) => { components?.forEach((component) => component.open = open); activeComponents = open ? components : null; }; const closeActiveHoverComponents = () => { toggleHoverComponents(activeComponents, false); }; const hoverKeyDownHandler = (event) => { if (event.key === "Escape" && !event.defaultPrevented) { const openActiveHoverComponents = activeComponents?.filter((component) => component?.open); if (openActiveHoverComponents?.length) { clearHoverTimeout(); closeActiveHoverComponents(); const composedPath = event.composedPath(); if (openActiveHoverComponents.some( (component) => component.referenceEl instanceof Element && composedPath.includes(component.referenceEl) || composedPath.includes(component.el) )) { event.preventDefault(); } } } }; const hoverClickHandler = (event) => { if (event.defaultPrevented) { return; } clickedComponents = null; const composedPath = event.composedPath(); const components = queryComponents(composedPath, "hover"); if (pathHasOpenHoverComponent(components, composedPath)) { clearHoverTimeout(); return; } closeActiveHoverComponents(); if (!components?.length) { return; } clearHoverTimeout(); const closeOnClickHoverComponents = components.filter((component) => component.closeOnClick); const nonCloseOnClickHoverComponents = components.filter((component) => !component.closeOnClick); if (closeOnClickHoverComponents?.length) { clickedComponents = closeOnClickHoverComponents; toggleHoverComponents(closeOnClickHoverComponents, false); } toggleHoverComponents(nonCloseOnClickHoverComponents, true); }; const pointerDownHandler = (event) => { if (event.defaultPrevented || !isPrimaryPointerButton(event)) { return; } const { clientX, clientY } = event; pointerDownPosition = { x: clientX, y: clientY }; }; const keyDownHandler = (event) => { if (event.defaultPrevented) { return; } if (event.key === "Escape") { closeAllComponents(); } else if (isActivationKey(event.key)) { toggleComponents(event, "click"); } }; const onReferenceElementKeyDown = (event) => { const components = queryComponents(event.composedPath()); components?.forEach((component) => component.onReferenceElementKeyDown?.(event)); }; const closeAllComponents = () => { Array.from(registeredElements.values()).flat().forEach((component) => component.open = false); }; const closeComponentsIfNotActive = (components) => { if (!haveSameComponents(components ?? [], activeComponents ?? [])) { closeActiveHoverComponents(); } }; const openHoveredComponents = (components) => { hoverOpenTimeout = window.setTimeout( () => { if (hoverOpenTimeout === null || !haveSameComponents(components ?? [], hoveredComponents ?? [])) { return; } clearHoverCloseTimeout(); closeComponentsIfNotActive(components); toggleHoverComponents(components, true); }, activeComponents?.some((component) => component.open) ? HOVER_QUICK_OPEN_DELAY_MS : HOVER_OPEN_DELAY_MS ); }; const closeHoveredComponents = () => { hoverCloseTimeout = window.setTimeout(() => { if (hoverCloseTimeout === null) { return; } closeActiveHoverComponents(); }, HOVER_CLOSE_DELAY_MS); }; const pointerMoveHandler = (event) => { if (event.defaultPrevented) { closeActiveHoverComponents(); return; } const composedPath = event.composedPath(); const components = queryComponents(composedPath, "hover"); if (pathHasOpenHoverComponent(components, composedPath)) { clearHoverTimeout(); return; } if (components?.some((component) => clickedComponents?.includes(component))) { return; } if (!components?.some((component) => hoveredComponents?.includes(component))) { clearHoverOpenTimeout(); } hoveredComponents = components; if (components?.length) { openHoveredComponents(components); } else if (activeComponents?.some((component) => component?.open)) { closeHoveredComponents(); } clickedComponents = null; }; const blurHandler = () => { closeActiveHoverComponents(); }; const pointerLeaveHandler = (event) => { if (event.defaultPrevented) { return; } clearHoverTimeout(); closeHoveredComponents(); }; const clickListener = (event) => { if (options.click) { clickHandler(event); } if (options.hover) { hoverClickHandler(event); } }; const keyDownListener = (event) => { onReferenceElementKeyDown(event); if (options.click) { keyDownHandler(event); } if (options.hover) { hoverKeyDownHandler(event); } }; const addListeners = () => { if (options.click || options.hover) { window.addEventListener("click", clickListener); window.addEventListener("keydown", keyDownListener); } if (options.click) { window.addEventListener("pointerdown", pointerDownHandler); } if (options.hover) { window.addEventListener("pointermove", pointerMoveHandler); window.addEventListener("focusin", focusInHandler); window.addEventListener("blur", blurHandler); document.addEventListener("pointerleave", pointerLeaveHandler); } }; const removeListeners = () => { if (options.click || options.hover) { window.removeEventListener("click", clickListener); window.removeEventListener("keydown", keyDownListener); } if (options.click) { window.removeEventListener("pointerdown", pointerDownHandler); } if (options.hover) { window.removeEventListener("pointermove", pointerMoveHandler); window.removeEventListener("focusin", focusInHandler); window.removeEventListener("blur", blurHandler); document.removeEventListener("pointerleave", pointerLeaveHandler); } }; const toggleFocusedComponents = (components, open) => { { clearHoverTimeout(); } toggleHoverComponents(components, open); }; const getReferenceElShadowRootNode = (referenceEl) => { return referenceEl instanceof Element ? getShadowRootNode(referenceEl) : null; }; const focusInHandler = (event) => { if (event.defaultPrevented) { return; } const composedPath = event.composedPath(); const components = queryComponents(composedPath, "hover"); if (pathHasOpenHoverComponent(components, composedPath)) { clearHoverTimeout(); return; } if (components?.some((component) => clickedComponents?.includes(component))) { return; } clickedComponents = null; closeComponentsIfNotActive(components); if (!components?.length) { return; } toggleFocusedComponents(components, true); }; const updateElement = (component, referenceEl) => { if (!referenceEl || !component.referenceElementType) { return; } if (options.click && "ariaExpanded" in referenceEl) { const existingComponents = registeredElements.get(referenceEl) ?? []; const existingComponentOpen = existingComponents?.some((component2) => component2.open) ?? false; referenceEl.ariaExpanded = toAriaBoolean(component.open || existingComponentOpen); } }; const addShadowListeners = (shadowRoot) => { shadowRoot.addEventListener("focusin", focusInHandler); }; const removeShadowListeners = (shadowRoot) => { shadowRoot.removeEventListener("focusin", focusInHandler); }; const registerShadowRoot = (shadowRoot) => { const count = registeredShadowRootCounts.get(shadowRoot); const newCount = (typeof count === "number" ? count : 0) + 1; if (newCount === 1) { addShadowListeners(shadowRoot); } registeredShadowRootCounts.set(shadowRoot, newCount); }; const unregisterShadowRoot = (shadowRoot) => { const count = registeredShadowRootCounts.get(shadowRoot); const currentCount = typeof count === "number" ? count : 0; const newCount = Math.max(0, currentCount - 1); if (currentCount > 0 && newCount === 0) { removeShadowListeners(shadowRoot); registeredShadowRootCounts.delete(shadowRoot); return; } if (newCount > 0) { registeredShadowRootCounts.set(shadowRoot, newCount); } }; const registerElement = (component, referenceEl) => { if (!referenceEl || !component.referenceElementType) { return; } const existingComponents = registeredElements.get(referenceEl) ?? []; if (existingComponents.includes(component)) { return; } if (options.click && "ariaControlsElements" in referenceEl) { const currentElements = referenceEl.ariaControlsElements ?? []; if (!currentElements.includes(component.el)) { const updatedElements = [...currentElements, component.el]; referenceEl.ariaControlsElements = updatedElements; } } if (options.hover && "ariaDescribedByElements" in referenceEl) { const currentElements = referenceEl.ariaDescribedByElements ?? []; if (!currentElements.includes(component.el)) { const updatedElements = [...currentElements, component.el]; referenceEl.ariaDescribedByElements = updatedElements; } } registeredComponentCount++; registeredElements.set(referenceEl, [...existingComponents, component]); const shadowRoot = options.hover ? getReferenceElShadowRootNode(referenceEl) : null; if (shadowRoot) { registerShadowRoot(shadowRoot); } if (registeredComponentCount === 1) { addListeners(); } updateElement(component, referenceEl); }; const decrementRegisteredElementCount = (shadowRoot) => { registeredComponentCount--; if (shadowRoot) { unregisterShadowRoot(shadowRoot); } }; const unregisterElement = (component, referenceEl) => { if (!referenceEl || !component.referenceElementType) { return; } const shadowRoot = options.hover ? getReferenceElShadowRootNode(referenceEl) : null; const existingComponents = registeredElements.get(referenceEl) ?? []; const updatedComponents = existingComponents.filter((p) => p !== component); if (updatedComponents.length > 0) { registeredElements.set(referenceEl, updatedComponents); if (updatedComponents.length !== existingComponents.length) { decrementRegisteredElementCount(shadowRoot); } } else if (registeredElements.delete(referenceEl)) { decrementRegisteredElementCount(shadowRoot); } if (registeredComponentCount === 0) { removeListeners(); clearHoverTimeout(); } if (options.click && "ariaControlsElements" in referenceEl) { const newElements = referenceEl.ariaControlsElements?.filter((element) => element !== component.el); referenceEl.ariaControlsElements = newElements?.length > 0 ? newElements : null; } if (options.click && "ariaExpanded" in referenceEl) { const hasRegisteredComponents = (updatedComponents?.length ?? 0) > 0; if (hasRegisteredComponents) { const existingComponentOpen = updatedComponents?.some((component2) => component2.open) ?? false; referenceEl.ariaExpanded = toAriaBoolean(existingComponentOpen); } else { referenceEl.ariaExpanded = null; } } if (options.hover && "ariaDescribedByElements" in referenceEl) { const newElements = referenceEl.ariaDescribedByElements?.filter((element) => element !== component.el); referenceEl.ariaDescribedByElements = newElements?.length > 0 ? newElements : null; } }; return { registerElement, unregisterElement, updateElement }; }; export { referenceElementManager as r, useReferenceElement as u };