@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
279 lines (278 loc) • 8.59 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 { 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
};