Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- `Popover`: make sure offset middleware always applies the latest frame offset values ([#43329](https://github.com/WordPress/gutenberg/pull/43329/)).
- `Dropdown`: anchor popover to the dropdown wrapper (instead of the toggle) ([#43377](https://github.com/WordPress/gutenberg/pull/43377/)).
- `Guide`: Fix error when rendering with no pages ([#43380](https://github.com/WordPress/gutenberg/pull/43380/)).
- `Popover`: Ensure position is correct when a nested popover's parent references an element in an iframe and the iframe is scrolled ([#43544](https://github.com/WordPress/gutenberg/pull/43544/)).

### Enhancements

Expand Down
158 changes: 98 additions & 60 deletions packages/components/src/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import {
positionToPlacement,
placementToMotionAnimationProps,
} from './utils';
import {
useRootReferenceDocument,
RootReferenceDocumentProvider,
} from './root-reference-document-context';

/**
* Name of slot in which popover should fill.
Expand Down Expand Up @@ -430,74 +434,108 @@ const Popover = (
};
}, [ referenceOwnerDocument, update ] );

// If the root document is scrolled trigger an update of the popover
// position. This covers cases where a popover is a child of another
// popover, and the first popover in the chain references an element
// in an iframe.
const rootReferenceDocument = useRootReferenceDocument();
useLayoutEffect( () => {
// Return early if the root document is the same as the owner document,
// as the scroll event will be listened to in other code.
const isSameDocument = rootReferenceDocument === referenceOwnerDocument;

// Return early if this isn't an iframe document.
const isNotIframeDocument = rootReferenceDocument === document;

if (
! rootReferenceDocument ||
isSameDocument ||
isNotIframeDocument
) {
return;
}

rootReferenceDocument.addEventListener( 'scroll', update );
return () =>
rootReferenceDocument?.removeEventListener( 'scroll', update );
}, [ referenceOwnerDocument, rootReferenceDocument, update ] );

const mergedFloatingRef = 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
<MaybeAnimatedWrapper
shouldAnimate={ animate && ! isExpanded }
placement={ computedPlacement }
className={ classnames( 'components-popover', className, {
'is-expanded': isExpanded,
'is-alternate': isAlternate,
} ) }
{ ...contentProps }
ref={ mergedFloatingRef }
{ ...dialogProps }
tabIndex="-1"
style={
isExpanded
? undefined
: {
position: strategy,
left: Number.isNaN( x ) ? 0 : x,
top: Number.isNaN( y ) ? 0 : y,
}
}
<RootReferenceDocumentProvider
// Don't overwrite the root document if it's already set.
// This ensures only the reference document from the very
// root popover is provided.
value={ rootReferenceDocument ?? referenceOwnerDocument }
>
{ /* Prevents scroll on the document */ }
{ isExpanded && <ScrollLock /> }
{ isExpanded && (
<div className="components-popover__header">
<span className="components-popover__header-title">
{ headerTitle }
</span>
<Button
className="components-popover__close"
icon={ close }
onClick={ onClose }
/>
</div>
) }
<div className="components-popover__content">{ children }</div>
{ hasArrow && (
<div
ref={ arrowRef }
className={ [
'components-popover__arrow',
`is-${ computedPlacement.split( '-' )[ 0 ] }`,
].join( ' ' ) }
style={ {
left: Number.isFinite( arrowData?.x )
? `${ arrowData.x }px`
: '',
top: Number.isFinite( arrowData?.y )
? `${ arrowData.y }px`
: '',
} }
>
<ArrowTriangle />
</div>
) }
</MaybeAnimatedWrapper>
{
// Disable reason: We care to capture the _bubbled_ events from inputs
// within popover as inferring close intent.
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
}
<MaybeAnimatedWrapper
shouldAnimate={ animate && ! isExpanded }
placement={ computedPlacement }
className={ classnames( 'components-popover', className, {
'is-expanded': isExpanded,
'is-alternate': isAlternate,
} ) }
{ ...contentProps }
ref={ mergedFloatingRef }
{ ...dialogProps }
tabIndex="-1"
style={
isExpanded
? undefined
: {
position: strategy,
left: Number.isNaN( x ) ? 0 : x,
top: Number.isNaN( y ) ? 0 : y,
}
}
>
{ /* Prevents scroll on the document */ }
{ isExpanded && <ScrollLock /> }
{ isExpanded && (
<div className="components-popover__header">
<span className="components-popover__header-title">
{ headerTitle }
</span>
<Button
className="components-popover__close"
icon={ close }
onClick={ onClose }
/>
</div>
) }
<div className="components-popover__content">{ children }</div>
{ hasArrow && (
<div
ref={ arrowRef }
className={ [
'components-popover__arrow',
`is-${ computedPlacement.split( '-' )[ 0 ] }`,
].join( ' ' ) }
style={ {
left: Number.isFinite( arrowData?.x )
? `${ arrowData.x }px`
: '',
top: Number.isFinite( arrowData?.y )
? `${ arrowData.y }px`
: '',
} }
>
<ArrowTriangle />
</div>
) }
</MaybeAnimatedWrapper>
</RootReferenceDocumentProvider>
);

if ( slot.ref ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* WordPress dependencies
*/
import { createContext, useContext } from '@wordpress/element';

const RootReferenceDocumentContext = createContext( undefined );
export const useRootReferenceDocument = () =>
useContext( RootReferenceDocumentContext );
export const RootReferenceDocumentProvider =
RootReferenceDocumentContext.Provider;