UNPKG

@wordpress/components

Version:
345 lines (344 loc) 12.2 kB
// packages/components/src/popover/index.tsx import clsx from "clsx"; import { useFloating, flip as flipMiddleware, shift as shiftMiddleware, limitShift, autoUpdate, arrow, offset as offsetMiddleware, size } from "@floating-ui/react-dom"; import { motion } from "framer-motion"; import { useRef, useLayoutEffect, forwardRef, createContext, useContext, useMemo, useState, useCallback, createPortal } from "@wordpress/element"; import { useReducedMotion, useViewportMatch, useMergeRefs, __experimentalUseDialog as useDialog } from "@wordpress/compose"; import { close } from "@wordpress/icons"; import deprecated from "@wordpress/deprecated"; import { Path, SVG } from "@wordpress/primitives"; import { __ } from "@wordpress/i18n"; import Button from "../button"; import ScrollLock from "../scroll-lock"; import { Slot, Fill, useSlot } from "../slot-fill"; import { computePopoverPosition, positionToPlacement, placementToMotionAnimationProps, getReferenceElement } from "./utils"; import { contextConnect, useContextSystem } from "../context"; import { overlayMiddlewares } from "./overlay-middlewares"; import { StyleProvider } from "../style-provider"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; var SLOT_NAME = "Popover"; var OVERFLOW_PADDING = 8; var ArrowTriangle = () => /* @__PURE__ */ _jsxs(SVG, { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 100 100", className: "components-popover__triangle", role: "presentation", children: [/* @__PURE__ */ _jsx(Path, { className: "components-popover__triangle-bg", d: "M 0 0 L 50 50 L 100 0" }), /* @__PURE__ */ _jsx(Path, { className: "components-popover__triangle-border", d: "M 0 0 L 50 50 L 100 0", vectorEffect: "non-scaling-stroke" })] }); var slotNameContext = createContext(void 0); slotNameContext.displayName = "__unstableSlotNameContext"; var fallbackContainerClassname = "components-popover__fallback-container"; var getPopoverFallbackContainer = () => { let container = document.body.querySelector("." + fallbackContainerClassname); if (!container) { container = document.createElement("div"); container.className = fallbackContainerClassname; document.body.append(container); } return container; }; var UnforwardedPopover = (props, forwardedRef) => { const { animate = true, headerTitle, constrainTabbing, onClose, children, className, noArrow = true, position, placement: placementProp = "bottom-start", offset: offsetProp = 0, focusOnMount = "firstElement", anchor, expandOnMobile, onFocusOutside, __unstableSlotName = SLOT_NAME, flip = true, resize = true, shift = false, inline = false, variant, style: contentStyle, // Deprecated props __unstableForcePosition, anchorRef, anchorRect, getAnchorRect, isAlternate, // Rest ...contentProps } = useContextSystem(props, "Popover"); let computedFlipProp = flip; let computedResizeProp = resize; if (__unstableForcePosition !== void 0) { deprecated("`__unstableForcePosition` prop in wp.components.Popover", { since: "6.1", version: "6.3", alternative: "`flip={ false }` and `resize={ false }`" }); computedFlipProp = !__unstableForcePosition; computedResizeProp = !__unstableForcePosition; } if (anchorRef !== void 0) { deprecated("`anchorRef` prop in wp.components.Popover", { since: "6.1", alternative: "`anchor` prop" }); } if (anchorRect !== void 0) { deprecated("`anchorRect` prop in wp.components.Popover", { since: "6.1", alternative: "`anchor` prop" }); } if (getAnchorRect !== void 0) { deprecated("`getAnchorRect` prop in wp.components.Popover", { since: "6.1", alternative: "`anchor` prop" }); } const computedVariant = isAlternate ? "toolbar" : variant; if (isAlternate !== void 0) { deprecated("`isAlternate` prop in wp.components.Popover", { since: "6.2", alternative: "`variant` prop with the `'toolbar'` value" }); } const arrowRef = useRef(null); const [fallbackReferenceElement, setFallbackReferenceElement] = useState(null); const anchorRefFallback = useCallback((node) => { setFallbackReferenceElement(node); }, []); const isMobileViewport = useViewportMatch("medium", "<"); const isExpanded = expandOnMobile && isMobileViewport; const hasArrow = !isExpanded && !noArrow; const normalizedPlacementFromProps = position ? positionToPlacement(position) : placementProp; const middleware = [...placementProp === "overlay" ? overlayMiddlewares() : [], offsetMiddleware(offsetProp), computedFlipProp && flipMiddleware(), computedResizeProp && size({ padding: OVERFLOW_PADDING, apply(sizeProps) { var _refs$floating$curren; const { firstElementChild } = (_refs$floating$curren = refs.floating.current) !== null && _refs$floating$curren !== void 0 ? _refs$floating$curren : {}; if (!(firstElementChild instanceof HTMLElement)) { return; } Object.assign(firstElementChild.style, { maxHeight: `${Math.max(0, sizeProps.availableHeight)}px`, overflow: "auto" }); } }), shift && shiftMiddleware({ crossAxis: true, limiter: limitShift(), padding: 1 // Necessary to avoid flickering at the edge of the viewport. }), arrow({ element: arrowRef })]; const slotName = useContext(slotNameContext) || __unstableSlotName; const slot = useSlot(slotName); let onDialogClose; if (onClose || onFocusOutside) { onDialogClose = (type, event) => { if (type === "focus-outside") { const blurTarget = event?.target; const referenceElement = refs.reference.current; const floatingElement = refs.floating.current; const isBlurFromThisPopover = referenceElement && "contains" in referenceElement && referenceElement.contains(blurTarget) || floatingElement?.contains(blurTarget); if (!isBlurFromThisPopover) { return; } if (onFocusOutside) { onFocusOutside(event); } else if (onClose) { onClose(); } } else if (onClose) { onClose(); } }; } const [dialogRef, dialogProps] = useDialog({ constrainTabbing, focusOnMount, __unstableOnClose: onDialogClose, // @ts-expect-error The __unstableOnClose property needs to be deprecated first (see https://github.com/WordPress/gutenberg/pull/27675) onClose: onDialogClose }); const { // Positioning coordinates x, y, // Object with "regular" refs to both "reference" and "floating" refs, // Type of CSS position property to use (absolute or fixed) strategy, update, placement: computedPlacement, middlewareData: { arrow: arrowData } } = useFloating({ placement: normalizedPlacementFromProps === "overlay" ? void 0 : normalizedPlacementFromProps, middleware, whileElementsMounted: (referenceParam, floatingParam, updateParam) => autoUpdate(referenceParam, floatingParam, updateParam, { layoutShift: false, animationFrame: true }) }); const arrowCallbackRef = useCallback((node) => { arrowRef.current = node; update(); }, [update]); const anchorRefTop = anchorRef?.top; const anchorRefBottom = anchorRef?.bottom; const anchorRefStartContainer = anchorRef?.startContainer; const anchorRefCurrent = anchorRef?.current; useLayoutEffect(() => { const resultingReferenceElement = getReferenceElement({ anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement }); refs.setReference(resultingReferenceElement); }, [anchor, anchorRef, anchorRefTop, anchorRefBottom, anchorRefStartContainer, anchorRefCurrent, anchorRect, getAnchorRect, fallbackReferenceElement, refs]); const mergedFloatingRef = useMergeRefs([refs.setFloating, dialogRef, forwardedRef]); const style = isExpanded ? void 0 : { position: strategy, top: 0, left: 0, // `x` and `y` are framer-motion specific props and are shorthands // for `translateX` and `translateY`. Currently it is not possible // to use `translateX` and `translateY` because those values would // be overridden by the return value of the // `placementToMotionAnimationProps` function. x: computePopoverPosition(x), y: computePopoverPosition(y) }; const shouldReduceMotion = useReducedMotion(); const shouldAnimate = animate && !isExpanded && !shouldReduceMotion; const [animationFinished, setAnimationFinished] = useState(false); const { style: motionInlineStyles, ...otherMotionProps } = useMemo(() => placementToMotionAnimationProps(computedPlacement), [computedPlacement]); const animationProps = shouldAnimate ? { style: { ...contentStyle, ...motionInlineStyles, ...style }, onAnimationComplete: () => setAnimationFinished(true), ...otherMotionProps } : { animate: false, style: { ...contentStyle, ...style } }; const isPositioned = (!shouldAnimate || animationFinished) && x !== null && y !== null; let content = /* @__PURE__ */ _jsxs(motion.div, { className: clsx(className, { "is-expanded": isExpanded, "is-positioned": isPositioned, // Use the 'alternate' classname for 'toolbar' variant for back compat. [`is-${computedVariant === "toolbar" ? "alternate" : computedVariant}`]: computedVariant }), ...animationProps, ...contentProps, ref: mergedFloatingRef, ...dialogProps, tabIndex: -1, children: [isExpanded && /* @__PURE__ */ _jsx(ScrollLock, {}), isExpanded && /* @__PURE__ */ _jsxs("div", { className: "components-popover__header", children: [/* @__PURE__ */ _jsx("span", { className: "components-popover__header-title", children: headerTitle }), /* @__PURE__ */ _jsx(Button, { className: "components-popover__close", size: "small", icon: close, onClick: onClose, label: __("Close") })] }), /* @__PURE__ */ _jsx("div", { className: "components-popover__content", children }), hasArrow && /* @__PURE__ */ _jsx("div", { ref: arrowCallbackRef, className: ["components-popover__arrow", `is-${computedPlacement.split("-")[0]}`].join(" "), style: { left: typeof arrowData?.x !== "undefined" && Number.isFinite(arrowData.x) ? `${arrowData.x}px` : "", top: typeof arrowData?.y !== "undefined" && Number.isFinite(arrowData.y) ? `${arrowData.y}px` : "" }, children: /* @__PURE__ */ _jsx(ArrowTriangle, {}) })] }); const shouldRenderWithinSlot = slot.ref && !inline; const hasAnchor = anchorRef || anchorRect || anchor; if (shouldRenderWithinSlot) { content = /* @__PURE__ */ _jsx(Fill, { name: slotName, children: content }); } else if (!inline) { content = createPortal(/* @__PURE__ */ _jsx(StyleProvider, { document, children: content }), getPopoverFallbackContainer()); } if (hasAnchor) { return content; } return /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx("span", { ref: anchorRefFallback }), content] }); }; var PopoverSlot = forwardRef(({ name = SLOT_NAME }, ref) => { return /* @__PURE__ */ _jsx(Slot, { bubblesVirtually: true, name, className: "popover-slot", ref }); }); var Popover = Object.assign(contextConnect(UnforwardedPopover, "Popover"), { /** * Renders a slot that is used internally by Popover for rendering content. */ Slot: Object.assign(PopoverSlot, { displayName: "Popover.Slot" }), /** * Provides a context to manage popover slot names. * * This is marked as unstable and should not be used directly. */ __unstableSlotNameProvider: Object.assign(slotNameContext.Provider, { displayName: "Popover.__unstableSlotNameProvider" }) }); var popover_default = Popover; export { Popover, PopoverSlot, SLOT_NAME, popover_default as default }; //# sourceMappingURL=index.js.map