UNPKG

@wordpress/components

Version:
321 lines (305 loc) 10.3 kB
/** * External dependencies */ // eslint-disable-next-line no-restricted-imports import type { MotionProps } from 'framer-motion'; import type { ReferenceType } from '@floating-ui/react-dom'; /** * Internal dependencies */ import type { PopoverProps, PopoverAnchorRefReference, PopoverAnchorRefTopBottom, } from './types'; const POSITION_TO_PLACEMENT: Record< NonNullable< PopoverProps[ 'position' ] >, NonNullable< PopoverProps[ 'placement' ] > > = { bottom: 'bottom', top: 'top', 'middle left': 'left', 'middle right': 'right', 'bottom left': 'bottom-end', 'bottom center': 'bottom', 'bottom right': 'bottom-start', 'top left': 'top-end', 'top center': 'top', 'top right': 'top-start', 'middle left left': 'left', 'middle left right': 'left', 'middle left bottom': 'left-end', 'middle left top': 'left-start', 'middle right left': 'right', 'middle right right': 'right', 'middle right bottom': 'right-end', 'middle right top': 'right-start', 'bottom left left': 'bottom-end', 'bottom left right': 'bottom-end', 'bottom left bottom': 'bottom-end', 'bottom left top': 'bottom-end', 'bottom center left': 'bottom', 'bottom center right': 'bottom', 'bottom center bottom': 'bottom', 'bottom center top': 'bottom', 'bottom right left': 'bottom-start', 'bottom right right': 'bottom-start', 'bottom right bottom': 'bottom-start', 'bottom right top': 'bottom-start', 'top left left': 'top-end', 'top left right': 'top-end', 'top left bottom': 'top-end', 'top left top': 'top-end', 'top center left': 'top', 'top center right': 'top', 'top center bottom': 'top', 'top center top': 'top', 'top right left': 'top-start', 'top right right': 'top-start', 'top right bottom': 'top-start', 'top right top': 'top-start', // `middle`/`middle center [corner?]` positions are associated to a fallback // `bottom` placement because there aren't any corresponding placement values. middle: 'bottom', 'middle center': 'bottom', 'middle center bottom': 'bottom', 'middle center left': 'bottom', 'middle center right': 'bottom', 'middle center top': 'bottom', }; /** * Converts the `Popover`'s legacy "position" prop to the new "placement" prop * (used by `floating-ui`). * * @param position The legacy position * @return The corresponding placement */ export const positionToPlacement = ( position: NonNullable< PopoverProps[ 'position' ] > ): NonNullable< PopoverProps[ 'placement' ] > => POSITION_TO_PLACEMENT[ position ] ?? 'bottom'; /** * @typedef AnimationOrigin * @type {Object} * @property {number} originX A number between 0 and 1 (in CSS logical properties jargon, 0 is "start", 0.5 is "center", and 1 is "end") * @property {number} originY A number between 0 and 1 (0 is top, 0.5 is center, and 1 is bottom) */ const PLACEMENT_TO_ANIMATION_ORIGIN: Record< NonNullable< PopoverProps[ 'placement' ] >, { originX: number; originY: number } > = { top: { originX: 0.5, originY: 1 }, // open from bottom, center 'top-start': { originX: 0, originY: 1 }, // open from bottom, left 'top-end': { originX: 1, originY: 1 }, // open from bottom, right right: { originX: 0, originY: 0.5 }, // open from middle, left 'right-start': { originX: 0, originY: 0 }, // open from top, left 'right-end': { originX: 0, originY: 1 }, // open from bottom, left bottom: { originX: 0.5, originY: 0 }, // open from top, center 'bottom-start': { originX: 0, originY: 0 }, // open from top, left 'bottom-end': { originX: 1, originY: 0 }, // open from top, right left: { originX: 1, originY: 0.5 }, // open from middle, right 'left-start': { originX: 1, originY: 0 }, // open from top, right 'left-end': { originX: 1, originY: 1 }, // open from bottom, right overlay: { originX: 0.5, originY: 0.5 }, // open from center, center }; /** * Given the floating-ui `placement`, compute the framer-motion props for the * popover's entry animation. * * @param placement A placement string from floating ui * @return The object containing the motion props */ export const placementToMotionAnimationProps = ( placement: NonNullable< PopoverProps[ 'placement' ] > ): MotionProps => { const translateProp = placement.startsWith( 'top' ) || placement.startsWith( 'bottom' ) ? 'translateY' : 'translateX'; const translateDirection = placement.startsWith( 'top' ) || placement.startsWith( 'left' ) ? 1 : -1; return { style: PLACEMENT_TO_ANIMATION_ORIGIN[ placement ], initial: { opacity: 0, scale: 0, [ translateProp ]: `${ 2 * translateDirection }em`, }, animate: { opacity: 1, scale: 1, [ translateProp ]: 0 }, transition: { duration: 0.1, ease: [ 0, 0, 0.2, 1 ] }, }; }; /** * Returns the offset of a document's frame element. * * @param document The iframe's owner document. * * @return The offset of the document's frame element, or undefined if the * document has no frame element. */ export const getFrameOffset = ( document?: Document ): { x: number; y: number } | undefined => { const frameElement = document?.defaultView?.frameElement; if ( ! frameElement ) { return; } const iframeRect = frameElement.getBoundingClientRect(); return { x: iframeRect.left, y: iframeRect.top }; }; export const getFrameScale = ( document?: Document ): { x: number; y: number; } => { const frameElement = document?.defaultView?.frameElement as HTMLElement; if ( ! frameElement ) { return { x: 1, y: 1 }; } const rect = frameElement.getBoundingClientRect(); return { x: rect.width / frameElement.offsetWidth, y: rect.height / frameElement.offsetHeight, }; }; export const getReferenceOwnerDocument = ( { anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement, fallbackDocument, }: Pick< PopoverProps, 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' > & { fallbackReferenceElement: Element | null; fallbackDocument: Document; } ): Document => { // In floating-ui's terms: // - "reference" refers to the popover's anchor element. // - "floating" refers the floating popover's element. // A floating element can also be positioned relative to a virtual element, // instead of a real one. A virtual element is represented by an object // with the `getBoundingClientRect()` function (like real elements). // See https://floating-ui.com/docs/virtual-elements for more info. let resultingReferenceOwnerDoc; if ( anchor ) { resultingReferenceOwnerDoc = anchor.ownerDocument; } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { resultingReferenceOwnerDoc = ( anchorRef as PopoverAnchorRefTopBottom ) ?.top.ownerDocument; } else if ( ( anchorRef as Range | undefined )?.startContainer ) { resultingReferenceOwnerDoc = ( anchorRef as Range ).startContainer .ownerDocument; } else if ( ( anchorRef as PopoverAnchorRefReference | undefined )?.current ) { resultingReferenceOwnerDoc = ( ( anchorRef as PopoverAnchorRefReference ).current as Element ).ownerDocument; } else if ( anchorRef as Element | undefined ) { // This one should be deprecated. resultingReferenceOwnerDoc = ( anchorRef as Element ).ownerDocument; } else if ( anchorRect && anchorRect?.ownerDocument ) { resultingReferenceOwnerDoc = anchorRect.ownerDocument; } else if ( getAnchorRect ) { resultingReferenceOwnerDoc = getAnchorRect( fallbackReferenceElement )?.ownerDocument; } return resultingReferenceOwnerDoc ?? fallbackDocument; }; export const getReferenceElement = ( { anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement, scale, }: Pick< PopoverProps, 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' > & { fallbackReferenceElement: Element | null; scale: { x: number; y: number }; } ): ReferenceType | null => { let referenceElement = null; if ( anchor ) { referenceElement = anchor; } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { // Create a virtual element for the ref. The expectation is that // if anchorRef.top is defined, then anchorRef.bottom is defined too. // Seems to be used by the block toolbar, when multiple blocks are selected // (top and bottom blocks are used to calculate the resulting rect). referenceElement = { getBoundingClientRect() { const topRect = ( anchorRef as PopoverAnchorRefTopBottom ).top.getBoundingClientRect(); const bottomRect = ( anchorRef as PopoverAnchorRefTopBottom ).bottom.getBoundingClientRect(); return new window.DOMRect( topRect.x, topRect.y, topRect.width, bottomRect.bottom - topRect.top ); }, }; } else if ( ( anchorRef as PopoverAnchorRefReference | undefined )?.current ) { // Standard React ref. referenceElement = ( anchorRef as PopoverAnchorRefReference ).current; } else if ( anchorRef as Element | undefined ) { // If `anchorRef` holds directly the element's value (no `current` key) // This is a weird scenario and should be deprecated. referenceElement = anchorRef as Element; } else if ( anchorRect ) { // Create a virtual element for the ref. referenceElement = { getBoundingClientRect() { return anchorRect; }, }; } else if ( getAnchorRect ) { // Create a virtual element for the ref. referenceElement = { getBoundingClientRect() { const rect = getAnchorRect( fallbackReferenceElement ); return new window.DOMRect( rect.x ?? rect.left, rect.y ?? rect.top, rect.width ?? rect.right - rect.left, rect.height ?? rect.bottom - rect.top ); }, }; } else if ( fallbackReferenceElement ) { // If no explicit ref is passed via props, fall back to // anchoring to the popover's parent node. referenceElement = fallbackReferenceElement.parentElement; } if ( referenceElement && ( scale.x !== 1 || scale.y !== 1 ) ) { // If the popover is inside an iframe, the coordinates of the // reference element need to be scaled to match the iframe's scale. const rect = referenceElement.getBoundingClientRect(); referenceElement = { getBoundingClientRect() { return new window.DOMRect( rect.x * scale.x, rect.y * scale.y, rect.width * scale.x, rect.height * scale.y ); }, }; } // Convert any `undefined` value to `null`. return referenceElement ?? null; };