@wordpress/components
Version:
UI components for WordPress.
395 lines (381 loc) • 13.5 kB
JavaScript
/**
* External dependencies
*/
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';
/**
* WordPress dependencies
*/
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';
/**
* Internal dependencies
*/
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';
/**
* Name of slot in which popover should fill.
*
* @type {string}
*/
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
export const SLOT_NAME = 'Popover';
/**
* Virtual padding to account for overflow boundaries.
*
* @type {number}
*/
const OVERFLOW_PADDING = 8;
// An SVG displaying a triangle facing down, filled with a solid
// color and bordered in such a way to create an arrow-like effect.
// Keeping the SVG's viewbox squared simplify the arrow positioning
// calculations.
const 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"
})]
});
const slotNameContext = createContext(undefined);
const fallbackContainerClassname = 'components-popover__fallback-container';
const getPopoverFallbackContainer = () => {
let container = document.body.querySelector('.' + fallbackContainerClassname);
if (!container) {
container = document.createElement('div');
container.className = fallbackContainerClassname;
document.body.append(container);
}
return container;
};
const 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 !== undefined) {
deprecated('`__unstableForcePosition` prop in wp.components.Popover', {
since: '6.1',
version: '6.3',
alternative: '`flip={ false }` and `resize={ false }`'
});
// Back-compat, set the `flip` and `resize` props
// to `false` to replicate `__unstableForcePosition`.
computedFlipProp = !__unstableForcePosition;
computedResizeProp = !__unstableForcePosition;
}
if (anchorRef !== undefined) {
deprecated('`anchorRef` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
if (anchorRect !== undefined) {
deprecated('`anchorRect` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
if (getAnchorRect !== undefined) {
deprecated('`getAnchorRect` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
const computedVariant = isAlternate ? 'toolbar' : variant;
if (isAlternate !== undefined) {
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 : {};
// Only HTMLElement instances have the `style` property.
if (!(firstElementChild instanceof HTMLElement)) {
return;
}
// Reduce the height of the popover to the available space.
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) => {
// Ideally the popover should have just a single onClose prop and
// not three props that potentially do the same thing.
if (type === 'focus-outside' && onFocusOutside) {
onFocusOutside(event);
} 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' ? undefined : normalizedPlacementFromProps,
middleware,
whileElementsMounted: (referenceParam, floatingParam, updateParam) => autoUpdate(referenceParam, floatingParam, updateParam, {
layoutShift: false,
animationFrame: true
})
});
const arrowCallbackRef = useCallback(node => {
arrowRef.current = node;
update();
}, [update]);
// When any of the possible anchor "sources" change,
// recompute the reference element (real or virtual) and its owner document.
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 ? undefined : {
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
}
};
// When Floating UI has finished positioning and Framer Motion has finished animating
// the popover, add the `is-positioned` class to signal that all transitions have finished.
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: 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: document,
children: content
}), getPopoverFallbackContainer());
}
if (hasAnchor) {
return content;
}
return /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx("span", {
ref: anchorRefFallback
}), content]
});
};
// Export the PopoverSlot individually to allow typescript to pick the types up.
export const PopoverSlot = forwardRef(({
name = SLOT_NAME
}, ref) => {
return /*#__PURE__*/_jsx(Slot, {
bubblesVirtually: true,
name: name,
className: "popover-slot",
ref: ref
});
});
/**
* `Popover` renders its content in a floating modal. If no explicit anchor is passed via props, it anchors to its parent element by default.
*
* ```jsx
* import { Button, Popover } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyPopover = () => {
* const [ isVisible, setIsVisible ] = useState( false );
* const toggleVisible = () => {
* setIsVisible( ( state ) => ! state );
* };
*
* return (
* <Button variant="secondary" onClick={ toggleVisible }>
* Toggle Popover!
* { isVisible && <Popover>Popover is toggled!</Popover> }
* </Button>
* );
* };
* ```
*
*/
export const 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'
})
});
export default Popover;
//# sourceMappingURL=index.js.map