@wordpress/components
Version:
UI components for WordPress.
477 lines (413 loc) • 17.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.Popover = void 0;
var _element = require("@wordpress/element");
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _classnames = _interopRequireDefault(require("classnames"));
var _reactDom = require("@floating-ui/react-dom");
var _framerMotion = require("framer-motion");
var _compose = require("@wordpress/compose");
var _icons = require("@wordpress/icons");
var _deprecated = _interopRequireDefault(require("@wordpress/deprecated"));
var _primitives = require("@wordpress/primitives");
var _dom = require("@wordpress/dom");
var _button = _interopRequireDefault(require("../button"));
var _scrollLock = _interopRequireDefault(require("../scroll-lock"));
var _slotFill = require("../slot-fill");
var _utils = require("./utils");
var _limitShift = require("./limit-shift");
var _overlayMiddlewares = require("./overlay-middlewares");
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
/**
* Name of slot in which popover should fill.
*
* @type {string}
*/
const SLOT_NAME = 'Popover'; // 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 = () => (0, _element.createElement)(_primitives.SVG, {
xmlns: "http://www.w3.org/2000/svg",
viewBox: `0 0 100 100`,
className: "components-popover__triangle",
role: "presentation"
}, (0, _element.createElement)(_primitives.Path, {
className: "components-popover__triangle-bg",
d: "M 0 0 L 50 50 L 100 0"
}), (0, _element.createElement)(_primitives.Path, {
className: "components-popover__triangle-border",
d: "M 0 0 L 50 50 L 100 0",
vectorEffect: "non-scaling-stroke"
}));
const AnimatedWrapper = (0, _element.forwardRef)((_ref, forwardedRef) => {
let {
style: receivedInlineStyles,
placement,
shouldAnimate = false,
...props
} = _ref;
const shouldReduceMotion = (0, _framerMotion.useReducedMotion)();
const {
style: motionInlineStyles,
...otherMotionProps
} = (0, _element.useMemo)(() => (0, _utils.placementToMotionAnimationProps)(placement), [placement]);
const computedAnimationProps = shouldAnimate && !shouldReduceMotion ? {
style: { ...motionInlineStyles,
...receivedInlineStyles
},
...otherMotionProps
} : {
animate: false,
style: receivedInlineStyles
};
return (0, _element.createElement)(_framerMotion.motion.div, (0, _extends2.default)({}, computedAnimationProps, props, {
ref: forwardedRef
}));
});
const slotNameContext = (0, _element.createContext)(undefined);
const UnforwardedPopover = (props, forwardedRef) => {
var _frameOffsetRef$curre, _frameOffsetRef$curre2, _frameOffsetRef$curre3, _frameOffsetRef$curre4;
const {
animate = true,
headerTitle,
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,
variant,
// Deprecated props
__unstableForcePosition,
anchorRef,
anchorRect,
getAnchorRect,
isAlternate,
// Rest
...contentProps
} = props;
let computedFlipProp = flip;
let computedResizeProp = resize;
if (__unstableForcePosition !== undefined) {
(0, _deprecated.default)('`__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) {
(0, _deprecated.default)('`anchorRef` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
if (anchorRect !== undefined) {
(0, _deprecated.default)('`anchorRect` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
if (getAnchorRect !== undefined) {
(0, _deprecated.default)('`getAnchorRect` prop in wp.components.Popover', {
since: '6.1',
alternative: '`anchor` prop'
});
}
const computedVariant = isAlternate ? 'toolbar' : variant;
if (isAlternate !== undefined) {
(0, _deprecated.default)('`isAlternate` prop in wp.components.Popover', {
since: '6.2',
alternative: "`variant` prop with the `'toolbar'` value"
});
}
const arrowRef = (0, _element.useRef)(null);
const [fallbackReferenceElement, setFallbackReferenceElement] = (0, _element.useState)(null);
const [referenceOwnerDocument, setReferenceOwnerDocument] = (0, _element.useState)();
const anchorRefFallback = (0, _element.useCallback)(node => {
setFallbackReferenceElement(node);
}, []);
const isMobileViewport = (0, _compose.useViewportMatch)('medium', '<');
const isExpanded = expandOnMobile && isMobileViewport;
const hasArrow = !isExpanded && !noArrow;
const normalizedPlacementFromProps = position ? (0, _utils.positionToPlacement)(position) : placementProp;
/**
* Offsets the position of the popover when the anchor is inside an iframe.
*
* Store the offset in a ref, due to constraints with floating-ui:
* https://floating-ui.com/docs/react-dom#variables-inside-middleware-functions.
*/
const frameOffsetRef = (0, _element.useRef)((0, _utils.getFrameOffset)(referenceOwnerDocument));
const middleware = [...(placementProp === 'overlay' ? (0, _overlayMiddlewares.overlayMiddlewares)() : []), // Custom middleware which adjusts the popover's position by taking into
// account the offset of the anchor's iframe (if any) compared to the page.
{
name: 'frameOffset',
fn(_ref2) {
let {
x,
y
} = _ref2;
if (!frameOffsetRef.current) {
return {
x,
y
};
}
return {
x: x + frameOffsetRef.current.x,
y: y + frameOffsetRef.current.y,
data: {
// This will be used in the customLimitShift() function.
amount: frameOffsetRef.current
}
};
}
}, (0, _reactDom.offset)(offsetProp), computedFlipProp ? (0, _reactDom.flip)() : undefined, computedResizeProp ? (0, _reactDom.size)({
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: `${sizeProps.availableHeight}px`,
overflow: 'auto'
});
}
}) : undefined, shift ? (0, _reactDom.shift)({
crossAxis: true,
limiter: (0, _limitShift.limitShift)(),
padding: 1 // Necessary to avoid flickering at the edge of the viewport.
}) : undefined, (0, _reactDom.arrow)({
element: arrowRef
})].filter(m => m !== undefined);
const slotName = (0, _element.useContext)(slotNameContext) || __unstableSlotName;
const slot = (0, _slotFill.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] = (0, _compose.__experimentalUseDialog)({
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,
// Callback refs (not regular refs). This allows the position to be updated.
// when either elements change.
reference: referenceCallbackRef,
floating,
// 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
}
} = (0, _reactDom.useFloating)({
placement: normalizedPlacementFromProps === 'overlay' ? undefined : normalizedPlacementFromProps,
middleware,
whileElementsMounted: (referenceParam, floatingParam, updateParam) => (0, _reactDom.autoUpdate)(referenceParam, floatingParam, updateParam, {
animationFrame: true
})
});
const arrowCallbackRef = (0, _element.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 === null || anchorRef === void 0 ? void 0 : anchorRef.top;
const anchorRefBottom = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.bottom;
const anchorRefStartContainer = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.startContainer;
const anchorRefCurrent = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
(0, _element.useLayoutEffect)(() => {
const resultingReferenceOwnerDoc = (0, _utils.getReferenceOwnerDocument)({
anchor,
anchorRef,
anchorRect,
getAnchorRect,
fallbackReferenceElement,
fallbackDocument: document
});
const scale = (0, _utils.getFrameScale)(resultingReferenceOwnerDoc);
const resultingReferenceElement = (0, _utils.getReferenceElement)({
anchor,
anchorRef,
anchorRect,
getAnchorRect,
fallbackReferenceElement,
scale
});
referenceCallbackRef(resultingReferenceElement);
setReferenceOwnerDocument(resultingReferenceOwnerDoc);
}, [anchor, anchorRef, anchorRefTop, anchorRefBottom, anchorRefStartContainer, anchorRefCurrent, anchorRect, getAnchorRect, fallbackReferenceElement, referenceCallbackRef]); // If the reference element is in a different ownerDocument (e.g. iFrame),
// we need to manually update the floating's position as the reference's owner
// document scrolls. Also update the frame offset if the view resizes.
(0, _element.useLayoutEffect)(() => {
var _refs$floating$curren2, _referenceOwnerDocume;
if ( // Reference and root documents are the same.
referenceOwnerDocument === document || // Reference and floating are in the same document.
referenceOwnerDocument === ((_refs$floating$curren2 = refs.floating.current) === null || _refs$floating$curren2 === void 0 ? void 0 : _refs$floating$curren2.ownerDocument) || // The reference's document has no view (i.e. window)
// or frame element (ie. it's not an iframe).
!(referenceOwnerDocument !== null && referenceOwnerDocument !== void 0 && (_referenceOwnerDocume = referenceOwnerDocument.defaultView) !== null && _referenceOwnerDocume !== void 0 && _referenceOwnerDocume.frameElement)) {
frameOffsetRef.current = undefined;
return;
}
const {
defaultView
} = referenceOwnerDocument;
const {
frameElement
} = defaultView;
const scrollContainer = frameElement ? (0, _dom.getScrollContainer)(frameElement) : null;
const updateFrameOffset = () => {
frameOffsetRef.current = (0, _utils.getFrameOffset)(referenceOwnerDocument);
update();
};
defaultView.addEventListener('resize', updateFrameOffset);
scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.addEventListener('scroll', updateFrameOffset);
updateFrameOffset();
return () => {
defaultView.removeEventListener('resize', updateFrameOffset);
scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.removeEventListener('scroll', updateFrameOffset);
};
}, [referenceOwnerDocument, update, refs.floating]);
const mergedFloatingRef = (0, _compose.useMergeRefs)([floating, dialogRef, forwardedRef]); // Disable reason: We care to capture the _bubbled_ events from inputs
// within popover as inferring close intent.
let content = // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
(0, _element.createElement)(AnimatedWrapper, (0, _extends2.default)({
shouldAnimate: animate && !isExpanded,
placement: computedPlacement,
className: (0, _classnames.default)('components-popover', className, {
'is-expanded': isExpanded,
'is-positioned': x !== null && y !== null,
// Use the 'alternate' classname for 'toolbar' variant for back compat.
[`is-${computedVariant === 'toolbar' ? 'alternate' : computedVariant}`]: computedVariant
})
}, contentProps, {
ref: mergedFloatingRef
}, dialogProps, {
tabIndex: -1,
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 in `AnimatedWrapper`
x: Math.round(x !== null && x !== void 0 ? x : 0) || undefined,
y: Math.round(y !== null && y !== void 0 ? y : 0) || undefined
}
}), isExpanded && (0, _element.createElement)(_scrollLock.default, null), isExpanded && (0, _element.createElement)("div", {
className: "components-popover__header"
}, (0, _element.createElement)("span", {
className: "components-popover__header-title"
}, headerTitle), (0, _element.createElement)(_button.default, {
className: "components-popover__close",
icon: _icons.close,
onClick: onClose
})), (0, _element.createElement)("div", {
className: "components-popover__content"
}, children), hasArrow && (0, _element.createElement)("div", {
ref: arrowCallbackRef,
className: ['components-popover__arrow', `is-${computedPlacement.split('-')[0]}`].join(' '),
style: {
left: typeof (arrowData === null || arrowData === void 0 ? void 0 : arrowData.x) !== 'undefined' && Number.isFinite(arrowData.x) ? `${arrowData.x + ((_frameOffsetRef$curre = (_frameOffsetRef$curre2 = frameOffsetRef.current) === null || _frameOffsetRef$curre2 === void 0 ? void 0 : _frameOffsetRef$curre2.x) !== null && _frameOffsetRef$curre !== void 0 ? _frameOffsetRef$curre : 0)}px` : '',
top: typeof (arrowData === null || arrowData === void 0 ? void 0 : arrowData.y) !== 'undefined' && Number.isFinite(arrowData.y) ? `${arrowData.y + ((_frameOffsetRef$curre3 = (_frameOffsetRef$curre4 = frameOffsetRef.current) === null || _frameOffsetRef$curre4 === void 0 ? void 0 : _frameOffsetRef$curre4.y) !== null && _frameOffsetRef$curre3 !== void 0 ? _frameOffsetRef$curre3 : 0)}px` : ''
}
}, (0, _element.createElement)(ArrowTriangle, null)));
if (slot.ref) {
content = (0, _element.createElement)(_slotFill.Fill, {
name: slotName
}, content);
}
if (anchorRef || anchorRect || anchor) {
return content;
}
return (0, _element.createElement)("span", {
ref: anchorRefFallback
}, content);
};
/**
* `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>
* );
* };
* ```
*
*/
const Popover = (0, _element.forwardRef)(UnforwardedPopover);
exports.Popover = Popover;
function PopoverSlot(_ref3, ref) {
let {
name = SLOT_NAME
} = _ref3;
return (0, _element.createElement)(_slotFill.Slot // @ts-expect-error Need to type `SlotFill`
, {
bubblesVirtually: true,
name: name,
className: "popover-slot",
ref: ref
});
} // @ts-expect-error For Legacy Reasons
Popover.Slot = (0, _element.forwardRef)(PopoverSlot); // @ts-expect-error For Legacy Reasons
Popover.__unstableSlotNameProvider = slotNameContext.Provider;
var _default = Popover;
exports.default = _default;
//# sourceMappingURL=index.js.map