UNPKG

@ariakit/react-core

Version:

Ariakit React core

391 lines (388 loc) 12.1 kB
"use client"; import { createDialogComponent, useDialog } from "./CAGBPNDP.js"; import { PopoverScopedContextProvider, usePopoverProviderContext } from "./Y67KZUMI.js"; import { createElement, createHook, forwardRef } from "./VOQWLFSQ.js"; import { useEvent, usePortalRef, useSafeLayoutEffect, useWrapElement } from "./5GGHRIN3.js"; import { __objRest, __spreadProps, __spreadValues } from "./3YLGPPWQ.js"; // src/popover/popover.tsx import { invariant } from "@ariakit/core/utils/misc"; import { arrow, autoUpdate, computePosition, flip, limitShift, offset, shift, size } from "@floating-ui/dom"; import { useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; var TagName = "div"; 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 __spreadProps(__spreadValues({}, 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) { const contextElement = anchorElement || void 0; return { contextElement, getBoundingClientRect: () => { const anchor = anchorElement; const anchorRect = getAnchorRect == null ? void 0 : getAnchorRect(anchor); if (anchorRect || !anchor) { return getDOMRect(anchorRect); } return anchor.getBoundingClientRect(); } }; } function isValidPlacement(flip2) { return /^(?:top|bottom|left|right)(?:-(?:start|end))?$/.test(flip2); } function roundByDPR(value) { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; } function getOffsetMiddleware(arrowElement, props) { return offset(({ placement }) => { var _a; const arrowOffset = ((arrowElement == null ? void 0 : arrowElement.clientHeight) || 0) / 2; const finalGutter = typeof props.gutter === "number" ? props.gutter + arrowOffset : (_a = props.gutter) != null ? _a : arrowOffset; const hasAlignment = !!placement.split("-")[1]; return { crossAxis: !hasAlignment ? props.shift : void 0, mainAxis: finalGutter, alignmentAxis: props.shift }; }); } function getFlipMiddleware(props) { if (props.flip === false) return; const fallbackPlacements = typeof props.flip === "string" ? props.flip.split(" ") : void 0; invariant( !fallbackPlacements || fallbackPlacements.every(isValidPlacement), process.env.NODE_ENV !== "production" && "`flip` expects a spaced-delimited list of placements" ); return flip({ padding: props.overflowPadding, fallbackPlacements }); } function getShiftMiddleware(props) { if (!props.slide && !props.overlap) return; return shift({ mainAxis: props.slide, crossAxis: props.overlap, padding: props.overflowPadding, limiter: limitShift() }); } function getSizeMiddleware(props) { return size({ padding: props.overflowPadding, apply({ elements, availableWidth, availableHeight, rects }) { const wrapper = elements.floating; const referenceWidth = Math.round(rects.reference.width); availableWidth = Math.floor(availableWidth); availableHeight = Math.floor(availableHeight); wrapper.style.setProperty( "--popover-anchor-width", `${referenceWidth}px` ); wrapper.style.setProperty( "--popover-available-width", `${availableWidth}px` ); wrapper.style.setProperty( "--popover-available-height", `${availableHeight}px` ); if (props.sameWidth) { wrapper.style.width = `${referenceWidth}px`; } if (props.fitViewport) { wrapper.style.maxWidth = `${availableWidth}px`; wrapper.style.maxHeight = `${availableHeight}px`; } } }); } function getArrowMiddleware(arrowElement, props) { if (!arrowElement) return; return arrow({ element: arrowElement, padding: props.arrowPadding }); } var usePopover = createHook( function usePopover2(_a) { var _b = _a, { store, modal = false, portal = !!modal, preserveTabOrder = true, autoFocusOnShow = true, wrapperProps, fixed = false, flip: flip2 = true, shift: shift2 = 0, slide = true, overlap = false, sameWidth = false, fitViewport = false, gutter, arrowPadding = 4, overflowPadding = 8, getAnchorRect, updatePosition } = _b, props = __objRest(_b, [ "store", "modal", "portal", "preserveTabOrder", "autoFocusOnShow", "wrapperProps", "fixed", "flip", "shift", "slide", "overlap", "sameWidth", "fitViewport", "gutter", "arrowPadding", "overflowPadding", "getAnchorRect", "updatePosition" ]); const context = usePopoverProviderContext(); store = store || context; invariant( store, process.env.NODE_ENV !== "production" && "Popover must receive a `store` prop or be wrapped in a PopoverProvider component." ); const arrowElement = store.useState("arrowElement"); const anchorElement = store.useState("anchorElement"); const disclosureElement = store.useState("disclosureElement"); const popoverElement = store.useState("popoverElement"); const contentElement = store.useState("contentElement"); const placement = store.useState("placement"); const mounted = store.useState("mounted"); const rendered = store.useState("rendered"); const defaultArrowElementRef = useRef(null); const [positioned, setPositioned] = useState(false); const { portalRef, domReady } = usePortalRef(portal, props.portalRef); const getAnchorRectProp = useEvent(getAnchorRect); const updatePositionProp = useEvent(updatePosition); const hasCustomUpdatePosition = !!updatePosition; useSafeLayoutEffect(() => { if (!(popoverElement == null ? void 0 : popoverElement.isConnected)) return; popoverElement.style.setProperty( "--popover-overflow-padding", `${overflowPadding}px` ); const anchor = getAnchorElement(anchorElement, getAnchorRectProp); const updatePosition2 = async () => { if (!mounted) return; if (!arrowElement) { defaultArrowElementRef.current = defaultArrowElementRef.current || document.createElement("div"); } const arrow2 = arrowElement || defaultArrowElementRef.current; const middleware = [ getOffsetMiddleware(arrow2, { gutter, shift: shift2 }), getFlipMiddleware({ flip: flip2, overflowPadding }), getShiftMiddleware({ slide, shift: shift2, overlap, overflowPadding }), getArrowMiddleware(arrow2, { arrowPadding }), getSizeMiddleware({ sameWidth, fitViewport, overflowPadding }) ]; const pos = await computePosition(anchor, popoverElement, { placement, strategy: fixed ? "fixed" : "absolute", middleware }); store == null ? void 0 : store.setState("currentPlacement", pos.placement); setPositioned(true); const x = roundByDPR(pos.x); const y = roundByDPR(pos.y); Object.assign(popoverElement.style, { top: "0", left: "0", transform: `translate3d(${x}px,${y}px,0)` }); if (arrow2 && pos.middlewareData.arrow) { const { x: arrowX, y: arrowY } = pos.middlewareData.arrow; const side = pos.placement.split("-")[0]; const centerX = arrow2.clientWidth / 2; const centerY = arrow2.clientHeight / 2; const originX = arrowX != null ? arrowX + centerX : -centerX; const originY = arrowY != null ? arrowY + centerY : -centerY; popoverElement.style.setProperty( "--popover-transform-origin", { top: `${originX}px calc(100% + ${centerY}px)`, bottom: `${originX}px ${-centerY}px`, left: `calc(100% + ${centerX}px) ${originY}px`, right: `${-centerX}px ${originY}px` }[side] ); Object.assign(arrow2.style, { left: arrowX != null ? `${arrowX}px` : "", top: arrowY != null ? `${arrowY}px` : "", [side]: "100%" }); } }; const update = async () => { if (hasCustomUpdatePosition) { await updatePositionProp({ updatePosition: updatePosition2 }); setPositioned(true); } else { await updatePosition2(); } }; const cancelAutoUpdate = autoUpdate(anchor, popoverElement, update, { // JSDOM doesn't support ResizeObserver elementResize: typeof ResizeObserver === "function" }); return () => { setPositioned(false); cancelAutoUpdate(); }; }, [ store, rendered, popoverElement, arrowElement, anchorElement, popoverElement, placement, mounted, domReady, fixed, flip2, shift2, slide, overlap, sameWidth, fitViewport, gutter, arrowPadding, overflowPadding, getAnchorRectProp, hasCustomUpdatePosition, updatePositionProp ]); useSafeLayoutEffect(() => { if (!mounted) return; if (!domReady) return; if (!(popoverElement == null ? void 0 : popoverElement.isConnected)) return; if (!(contentElement == null ? void 0 : contentElement.isConnected)) return; const applyZIndex = () => { popoverElement.style.zIndex = getComputedStyle(contentElement).zIndex; }; applyZIndex(); let raf = requestAnimationFrame(() => { raf = requestAnimationFrame(applyZIndex); }); return () => cancelAnimationFrame(raf); }, [mounted, domReady, popoverElement, contentElement]); const position = fixed ? "fixed" : "absolute"; props = useWrapElement( props, (element) => /* @__PURE__ */ jsx( "div", __spreadProps(__spreadValues({}, wrapperProps), { style: __spreadValues({ // https://floating-ui.com/docs/computeposition#initial-layout position, top: 0, left: 0, width: "max-content" }, wrapperProps == null ? void 0 : wrapperProps.style), ref: store == null ? void 0 : store.setPopoverElement, children: element }) ), [store, position, wrapperProps] ); props = useWrapElement( props, (element) => /* @__PURE__ */ jsx(PopoverScopedContextProvider, { value: store, children: element }), [store] ); props = __spreadProps(__spreadValues({ // data-placing is not part of the public API. We're setting this here so // we can wait for the popover to be positioned before other components // move focus into it. For example, this attribute is observed by the // Combobox component with the autoSelect behavior. "data-placing": !positioned || void 0 }, props), { style: __spreadValues({ position: "relative" }, props.style) }); props = useDialog(__spreadProps(__spreadValues({ store, modal, portal, preserveTabOrder, preserveTabOrderAnchor: disclosureElement || anchorElement, autoFocusOnShow: positioned && autoFocusOnShow }, props), { portalRef })); return props; } ); var Popover = createDialogComponent( forwardRef(function Popover2(props) { const htmlProps = usePopover(props); return createElement(TagName, htmlProps); }), usePopoverProviderContext ); export { usePopover, Popover };