@zag-js/popper
Version:
Dynamic positioning logic for ui machines
331 lines (323 loc) • 10.7 kB
JavaScript
;
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;