Skip to content

Commit

Permalink
Popover: fix limitShift logic by adding iframe offset correctly (and …
Browse files Browse the repository at this point in the history
…a custom shift limiter) (#42950)

* Extract placement utilities

* Add offset to limitShift functionality to compensate iframe s offset

* CHANGELOG

* Custom limitShift implementation

* Add offset to the iframe via a custom middleware

* Remove unused utilities

* Remove unnecessary offset ref

* Update CHANGELOG

* Add reference to floating-ui s MIT license
  • Loading branch information
ciampo authored Sep 22, 2022
1 parent e48ce82 commit 9afe679
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- `Button`: Remove unexpected `has-text` class when empty children are passed ([#44198](https://github.com/WordPress/gutenberg/pull/44198)).
- The `LinkedButton` to unlink sides in `BoxControl`, `BorderBoxControl` and `BorderRadiusControl` have changed from a rectangular primary button to an icon-only button, with a sentence case tooltip, and default-size icon for better legibility. The `Button` component has been fixed so when `isSmall` and `icon` props are set, and no text is present, the button shape is square rather than rectangular.
- `Popover`: fix limitShift logic by adding iframe offset correctly [#42950](https://github.com/WordPress/gutenberg/pull/42950)).

### Internal

Expand Down
68 changes: 26 additions & 42 deletions packages/components/src/popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
autoUpdate,
arrow,
offset as offsetMiddleware,
limitShift,
size,
Middleware,
MiddlewareArguments,
} from '@floating-ui/react-dom';
// eslint-disable-next-line no-restricted-imports
import {
Expand All @@ -34,7 +34,6 @@ import {
useMemo,
useState,
useCallback,
useEffect,
} from '@wordpress/element';
import {
useViewportMatch,
Expand Down Expand Up @@ -65,6 +64,7 @@ import type {
PopoverAnchorRefReference,
PopoverAnchorRefTopBottom,
} from './types';
import { limitShift as customLimitShift } from './limit-shift';

/**
* Name of slot in which popover should fill.
Expand Down Expand Up @@ -281,42 +281,31 @@ const UnforwardedPopover = (
* https://floating-ui.com/docs/react-dom#variables-inside-middleware-functions.
*/
const frameOffsetRef = useRef( getFrameOffset( referenceOwnerDocument ) );
/**
* Store the offset prop in a ref, due to constraints with floating-ui:
* https://floating-ui.com/docs/react-dom#variables-inside-middleware-functions.
*/
const offsetRef = useRef( offsetProp );

const middleware = [
offsetMiddleware( ( { placement: currentPlacement } ) => {
if ( ! frameOffsetRef.current ) {
return offsetRef.current;
}

const isTopBottomPlacement =
currentPlacement.includes( 'top' ) ||
currentPlacement.includes( 'bottom' );

// The main axis should represent the gap between the
// floating element and the reference element. The cross
// axis is always perpendicular to the main axis.
const mainAxis = isTopBottomPlacement ? 'y' : 'x';
const crossAxis = mainAxis === 'x' ? 'y' : 'x';

// When the popover is before the reference, subtract the offset,
// of the main axis else add it.
const hasBeforePlacement =
currentPlacement.includes( 'top' ) ||
currentPlacement.includes( 'left' );
const mainAxisModifier = hasBeforePlacement ? -1 : 1;

return {
mainAxis:
offsetRef.current +
frameOffsetRef.current[ mainAxis ] * mainAxisModifier,
crossAxis: frameOffsetRef.current[ crossAxis ],
};
} ),
// 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( { x, y }: MiddlewareArguments ) {
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,
},
};
},
},
offsetMiddleware( offsetProp ),
computedFlipProp ? flipMiddleware() : undefined,
computedResizeProp
? size( {
Expand All @@ -339,7 +328,7 @@ const UnforwardedPopover = (
shouldShift
? shiftMiddleware( {
crossAxis: true,
limiter: limitShift(),
limiter: customLimitShift(),
padding: 1, // Necessary to avoid flickering at the edge of the viewport.
} )
: undefined,
Expand Down Expand Up @@ -395,11 +384,6 @@ const UnforwardedPopover = (
} ),
} );

useEffect( () => {
offsetRef.current = offsetProp;
update();
}, [ offsetProp, update ] );

const arrowCallbackRef = useCallback(
( node ) => {
arrowRef.current = node;
Expand Down
205 changes: 205 additions & 0 deletions packages/components/src/popover/limit-shift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* External dependencies
*/
import type {
Axis,
Coords,
Placement,
Side,
MiddlewareArguments,
} from '@floating-ui/react-dom';

/**
* Parts of this source were derived and modified from `floating-ui`,
* released under the MIT license.
*
* https://github.com/floating-ui/floating-ui
*
* Copyright (c) 2021 Floating UI contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

/**
* Custom limiter function for the `shift` middleware.
* This function is mostly identical default `limitShift` from ``@floating-ui`;
* the only difference is that, when computing the min/max shift limits, it
* also takes into account the iframe offset that is added by the
* custom "frameOffset" middleware.
*
* All unexported types and functions are also from the `@floating-ui` library,
* and have been copied to this file for convenience.
*/

type LimitShiftOffset =
| ( ( args: MiddlewareArguments ) =>
| number
| {
/**
* Offset the limiting of the axis that runs along the alignment of the
* floating element.
*/
mainAxis?: number;
/**
* Offset the limiting of the axis that runs along the side of the
* floating element.
*/
crossAxis?: number;
} )
| number
| {
/**
* Offset the limiting of the axis that runs along the alignment of the
* floating element.
*/
mainAxis?: number;
/**
* Offset the limiting of the axis that runs along the side of the
* floating element.
*/
crossAxis?: number;
};

type LimitShiftOptions = {
/**
* Offset when limiting starts. `0` will limit when the opposite edges of the
* reference and floating elements are aligned.
* - positive = start limiting earlier
* - negative = start limiting later
*/
offset: LimitShiftOffset;
/**
* Whether to limit the axis that runs along the alignment of the floating
* element.
*/
mainAxis: boolean;
/**
* Whether to limit the axis that runs along the side of the floating element.
*/
crossAxis: boolean;
};

function getSide( placement: Placement ): Side {
return placement.split( '-' )[ 0 ] as Side;
}

function getMainAxisFromPlacement( placement: Placement ): Axis {
return [ 'top', 'bottom' ].includes( getSide( placement ) ) ? 'x' : 'y';
}

function getCrossAxis( axis: Axis ): Axis {
return axis === 'x' ? 'y' : 'x';
}

export const limitShift = (
options: Partial< LimitShiftOptions > = {}
): {
options: Partial< LimitShiftOffset >;
fn: ( middlewareArguments: MiddlewareArguments ) => Coords;
} => ( {
options,
fn( middlewareArguments ) {
const { x, y, placement, rects, middlewareData } = middlewareArguments;
const {
offset = 0,
mainAxis: checkMainAxis = true,
crossAxis: checkCrossAxis = true,
} = options;

const coords = { x, y };
const mainAxis = getMainAxisFromPlacement( placement );
const crossAxis = getCrossAxis( mainAxis );

let mainAxisCoord = coords[ mainAxis ];
let crossAxisCoord = coords[ crossAxis ];

const rawOffset =
typeof offset === 'function'
? offset( middlewareArguments )
: offset;
const computedOffset =
typeof rawOffset === 'number'
? { mainAxis: rawOffset, crossAxis: 0 }
: { mainAxis: 0, crossAxis: 0, ...rawOffset };

// At the moment of writing, this is the only difference
// with the `limitShift` function from `@floating-ui`.
// This offset needs to be added to all min/max limits
// in order to make the shift-limiting work as expected.
const additionalFrameOffset = {
x: 0,
y: 0,
...middlewareData.frameOffset?.amount,
};

if ( checkMainAxis ) {
const len = mainAxis === 'y' ? 'height' : 'width';
const limitMin =
rects.reference[ mainAxis ] -
rects.floating[ len ] +
computedOffset.mainAxis +
additionalFrameOffset[ mainAxis ];
const limitMax =
rects.reference[ mainAxis ] +
rects.reference[ len ] -
computedOffset.mainAxis +
additionalFrameOffset[ mainAxis ];

if ( mainAxisCoord < limitMin ) {
mainAxisCoord = limitMin;
} else if ( mainAxisCoord > limitMax ) {
mainAxisCoord = limitMax;
}
}

if ( checkCrossAxis ) {
const len = mainAxis === 'y' ? 'width' : 'height';
const isOriginSide = [ 'top', 'left' ].includes(
getSide( placement )
);
const limitMin =
rects.reference[ crossAxis ] -
rects.floating[ len ] +
( isOriginSide
? middlewareData.offset?.[ crossAxis ] ?? 0
: 0 ) +
( isOriginSide ? 0 : computedOffset.crossAxis ) +
additionalFrameOffset[ crossAxis ];
const limitMax =
rects.reference[ crossAxis ] +
rects.reference[ len ] +
( isOriginSide
? 0
: middlewareData.offset?.[ crossAxis ] ?? 0 ) -
( isOriginSide ? computedOffset.crossAxis : 0 ) +
additionalFrameOffset[ crossAxis ];

if ( crossAxisCoord < limitMin ) {
crossAxisCoord = limitMin;
} else if ( crossAxisCoord > limitMax ) {
crossAxisCoord = limitMax;
}
}

return {
[ mainAxis ]: mainAxisCoord,
[ crossAxis ]: crossAxisCoord,
} as Coords;
},
} );

0 comments on commit 9afe679

Please sign in to comment.