UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

279 lines (278 loc) • 8.59 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 { isServer } from "lit"; import { autoUpdate, computePosition, shift, hide, flip, offset, autoPlacement, arrow, platform } from "@floating-ui/dom"; import { debounce } from "lodash-es"; import { offsetParent } from "composed-offset-position"; import { D as DEBOUNCE } from "./runtime.js"; import { g as getElementDir } from "./dom.js"; (function setUpFloatingUiForShadowDomPositioning() { if (!isServer) { const originalGetOffsetParent = platform.getOffsetParent; platform.getOffsetParent = (element) => originalGetOffsetParent(element, offsetParent); } })(); function roundByDPR(value) { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; } const positionFloatingUI = ( /* we export arrow function to allow us to spy on it during testing */ async (component, { referenceEl, floatingEl, overlayPositioning = "absolute", placement, flipDisabled, flipPlacements: flipPlacements2, offsetDistance, offsetSkidding, arrowEl, type }) => { if (!referenceEl || !floatingEl) { return; } const isRTL = getElementDir(floatingEl) === "rtl"; const { x, y, placement: effectivePlacement, strategy: position, middlewareData } = await computePosition(referenceEl, floatingEl, { strategy: overlayPositioning, placement: placement === "auto" || placement === "auto-start" || placement === "auto-end" ? void 0 : getEffectivePlacement(placement, isRTL), middleware: getMiddleware({ placement, flipDisabled, flipPlacements: flipPlacements2?.map((placement2) => getEffectivePlacement(placement2, isRTL)), offsetDistance, offsetSkidding, arrowEl, type }) }); if (arrowEl && middlewareData.arrow) { const { x: x2, y: y2 } = middlewareData.arrow; const side = effectivePlacement.split("-")[0]; const alignment = x2 != null ? "left" : "top"; const transform = ARROW_CSS_TRANSFORM[side]; const reset = { left: "", top: "", bottom: "", right: "" }; if ("floatingLayout" in component) { component.floatingLayout = side === "left" || side === "right" ? "horizontal" : "vertical"; } Object.assign(arrowEl.style, { ...reset, [alignment]: `${alignment == "left" ? x2 : y2}px`, [side]: "100%", transform }); } const referenceHidden = middlewareData.hide?.referenceHidden; const visibility = referenceHidden ? "hidden" : null; const pointerEvents = visibility ? "none" : null; floatingEl.setAttribute(placementDataAttribute, effectivePlacement); Object.assign(floatingEl.style, { pointerEvents, position, transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`, visibility }); } ); const placementDataAttribute = "data-placement"; const flipPlacements = [ "top", "bottom", "right", "left", "top-start", "top-end", "bottom-start", "bottom-end", "right-start", "right-end", "left-start", "left-end", "leading", "trailing", "leading-start", "leading-end", "trailing-start", "trailing-end" ]; const defaultMenuPlacement = "bottom-start"; const defaultEndMenuPlacement = "bottom-end"; const FloatingCSS = { animation: "calcite-floating-ui-anim", animationActive: "calcite-floating-ui-anim--active" }; function getMiddleware({ placement, flipDisabled, flipPlacements: flipPlacements2, offsetDistance, offsetSkidding, arrowEl, type }) { const middleware = [shift(), hide()]; if (type === "menu") { middleware.push( flip({ fallbackPlacements: flipPlacements2 || ["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"] }) ); } middleware.push( offset({ mainAxis: typeof offsetDistance === "number" ? offsetDistance : 0, crossAxis: typeof offsetSkidding === "number" ? offsetSkidding : 0 }) ); if (placement === "auto" || placement === "auto-start" || placement === "auto-end") { middleware.push( autoPlacement({ alignment: placement === "auto-start" ? "start" : placement === "auto-end" ? "end" : null }) ); } else if (!flipDisabled) { middleware.push(flip(flipPlacements2 ? { fallbackPlacements: flipPlacements2 } : {})); } if (arrowEl) { middleware.push( arrow({ element: arrowEl }) ); } return middleware; } function filterValidFlipPlacements(placements2, el) { const filteredPlacements = placements2.filter( (placement) => flipPlacements.includes(placement) ); if (filteredPlacements.length !== placements2.length) { console.warn( `${el.tagName}: Invalid value found in: flipPlacements. Try any of these: ${flipPlacements.map((placement) => `"${placement}"`).join(", ").trim()}`, { el } ); } return filteredPlacements; } function getEffectivePlacement(placement, isRTL = false) { const placements2 = ["left", "right"]; if (isRTL) { placements2.reverse(); } return placement.replace(/leading/gi, placements2[0]).replace(/trailing/gi, placements2[1]); } async function reposition(component, options, delayed = false) { if (!component.open || !options.floatingEl || !options.referenceEl) { return; } Object.assign(options.floatingEl.style, { display: "block", // initial positioning based on https://floating-ui.com/docs/computePosition#initial-layout position: options.overlayPositioning ?? "absolute" }); const trackedState = autoUpdatingComponentMap.get(component); if (!trackedState) { return runAutoUpdate(component); } const positionFunction = delayed ? getDebouncedReposition(component) : positionFloatingUI; await positionFunction(component, options); } function getDebouncedReposition(component) { let debounced = componentToDebouncedRepositionMap.get(component); if (debounced) { return debounced; } debounced = debounce(positionFloatingUI, DEBOUNCE.reposition, { leading: true, maxWait: DEBOUNCE.reposition }); componentToDebouncedRepositionMap.set(component, debounced); return debounced; } const ARROW_CSS_TRANSFORM = { top: "", left: "rotate(-90deg)", bottom: "rotate(180deg)", right: "rotate(90deg)" }; const autoUpdatingComponentMap = /* @__PURE__ */ new WeakMap(); const componentToDebouncedRepositionMap = /* @__PURE__ */ new WeakMap(); async function runAutoUpdate(component) { const { referenceEl, floatingEl } = component; if (!floatingEl.isConnected) { return; } const effectiveAutoUpdate = !isServer ? autoUpdate : (_refEl, _floatingEl, updateCallback) => { updateCallback(); return () => { }; }; autoUpdatingComponentMap.set(component, { state: "pending" }); let repositionPromise; const cleanUp = effectiveAutoUpdate( referenceEl, floatingEl, // callback is invoked immediately () => { const promise = component.reposition(); if (!repositionPromise) { repositionPromise = promise; } } ); autoUpdatingComponentMap.set(component, { state: "active", cleanUp }); return repositionPromise; } function hideFloatingUI(component) { const { floatingEl } = component; if (!floatingEl) { return; } Object.assign(floatingEl.style, { display: "", pointerEvents: "", position: "", transform: "", visibility: "" }); } async function connectFloatingUI(component) { const { floatingEl, referenceEl } = component; hideFloatingUI(component); if (!floatingEl || !referenceEl) { return; } disconnectFloatingUI(component); if (!component.open) { return; } return runAutoUpdate(component); } function disconnectFloatingUI(component) { const trackedState = autoUpdatingComponentMap.get(component); if (trackedState?.state === "active") { trackedState.cleanUp(); } autoUpdatingComponentMap.delete(component); componentToDebouncedRepositionMap.get(component)?.cancel(); componentToDebouncedRepositionMap.delete(component); } const visiblePointerSize = 4; const defaultOffsetDistance = Math.ceil(Math.hypot(visiblePointerSize, visiblePointerSize)); export { FloatingCSS as F, disconnectFloatingUI as a, defaultMenuPlacement as b, connectFloatingUI as c, defaultOffsetDistance as d, defaultEndMenuPlacement as e, filterValidFlipPlacements as f, hideFloatingUI as h, reposition as r };