Skip to content

Block Toolbar: Prevent position shifts when using mover control#77798

Open
shrivastavanolo wants to merge 2 commits intoWordPress:trunkfrom
shrivastavanolo:fix/block-toolbar-position-mover
Open

Block Toolbar: Prevent position shifts when using mover control#77798
shrivastavanolo wants to merge 2 commits intoWordPress:trunkfrom
shrivastavanolo:fix/block-toolbar-position-mover

Conversation

@shrivastavanolo
Copy link
Copy Markdown
Contributor

@shrivastavanolo shrivastavanolo commented Apr 29, 2026

What?

Closes #61435

Stops the block toolbar from jumping around when using the mover controls.

Why?

When a block is moved, its transform attribute updates on every animation frame. The toolbar was repositioning itself on each of those updates — mid-animation — causing it to shift erratically.

How?

Wrapped the popover recompute in requestAnimationFrame so it waits for the current animation frame to finish before repositioning, instead of firing on every transform change.

Testing Instructions

  1. Open a post with many blocks
  2. Select the first block and scroll down until it's partially off-screen
  3. Click the mover arrows up and down repeatedly
  4. Verify the toolbar stays in place and doesn't jump

Screenshots or screencast

Before

before.mov

After

after.mov

Use of AI Tools

Claude (Anthropic) was used to help draft this PR. All changes were reviewed manually.

@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Apr 29, 2026
@shrivastavanolo shrivastavanolo marked this pull request as ready for review April 29, 2026 12:51
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: shrivastavanolo <shreya0shrivastava@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@Mamaduka Mamaduka added General Interface Parts of the UI which don't fall neatly under other labels. [Type] Bug An existing feature does not function as intended labels Apr 29, 2026
Comment on lines +56 to +58
const debouncedRecompute = useCallback( () => {
window.requestAnimationFrame( () => forceRecomputePopoverDimensions() );
}, [ forceRecomputePopoverDimensions ] );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new debouncedRecompute shouldn't be needed; we could just inline rAF and call it directly, no redundant memoization or dependencies.

cc @ciampo, it looks like you worked on previous optimizaton (#44301), any suggestion here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Removed the useCallback wrapper and inlined the requestAnimationFrame without the extra memoization.

@shrivastavanolo shrivastavanolo force-pushed the fix/block-toolbar-position-mover branch from 2b24937 to 4cd2c44 Compare April 30, 2026 05:38
@ciampo ciampo requested a review from Copilot April 30, 2026 08:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MutationObserver handler in BlockPopover to defer forceRecomputePopoverDimensions via requestAnimationFrame.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to 71
const observer = new window.MutationObserver( () =>
window.requestAnimationFrame( () =>
forceRecomputePopoverDimensions()
)
);
@ciampo
Copy link
Copy Markdown
Contributor

ciampo commented Apr 30, 2026

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 IMO

The MutationObserver was added in #44301 to make the toolbar follow blocks during the mover animation. At that point, the Popover component had already been switched to Floating UI's autoUpdate({ animationFrame: true }) in #43617 — and that PR explicitly stated that the previous __unstableObservedElement/MutationObserver mechanism was "not necessary anymore" precisely because animationFrame: true polls getBoundingClientRect() every frame.

What's happening on trunk today:

  • useMovingAnimation writes style.transform on every spring tick.
  • The MutationObserver here fires synchronously after each mutation and bumps popoverDimensionsRecomputeCounter.
  • That counter is in the useMemo deps for popoverAnchor, so a new virtual element object is returned on every frame.
  • Popover has a useLayoutEffect that calls refs.setReference( ... ) whenever anchor changes identity. Floating UI's useFloating then runs its whileElementsMounted cleanup and re-creates autoUpdate from scratch (cancels the running animation-frame loop, re-sets up ResizeObserver and ancestor listeners, calls update() once).
  • Meanwhile, the existing autoUpdate frameLoop (which is already calling our anchor's getBoundingClientRect() every frame and calling update() whenever the rect differs) gets torn down and rebuilt on every animation tick.

That continuous teardown/rebuild fighting with the in-flight frameLoop is, I believe, what produces the visible jump.

If that analysis is right, then the right fix is most likely to remove the MutationObserver entirely and rely on Floating UI's per-frame autoUpdate, which is already doing the job of calling our anchor's custom getBoundingClientRect() (which returns rectUnion( selectedElement, lastSelectedElement ) already). No invalidation step needed.

Can we try just removing the MutationObserver and the reducer/counter, and re-test the original mover scenario plus the cases the observer was introduced for (#44220, #44281)? That feels like the real fix to me.

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 popoverAnchor's identity. We should not be calling setReference on every animation frame; that's the expensive operation. Triggering a position-only update (e.g. via the floating UI update function exposed by the Popover consumer, or by reading selectedElement.getBoundingClientRect() directly) would avoid the rebuild churn even if we keep an observer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

General Interface Parts of the UI which don't fall neatly under other labels. [Package] Block editor /packages/block-editor [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Block Toolbar: Position shifts when using mover control

4 participants