From 05beb0c092be2ba064101da2380096d8bb375f1c Mon Sep 17 00:00:00 2001 From: Shreya Shrivastava Date: Wed, 29 Apr 2026 17:02:07 +0530 Subject: [PATCH 1/3] fix(block-popover): defer popover recompute to next animation frame to prevent toolbar position shift --- .../src/components/block-popover/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 06409bc6e65f42..8730c108d73709 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -10,6 +10,7 @@ import { useMergeRefs } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { forwardRef, + useCallback, useMemo, useReducer, useLayoutEffect, @@ -52,6 +53,10 @@ function BlockPopover( 0 ); + const debouncedRecompute = useCallback( () => { + window.requestAnimationFrame( () => forceRecomputePopoverDimensions() ); + }, [ forceRecomputePopoverDimensions ] ); + // When blocks are moved up/down, they are animated to their new position by // updating the `transform` property manually (i.e. without using CSS // transitions or animations). The animation, which can also scroll the block @@ -64,15 +69,13 @@ function BlockPopover( return; } - const observer = new window.MutationObserver( - forceRecomputePopoverDimensions - ); + const observer = new window.MutationObserver( debouncedRecompute ); observer.observe( selectedElement, { attributes: true } ); return () => { observer.disconnect(); }; - }, [ selectedElement ] ); + }, [ selectedElement, debouncedRecompute ] ); const popoverAnchor = useMemo( () => { if ( From 446f199b2285e5fe93d8b6e3486f56123fc08816 Mon Sep 17 00:00:00 2001 From: Shreya Shrivastava Date: Thu, 30 Apr 2026 11:05:50 +0530 Subject: [PATCH 2/3] refactor: simplify rAF call --- .../src/components/block-popover/index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 8730c108d73709..9264dca70fc182 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -10,7 +10,6 @@ import { useMergeRefs } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { forwardRef, - useCallback, useMemo, useReducer, useLayoutEffect, @@ -53,10 +52,6 @@ function BlockPopover( 0 ); - const debouncedRecompute = useCallback( () => { - window.requestAnimationFrame( () => forceRecomputePopoverDimensions() ); - }, [ forceRecomputePopoverDimensions ] ); - // When blocks are moved up/down, they are animated to their new position by // updating the `transform` property manually (i.e. without using CSS // transitions or animations). The animation, which can also scroll the block @@ -69,13 +64,17 @@ function BlockPopover( return; } - const observer = new window.MutationObserver( debouncedRecompute ); + const observer = new window.MutationObserver( () => + window.requestAnimationFrame( () => + forceRecomputePopoverDimensions() + ) + ); observer.observe( selectedElement, { attributes: true } ); return () => { observer.disconnect(); }; - }, [ selectedElement, debouncedRecompute ] ); + }, [ selectedElement ] ); const popoverAnchor = useMemo( () => { if ( From 9b8ac11c5d652ef8755eb66d381652e3ea55177f Mon Sep 17 00:00:00 2001 From: Shreya Shrivastava Date: Tue, 5 May 2026 19:44:04 +0530 Subject: [PATCH 3/3] refactor: coalesce MutationObserver callbacks via rAF --- .../src/components/block-popover/index.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 9264dca70fc182..c49e990b59e546 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -52,27 +52,35 @@ function BlockPopover( 0 ); - // When blocks are moved up/down, they are animated to their new position by - // updating the `transform` property manually (i.e. without using CSS - // transitions or animations). The animation, which can also scroll the block - // editor, can sometimes cause the position of the Popover to get out of sync. - // A MutationObserver is therefore used to make sure that changes to the - // selectedElement's attribute (i.e. `transform`) can be tracked and used to - // trigger the Popover to rerender. + // `useMovingAnimation` writes the block's `transform` on every spring tick. + // Reacting synchronously to each mutation would race with Floating UI's own + // autoUpdate frame loop and cause the toolbar to visibly jump. Coalescing + // to one recompute per animation frame avoids that. The observer can't + // simply be removed: with autoUpdate's animationFrame mode alone, the + // toolbar trails the block by ~1 frame because the spring's rAF and + // autoUpdate's rAF are independently scheduled. useLayoutEffect( () => { if ( ! selectedElement ) { return; } - const observer = new window.MutationObserver( () => - window.requestAnimationFrame( () => - forceRecomputePopoverDimensions() - ) - ); + 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 ] );