Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Toolbar & Popover component - Prevent sticky position from causing permanently obscured areas of the selected block. #33981

Merged
merged 19 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function selector( select ) {
isCaretWithinFormattedText,
getSettings,
getLastMultiSelectedBlockClientId,
__unstableGetIframedEditorCanvasWrapper,
} = select( blockEditorStore );
return {
isNavigationMode: isNavigationMode(),
Expand All @@ -43,6 +44,7 @@ function selector( select ) {
hasMultiSelection: hasMultiSelection(),
hasFixedToolbar: getSettings().hasFixedToolbar,
lastClientId: getLastMultiSelectedBlockClientId(),
iframedEditorCanvasWrapper: __unstableGetIframedEditorCanvasWrapper(),
};
}

Expand All @@ -63,6 +65,7 @@ function BlockPopover( {
hasMultiSelection,
hasFixedToolbar,
lastClientId,
iframedEditorCanvasWrapper,
} = useSelect( selector, [] );
const isInsertionPointVisible = useSelect(
( select ) => {
Expand Down Expand Up @@ -213,6 +216,9 @@ function BlockPopover( {
// Observe movement for block animations (especially horizontal).
__unstableObserveElement={ node }
shouldAnchorIncludePadding
// Used to safeguard sticky position behavior against cases where it would permanently
// obscure specific sections of a block.
__unstableEditorCanvasWrapper={ iframedEditorCanvasWrapper }
>
{ ( shouldShowContextualToolbar || isToolbarForced ) && (
<div
Expand Down
6 changes: 6 additions & 0 deletions packages/block-editor/src/components/iframe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
import { __ } from '@wordpress/i18n';
import { useMergeRefs } from '@wordpress/compose';
import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
import { dispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { useBlockSelectionClearer } from '../block-selection-clearer';
import { useWritingFlow } from '../writing-flow';
import { store as blockEditorStore } from '../../store';

const BODY_CLASS_NAME = 'editor-styles-wrapper';
const BLOCK_PREFIX = 'wp-block';
Expand Down Expand Up @@ -151,6 +153,10 @@ function setBodyClassName( doc ) {
doc.dir = document.dir;
doc.body.className = BODY_CLASS_NAME;

dispatch( blockEditorStore ).__unstableSetIframedEditorCanvasWrapper(
doc.body
);
Addison-Stavlo marked this conversation as resolved.
Show resolved Hide resolved

for ( const name of document.body.classList ) {
if ( name.startsWith( 'admin-color-' ) ) {
doc.body.classList.add( name );
Expand Down
12 changes: 12 additions & 0 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1404,3 +1404,15 @@ export function setHasControlledInnerBlocks(
clientId,
};
}

/**
* Returns an action object that sets the node for the editor styles wrapper inside an iframed editor.
*
* @param {Object|null} node The iframed editor wrapper's html node.
*/
export function __unstableSetIframedEditorCanvasWrapper( node ) {
return {
type: 'SET_IFRAMED_EDITOR_CANVAS_WRAPPER',
node,
};
}
16 changes: 16 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,21 @@ export function lastBlockInserted( state = {}, action ) {
return state;
}

/**
* Reducer returning the html node of the iframed editor's wrapper.
*
* @param {Object|null} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object|null} Updated state.
*/
export function iframedEditorCanvasWrapper( state = null, action ) {
if ( action.type === 'SET_IFRAMED_EDITOR_CANVAS_WRAPPER' ) {
return action.node;
}
return state;
}

Addison-Stavlo marked this conversation as resolved.
Show resolved Hide resolved
export default combineReducers( {
blocks,
isTyping,
Expand All @@ -1732,4 +1747,5 @@ export default combineReducers( {
automaticChangeStatus,
highlightedBlock,
lastBlockInserted,
iframedEditorCanvasWrapper,
} );
11 changes: 11 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2245,3 +2245,14 @@ export function wasBlockJustInserted( state, clientId, source ) {
lastBlockInserted.source === source
);
}

/**
* Returns the current editor wrapper node for the iframed editor.
*
* @param {Object} state Global application state.
*
* @return {Object|null} The iframed editor wrapper's html node.
*/
export function __unstableGetIframedEditorCanvasWrapper( state ) {
return state.iframedEditorCanvasWrapper;
}
12 changes: 12 additions & 0 deletions packages/block-editor/src/store/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const {
updateSettings,
selectionChange,
validateBlocksToTemplate,
__unstableSetIframedEditorCanvasWrapper,
} = actions;

describe( 'actions', () => {
Expand Down Expand Up @@ -1531,4 +1532,15 @@ describe( 'actions', () => {
expect( result ).toEqual( false );
} );
} );

describe( '__unstableSetIframedEditorCanvasWrapper', () => {
it( 'should return the SET_IFRAMED_EDITOR_CANVAS_WRAPPER action (string) and input node (object)', () => {
const node = {};
const result = __unstableSetIframedEditorCanvasWrapper( node );
expect( result ).toEqual( {
type: 'SET_IFRAMED_EDITOR_CANVAS_WRAPPER',
node,
} );
} );
} );
} );
28 changes: 28 additions & 0 deletions packages/block-editor/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
blockListSettings,
lastBlockAttributesChange,
lastBlockInserted,
iframedEditorCanvasWrapper,
} from '../reducer';

describe( 'state', () => {
Expand Down Expand Up @@ -2969,4 +2970,31 @@ describe( 'state', () => {
expect( state ).toEqual( expectedState );
} );
} );

describe( 'iframedEditorCanvasWrapper()', () => {
it( 'should return null if nothing has been set', () => {
const initialState = iframedEditorCanvasWrapper( undefined, {} );
expect( initialState ).toBe( null );
} );

it( 'should set action.node when type "SET_IFRAMED_EDITOR_CANVAS_WRAPPER"', () => {
const node = {};
const action = {
type: 'SET_IFRAMED_EDITOR_CANVAS_WRAPPER',
node,
};
const state = iframedEditorCanvasWrapper( undefined, action );
expect( state ).toBe( node );
} );

it( 'should not change state with other action types', () => {
const initialState = {};
const action = {
type: 'FOOBAR',
node: {},
};
const state = iframedEditorCanvasWrapper( initialState, action );
expect( state ).toBe( initialState );
} );
} );
} );
11 changes: 11 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const {
__unstableGetClientIdsTree,
__experimentalGetPatternTransformItems,
wasBlockJustInserted,
__unstableGetIframedEditorCanvasWrapper,
} = selectors;

describe( 'selectors', () => {
Expand Down Expand Up @@ -3842,3 +3843,13 @@ describe( '__unstableGetClientIdsTree', () => {
] );
} );
} );

describe( '__unstableGetIframedEditorCanvasWrapper', () => {
it( 'should return the iframedEditorCanvasWrapper state', () => {
const state = {
iframedEditorCanvasWrapper: {},
};
const result = __unstableGetIframedEditorCanvasWrapper( state );
expect( result ).toBe( state.iframedEditorCanvasWrapper );
} );
} );
4 changes: 3 additions & 1 deletion packages/components/src/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ const Popover = (
__unstableBoundaryParent,
__unstableForcePosition,
__unstableForceXAlignment,
__unstableEditorCanvasWrapper,
/* eslint-enable no-unused-vars */
...contentProps
},
Expand Down Expand Up @@ -352,7 +353,8 @@ const Popover = (
relativeOffsetTop,
boundaryElement,
__unstableForcePosition,
__unstableForceXAlignment
__unstableForceXAlignment,
__unstableEditorCanvasWrapper
);

if (
Expand Down
99 changes: 65 additions & 34 deletions packages/components/src/popover/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,18 @@ export function computePopoverXAxisPosition(
/**
* 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 {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(
Expand All @@ -181,18 +181,47 @@ export function computePopoverYAxisPosition(
stickyBoundaryElement,
anchorRef,
relativeOffsetTop,
forcePosition
forcePosition,
editorWrapper
) {
const { height } = contentSize;

if ( stickyBoundaryElement ) {
const stickyRect = stickyBoundaryElement.getBoundingClientRect();
const stickyPosition = stickyRect.top + height - relativeOffsetTop;

if ( anchorRect.top <= stickyPosition ) {
const stickyPositionTop = stickyRect.top + height - relativeOffsetTop;
const stickyPositionBottom =
stickyRect.bottom - height - relativeOffsetTop;
Copons marked this conversation as resolved.
Show resolved Hide resolved

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, stickyPosition ),
popoverTop: Math.min( anchorRect.bottom, stickyPositionTop ),
};
}
}
Expand Down Expand Up @@ -274,22 +303,22 @@ export function computePopoverYAxisPosition(
}

/**
* 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
* 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(
Expand All @@ -301,7 +330,8 @@ export function computePopoverPosition(
relativeOffsetTop,
boundaryElement,
forcePosition,
forceXAlignment
forceXAlignment,
editorWrapper
) {
const [ yAxis, xAxis = 'center', corner ] = position.split( ' ' );

Expand All @@ -313,7 +343,8 @@ export function computePopoverPosition(
stickyBoundaryElement,
anchorRef,
relativeOffsetTop,
forcePosition
forcePosition,
editorWrapper
);
const xAxisPosition = computePopoverXAxisPosition(
anchorRect,
Expand Down