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