UNPKG

@zag-js/popper

Version:

Dynamic positioning logic for ui machines

331 lines (323 loc) • 10.7 kB
'use strict'; var dom = require('@floating-ui/dom'); var domQuery = require('@zag-js/dom-query'); var utils = require('@zag-js/utils'); // src/get-placement.ts function createDOMRect(x = 0, y = 0, width = 0, height = 0) { if (typeof DOMRect === "function") { return new DOMRect(x, y, width, height); } const rect = { x, y, width, height, top: y, right: x + width, bottom: y + height, left: x }; return { ...rect, toJSON: () => rect }; } function getDOMRect(anchorRect) { if (!anchorRect) return createDOMRect(); const { x, y, width, height } = anchorRect; return createDOMRect(x, y, width, height); } function getAnchorElement(anchorElement, getAnchorRect) { return { contextElement: domQuery.isHTMLElement(anchorElement) ? anchorElement : void 0, getBoundingClientRect: () => { const anchor = anchorElement; const anchorRect = getAnchorRect?.(anchor); if (anchorRect || !anchor) { return getDOMRect(anchorRect); } return anchor.getBoundingClientRect(); } }; } // src/middleware.ts var toVar = (value) => ({ variable: value, reference: `var(${value})` }); var cssVars = { arrowSize: toVar("--arrow-size"), arrowSizeHalf: toVar("--arrow-size-half"), arrowBg: toVar("--arrow-background"), transformOrigin: toVar("--transform-origin"), arrowOffset: toVar("--arrow-offset") }; var getTransformOrigin = (arrow2) => ({ top: "bottom center", "top-start": arrow2 ? `${arrow2.x}px bottom` : "left bottom", "top-end": arrow2 ? `${arrow2.x}px bottom` : "right bottom", bottom: "top center", "bottom-start": arrow2 ? `${arrow2.x}px top` : "top left", "bottom-end": arrow2 ? `${arrow2.x}px top` : "top right", left: "right center", "left-start": arrow2 ? `right ${arrow2.y}px` : "right top", "left-end": arrow2 ? `right ${arrow2.y}px` : "right bottom", right: "left center", "right-start": arrow2 ? `left ${arrow2.y}px` : "left top", "right-end": arrow2 ? `left ${arrow2.y}px` : "left bottom" }); var transformOriginMiddleware = { name: "transformOrigin", fn({ placement, elements, middlewareData }) { const { arrow: arrow2 } = middlewareData; const transformOrigin = getTransformOrigin(arrow2)[placement]; const { floating } = elements; floating.style.setProperty(cssVars.transformOrigin.variable, transformOrigin); return { data: { transformOrigin } }; } }; var rectMiddleware = { name: "rects", fn({ rects }) { return { data: rects }; } }; var shiftArrowMiddleware = (arrowEl) => { if (!arrowEl) return; return { name: "shiftArrow", fn({ placement, middlewareData }) { if (!middlewareData.arrow) return {}; const { x, y } = middlewareData.arrow; const dir = placement.split("-")[0]; Object.assign(arrowEl.style, { left: x != null ? `${x}px` : "", top: y != null ? `${y}px` : "", [dir]: `calc(100% + ${cssVars.arrowOffset.reference})` }); return {}; } }; }; // src/placement.ts function isValidPlacement(v) { return /^(?:top|bottom|left|right)(?:-(?:start|end))?$/.test(v); } function getPlacementDetails(placement) { const [side, align] = placement.split("-"); return { side, align, hasAlign: align != null }; } function getPlacementSide(placement) { return placement.split("-")[0]; } // src/get-placement.ts var defaultOptions = { strategy: "absolute", placement: "bottom", listeners: true, gutter: 8, flip: true, slide: true, overlap: false, sameWidth: false, fitViewport: false, overflowPadding: 8, arrowPadding: 4 }; function roundByDpr(win, value) { const dpr = win.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; } function getBoundaryMiddleware(opts) { return utils.runIfFn(opts.boundary); } function getArrowMiddleware(arrowElement, opts) { if (!arrowElement) return; return dom.arrow({ element: arrowElement, padding: opts.arrowPadding }); } function getOffsetMiddleware(arrowElement, opts) { if (utils.isNull(opts.offset ?? opts.gutter)) return; return dom.offset(({ placement }) => { const arrowOffset = (arrowElement?.clientHeight || 0) / 2; const gutter = opts.offset?.mainAxis ?? opts.gutter; const mainAxis = typeof gutter === "number" ? gutter + arrowOffset : gutter ?? arrowOffset; const { hasAlign } = getPlacementDetails(placement); const shift2 = !hasAlign ? opts.shift : void 0; const crossAxis = opts.offset?.crossAxis ?? shift2; return utils.compact({ crossAxis, mainAxis, alignmentAxis: opts.shift }); }); } function getFlipMiddleware(opts) { if (!opts.flip) return; return dom.flip({ boundary: getBoundaryMiddleware(opts), padding: opts.overflowPadding, fallbackPlacements: opts.flip === true ? void 0 : opts.flip }); } function getShiftMiddleware(opts) { if (!opts.slide && !opts.overlap) return; return dom.shift({ boundary: getBoundaryMiddleware(opts), mainAxis: opts.slide, crossAxis: opts.overlap, padding: opts.overflowPadding, limiter: dom.limitShift() }); } function getSizeMiddleware(opts) { return dom.size({ padding: opts.overflowPadding, apply({ elements, rects, availableHeight, availableWidth }) { const floating = elements.floating; const referenceWidth = Math.round(rects.reference.width); availableWidth = Math.floor(availableWidth); availableHeight = Math.floor(availableHeight); floating.style.setProperty("--reference-width", `${referenceWidth}px`); floating.style.setProperty("--available-width", `${availableWidth}px`); floating.style.setProperty("--available-height", `${availableHeight}px`); } }); } function hideWhenDetachedMiddleware(opts) { if (!opts.hideWhenDetached) return; return dom.hide({ strategy: "referenceHidden", boundary: opts.boundary?.() ?? "clippingAncestors" }); } function getAutoUpdateOptions(opts) { if (!opts) return {}; if (opts === true) { return { ancestorResize: true, ancestorScroll: true, elementResize: true, layoutShift: true }; } return opts; } function getPlacementImpl(referenceOrVirtual, floating, opts = {}) { const reference = getAnchorElement(referenceOrVirtual, opts.getAnchorRect); if (!floating || !reference) return; const options = Object.assign({}, defaultOptions, opts); const arrowEl = floating.querySelector("[data-part=arrow]"); const middleware = [ getOffsetMiddleware(arrowEl, options), getFlipMiddleware(options), getShiftMiddleware(options), getArrowMiddleware(arrowEl, options), shiftArrowMiddleware(arrowEl), transformOriginMiddleware, getSizeMiddleware(options), hideWhenDetachedMiddleware(options), rectMiddleware ]; const { placement, strategy, onComplete, onPositioned } = options; const updatePosition = async () => { if (!reference || !floating) return; const pos = await dom.computePosition(reference, floating, { placement, middleware, strategy }); onComplete?.(pos); onPositioned?.({ placed: true }); const win = domQuery.getWindow(floating); const x = roundByDpr(win, pos.x); const y = roundByDpr(win, pos.y); floating.style.setProperty("--x", `${x}px`); floating.style.setProperty("--y", `${y}px`); if (options.hideWhenDetached) { const isHidden = pos.middlewareData.hide?.referenceHidden; if (isHidden) { floating.style.setProperty("visibility", "hidden"); floating.style.setProperty("pointer-events", "none"); } else { floating.style.removeProperty("visibility"); floating.style.removeProperty("pointer-events"); } } const contentEl = floating.firstElementChild; if (contentEl) { const styles = domQuery.getComputedStyle(contentEl); floating.style.setProperty("--z-index", styles.zIndex); } }; const update = async () => { if (opts.updatePosition) { await opts.updatePosition({ updatePosition, floatingElement: floating }); onPositioned?.({ placed: true }); } else { await updatePosition(); } }; const autoUpdateOptions = getAutoUpdateOptions(options.listeners); const cancelAutoUpdate = options.listeners ? dom.autoUpdate(reference, floating, update, autoUpdateOptions) : utils.noop; update(); return () => { cancelAutoUpdate?.(); onPositioned?.({ placed: false }); }; } function getPlacement(referenceOrFn, floatingOrFn, opts = {}) { const { defer, ...options } = opts; const func = defer ? domQuery.raf : (v) => v(); const cleanups = []; cleanups.push( func(() => { const reference = typeof referenceOrFn === "function" ? referenceOrFn() : referenceOrFn; const floating = typeof floatingOrFn === "function" ? floatingOrFn() : floatingOrFn; cleanups.push(getPlacementImpl(reference, floating, options)); }) ); return () => { cleanups.forEach((fn) => fn?.()); }; } // src/get-styles.ts var ARROW_FLOATING_STYLE = { bottom: "rotate(45deg)", left: "rotate(135deg)", top: "rotate(225deg)", right: "rotate(315deg)" }; function getPlacementStyles(options = {}) { const { placement, sameWidth, fitViewport, strategy = "absolute" } = options; return { arrow: { position: "absolute", width: cssVars.arrowSize.reference, height: cssVars.arrowSize.reference, [cssVars.arrowSizeHalf.variable]: `calc(${cssVars.arrowSize.reference} / 2)`, [cssVars.arrowOffset.variable]: `calc(${cssVars.arrowSizeHalf.reference} * -1)` }, arrowTip: { // @ts-expect-error - Fix this transform: placement ? ARROW_FLOATING_STYLE[placement.split("-")[0]] : void 0, background: cssVars.arrowBg.reference, top: "0", left: "0", width: "100%", height: "100%", position: "absolute", zIndex: "inherit" }, floating: { position: strategy, isolation: "isolate", minWidth: sameWidth ? void 0 : "max-content", width: sameWidth ? "var(--reference-width)" : void 0, maxWidth: fitViewport ? "var(--available-width)" : void 0, maxHeight: fitViewport ? "var(--available-height)" : void 0, pointerEvents: !placement ? "none" : void 0, top: "0px", left: "0px", // move off-screen if placement is not defined transform: placement ? "translate3d(var(--x), var(--y), 0)" : "translate3d(0, -100vh, 0)", zIndex: "var(--z-index)" } }; } exports.getPlacement = getPlacement; exports.getPlacementSide = getPlacementSide; exports.getPlacementStyles = getPlacementStyles; exports.isValidPlacement = isValidPlacement;