Skip to content

Commit ed8d25e

Browse files
authored
Block Toolbar: update position when moving blocks (#44301)
* Block Toolbar: update position when moving blocks * Make sure counter does not overflow * Add explainer for "< 0" check * Apply same enhancements to inbetween inserter
1 parent afd40a5 commit ed8d25e

File tree

2 files changed

+81
-12
lines changed

2 files changed

+81
-12
lines changed

packages/block-editor/src/components/block-popover/inbetween.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { store as blockEditorStore } from '../../store';
2323
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
2424
import usePopoverScroll from './use-popover-scroll';
2525

26+
const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER;
27+
2628
export const InsertionPointOpenRef = createContext();
2729

2830
function BlockPopoverInbetween( {
@@ -34,8 +36,9 @@ function BlockPopoverInbetween( {
3436
...props
3537
} ) {
3638
// This is a temporary hack to get the inbetween inserter to recompute properly.
37-
const [ positionRecompute, forceRecompute ] = useReducer(
38-
( s ) => s + 1,
39+
const [ popoverRecomputeCounter, forcePopoverRecompute ] = useReducer(
40+
// Module is there to make sure that the counter doesn't overflow.
41+
( s ) => ( s + 1 ) % MAX_POPOVER_RECOMPUTE_COUNTER,
3942
0
4043
);
4144

@@ -66,7 +69,14 @@ function BlockPopoverInbetween( {
6669
const nextElement = useBlockElement( nextClientId );
6770
const isVertical = orientation === 'vertical';
6871
const style = useMemo( () => {
69-
if ( ( ! previousElement && ! nextElement ) || ! isVisible ) {
72+
if (
73+
// popoverRecomputeCounter is by definition always equal or greater than 0.
74+
// This check is only there to satisfy the correctness of the
75+
// exhaustive-deps rule for the `useMemo` hook.
76+
popoverRecomputeCounter < 0 ||
77+
( ! previousElement && ! nextElement ) ||
78+
! isVisible
79+
) {
7080
return {};
7181
}
7282

@@ -102,12 +112,19 @@ function BlockPopoverInbetween( {
102112
previousElement,
103113
nextElement,
104114
isVertical,
105-
positionRecompute,
115+
popoverRecomputeCounter,
106116
isVisible,
107117
] );
108118

109119
const popoverAnchor = useMemo( () => {
110-
if ( ( ! previousElement && ! nextElement ) || ! isVisible ) {
120+
if (
121+
// popoverRecomputeCounter is by definition always equal or greater than 0.
122+
// This check is only there to satisfy the correctness of the
123+
// exhaustive-deps rule for the `useMemo` hook.
124+
popoverRecomputeCounter < 0 ||
125+
( ! previousElement && ! nextElement ) ||
126+
! isVisible
127+
) {
111128
return undefined;
112129
}
113130

@@ -161,19 +178,26 @@ function BlockPopoverInbetween( {
161178
}, [
162179
previousElement,
163180
nextElement,
164-
positionRecompute,
181+
popoverRecomputeCounter,
165182
isVertical,
166183
isVisible,
167184
] );
168185

169186
const popoverScrollRef = usePopoverScroll( __unstableContentRef );
170187

171188
// This is only needed for a smooth transition when moving blocks.
189+
// When blocks are moved up/down, their position can be set by
190+
// updating the `transform` property manually (i.e. without using CSS
191+
// transitions or animations). The animation, which can also scroll the block
192+
// editor, can sometimes cause the position of the Popover to get out of sync.
193+
// A MutationObserver is therefore used to make sure that changes to the
194+
// selectedElement's attribute (i.e. `transform`) can be tracked and used to
195+
// trigger the Popover to rerender.
172196
useLayoutEffect( () => {
173197
if ( ! previousElement ) {
174198
return;
175199
}
176-
const observer = new window.MutationObserver( forceRecompute );
200+
const observer = new window.MutationObserver( forcePopoverRecompute );
177201
observer.observe( previousElement, { attributes: true } );
178202

179203
return () => {
@@ -185,7 +209,7 @@ function BlockPopoverInbetween( {
185209
if ( ! nextElement ) {
186210
return;
187211
}
188-
const observer = new window.MutationObserver( forceRecompute );
212+
const observer = new window.MutationObserver( forcePopoverRecompute );
189213
observer.observe( nextElement, { attributes: true } );
190214

191215
return () => {
@@ -199,12 +223,12 @@ function BlockPopoverInbetween( {
199223
}
200224
previousElement.ownerDocument.defaultView.addEventListener(
201225
'resize',
202-
forceRecompute
226+
forcePopoverRecompute
203227
);
204228
return () => {
205229
previousElement.ownerDocument.defaultView.removeEventListener(
206230
'resize',
207-
forceRecompute
231+
forcePopoverRecompute
208232
);
209233
};
210234
}, [ previousElement ] );

packages/block-editor/src/components/block-popover/index.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import classnames from 'classnames';
88
*/
99
import { useMergeRefs } from '@wordpress/compose';
1010
import { Popover } from '@wordpress/components';
11-
import { forwardRef, useMemo } from '@wordpress/element';
11+
import {
12+
forwardRef,
13+
useMemo,
14+
useReducer,
15+
useLayoutEffect,
16+
} from '@wordpress/element';
1217

1318
/**
1419
* Internal dependencies
1520
*/
1621
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
1722
import usePopoverScroll from './use-popover-scroll';
1823

24+
const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER;
25+
1926
function BlockPopover(
2027
{
2128
clientId,
@@ -47,8 +54,41 @@ function BlockPopover(
4754
};
4855
}, [ selectedElement, lastSelectedElement, __unstableRefreshSize ] );
4956

57+
const [ popoverAnchorRecomputeCounter, forceRecomputePopoverAnchor ] =
58+
useReducer(
59+
// Module is there to make sure that the counter doesn't overflow.
60+
( s ) => ( s + 1 ) % MAX_POPOVER_RECOMPUTE_COUNTER,
61+
0
62+
);
63+
64+
// When blocks are moved up/down, they are animated to their new position by
65+
// updating the `transform` property manually (i.e. without using CSS
66+
// transitions or animations). The animation, which can also scroll the block
67+
// editor, can sometimes cause the position of the Popover to get out of sync.
68+
// A MutationObserver is therefore used to make sure that changes to the
69+
// selectedElement's attribute (i.e. `transform`) can be tracked and used to
70+
// trigger the Popover to rerender.
71+
useLayoutEffect( () => {
72+
if ( ! selectedElement ) {
73+
return;
74+
}
75+
76+
const observer = new window.MutationObserver(
77+
forceRecomputePopoverAnchor
78+
);
79+
observer.observe( selectedElement, { attributes: true } );
80+
81+
return () => {
82+
observer.disconnect();
83+
};
84+
}, [ selectedElement ] );
85+
5086
const popoverAnchor = useMemo( () => {
5187
if (
88+
// popoverAnchorRecomputeCounter is by definition always equal or greater
89+
// than 0. This check is only there to satisfy the correctness of the
90+
// exhaustive-deps rule for the `useMemo` hook.
91+
popoverAnchorRecomputeCounter < 0 ||
5292
! selectedElement ||
5393
( bottomClientId && ! lastSelectedElement )
5494
) {
@@ -88,7 +128,12 @@ function BlockPopover(
88128
},
89129
ownerDocument: selectedElement.ownerDocument,
90130
};
91-
}, [ bottomClientId, lastSelectedElement, selectedElement ] );
131+
}, [
132+
bottomClientId,
133+
lastSelectedElement,
134+
selectedElement,
135+
popoverAnchorRecomputeCounter,
136+
] );
92137

93138
if ( ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) {
94139
return null;

0 commit comments

Comments
 (0)