Block Toolbar: Prevent position shifts when using mover control#77798
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| const debouncedRecompute = useCallback( () => { | ||
| window.requestAnimationFrame( () => forceRecomputePopoverDimensions() ); | ||
| }, [ forceRecomputePopoverDimensions ] ); |
There was a problem hiding this comment.
Thanks for the feedback! Removed the useCallback wrapper and inlined the requestAnimationFrame without the extra memoization.
2b24937 to
4cd2c44
Compare
There was a problem hiding this comment.
Pull request overview
This PR aims to stabilize the block toolbar’s positioning during block move animations by deferring popover dimension recomputation so it doesn’t react mid-frame to frequent transform attribute updates.
Changes:
- Adjusted the
MutationObserverhandler inBlockPopoverto deferforceRecomputePopoverDimensionsviarequestAnimationFrame.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Thank you for working on this! While this PR seems to fix the issue, it's likely fixing a symptom, rather than curing the root cause. Here is a more detailed explanation of what's going on IMOThe What's happening on
That continuous teardown/rebuild fighting with the in-flight If that analysis is right, then the right fix is most likely to remove the Can we try just removing the Example code changes import {
forwardRef,
useMemo,
- useReducer,
- useLayoutEffect,
} from '@wordpress/element';
...
-const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER;
-
function BlockPopover( ... ) {
- const [
- popoverDimensionsRecomputeCounter,
- forceRecomputePopoverDimensions,
- ] = useReducer(
- ( s ) => ( s + 1 ) % MAX_POPOVER_RECOMPUTE_COUNTER,
- 0
- );
-
- useLayoutEffect( () => {
- if ( ! selectedElement ) {
- return;
- }
- const observer = new window.MutationObserver( () =>
- window.requestAnimationFrame( () =>
- forceRecomputePopoverDimensions()
- )
- );
- observer.observe( selectedElement, { attributes: true } );
- return () => observer.disconnect();
- }, [ selectedElement ] );
const popoverAnchor = useMemo( () => {
if (
- popoverDimensionsRecomputeCounter < 0 ||
! selectedElement ||
( bottomClientId && ! lastSelectedElement )
) {
return undefined;
}
return {
getBoundingClientRect() { ... },
contextElement: selectedElement,
};
- }, [ popoverDimensionsRecomputeCounter, selectedElement, bottomClientId, lastSelectedElement ] );
+ }, [ selectedElement, bottomClientId, lastSelectedElement ] );If removing it does regress some scenario, the next-best alternative is to keep the observer but stop having it change |
|
@ciampo Thanks for the detailed breakdown! Confirmed: removing the MutationObserver entirely does regress things including the changes of this PR. In the original mover scenario, the toolbar drifts to overlap the block during the spring animation rather than tracking above it. I've attached a video showing the regression. Screen.Recording.2026-05-04.at.3.43.48.PM.movSo I'd like to explore your secondary suggestion: keep the observer but avoid changing The idea would be something like: const updateRef = useRef();
// Expose Floating UI's update() via a callback ref on the Popover
// so the observer can call it directly without touching the anchor object.
useLayoutEffect( () => {
if ( ! selectedElement ) return;
const observer = new window.MutationObserver( () => {
updateRef.current?.();
} );
observer.observe( selectedElement, { attributes: true } );
return () => observer.disconnect();
}, [ selectedElement ] );The anchor identity stays stable, so I checked the current Popover source — Alternatively, rAF coalescing on the existing counter approach might also work — keeping the observer and the counter but limiting setReference() to at most once per frame, which would match the cadence of the spring animation and avoid the visible jump without needing any changes to Popover. Something like this: useLayoutEffect( () => {
if ( ! selectedElement ) {
return;
}
let rafId;
const observer = new window.MutationObserver( () => {
if ( rafId ) {
return;
}
rafId = window.requestAnimationFrame( () => {
rafId = null;
forceRecomputePopoverDimensions();
} );
} );
observer.observe( selectedElement, { attributes: true } );
return () => {
observer.disconnect();
if ( rafId ) {
window.cancelAnimationFrame( rafId );
}
};
}, [ selectedElement ] );Happy to explore either path. cc: @Mamaduka |
|
Thanks for the regression video and the two alternatives — that nails down what the observer is actually buying us. Quick framing on why we still need it: the drift you captured is a one-frame ordering issue between the spring's rAF (in Between your two options, let's go with your rAF-coalesce variant (the second one). I'd skip exposing
Two small things to add on top of your snippet:
Happy to take another look once it's pushed. |
4cd2c44 to
9b8ac11
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Pushed the changes with the rAF coalescing approach. Tested the following scenarios and attached videos: Post editor - mover controls and rapid clicks post-editor.movSite Editor - mover controls and rapid clicks site-ediotr.movprefers-reduced-motion reduced-motion.mov |
There was a problem hiding this comment.
LGTM 🚀
@Mamaduka does it look good for you, too?
@shrivastavanolo , do you mind updating the PR description so that it reflect the latest changes, including the latest screen captures?
Mamaduka
left a comment
There was a problem hiding this comment.
Sorry for the late review. The change looks good. Thanks, @shrivastavanolo!
What?
Closes #61435
Stops the block toolbar from jumping around when using the mover controls.
Why?
When a block is moved, useMovingAnimation writes transform on every spring tick. The MutationObserver was firing synchronously on each mutation, racing with Floating UI's own autoUpdate frame loop — causing the toolbar to visibly jump mid-animation.
How?
Coalesced the MutationObserver callbacks via requestAnimationFrame so at most one recompute is triggered per frame, matching the cadence of the spring animation. Also added proper cleanup via cancelAnimationFrame on unmount to prevent stale updates.
The observer itself can't simply be removed: without it, Floating UI's animationFrame mode alone causes the toolbar to trail the block by ~1 frame throughout the animation because the spring's rAF and autoUpdate's rAF are independently scheduled.
Testing Instructions
Screenshots or screencast
Before
before.mov
After (Post editor)
post-editor.mov
After (Site editor)
site-ediotr.mov
After (reduced-motion enabled)
reduced-motion.mov
Use of AI Tools
Claude (Anthropic) was used to help draft this PR. All changes were reviewed manually.
note: please check this for further explanation