123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- /*!
- * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
- * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
- * v1.0.0-beta.97
- */
- import { arrow, autoPlacement, autoUpdate, computePosition, flip, hide, offset, platform, shift } from "@floating-ui/dom";
- import { closestElementCrossShadowBoundary, getElementDir } from "./dom";
- import { debounce } from "lodash-es";
- import { Build } from "@stencil/core";
- import { config } from "./config";
- const floatingUIBrowserCheck = patchFloatingUiForNonChromiumBrowsers();
- async function patchFloatingUiForNonChromiumBrowsers() {
- function getUAString() {
- const uaData = navigator.userAgentData;
- if (uaData === null || uaData === void 0 ? void 0 : uaData.brands) {
- return uaData.brands.map((item) => `${item.brand}/${item.version}`).join(" ");
- }
- return navigator.userAgent;
- }
- if (Build.isBrowser &&
- config.floatingUINonChromiumPositioningFix &&
- // ⚠️ browser-sniffing is not a best practice and should be avoided ⚠️
- /firefox|safari/i.test(getUAString())) {
- const { getClippingRect, getElementRects, getOffsetParent } = await import("./floating-ui/nonChromiumPlatformUtils");
- platform.getClippingRect = getClippingRect;
- platform.getOffsetParent = getOffsetParent;
- platform.getElementRects = getElementRects;
- }
- }
- const placementDataAttribute = "data-placement";
- /**
- * Exported for testing purposes only
- */
- export const repositionDebounceTimeout = 100;
- export const placements = [
- "auto",
- "auto-start",
- "auto-end",
- "top",
- "top-start",
- "top-end",
- "bottom",
- "bottom-start",
- "bottom-end",
- "right",
- "right-start",
- "right-end",
- "left",
- "left-start",
- "left-end",
- "leading-start",
- "leading",
- "leading-end",
- "trailing-end",
- "trailing",
- "trailing-start"
- ];
- export const effectivePlacements = [
- "top",
- "bottom",
- "right",
- "left",
- "top-start",
- "top-end",
- "bottom-start",
- "bottom-end",
- "right-start",
- "right-end",
- "left-start",
- "left-end"
- ];
- export const menuPlacements = ["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"];
- export const menuEffectivePlacements = [
- "top-start",
- "top",
- "top-end",
- "bottom-start",
- "bottom",
- "bottom-end"
- ];
- export const flipPlacements = [
- "top",
- "bottom",
- "right",
- "left",
- "top-start",
- "top-end",
- "bottom-start",
- "bottom-end",
- "right-start",
- "right-end",
- "left-start",
- "left-end"
- ];
- export const defaultMenuPlacement = "bottom-start";
- export const FloatingCSS = {
- animation: "calcite-floating-ui-anim",
- animationActive: "calcite-floating-ui-anim--active"
- };
- function getMiddleware({ placement, disableFlip, flipPlacements, offsetDistance, offsetSkidding, arrowEl, type }) {
- const defaultMiddleware = [shift(), hide()];
- if (type === "menu") {
- return [
- ...defaultMiddleware,
- flip({
- fallbackPlacements: flipPlacements || ["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"]
- })
- ];
- }
- if (type === "popover" || type === "tooltip") {
- const middleware = [
- ...defaultMiddleware,
- 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 (!disableFlip) {
- middleware.push(flip(flipPlacements ? { fallbackPlacements: flipPlacements } : {}));
- }
- if (arrowEl) {
- middleware.push(arrow({
- element: arrowEl
- }));
- }
- return middleware;
- }
- return [];
- }
- export function filterComputedPlacements(placements, el) {
- const filteredPlacements = placements.filter((placement) => effectivePlacements.includes(placement));
- if (filteredPlacements.length !== placements.length) {
- console.warn(`${el.tagName}: Invalid value found in: flipPlacements. Try any of these: ${effectivePlacements
- .map((placement) => `"${placement}"`)
- .join(", ")
- .trim()}`, { el });
- }
- return filteredPlacements;
- }
- /*
- In floating-ui, "*-start" and "*-end" are already flipped in RTL.
- There is no need for our "*-leading" and "*-trailing" values anymore.
- https://github.com/floating-ui/floating-ui/issues/1530
- https://github.com/floating-ui/floating-ui/issues/1563
- */
- export function getEffectivePlacement(floatingEl, placement) {
- const placements = ["left", "right"];
- if (getElementDir(floatingEl) === "rtl") {
- placements.reverse();
- }
- return placement
- .replace(/-leading/gi, "-start")
- .replace(/-trailing/gi, "-end")
- .replace(/leading/gi, placements[0])
- .replace(/trailing/gi, placements[1]);
- }
- /**
- * Convenience function to manage `reposition` calls for FloatingUIComponents that use `positionFloatingUI.
- *
- * Note: this is not needed for components that use `calcite-popover`.
- *
- * @param component
- * @param options
- * @param options.referenceEl
- * @param options.floatingEl
- * @param options.overlayPositioning
- * @param options.placement
- * @param options.disableFlip
- * @param options.flipPlacements
- * @param options.offsetDistance
- * @param options.offsetSkidding
- * @param options.arrowEl
- * @param options.type
- * @param delayed
- */
- export async function reposition(component, options, delayed = false) {
- if (!component.open) {
- return;
- }
- return delayed ? debouncedReposition(options) : positionFloatingUI(options);
- }
- const debouncedReposition = debounce(positionFloatingUI, repositionDebounceTimeout, {
- leading: true,
- maxWait: repositionDebounceTimeout
- });
- /**
- * Positions the floating element relative to the reference element.
- *
- * **Note:** exported for testing purposes only
- *
- * @param root0
- * @param root0.referenceEl
- * @param root0.floatingEl
- * @param root0.overlayPositioning
- * @param root0.placement
- * @param root0.disableFlip
- * @param root0.flipPlacements
- * @param root0.offsetDistance
- * @param root0.offsetSkidding
- * @param root0.arrowEl
- * @param root0.type
- * @param root0.includeArrow
- */
- export async function positionFloatingUI({ referenceEl, floatingEl, overlayPositioning = "absolute", placement, disableFlip, flipPlacements, offsetDistance, offsetSkidding, includeArrow = false, arrowEl, type }) {
- var _a;
- if (!referenceEl || !floatingEl || (includeArrow && !arrowEl)) {
- return null;
- }
- await floatingUIBrowserCheck;
- const { x, y, placement: effectivePlacement, strategy: position, middlewareData } = await computePosition(referenceEl, floatingEl, {
- strategy: overlayPositioning,
- placement: placement === "auto" || placement === "auto-start" || placement === "auto-end"
- ? undefined
- : getEffectivePlacement(floatingEl, placement),
- middleware: getMiddleware({
- placement,
- disableFlip,
- flipPlacements,
- offsetDistance,
- offsetSkidding,
- arrowEl,
- type
- })
- });
- if (middlewareData === null || middlewareData === void 0 ? void 0 : middlewareData.arrow) {
- const { x: arrowX, y: arrowY } = middlewareData.arrow;
- Object.assign(arrowEl.style, {
- left: arrowX != null ? `${arrowX}px` : "",
- top: arrowY != null ? `${arrowY}px` : ""
- });
- }
- const referenceHidden = (_a = middlewareData === null || middlewareData === void 0 ? void 0 : middlewareData.hide) === null || _a === void 0 ? void 0 : _a.referenceHidden;
- const visibility = referenceHidden ? "hidden" : null;
- const pointerEvents = visibility ? "none" : null;
- floatingEl.setAttribute(placementDataAttribute, effectivePlacement);
- const transform = `translate(${Math.round(x)}px,${Math.round(y)}px)`;
- Object.assign(floatingEl.style, {
- visibility,
- pointerEvents,
- position,
- top: "0",
- left: "0",
- transform
- });
- }
- /**
- * Exported for testing purposes only
- *
- * @internal
- */
- export const cleanupMap = new WeakMap();
- /**
- * Helper to set up floating element interactions on connectedCallback.
- *
- * @param component
- * @param referenceEl
- * @param floatingEl
- */
- export function connectFloatingUI(component, referenceEl, floatingEl) {
- if (!floatingEl || !referenceEl) {
- return;
- }
- disconnectFloatingUI(component, referenceEl, floatingEl);
- const position = component.overlayPositioning;
- // ensure position matches for initial positioning
- floatingEl.style.position = position;
- if (position === "absolute") {
- moveOffScreen(floatingEl);
- }
- const runAutoUpdate = Build.isBrowser
- ? autoUpdate
- : (_refEl, _floatingEl, updateCallback) => {
- updateCallback();
- return () => {
- /* noop */
- };
- };
- cleanupMap.set(component, runAutoUpdate(referenceEl, floatingEl, () => component.reposition()));
- }
- /**
- * Helper to tear down floating element interactions on disconnectedCallback.
- *
- * @param component
- * @param referenceEl
- * @param floatingEl
- */
- export function disconnectFloatingUI(component, referenceEl, floatingEl) {
- if (!floatingEl || !referenceEl) {
- return;
- }
- getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd);
- const cleanup = cleanupMap.get(component);
- if (cleanup) {
- cleanup();
- }
- cleanupMap.delete(component);
- }
- const visiblePointerSize = 4;
- /**
- * Default offset the position of the floating element away from the reference element.
- *
- * @default 6
- */
- export const defaultOffsetDistance = Math.ceil(Math.hypot(visiblePointerSize, visiblePointerSize));
- /**
- * This utils applies floating element styles to avoid affecting layout when closed.
- *
- * This should be called when the closing transition will start.
- *
- * @param floatingEl
- */
- export function updateAfterClose(floatingEl) {
- if (!floatingEl || floatingEl.style.position !== "absolute") {
- return;
- }
- getTransitionTarget(floatingEl).addEventListener("transitionend", handleTransitionElTransitionEnd);
- }
- function getTransitionTarget(floatingEl) {
- // assumes floatingEl w/ shadowRoot is a FloatingUIComponent
- return floatingEl.shadowRoot || floatingEl;
- }
- function handleTransitionElTransitionEnd(event) {
- const floatingTransitionEl = event.target;
- if (
- // using any prop from floating-ui transition
- event.propertyName === "opacity" &&
- floatingTransitionEl.classList.contains(FloatingCSS.animation)) {
- const floatingEl = getFloatingElFromTransitionTarget(floatingTransitionEl);
- moveOffScreen(floatingEl);
- getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd);
- }
- }
- function moveOffScreen(floatingEl) {
- floatingEl.style.transform = "";
- floatingEl.style.top = "-99999px";
- floatingEl.style.left = "-99999px";
- }
- function getFloatingElFromTransitionTarget(floatingTransitionEl) {
- return closestElementCrossShadowBoundary(floatingTransitionEl, `[${placementDataAttribute}]`);
- }
|