-
-
-
-+
-+ Code is Poetry
-+
-
-
{
- const baseFlyoutId = 'base-flyout';
- beforeEach( () => {
- render(
-
WordPress.org }
- visible
- >
- Code is Poetry
-
- );
- } );
-
- test( 'should render correctly', () => {
- const dialog = screen.getByRole( 'dialog' );
- expect( dialog.firstChild ).toMatchSnapshot();
- } );
-
- test( 'should render invisible', () => {
- const invisibleFlyoutTriggerContent =
- 'WordPress.org - invisible flyout';
- render(
-
{ invisibleFlyoutTriggerContent } }
- visible={ false }
- >
- Code is Poetry
-
- );
-
- const flyouts = screen.getAllByRole( 'dialog' );
- const trigger = screen.getByText( invisibleFlyoutTriggerContent );
- // Assert only the base flyout rendered.
- expect( flyouts ).toHaveLength( 1 );
- expect( flyouts[ 0 ].id ).toBe( baseFlyoutId );
- expect( trigger ).not.toBeUndefined();
- } );
-
- test( 'should render without trigger', () => {
- const triggerlessFlyoutId = 'triggerless-flyout';
- render(
-
- Code is Poetry
-
- );
- const flyouts = screen.getAllByRole( 'dialog' );
- const triggerlessFlyout = flyouts.find(
- ( p ) => p.id === triggerlessFlyoutId
- );
- expect( triggerlessFlyout ).not.toBeUndefined();
- } );
-
- test( 'should render without content', () => {
- const contentlessFlyoutId = 'contentless-flyout';
- render(
-
WordPress.org }
- visible
- />
- );
- const flyouts = screen.getAllByRole( 'dialog' );
- const contentlessFlyout = flyouts.find(
- ( p ) => p.id === contentlessFlyoutId
- );
- const baseFlyout = flyouts.find( ( p ) => p.id === baseFlyoutId );
- expect( contentlessFlyout ).toMatchDiffSnapshot( baseFlyout );
- } );
-
- test( 'should render label', () => {
- const labelledFlyoutId = 'labelled-flyout';
- render(
- WordPress.org }
- visible
- >
- Code is Poetry
-
- );
- const flyouts = screen.getAllByRole( 'dialog' );
- const labelledFlyout = flyouts.find(
- ( p ) => p.id === labelledFlyoutId
- );
- const baseFlyout = flyouts.find( ( p ) => p.id === baseFlyoutId );
- expect( labelledFlyout ).toMatchDiffSnapshot( baseFlyout );
- } );
-} );
diff --git a/packages/components/src/flyout/types.ts b/packages/components/src/flyout/types.ts
deleted file mode 100644
index 0a7a861e62580e..00000000000000
--- a/packages/components/src/flyout/types.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * External dependencies
- */
-// eslint-disable-next-line no-restricted-imports
-import type { PopoverStateReturn } from 'reakit';
-import type { CSSProperties, FunctionComponentElement } from 'react';
-
-/**
- * Internal dependencies
- */
-import type { PopperProps } from '../utils/types';
-
-export type Context = {
- flyoutState?: PopoverStateReturn;
- label?: string;
-};
-
-export type Props = PopperProps & {
- state?: PopoverStateReturn;
- label?: string;
- /**
- * Determines if `Flyout` has animations.
- *
- * @default true
- */
- animated?: boolean;
- /**
- * The duration of `Flyout` animations.
- *
- * @default 160
- */
- animationDuration?: boolean;
- /**
- * ID that will serve as a base for all the items IDs.
- *
- * @see https://reakit.io/docs/popover/#usepopoverstate
- */
- baseId?: string;
- /**
- * Renders `Elevation` styles for the `Flyout`.
- *
- * @default 5
- */
- elevation?: number;
- /**
- * Max-width for the `Flyout` element.
- *
- * @default 360
- */
- maxWidth?: CSSProperties[ 'maxWidth' ];
- /**
- * Callback for when the `visible` state changes.
- */
- onVisibleChange?: ( ...args: any ) => void;
- /**
- * Element that triggers the `visible` state of `Flyout` when clicked.
- *
- * @example
- * ```jsx
- * Greet}>
- * Hi! I'm Olaf!
- *
- * ```
- */
- trigger: FunctionComponentElement< any >;
- /**
- * Whether `Flyout` is visible.
- *
- * @default false
- *
- * @see https://reakit.io/docs/popover/#usepopoverstate
- */
- visible?: boolean;
- /**
- * The children elements.
- */
- children: React.ReactNode;
-};
-
-export type ContentProps = {
- elevation: number;
- maxWidth: CSSProperties[ 'maxWidth' ];
- children: React.ReactNode;
-};
diff --git a/packages/components/src/flyout/utils.js b/packages/components/src/flyout/utils.js
deleted file mode 100644
index 7258e8d72d8dc4..00000000000000
--- a/packages/components/src/flyout/utils.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- useIsomorphicLayoutEffect,
- useResizeObserver,
-} from '@wordpress/compose';
-
-/**
- *
- * @param { { onResize?: () => any } } onResize
- */
-export function useFlyoutResizeUpdater( { onResize } ) {
- const [ resizeListener, sizes ] = useResizeObserver();
-
- useIsomorphicLayoutEffect( () => {
- onResize?.();
- }, [ sizes.width, sizes.height ] );
-
- return resizeListener;
-}
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index 6b90c5a1142e31..0c142746ab2af5 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -71,7 +71,6 @@ export { DuotoneSwatch, DuotonePicker } from './duotone-picker';
export { Elevation as __experimentalElevation } from './elevation';
export { default as ExternalLink } from './external-link';
export { Flex, FlexBlock, FlexItem } from './flex';
-export { Flyout as __experimentalFlyout } from './flyout';
export { default as FocalPointPicker } from './focal-point-picker';
export { default as FocusableIframe } from './focusable-iframe';
export { default as FontSizePicker } from './font-size-picker';
diff --git a/packages/components/src/navigator/stories/index.js b/packages/components/src/navigator/stories/index.js
index bb6cb6bab671bc..e8078ba74b32e5 100644
--- a/packages/components/src/navigator/stories/index.js
+++ b/packages/components/src/navigator/stories/index.js
@@ -9,7 +9,7 @@ import { css } from '@emotion/react';
import Button from '../../button';
import { Card, CardBody, CardFooter, CardHeader } from '../../card';
import { HStack } from '../../h-stack';
-import { Flyout } from '../../flyout';
+import { Popover } from '../../popover';
import { useCx } from '../../utils/hooks/use-cx';
import {
NavigatorProvider,
@@ -54,17 +54,13 @@ const MyNavigation = () => {
Navigate to screen with sticky content.
-
- Open test dialog
-
- }
- placement="bottom-start"
- >
- Go
- Stuff
-
+
diff --git a/packages/components/src/palette-edit/index.js b/packages/components/src/palette-edit/index.js
index b198fc3fb404b1..25411ba93adb5e 100644
--- a/packages/components/src/palette-edit/index.js
+++ b/packages/components/src/palette-edit/index.js
@@ -156,7 +156,8 @@ function Option( {
{ isEditing && (
{ ! isGradient && (
diff --git a/packages/components/src/palette-edit/style.scss b/packages/components/src/palette-edit/style.scss
index ef78dd30fe9252..d23c88c9e182fe 100644
--- a/packages/components/src/palette-edit/style.scss
+++ b/packages/components/src/palette-edit/style.scss
@@ -1,10 +1,3 @@
-@include break-medium() {
- .components-palette-edit__popover.components-popover .components-popover__content.components-popover__content.components-popover__content {
- margin-right: #{ math.div($sidebar-width, 2) + $grid-unit-20 };
- margin-top: #{ -($grid-unit-60 + $border-width) };
- }
-}
-
.components-palette-edit__popover {
.components-custom-gradient-picker__gradient-bar {
margin-top: 0;
diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md
index f39e5f79e4e3b7..3e8a5e2cf28550 100644
--- a/packages/components/src/popover/README.md
+++ b/packages/components/src/popover/README.md
@@ -59,13 +59,17 @@ Set this prop to `false` to disable focus changing entirely. This should only be
- Required: No
- Default: `"firstElement"`
-### position
+### placement
-The direction in which the popover should open relative to its parent node. Specify y- and x-axis as a space-separated string. Supports `"top"`, `"middle"`, `"bottom"` y axis, and `"left"`, `"center"`, `"right"` x axis.
+The direction in which the popover should open relative to its parent node or anchor node.
+
+The available base placements are 'top', 'right', 'bottom', 'left'.
+
+Each of these base placements has an alignment in the form -start and -end. For example, 'right-start', or 'bottom-end'. These allow you to align the tooltip to the edges of the button, rather than centering it.
- Type: `String`
- Required: No
-- Default: `"bottom right"`
+- Default: `"bottom-start"`
### children
@@ -137,9 +141,3 @@ If you need the `DOMRect` object i.e., the position of popover to be calculated
- Type: `Function`
- Required: No
-
-## Methods
-
-### refresh
-
-Calling `refresh()` will force the Popover to recalculate its size and position. This is useful when a DOM change causes the anchor node to change position.
diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js
index 042d3a4a20bfa3..b24013bc7c45ee 100644
--- a/packages/components/src/popover/index.js
+++ b/packages/components/src/popover/index.js
@@ -3,31 +3,39 @@
* External dependencies
*/
import classnames from 'classnames';
+import {
+ useFloating,
+ flip,
+ shift,
+ autoUpdate,
+ arrow,
+ offset as offsetMiddleware,
+ limitShift,
+ size,
+} from '@floating-ui/react-dom';
/**
* WordPress dependencies
*/
import {
useRef,
- useState,
useLayoutEffect,
forwardRef,
createContext,
useContext,
+ useMemo,
} from '@wordpress/element';
-import { getRectangleFromRange } from '@wordpress/dom';
import {
useViewportMatch,
- useResizeObserver,
useMergeRefs,
__experimentalUseDialog as useDialog,
} from '@wordpress/compose';
import { close } from '@wordpress/icons';
+import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
-import { computePopoverPosition, offsetIframe } from './utils';
import Button from '../button';
import ScrollLock from '../scroll-lock';
import { Slot, Fill, useSlot } from '../slot-fill';
@@ -42,405 +50,156 @@ const SLOT_NAME = 'Popover';
const slotNameContext = createContext();
-function computeAnchorRect(
- anchorRefFallback,
- anchorRect,
- getAnchorRect,
- anchorRef = false,
- container
-) {
- if ( anchorRect ) {
- return anchorRect;
- }
-
- if ( getAnchorRect ) {
- if ( ! anchorRefFallback.current ) {
- return;
- }
-
- const rect = getAnchorRect( anchorRefFallback.current );
- return offsetIframe(
- rect,
- rect.ownerDocument || anchorRefFallback.current.ownerDocument,
- container
- );
- }
+const positionToPlacement = ( position ) => {
+ const [ x, y, z ] = position.split( ' ' );
- if ( anchorRef !== false ) {
- if (
- ! anchorRef ||
- ! window.Range ||
- ! window.Element ||
- ! window.DOMRect
- ) {
- return;
+ if ( [ 'top', 'bottom' ].includes( x ) ) {
+ let suffix = '';
+ if ( ( !! z && z === 'left' ) || y === 'right' ) {
+ suffix = '-start';
+ } else if ( ( !! z && z === 'right' ) || y === 'left' ) {
+ suffix = '-end';
}
-
- // Duck-type to check if `anchorRef` is an instance of Range
- // `anchorRef instanceof window.Range` checks will break across document boundaries
- // such as in an iframe.
- if ( typeof anchorRef?.cloneRange === 'function' ) {
- return offsetIframe(
- getRectangleFromRange( anchorRef ),
- anchorRef.endContainer.ownerDocument,
- container
- );
- }
-
- // Duck-type to check if `anchorRef` is an instance of Element
- // `anchorRef instanceof window.Element` checks will break across document boundaries
- // such as in an iframe.
- if ( typeof anchorRef?.getBoundingClientRect === 'function' ) {
- const rect = offsetIframe(
- anchorRef.getBoundingClientRect(),
- anchorRef.ownerDocument,
- container
- );
-
- return rect;
- }
-
- const { top, bottom } = anchorRef;
- const topRect = top.getBoundingClientRect();
- const bottomRect = bottom.getBoundingClientRect();
-
- return offsetIframe(
- new window.DOMRect(
- topRect.left,
- topRect.top,
- topRect.width,
- bottomRect.bottom - topRect.top
- ),
- top.ownerDocument,
- container
- );
- }
-
- if ( ! anchorRefFallback.current ) {
- return;
+ return x + suffix;
}
- const { parentNode } = anchorRefFallback.current;
-
- return offsetIframe(
- parentNode.getBoundingClientRect(),
- parentNode.ownerDocument,
- container
- );
-}
+ return y;
+};
-/**
- * Sets or removes an element attribute.
- *
- * @param {Element} element The element to modify.
- * @param {string} name The attribute name to set or remove.
- * @param {?string} value The value to set. A falsy value will remove the
- * attribute.
- */
-function setAttribute( element, name, value ) {
- if ( ! value ) {
- if ( element.hasAttribute( name ) ) {
- element.removeAttribute( name );
+const placementToAnimationOrigin = ( placement ) => {
+ const [ a, b ] = placement.split( '-' );
+
+ let x, y;
+ if ( a === 'top' || a === 'bottom' ) {
+ x = a === 'top' ? 'bottom' : 'top';
+ y = 'middle';
+ if ( b === 'start' ) {
+ y = 'left';
+ } else if ( b === 'end' ) {
+ y = 'right';
}
- } else if ( element.getAttribute( name ) !== value ) {
- element.setAttribute( name, value );
}
-}
-
-/**
- * Sets or removes an element style property.
- *
- * @param {Element} element The element to modify.
- * @param {string} property The property to set or remove.
- * @param {?string} value The value to set. A falsy value will remove the
- * property.
- */
-function setStyle( element, property, value = '' ) {
- if ( element.style[ property ] !== value ) {
- element.style[ property ] = value;
- }
-}
-/**
- * Sets or removes an element class.
- *
- * @param {Element} element The element to modify.
- * @param {string} name The class to set or remove.
- * @param {boolean} toggle True to set the class, false to remove.
- */
-function setClass( element, name, toggle ) {
- if ( toggle ) {
- if ( ! element.classList.contains( name ) ) {
- element.classList.add( name );
+ if ( a === 'left' || a === 'right' ) {
+ x = 'center';
+ y = a === 'left' ? 'right' : 'left';
+ if ( b === 'start' ) {
+ x = 'top';
+ } else if ( b === 'end' ) {
+ x = 'bottom';
}
- } else if ( element.classList.contains( name ) ) {
- element.classList.remove( name );
- }
-}
-
-function getAnchorDocument( anchor ) {
- if ( ! anchor ) {
- return;
- }
-
- if ( anchor.endContainer ) {
- return anchor.endContainer.ownerDocument;
}
- if ( anchor.top ) {
- return anchor.top.ownerDocument;
- }
-
- return anchor.ownerDocument;
-}
+ return x + ' ' + y;
+};
const Popover = (
{
+ range,
+ animate = true,
headerTitle,
onClose,
children,
className,
noArrow = true,
isAlternate,
- // Disable reason: We generate the `...contentProps` rest as remainder
- // of props which aren't explicitly handled by this component.
- /* eslint-disable no-unused-vars */
- position = 'bottom right',
- range,
+ position,
+ placement = 'bottom-start',
+ offset,
focusOnMount = 'firstElement',
anchorRef,
anchorRect,
getAnchorRect,
expandOnMobile,
- animate = true,
onFocusOutside,
- __unstableStickyBoundaryElement,
__unstableSlotName = SLOT_NAME,
__unstableObserveElement,
- __unstableBoundaryParent,
__unstableForcePosition,
- __unstableForceXAlignment,
- __unstableEditorCanvasWrapper,
- /* eslint-enable no-unused-vars */
...contentProps
},
ref
) => {
- const anchorRefFallback = useRef( null );
- const contentRef = useRef( null );
- const containerRef = useRef();
+ if ( range ) {
+ deprecated( 'range prop in Popover component', {
+ since: '6.1',
+ version: '6.3',
+ } );
+ }
+
+ const arrowRef = useRef( null );
const isMobileViewport = useViewportMatch( 'medium', '<' );
- const [ animateOrigin, setAnimateOrigin ] = useState();
- const slotName = useContext( slotNameContext ) || __unstableSlotName;
- const slot = useSlot( slotName );
const isExpanded = expandOnMobile && isMobileViewport;
- const [ containerResizeListener, contentSize ] = useResizeObserver();
- noArrow = isExpanded || noArrow;
-
- useLayoutEffect( () => {
- if ( isExpanded ) {
- setClass( containerRef.current, 'is-without-arrow', noArrow );
- setClass( containerRef.current, 'is-alternate', isAlternate );
- setAttribute( containerRef.current, 'data-x-axis' );
- setAttribute( containerRef.current, 'data-y-axis' );
- setStyle( containerRef.current, 'top' );
- setStyle( containerRef.current, 'left' );
- setStyle( contentRef.current, 'maxHeight' );
- setStyle( contentRef.current, 'maxWidth' );
- return;
+ const hasArrow = ! isExpanded && ! noArrow;
+ const usedPlacement = position
+ ? positionToPlacement( position )
+ : placement;
+
+ /**
+ * Offsets the the position of the popover when the anchor is inside an iframe.
+ */
+ const frameOffset = useMemo( () => {
+ let ownerDocument = document;
+ if ( anchorRef?.top ) {
+ ownerDocument = anchorRef?.top.ownerDocument;
+ } else if ( anchorRef?.startContainer ) {
+ ownerDocument = anchorRef.startContainer.ownerDocument;
+ } else if ( anchorRef?.current ) {
+ ownerDocument = anchorRef.current.ownerDocument;
+ } else if ( anchorRef ) {
+ // This one should be deprecated.
+ ownerDocument = anchorRef.ownerDocument;
+ } else if ( anchorRect && anchorRect?.ownerDocument ) {
+ ownerDocument = anchorRect.ownerDocument;
+ } else if ( getAnchorRect ) {
+ ownerDocument = getAnchorRect()?.ownerDocument ?? document;
}
- const refresh = () => {
- if ( ! containerRef.current || ! contentRef.current ) {
- return;
- }
-
- let anchor = computeAnchorRect(
- anchorRefFallback,
- anchorRect,
- getAnchorRect,
- anchorRef,
- containerRef.current
- );
-
- if ( ! anchor ) {
- return;
- }
-
- const { offsetParent, ownerDocument } = containerRef.current;
-
- let relativeOffsetTop = 0;
-
- // If there is a positioned ancestor element that is not the body,
- // subtract the position from the anchor rect. If the position of
- // the popover is fixed, the offset parent is null or the body
- // element, in which case the position is relative to the viewport.
- // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
- if ( offsetParent && offsetParent !== ownerDocument.body ) {
- const offsetParentRect = offsetParent.getBoundingClientRect();
-
- relativeOffsetTop = offsetParentRect.top;
- anchor = new window.DOMRect(
- anchor.left - offsetParentRect.left,
- anchor.top - offsetParentRect.top,
- anchor.width,
- anchor.height
- );
- }
-
- let boundaryElement;
- if ( __unstableBoundaryParent ) {
- boundaryElement = containerRef.current.parentElement;
- }
-
- const usedContentSize = ! contentSize.height
- ? contentRef.current.getBoundingClientRect()
- : contentSize;
-
- const {
- popoverTop,
- popoverLeft,
- xAxis,
- yAxis,
- contentHeight,
- contentWidth,
- } = computePopoverPosition(
- anchor,
- usedContentSize,
- position,
- __unstableStickyBoundaryElement,
- containerRef.current,
- relativeOffsetTop,
- boundaryElement,
- __unstableForcePosition,
- __unstableForceXAlignment,
- __unstableEditorCanvasWrapper
- );
-
- if (
- typeof popoverTop === 'number' &&
- typeof popoverLeft === 'number'
- ) {
- setStyle( containerRef.current, 'top', popoverTop + 'px' );
- setStyle( containerRef.current, 'left', popoverLeft + 'px' );
- }
-
- setClass(
- containerRef.current,
- 'is-without-arrow',
- noArrow || ( xAxis === 'center' && yAxis === 'middle' )
- );
- setClass( containerRef.current, 'is-alternate', isAlternate );
- setAttribute( containerRef.current, 'data-x-axis', xAxis );
- setAttribute( containerRef.current, 'data-y-axis', yAxis );
- setStyle(
- contentRef.current,
- 'maxHeight',
- typeof contentHeight === 'number' ? contentHeight + 'px' : ''
- );
- setStyle(
- contentRef.current,
- 'maxWidth',
- typeof contentWidth === 'number' ? contentWidth + 'px' : ''
- );
-
- // Compute the animation position.
- const yAxisMapping = {
- top: 'bottom',
- bottom: 'top',
- };
- const xAxisMapping = {
- left: 'right',
- right: 'left',
- };
- const animateYAxis = yAxisMapping[ yAxis ] || 'middle';
- const animateXAxis = xAxisMapping[ xAxis ] || 'center';
-
- setAnimateOrigin( animateXAxis + ' ' + animateYAxis );
- };
-
- refresh();
-
- const { ownerDocument } = containerRef.current;
const { defaultView } = ownerDocument;
+ const { frameElement } = defaultView;
- /*
- * There are sometimes we need to reposition or resize the popover that
- * are not handled by the resize/scroll window events (i.e. CSS changes
- * in the layout that changes the position of the anchor).
- *
- * For these situations, we refresh the popover every 0.5s
- */
- const intervalHandle = defaultView.setInterval( refresh, 500 );
-
- let rafId;
-
- const refreshOnAnimationFrame = () => {
- defaultView.cancelAnimationFrame( rafId );
- rafId = defaultView.requestAnimationFrame( refresh );
- };
-
- // Sometimes a click trigger a layout change that affects the popover
- // position. This is an opportunity to immediately refresh rather than
- // at the interval.
- defaultView.addEventListener( 'click', refreshOnAnimationFrame );
- defaultView.addEventListener( 'resize', refresh );
- defaultView.addEventListener( 'scroll', refresh, true );
-
- const anchorDocument = getAnchorDocument( anchorRef );
-
- // If the anchor is within an iframe, the popover position also needs
- // to refrest when the iframe content is scrolled or resized.
- if ( anchorDocument && anchorDocument !== ownerDocument ) {
- anchorDocument.defaultView.addEventListener( 'resize', refresh );
- anchorDocument.defaultView.addEventListener(
- 'scroll',
- refresh,
- true
- );
+ if ( ! frameElement || ownerDocument === document ) {
+ return undefined;
}
- let observer;
-
- if ( __unstableObserveElement ) {
- observer = new defaultView.MutationObserver( refresh );
- observer.observe( __unstableObserveElement, { attributes: true } );
- }
-
- return () => {
- defaultView.clearInterval( intervalHandle );
- defaultView.removeEventListener( 'resize', refresh );
- defaultView.removeEventListener( 'scroll', refresh, true );
- defaultView.removeEventListener( 'click', refreshOnAnimationFrame );
- defaultView.cancelAnimationFrame( rafId );
-
- if ( anchorDocument && anchorDocument !== ownerDocument ) {
- anchorDocument.defaultView?.removeEventListener(
- 'resize',
- refresh
- );
- anchorDocument.defaultView?.removeEventListener(
- 'scroll',
- refresh,
- true
- );
- }
-
- if ( observer ) {
- observer.disconnect();
- }
+ const iframeRect = frameElement.getBoundingClientRect();
+ return {
+ name: 'iframeOffset',
+ fn( { x, y } ) {
+ return {
+ x: x + iframeRect.left,
+ y: y + iframeRect.top,
+ };
+ },
};
- }, [
- isExpanded,
- anchorRect,
- getAnchorRect,
- anchorRef,
- position,
- contentSize,
- __unstableStickyBoundaryElement,
- __unstableObserveElement,
- __unstableBoundaryParent,
- ] );
+ }, [ anchorRef, anchorRect, getAnchorRect ] );
+
+ const middlewares = [
+ frameOffset,
+ offset ? offsetMiddleware( offset ) : undefined,
+ __unstableForcePosition ? undefined : flip(),
+ __unstableForcePosition
+ ? undefined
+ : size( {
+ apply( { width, height } ) {
+ if ( ! refs.floating.current ) return;
+
+ Object.assign( refs.floating.current.firstChild.style, {
+ maxWidth: `${ width }px`,
+ maxHeight: `${ height }px`,
+ overflow: 'auto',
+ } );
+ },
+ } ),
+ ,
+ shift( {
+ crossAxis: true,
+ limiter: limitShift(),
+ } ),
+ hasArrow ? arrow( { element: arrowRef } ) : undefined,
+ ].filter( ( m ) => !! m );
+ const anchorRefFallback = useRef( null );
+ const slotName = useContext( slotNameContext ) || __unstableSlotName;
+ const slot = useSlot( slotName );
const onDialogClose = ( type, event ) => {
// Ideally the popover should have just a single onClose prop and
@@ -458,14 +217,104 @@ const Popover = (
onClose: onDialogClose,
} );
- const mergedRefs = useMergeRefs( [ containerRef, dialogRef, ref ] );
+ const {
+ x,
+ y,
+ reference,
+ floating,
+ strategy,
+ refs,
+ update,
+ placement: placementData,
+ middlewareData: { arrow: arrowData = {} },
+ } = useFloating( {
+ placement: usedPlacement,
+ middleware: middlewares,
+ } );
+ const staticSide = {
+ top: 'bottom',
+ right: 'left',
+ bottom: 'top',
+ left: 'right',
+ }[ placementData.split( '-' )[ 0 ] ];
+ const mergedRefs = useMergeRefs( [ floating, dialogRef, ref ] );
+
+ // Updates references
+ useLayoutEffect( () => {
+ // No ref or position have been passed
+ let usedRef;
+ if ( anchorRef?.top ) {
+ usedRef = {
+ getBoundingClientRect() {
+ const topRect = anchorRef.top.getBoundingClientRect();
+ const bottomRect = anchorRef.bottom.getBoundingClientRect();
+ return new window.DOMRect(
+ topRect.x,
+ topRect.y,
+ topRect.width,
+ bottomRect.bottom - topRect.top
+ );
+ },
+ };
+ } else if ( anchorRef?.current ) {
+ usedRef = anchorRef.current;
+ } else if ( anchorRef ) {
+ usedRef = anchorRef;
+ } else if ( anchorRect ) {
+ usedRef = {
+ getBoundingClientRect() {
+ return anchorRect;
+ },
+ };
+ } else if ( getAnchorRect ) {
+ usedRef = {
+ getBoundingClientRect() {
+ const rect = getAnchorRect();
+ return {
+ ...rect,
+ x: rect.x ?? rect.left,
+ y: rect.y ?? rect.top,
+ height: rect.height ?? rect.bottom - rect.top,
+ width: rect.width ?? rect.right - rect.left,
+ };
+ },
+ };
+ } else if ( anchorRefFallback.current ) {
+ usedRef = anchorRefFallback.current;
+ }
+
+ if ( ! usedRef ) {
+ return;
+ }
+
+ reference( usedRef );
+
+ if ( ! refs.floating.current ) {
+ return;
+ }
+
+ return autoUpdate( usedRef, refs.floating.current, update );
+ }, [ anchorRef, anchorRect, getAnchorRect ] );
+
+ // This is only needed for a smoth transition when moving blocks.
+ useLayoutEffect( () => {
+ if ( ! __unstableObserveElement ) {
+ return;
+ }
+ const observer = new window.MutationObserver( update );
+ observer.observe( __unstableObserveElement, { attributes: true } );
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [ __unstableObserveElement ] );
/** @type {false | string} */
const animateClassName =
- Boolean( animate && animateOrigin ) &&
+ !! animate &&
getAnimateClassName( {
type: 'appear',
- origin: animateOrigin,
+ origin: placementToAnimationOrigin( placementData ),
} );
// Disable reason: We care to capture the _bubbled_ events from inputs
@@ -481,7 +330,6 @@ const Popover = (
animateClassName,
{
'is-expanded': isExpanded,
- 'is-without-arrow': noArrow,
'is-alternate': isAlternate,
}
) }
@@ -489,6 +337,15 @@ const Popover = (
ref={ mergedRefs }
{ ...dialogProps }
tabIndex="-1"
+ style={
+ isExpanded
+ ? undefined
+ : {
+ position: strategy,
+ left: Number.isNaN( x ) ? 0 : x,
+ top: Number.isNaN( y ) ? 0 : y,
+ }
+ }
>
{ isExpanded && }
{ isExpanded && (
@@ -503,12 +360,26 @@ const Popover = (
/>
) }
-
-
- { containerResizeListener }
- { children }
-
-
+
{ children }
+ { hasArrow && (
+
+ ) }
);
diff --git a/packages/components/src/popover/style.scss b/packages/components/src/popover/style.scss
index 44446118161c6e..11697fedc2e6ad 100644
--- a/packages/components/src/popover/style.scss
+++ b/packages/components/src/popover/style.scss
@@ -1,222 +1,41 @@
-$arrow-size: 8px;
-
-/*!rtl:begin:ignore*/
.components-popover {
- position: fixed;
z-index: z-index(".components-popover");
- top: 0;
- left: 0;
-
- // Hide the popover element until the position has been calculated. The position
- // cannot be calculated until the popover element is rendered because the
- // position depends on the size of the popover.
- opacity: 0;
-
- &.is-expanded,
- &[data-x-axis][data-y-axis] {
- opacity: 1;
- }
&.is-expanded {
+ position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: z-index(".components-popover") !important;
}
-
- &:not(.is-without-arrow) {
- margin-left: 2px;
-
- &::before {
- border: $arrow-size solid $gray-400;
- }
-
- &.is-alternate::before {
- border-color: $gray-900;
- }
-
- &::after {
- border: $arrow-size solid $white;
- }
-
- &::before,
- &::after {
- content: "";
- position: absolute;
- height: 0;
- width: 0;
- line-height: 0;
- }
-
- &[data-y-axis="top"] {
- margin-top: - $arrow-size;
-
- &::before {
- bottom: - $arrow-size;
- }
-
- &::after {
- bottom: -6px;
- }
-
- &::before,
- &::after {
- border-bottom: none;
- border-left-color: transparent;
- border-right-color: transparent;
- border-top-style: solid;
- margin-left: -10px;
- }
- }
-
- &[data-y-axis="bottom"] {
- margin-top: $arrow-size;
-
- &::before {
- top: -$arrow-size;
- }
-
- &::after {
- top: -6px;
- }
-
- &::before,
- &::after {
- border-bottom-style: solid;
- border-left-color: transparent;
- border-right-color: transparent;
- border-top: none;
- margin-left: -10px;
- }
- }
-
- &[data-y-axis="middle"][data-x-axis="left"] {
- margin-left: -$arrow-size;
-
- &::before {
- right: -$arrow-size;
- }
-
- &::after {
- right: -6px;
- }
-
- &::before,
- &::after {
- border-bottom-color: transparent;
- border-left-style: solid;
- border-right: none;
- border-top-color: transparent;
- }
- }
-
- &[data-y-axis="middle"][data-x-axis="right"] {
- margin-left: $arrow-size;
-
- &::before {
- left: -$arrow-size;
- }
-
- &::after {
- left: -6px;
- }
-
- &::before,
- &::after {
- border-bottom-color: transparent;
- border-left: none;
- border-right-style: solid;
- border-top-color: transparent;
- }
- }
- }
-
- &[data-y-axis="top"] {
- bottom: 100%;
- }
-
- &[data-y-axis="bottom"] {
- top: 100%;
- }
-
- &[data-y-axis="middle"] {
- align-items: center;
- display: flex;
- }
-
- // Add spacing.
- &.is-from-top {
- margin-top: $grid-unit-15;
- }
-
- &.is-from-bottom {
- margin-top: -$grid-unit-15;
- }
-
- &.is-from-left:not(.is-from-top):not(.is-from-bottom) {
- margin-left: $grid-unit-15;
- }
-
- &.is-from-right:not(.is-from-top):not(.is-from-bottom) {
- margin-right: $grid-unit-15;
- }
}
.components-popover__content {
- height: 100%;
background: $white;
- border: $border-width solid $gray-400;
+ // Using outline instead of border to avoid impacting
+ // popover computations.
+ outline: $border-width solid $gray-400;
box-shadow: $shadow-popover;
border-radius: $radius-block-ui;
+ box-sizing: border-box;
+ width: min-content;
// Alternate treatment for popovers that put them at elevation zero with high contrast.
.is-alternate & {
- border: $border-width solid $gray-900;
+ outline: $border-width solid $gray-900;
box-shadow: none;
}
- .components-popover & {
- position: absolute;
- height: auto;
- overflow-y: auto;
- }
-
.components-popover.is-expanded & {
position: static;
height: calc(100% - #{ $panel-header-height });
overflow-y: visible;
min-width: auto;
border: none;
+ outline: none;
border-top: $border-width solid $gray-900;
}
-
- .components-popover[data-y-axis="top"] & {
- bottom: 100%;
- }
-
- .components-popover[data-x-axis="center"] & {
- left: 50%;
- transform: translateX(-50%);
- }
-
- .components-popover[data-x-axis="right"] & {
- position: absolute;
- left: 100%;
- }
-
- .components-popover:not([data-y-axis="middle"])[data-x-axis="right"] & {
- margin-left: -($grid-unit-30 + $border-width);
- }
-
- .components-popover[data-x-axis="left"] & {
- position: absolute;
- right: 100%;
- }
-
- .components-popover:not([data-y-axis="middle"])[data-x-axis="left"] & {
- margin-right: -($grid-unit-30 + $border-width);
- }
}
.components-popover__header {
@@ -238,4 +57,15 @@ $arrow-size: 8px;
.components-popover__close.components-button {
z-index: z-index(".components-popover__close");
}
-/*!rtl:end:ignore*/
+
+.components-popover__arrow {
+ position: absolute;
+ background: $gray-400;
+ width: 8px;
+ height: 8px;
+ transform: rotate(45deg);
+ z-index: -1;
+ .is-alternate & {
+ background: $gray-900;
+ }
+}
diff --git a/packages/components/src/popover/test/__snapshots__/index.js.snap b/packages/components/src/popover/test/__snapshots__/index.js.snap
index 2c7663281a154e..d985500486625f 100644
--- a/packages/components/src/popover/test/__snapshots__/index.js.snap
+++ b/packages/components/src/popover/test/__snapshots__/index.js.snap
@@ -3,25 +3,15 @@
exports[`Popover should pass additional props to portaled element 1`] = `
@@ -30,24 +20,14 @@ exports[`Popover should pass additional props to portaled element 1`] = `
exports[`Popover should render content 1`] = `
diff --git a/packages/components/src/popover/test/utils.js b/packages/components/src/popover/test/utils.js
deleted file mode 100644
index 3d6b3a8358db65..00000000000000
--- a/packages/components/src/popover/test/utils.js
+++ /dev/null
@@ -1,304 +0,0 @@
-/**
- * Internal dependencies
- */
-import {
- computePopoverPosition,
- computePopoverYAxisPosition,
- computePopoverXAxisPosition,
- offsetIframe,
-} from '../utils';
-
-describe( 'computePopoverYAxisPosition', () => {
- it( 'should leave the position as is there’s enought space', () => {
- const anchorRect = {
- top: 10,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverYAxisPosition( anchorRect, contentSize, 'bottom' )
- ).toEqual( {
- contentHeight: null,
- popoverTop: 30,
- yAxis: 'bottom',
- } );
- } );
-
- it( "should switch to bottom position if there's not enough space", () => {
- const anchorRect = {
- top: 10,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverYAxisPosition( anchorRect, contentSize, 'top' )
- ).toEqual( {
- contentHeight: null,
- popoverTop: 30,
- yAxis: 'bottom',
- } );
- } );
-
- it( "should set a maxHeight if there's not enough space in any direction", () => {
- const anchorRect = {
- top: 400,
- left: 10,
- bottom: 420,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 500,
- };
-
- expect(
- computePopoverYAxisPosition( anchorRect, contentSize, 'bottom' )
- ).toEqual( {
- contentHeight: 390,
- popoverTop: 400,
- yAxis: 'top',
- } );
- } );
-
- it( 'should position a popover in the middle', () => {
- const anchorRect = {
- top: 400,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverYAxisPosition( anchorRect, contentSize, 'middle' )
- ).toEqual( {
- contentHeight: null,
- popoverTop: 410,
- yAxis: 'middle',
- } );
- } );
-} );
-
-describe( 'computePopoverXAxisPosition', () => {
- it( 'should leave the position as is there’s enought space', () => {
- const anchorRect = {
- top: 10,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverXAxisPosition( anchorRect, contentSize, 'right' )
- ).toEqual( {
- contentWidth: null,
- popoverLeft: 20,
- xAxis: 'right',
- } );
- } );
-
- it( "should switch to right position if there's not enough space", () => {
- const anchorRect = {
- top: 10,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverXAxisPosition( anchorRect, contentSize, 'center' )
- ).toEqual( {
- contentWidth: null,
- popoverLeft: 20,
- xAxis: 'right',
- } );
- } );
-
- it( "should center popover if there's not enough space in any direction", () => {
- const anchorRect = {
- top: 10,
- left: 400,
- bottom: 30,
- right: 420,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 800,
- height: 300,
- };
-
- expect(
- computePopoverXAxisPosition( anchorRect, contentSize, 'right' )
- ).toEqual( {
- contentWidth: null,
- popoverLeft: 512,
- xAxis: 'center',
- } );
- } );
-
- it( 'should set the content width to the viewport width if content is too wide', () => {
- const anchorRect = {
- top: 10,
- left: 400,
- bottom: 30,
- right: 420,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 1500,
- height: 300,
- };
-
- expect(
- computePopoverXAxisPosition( anchorRect, contentSize, 'right' )
- ).toEqual( {
- contentWidth: 1024,
- popoverLeft: 512,
- xAxis: 'center',
- } );
- } );
-} );
-
-describe( 'computePopoverPosition', () => {
- it( 'should leave the position as is there’s enought space', () => {
- const anchorRect = {
- top: 10,
- left: 10,
- bottom: 30,
- right: 30,
- width: 20,
- height: 20,
- };
-
- const contentSize = {
- width: 200,
- height: 300,
- };
-
- expect(
- computePopoverPosition( anchorRect, contentSize, 'bottom right' )
- ).toEqual( {
- contentWidth: null,
- popoverLeft: 20,
- xAxis: 'right',
- contentHeight: null,
- popoverTop: 30,
- yAxis: 'bottom',
- } );
- } );
-} );
-
-describe( 'offsetIframe', () => {
- let parent;
-
- beforeEach( () => {
- parent = document.createElement( 'div' );
- document.body.appendChild( parent );
- } );
-
- afterEach( () => {
- parent.remove();
- } );
-
- it( 'returns rect without changes if element is not an iframe', () => {
- const rect = {
- left: 50,
- top: 50,
- bottom: 100,
- right: 100,
- width: 50,
- height: 50,
- };
- const offsettedRect = offsetIframe( rect, parent.ownerDocument );
-
- expect( offsettedRect ).toEqual( rect );
- } );
-
- it( 'returns offsetted rect if element is in an iframe', () => {
- const iframeLeft = 25;
- const iframeTop = 50;
- const childLeft = 10;
- const childTop = 100;
-
- const iframe = document.createElement( 'iframe' );
- parent.appendChild( iframe );
- // JSDom doesn't have a layout engine
- // so we need to mock getBoundingClientRect and DOMRect.
- iframe.getBoundingClientRect = jest.fn( () => ( {
- width: 100,
- height: 100,
- top: iframeTop,
- left: iframeLeft,
- } ) );
- iframe.contentWindow.DOMRect = jest.fn(
- ( left, top, width, height ) => ( {
- left,
- top,
- right: left + width,
- bottom: top + height,
- width,
- height,
- } )
- );
-
- const child = document.createElement( 'div' );
- iframe.contentWindow.document.body.appendChild( child );
- child.getBoundingClientRect = jest.fn( () => ( {
- width: 100,
- height: 100,
- top: childTop,
- left: childLeft,
- } ) );
-
- const rect = child.getBoundingClientRect();
- const offsettedRect = offsetIframe( rect, child.ownerDocument, parent );
-
- expect( offsettedRect.left ).toBe( iframeLeft + childLeft );
- expect( offsettedRect.top ).toBe( iframeTop + childTop );
- } );
-} );
diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js
deleted file mode 100644
index 012b2b3dec7483..00000000000000
--- a/packages/components/src/popover/utils.js
+++ /dev/null
@@ -1,396 +0,0 @@
-// @ts-nocheck
-/**
- * WordPress dependencies
- */
-import { isRTL } from '@wordpress/i18n';
-
-/**
- * Module constants
- */
-const HEIGHT_OFFSET = 10; // Used by the arrow and a bit of empty space.
-
-/**
- * Utility used to compute the popover position over the xAxis
- *
- * @param {Object} anchorRect Anchor Rect.
- * @param {Object} contentSize Content Size.
- * @param {string} xAxis Desired xAxis.
- * @param {string} corner Desired corner.
- * @param {boolean} stickyBoundaryElement The boundary element to use when
- * switching between sticky and normal
- * position.
- * @param {string} chosenYAxis yAxis to be used.
- * @param {Element} boundaryElement Boundary element.
- * @param {boolean} forcePosition Don't adjust position based on anchor.
- * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis
- *
- * @return {Object} Popover xAxis position and constraints.
- */
-export function computePopoverXAxisPosition(
- anchorRect,
- contentSize,
- xAxis,
- corner,
- stickyBoundaryElement,
- chosenYAxis,
- boundaryElement,
- forcePosition,
- forceXAlignment
-) {
- const { width } = contentSize;
-
- // Correct xAxis for RTL support.
- if ( xAxis === 'left' && isRTL() ) {
- xAxis = 'right';
- } else if ( xAxis === 'right' && isRTL() ) {
- xAxis = 'left';
- }
-
- if ( corner === 'left' && isRTL() ) {
- corner = 'right';
- } else if ( corner === 'right' && isRTL() ) {
- corner = 'left';
- }
-
- // X axis alignment choices.
- const anchorMidPoint = Math.round( anchorRect.left + anchorRect.width / 2 );
- const centerAlignment = {
- popoverLeft: anchorMidPoint,
- contentWidth:
- ( anchorMidPoint - width / 2 > 0 ? width / 2 : anchorMidPoint ) +
- ( anchorMidPoint + width / 2 > window.innerWidth
- ? window.innerWidth - anchorMidPoint
- : width / 2 ),
- };
-
- let leftAlignmentX = anchorRect.left;
-
- if ( corner === 'right' ) {
- leftAlignmentX = anchorRect.right;
- } else if ( chosenYAxis !== 'middle' && ! forceXAlignment ) {
- leftAlignmentX = anchorMidPoint;
- }
-
- let rightAlignmentX = anchorRect.right;
-
- if ( corner === 'left' ) {
- rightAlignmentX = anchorRect.left;
- } else if ( chosenYAxis !== 'middle' && ! forceXAlignment ) {
- rightAlignmentX = anchorMidPoint;
- }
-
- const leftAlignment = {
- popoverLeft: leftAlignmentX,
- contentWidth: leftAlignmentX - width > 0 ? width : leftAlignmentX,
- };
- const rightAlignment = {
- popoverLeft: rightAlignmentX,
- contentWidth:
- rightAlignmentX + width > window.innerWidth
- ? window.innerWidth - rightAlignmentX
- : width,
- };
-
- // Choosing the x axis.
- let chosenXAxis = xAxis;
- let contentWidth = null;
-
- if ( ! stickyBoundaryElement && ! forcePosition ) {
- if ( xAxis === 'center' && centerAlignment.contentWidth === width ) {
- chosenXAxis = 'center';
- } else if ( xAxis === 'left' && leftAlignment.contentWidth === width ) {
- chosenXAxis = 'left';
- } else if (
- xAxis === 'right' &&
- rightAlignment.contentWidth === width
- ) {
- chosenXAxis = 'right';
- } else {
- chosenXAxis =
- leftAlignment.contentWidth > rightAlignment.contentWidth
- ? 'left'
- : 'right';
- const chosenWidth =
- chosenXAxis === 'left'
- ? leftAlignment.contentWidth
- : rightAlignment.contentWidth;
-
- // Limit width of the content to the viewport width
- if ( width > window.innerWidth ) {
- contentWidth = window.innerWidth;
- }
-
- // If we can't find any alignment options that could fit
- // our content, then let's fallback to the center of the viewport.
- if ( chosenWidth !== width ) {
- chosenXAxis = 'center';
- centerAlignment.popoverLeft = window.innerWidth / 2;
- }
- }
- }
-
- let popoverLeft;
- if ( chosenXAxis === 'center' ) {
- popoverLeft = centerAlignment.popoverLeft;
- } else if ( chosenXAxis === 'left' ) {
- popoverLeft = leftAlignment.popoverLeft;
- } else {
- popoverLeft = rightAlignment.popoverLeft;
- }
-
- if ( boundaryElement ) {
- popoverLeft = Math.min(
- popoverLeft,
- boundaryElement.offsetLeft + boundaryElement.offsetWidth - width
- );
-
- // Avoid the popover being position beyond the left boundary if the
- // direction is left to right.
- if ( ! isRTL() ) {
- popoverLeft = Math.max( popoverLeft, 0 );
- }
- }
-
- return {
- xAxis: chosenXAxis,
- popoverLeft,
- contentWidth,
- };
-}
-
-/**
- * Utility used to compute the popover position over the yAxis
- *
- * @param {Object} anchorRect Anchor Rect.
- * @param {Object} contentSize Content Size.
- * @param {string} yAxis Desired yAxis.
- * @param {string} corner Desired corner.
- * @param {boolean} stickyBoundaryElement The boundary element to use when switching between sticky
- * and normal position.
- * @param {Element} anchorRef The anchor element.
- * @param {Element} relativeOffsetTop If applicable, top offset of the relative positioned
- * parent container.
- * @param {boolean} forcePosition Don't adjust position based on anchor.
- * @param {Element|null} editorWrapper Element that wraps the editor content. Used to access
- * scroll position to determine sticky behavior.
- * @return {Object} Popover xAxis position and constraints.
- */
-export function computePopoverYAxisPosition(
- anchorRect,
- contentSize,
- yAxis,
- corner,
- stickyBoundaryElement,
- anchorRef,
- relativeOffsetTop,
- forcePosition,
- editorWrapper
-) {
- const { height } = contentSize;
-
- if ( stickyBoundaryElement ) {
- const stickyRect = stickyBoundaryElement.getBoundingClientRect();
- const stickyPositionTop = stickyRect.top + height - relativeOffsetTop;
- const stickyPositionBottom =
- stickyRect.bottom - height - relativeOffsetTop;
-
- if ( anchorRect.top <= stickyPositionTop ) {
- if ( editorWrapper ) {
- // If a popover cannot be positioned above the anchor, even after scrolling, we must
- // ensure we use the bottom position instead of the popover slot. This prevents the
- // popover from always restricting block content and interaction while selected if the
- // block is near the top of the site editor.
-
- const isRoomAboveInCanvas =
- height + HEIGHT_OFFSET <
- editorWrapper.scrollTop + anchorRect.top;
- if ( ! isRoomAboveInCanvas ) {
- return {
- yAxis: 'bottom',
- // If the bottom of the block is also below the bottom sticky position (ex -
- // block is also taller than the editor window), return the bottom sticky
- // position instead. We do this instead of the top sticky position both to
- // allow a smooth transition and more importantly to ensure every section of
- // the block can be free from popover obscuration at some point in the
- // scroll position.
- popoverTop: Math.min(
- anchorRect.bottom,
- stickyPositionBottom
- ),
- };
- }
- }
- // Default sticky behavior.
- return {
- yAxis,
- popoverTop: Math.min( anchorRect.bottom, stickyPositionTop ),
- };
- }
- }
-
- // Y axis alignment choices.
- let anchorMidPoint = anchorRect.top + anchorRect.height / 2;
-
- if ( corner === 'bottom' ) {
- anchorMidPoint = anchorRect.bottom;
- } else if ( corner === 'top' ) {
- anchorMidPoint = anchorRect.top;
- }
-
- const middleAlignment = {
- popoverTop: anchorMidPoint,
- contentHeight:
- ( anchorMidPoint - height / 2 > 0 ? height / 2 : anchorMidPoint ) +
- ( anchorMidPoint + height / 2 > window.innerHeight
- ? window.innerHeight - anchorMidPoint
- : height / 2 ),
- };
-
- const topAlignment = {
- popoverTop: anchorRect.top,
- contentHeight:
- anchorRect.top - HEIGHT_OFFSET - height > 0
- ? height
- : anchorRect.top - HEIGHT_OFFSET,
- };
- const bottomAlignment = {
- popoverTop: anchorRect.bottom,
- contentHeight:
- anchorRect.bottom + HEIGHT_OFFSET + height > window.innerHeight
- ? window.innerHeight - HEIGHT_OFFSET - anchorRect.bottom
- : height,
- };
-
- // Choosing the y axis.
- let chosenYAxis = yAxis;
- let contentHeight = null;
-
- if ( ! stickyBoundaryElement && ! forcePosition ) {
- if ( yAxis === 'middle' && middleAlignment.contentHeight === height ) {
- chosenYAxis = 'middle';
- } else if ( yAxis === 'top' && topAlignment.contentHeight === height ) {
- chosenYAxis = 'top';
- } else if (
- yAxis === 'bottom' &&
- bottomAlignment.contentHeight === height
- ) {
- chosenYAxis = 'bottom';
- } else {
- chosenYAxis =
- topAlignment.contentHeight > bottomAlignment.contentHeight
- ? 'top'
- : 'bottom';
- const chosenHeight =
- chosenYAxis === 'top'
- ? topAlignment.contentHeight
- : bottomAlignment.contentHeight;
- contentHeight = chosenHeight !== height ? chosenHeight : null;
- }
- }
-
- let popoverTop;
- if ( chosenYAxis === 'middle' ) {
- popoverTop = middleAlignment.popoverTop;
- } else if ( chosenYAxis === 'top' ) {
- popoverTop = topAlignment.popoverTop;
- } else {
- popoverTop = bottomAlignment.popoverTop;
- }
-
- return {
- yAxis: chosenYAxis,
- popoverTop,
- contentHeight,
- };
-}
-
-/**
- * Utility used to compute the popover position and the content max width/height for a popover given
- * its anchor rect and its content size.
- *
- * @param {Object} anchorRect Anchor Rect.
- * @param {Object} contentSize Content Size.
- * @param {string} position Position.
- * @param {boolean} stickyBoundaryElement The boundary element to use when switching between
- * sticky and normal position.
- * @param {Element} anchorRef The anchor element.
- * @param {number} relativeOffsetTop If applicable, top offset of the relative positioned
- * parent container.
- * @param {Element} boundaryElement Boundary element.
- * @param {boolean} forcePosition Don't adjust position based on anchor.
- * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis
- * @param {Element|null} editorWrapper Element that wraps the editor content. Used to access
- * scroll position to determine sticky behavior.
- * @return {Object} Popover position and constraints.
- */
-export function computePopoverPosition(
- anchorRect,
- contentSize,
- position = 'top',
- stickyBoundaryElement,
- anchorRef,
- relativeOffsetTop,
- boundaryElement,
- forcePosition,
- forceXAlignment,
- editorWrapper
-) {
- const [ yAxis, xAxis = 'center', corner ] = position.split( ' ' );
-
- const yAxisPosition = computePopoverYAxisPosition(
- anchorRect,
- contentSize,
- yAxis,
- corner,
- stickyBoundaryElement,
- anchorRef,
- relativeOffsetTop,
- forcePosition,
- editorWrapper
- );
- const xAxisPosition = computePopoverXAxisPosition(
- anchorRect,
- contentSize,
- xAxis,
- corner,
- stickyBoundaryElement,
- yAxisPosition.yAxis,
- boundaryElement,
- forcePosition,
- forceXAlignment
- );
-
- return {
- ...xAxisPosition,
- ...yAxisPosition,
- };
-}
-
-/**
- * Offsets the given rect by the position of the iframe that contains the
- * element. If the owner document is not in an iframe then it returns with the
- * original rect. If the popover container document and the anchor document are
- * the same, the original rect will also be returned.
- *
- * @param {DOMRect} rect bounds of the element
- * @param {Document} ownerDocument document of the element
- * @param {Element} container The popover container to position.
- *
- * @return {DOMRect} offsetted bounds
- */
-export function offsetIframe( rect, ownerDocument, container ) {
- const { defaultView } = ownerDocument;
- const { frameElement } = defaultView;
-
- if ( ! frameElement || ownerDocument === container.ownerDocument ) {
- return rect;
- }
-
- const iframeRect = frameElement.getBoundingClientRect();
- return new defaultView.DOMRect(
- rect.left + iframeRect.left,
- rect.top + iframeRect.top,
- rect.width,
- rect.height
- );
-}
diff --git a/packages/components/src/tooltip/index.js b/packages/components/src/tooltip/index.js
index f395e528fb178c..b9177cf7826704 100644
--- a/packages/components/src/tooltip/index.js
+++ b/packages/components/src/tooltip/index.js
@@ -64,7 +64,7 @@ const addPopoverToGrandchildren = ( {
className="components-tooltip"
aria-hidden="true"
animate={ false }
- noArrow={ true }
+ offset={ 12 }
>
{ text }
{
};
function Tooltip( props ) {
- const { children, position, text, shortcut, delay = TOOLTIP_DELAY } = props;
+ const {
+ children,
+ position = 'bottom middle',
+ text,
+ shortcut,
+ delay = TOOLTIP_DELAY,
+ } = props;
/**
* Whether a mouse is currently pressed, used in determining whether
* to handle a focus event as displaying the tooltip immediately.
diff --git a/packages/components/src/tooltip/style.scss b/packages/components/src/tooltip/style.scss
index df3cfbcf4826a8..3de494fa60359e 100644
--- a/packages/components/src/tooltip/style.scss
+++ b/packages/components/src/tooltip/style.scss
@@ -10,16 +10,14 @@
background: $gray-900;
border-radius: $radius-block-ui;
border-width: 0;
+ outline: none;
color: $white;
white-space: nowrap;
text-align: center;
line-height: 1.4;
font-size: 12px;
box-shadow: none;
-
- > div {
- padding: $grid-unit-05 $grid-unit-10;
- }
+ padding: $grid-unit-05 $grid-unit-10;
}
.components-tooltip__shortcut {
diff --git a/packages/compose/src/hooks/use-focus-on-mount/index.js b/packages/compose/src/hooks/use-focus-on-mount/index.js
index a971c3dac54a10..cf0e97e661bfc6 100644
--- a/packages/compose/src/hooks/use-focus-on-mount/index.js
+++ b/packages/compose/src/hooks/use-focus-on-mount/index.js
@@ -50,6 +50,11 @@ export default function useFocusOnMount( focusOnMount = 'firstElement' ) {
}
}
- target.focus();
+ target.focus( {
+ // When focusing newly mounted dialogs,
+ // the position of the popover is often not right on the first render
+ // This prevents the layout shifts when focusing the dialogs.
+ preventScroll: true,
+ } );
}, [] );
}
diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap
index 86257779ea8b98..b175d82c81d1ff 100644
--- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap
+++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap
@@ -12,12 +12,6 @@ exports[`Heading can be created by prefixing number sign and a space 1`] = `
"
`;
-exports[`Heading should correctly apply custom colors 1`] = `
-"
-Heading
-"
-`;
-
exports[`Heading should correctly apply named colors 1`] = `
"
Heading
diff --git a/packages/e2e-tests/specs/editor/blocks/heading.test.js b/packages/e2e-tests/specs/editor/blocks/heading.test.js
index ecfb8fc236161b..504916d4c84cbc 100644
--- a/packages/e2e-tests/specs/editor/blocks/heading.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/heading.test.js
@@ -86,10 +86,14 @@ describe( 'Heading', () => {
await page.waitForSelector( COLOR_INPUT_FIELD_SELECTOR );
await page.click( COLOR_INPUT_FIELD_SELECTOR );
await pressKeyWithModifier( 'primary', 'A' );
- await page.keyboard.type( '0782f6' );
- await page.click( 'h3[data-type="core/heading"]' );
- await page.waitForXPath( '//button//span[contains(text(), "0782f6")]' );
- expect( await getEditedPostContent() ).toMatchSnapshot();
+ await page.keyboard.type( '4b7f4d' );
+ await page.waitForXPath( '//button//span[contains(text(), "4b7f4d")]' );
+ await page.click( '.wp-block-post-title' );
+ expect( await getEditedPostContent() ).toMatchInlineSnapshot( `
+ "
+ Heading
+ "
+ ` );
} );
it( 'should correctly apply named colors', async () => {
diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss
index 3f8757eb2e3e9a..8ad308b8057ed3 100644
--- a/packages/edit-post/src/components/header/style.scss
+++ b/packages/edit-post/src/components/header/style.scss
@@ -84,10 +84,8 @@
}
}
-.edit-post-post-preview-dropdown {
- .components-popover__content > div {
- padding-bottom: 0;
- }
+.edit-post-post-preview-dropdown .components-popover__content {
+ padding-bottom: 0;
}
/**
diff --git a/packages/edit-post/src/components/sidebar/post-schedule/index.js b/packages/edit-post/src/components/sidebar/post-schedule/index.js
index ea786d031b3d8e..5b6c9a0e6481dc 100644
--- a/packages/edit-post/src/components/sidebar/post-schedule/index.js
+++ b/packages/edit-post/src/components/sidebar/post-schedule/index.js
@@ -18,7 +18,7 @@ export function PostSchedule() {
{ __( 'Publish' ) }
(
diff --git a/packages/edit-post/src/components/sidebar/post-schedule/style.scss b/packages/edit-post/src/components/sidebar/post-schedule/style.scss
index e7f99795a33c63..1b41f731f8feb3 100644
--- a/packages/edit-post/src/components/sidebar/post-schedule/style.scss
+++ b/packages/edit-post/src/components/sidebar/post-schedule/style.scss
@@ -13,8 +13,8 @@
text-align: right;
}
-// Zero out a blanket padding that is set on the popover component.
+// Zero out a blanket padding that is set on the dropdown component.
// The datetime component has its own padding.
-.edit-post-post-schedule__dialog .components-popover__content > div {
+.edit-post-post-schedule__dialog .components-popover__content {
padding: 0;
}
diff --git a/packages/edit-site/src/components/global-styles/border-panel.js b/packages/edit-site/src/components/global-styles/border-panel.js
index 780b165f785703..71fe69769719ea 100644
--- a/packages/edit-site/src/components/global-styles/border-panel.js
+++ b/packages/edit-site/src/components/global-styles/border-panel.js
@@ -182,18 +182,8 @@ export default function BorderPanel( { name } ) {
onChange={ onBorderChange }
showStyle={ showBorderStyle }
value={ border }
- popoverClassNames={ {
- linked:
- 'edit-site-global-styles-sidebar__border-box-control__popover',
- top:
- 'edit-site-global-styles-sidebar__border-box-control__popover-top',
- right:
- 'edit-site-global-styles-sidebar__border-box-control__popover-right',
- bottom:
- 'edit-site-global-styles-sidebar__border-box-control__popover-bottom',
- left:
- 'edit-site-global-styles-sidebar__border-box-control__popover-left',
- } }
+ popoverPlacement="left-start"
+ popoverOffset={ 40 }
__experimentalHasMultipleOrigins={ true }
__experimentalIsRenderedInSidebar={ true }
/>
diff --git a/packages/edit-site/src/components/header/document-actions/style.scss b/packages/edit-site/src/components/header/document-actions/style.scss
index 6e10e1d8c9b711..b83a65938b19a7 100644
--- a/packages/edit-site/src/components/header/document-actions/style.scss
+++ b/packages/edit-site/src/components/header/document-actions/style.scss
@@ -66,7 +66,7 @@
}
}
-.edit-site-document-actions__info-dropdown > .components-popover__content > div {
+.edit-site-document-actions__info-dropdown > .components-popover__content {
padding: 0;
min-width: 240px;
}
diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss
index b2a4119e5c71ef..b3992b7774b0d3 100644
--- a/packages/edit-site/src/components/sidebar/style.scss
+++ b/packages/edit-site/src/components/sidebar/style.scss
@@ -88,55 +88,6 @@
line-height: 1;
}
-
-.edit-site-global-styles-sidebar__border-box-control__popover,
-.edit-site-global-styles-sidebar__border-box-control__popover-top,
-.edit-site-global-styles-sidebar__border-box-control__popover-right,
-.edit-site-global-styles-sidebar__border-box-control__popover-bottom,
-.edit-site-global-styles-sidebar__border-box-control__popover-left {
- .components-popover__content {
- width: 282px;
- }
-}
-
-$split-border-control-offset: 55px;
-
-@include break-medium() {
- .edit-site-global-styles-sidebar__border-box-control__popover,
- .edit-site-global-styles-sidebar__border-box-control__popover-left {
- .components-popover__content {
- margin-right: #{ $grid-unit-50 + $grid-unit-15 } !important;
- }
- }
-
- .edit-site-global-styles-sidebar__border-box-control__popover-top,
- .edit-site-global-styles-sidebar__border-box-control__popover-bottom {
- .components-popover__content {
- margin-right: #{ $grid-unit-50 + $grid-unit-15 + $split-border-control-offset } !important;
- }
- }
-
- .edit-site-global-styles-sidebar__border-box-control__popover-right {
- .components-popover__content {
- margin-right: #{ $grid-unit-50 + $grid-unit-15 + ( $split-border-control-offset * 2 )} !important;
- }
- }
-
- .edit-site-global-styles-sidebar__border-box-control__popover,
- .edit-site-global-styles-sidebar__border-box-control__popover-top,
- .edit-site-global-styles-sidebar__border-box-control__popover-right,
- .edit-site-global-styles-sidebar__border-box-control__popover-bottom,
- .edit-site-global-styles-sidebar__border-box-control__popover-left {
- &.is-from-top .components-popover__content {
- margin-top: #{ -($grid-unit-50 + $grid-unit-15) } !important;
- }
-
- &.is-from-bottom .components-popover__content {
- margin-bottom: #{ -($grid-unit-50 + $grid-unit-15) } !important;
- }
- }
-}
-
// Override the `hr` styles defined in the `ComplementaryArea` component
// from the `@wordpress/interface` package.
.edit-site-global-styles-sidebar hr {
diff --git a/packages/editor/src/components/table-of-contents/style.scss b/packages/editor/src/components/table-of-contents/style.scss
index 20b570b8799dc7..b1dc4249de0844 100644
--- a/packages/editor/src/components/table-of-contents/style.scss
+++ b/packages/editor/src/components/table-of-contents/style.scss
@@ -8,9 +8,7 @@
.table-of-contents__popover {
.components-popover__content {
- > div {
- padding: $grid-unit-20;
- }
+ padding: $grid-unit-20;
@include break-small {
max-height: calc(100vh - 120px);
diff --git a/packages/interface/src/components/more-menu-dropdown/style.scss b/packages/interface/src/components/more-menu-dropdown/style.scss
index 0e98b90de6c062..56852977fa05f7 100644
--- a/packages/interface/src/components/more-menu-dropdown/style.scss
+++ b/packages/interface/src/components/more-menu-dropdown/style.scss
@@ -21,7 +21,6 @@
// Let the menu scale to fit items.
@include break-mobile() {
- width: auto;
max-width: $break-mobile;
}
diff --git a/packages/list-reusable-blocks/src/components/import-dropdown/style.scss b/packages/list-reusable-blocks/src/components/import-dropdown/style.scss
index c12c7fd4d3f155..8c2dc9c94bd71f 100644
--- a/packages/list-reusable-blocks/src/components/import-dropdown/style.scss
+++ b/packages/list-reusable-blocks/src/components/import-dropdown/style.scss
@@ -1,3 +1,3 @@
-.list-reusable-blocks-import-dropdown__content .components-popover__content > div {
+.list-reusable-blocks-import-dropdown__content .components-popover__content {
padding: 10px;
}
diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js
index c604c456254280..6f6f09bdf289c0 100644
--- a/packages/nux/src/components/dot-tip/index.js
+++ b/packages/nux/src/components/dot-tip/index.js
@@ -48,7 +48,6 @@ export function DotTip( {
div {
- padding: 20px 18px;
- }
+ padding: 20px 18px;
@include break-small {
width: 450px;
diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
index a737545c530122..1071acfa25cd36 100644
--- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
+++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap
@@ -5,7 +5,6 @@ exports[`DotTip should render correctly 1`] = `
aria-label="Editor tips"
className="nux-dot-tip"
focusOnMount="container"
- noArrow={true}
onClick={[Function]}
onFocusOutside={[Function]}
position="middle right"
diff --git a/packages/widgets/src/blocks/legacy-widget/edit/form.js b/packages/widgets/src/blocks/legacy-widget/edit/form.js
index b7a3f0e3e9b98c..f164e0eed6f6eb 100644
--- a/packages/widgets/src/blocks/legacy-widget/edit/form.js
+++ b/packages/widgets/src/blocks/legacy-widget/edit/form.js
@@ -104,7 +104,7 @@ export default function Form( {