From 4f9ae7d5b7d5da04c81b97723f21ab89c9904b3c Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 16 Dec 2023 15:30:39 +0100 Subject: [PATCH 1/4] wip --- .../components/use-moving-animation/index.js | 102 +++++++----------- 1 file changed, 40 insertions(+), 62 deletions(-) diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index e6c888330fca71..9cf719df62c46e 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -1,29 +1,15 @@ /** * External dependencies */ -import { useSpring } from '@react-spring/web'; +import { Controller, SpringValue } from '@react-spring/web'; /** * WordPress dependencies */ -import { - useState, - useLayoutEffect, - useReducer, - useMemo, - useRef, -} from '@wordpress/element'; +import { useState, useLayoutEffect, useMemo, useRef } from '@wordpress/element'; import { useReducedMotion } from '@wordpress/compose'; import { getScrollContainer } from '@wordpress/dom'; -/** - * Simple reducer used to increment a counter. - * - * @param {number} state Previous counter value. - * @return {number} New state value. - */ -const counterReducer = ( state ) => state + 1; - const getAbsolutePosition = ( element ) => { return { top: element.offsetTop, @@ -56,12 +42,6 @@ function useMovingAnimation( { } ) { const ref = useRef(); const prefersReducedMotion = useReducedMotion() || ! enableAnimation; - const [ triggeredAnimation, triggerAnimation ] = useReducer( - counterReducer, - 0 - ); - const [ finishedAnimation, endAnimation ] = useReducer( counterReducer, 0 ); - const [ transform, setTransform ] = useState( { x: 0, y: 0 } ); const previous = useMemo( () => ( ref.current ? getAbsolutePosition( ref.current ) : null ), [ triggerAnimationOnChange ] @@ -91,11 +71,36 @@ function useMovingAnimation( { }; }, [ triggerAnimationOnChange, adjustScrolling ] ); + // Initialize SpringValue and Controller + const [ controller, setController ] = useState( null ); + useLayoutEffect( () => { - if ( triggeredAnimation ) { - endAnimation(); + const springConfig = { mass: 5, tension: 2000, friction: 200 }; + function onChange( { value } ) { + if ( ! ref.current ) { + return; + } + let { x, y } = value; + x = Math.round( x ); + y = Math.round( y ); + const finishedMoving = x === 0 && y === 0; + ref.current.style.transformOrigin = 'center center'; + ref.current.style.transform = finishedMoving + ? null // Set to `null` to explicitly remove the transform. + : `translate3d(${ x }px,${ y }px,0)`; + ref.current.style.zIndex = isSelected ? '1' : ''; + + preserveScrollPosition(); } - }, [ triggeredAnimation ] ); + setController( + new Controller( { + x: new SpringValue( 0, springConfig ), + y: new SpringValue( 0, springConfig ), + onChange, + } ) + ); + }, [ isSelected, preserveScrollPosition ] ); + useLayoutEffect( () => { if ( ! previous ) { return; @@ -112,44 +117,17 @@ function useMovingAnimation( { ref.current.style.transform = undefined; const destination = getAbsolutePosition( ref.current ); - triggerAnimation(); - setTransform( { - x: Math.round( previous.left - destination.left ), - y: Math.round( previous.top - destination.top ), - } ); - }, [ triggerAnimationOnChange ] ); + const x = Math.round( previous.left - destination.left ); + const y = Math.round( previous.top - destination.top ); - function onChange( { value } ) { - if ( ! ref.current ) { - return; - } - let { x, y } = value; - x = Math.round( x ); - y = Math.round( y ); - const finishedMoving = x === 0 && y === 0; - ref.current.style.transformOrigin = 'center center'; - ref.current.style.transform = finishedMoving - ? null // Set to `null` to explicitly remove the transform. - : `translate3d(${ x }px,${ y }px,0)`; - ref.current.style.zIndex = isSelected ? '1' : ''; - - preserveScrollPosition(); - } - - useSpring( { - from: { - x: transform.x, - y: transform.y, - }, - to: { - x: 0, - y: 0, - }, - reset: triggeredAnimation !== finishedAnimation, - config: { mass: 5, tension: 2000, friction: 200 }, - immediate: prefersReducedMotion, - onChange, - } ); + // ref.current.style.transformOrigin = 'center center'; + // ref.current.style.transform = `translate3d(${ x }px,${ y }px,0)`; + + controller.start( { x: 0, y: 0, from: { x, y } } ).then( () => {} ); + + // controller.update( { x, y } ); + // controller.start(); + }, [ triggerAnimationOnChange, controller ] ); return ref; } From c29fafb0b433e238dc3d65c1b0ff7b57ed0cf046 Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 16 Dec 2023 16:07:27 +0100 Subject: [PATCH 2/4] Fix everything --- .../components/use-moving-animation/index.js | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index 9cf719df62c46e..b8b9ed1d82db6a 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -1,21 +1,21 @@ /** * External dependencies */ -import { Controller, SpringValue } from '@react-spring/web'; +import { Controller } from '@react-spring/web'; /** * WordPress dependencies */ -import { useState, useLayoutEffect, useMemo, useRef } from '@wordpress/element'; +import { useLayoutEffect, useMemo, useRef } from '@wordpress/element'; import { useReducedMotion } from '@wordpress/compose'; import { getScrollContainer } from '@wordpress/dom'; -const getAbsolutePosition = ( element ) => { +function getAbsolutePosition( element ) { return { top: element.offsetTop, left: element.offsetLeft, }; -}; +} /** * Hook used to compute the styles required to move a div into a new position. @@ -42,92 +42,86 @@ function useMovingAnimation( { } ) { const ref = useRef(); const prefersReducedMotion = useReducedMotion() || ! enableAnimation; - const previous = useMemo( - () => ( ref.current ? getAbsolutePosition( ref.current ) : null ), + + // We do not want to trigger the animation when a block gets selected, so we + // use a ref to keep track of the value for use later in a callback. + const isSelectedRef = useRef( isSelected ); + const adjustScrollingRef = useRef( adjustScrolling ); + + useLayoutEffect( () => { + isSelectedRef.current = isSelected; + adjustScrollingRef.current = adjustScrolling; + }, [ isSelected, adjustScrolling ] ); + + // Whenever the trigger changes, we need to take a snapshot of the current + // position of the block to use it as a destination point for the animation. + const { previous, prevRect } = useMemo( + () => ( { + previous: ref.current && getAbsolutePosition( ref.current ), + prevRect: ref.current && ref.current.getBoundingClientRect(), + } ), + // eslint-disable-next-line react-hooks/exhaustive-deps [ triggerAnimationOnChange ] ); - // Calculate the previous position of the block relative to the viewport and - // return a function to maintain that position by scrolling. - const preserveScrollPosition = useMemo( () => { - if ( ! adjustScrolling || ! ref.current ) { - return () => {}; + useLayoutEffect( () => { + if ( ! previous || ! ref.current ) { + return; } const scrollContainer = getScrollContainer( ref.current ); - if ( ! scrollContainer ) { - return () => {}; - } + function preserveScrollPosition() { + if ( adjustScrollingRef.current && prevRect ) { + const blockRect = ref.current.getBoundingClientRect(); + const diff = blockRect.top - prevRect.top; - const prevRect = ref.current.getBoundingClientRect(); - return () => { - const blockRect = ref.current.getBoundingClientRect(); - const diff = blockRect.top - prevRect.top; - - if ( diff ) { - scrollContainer.scrollTop += diff; + if ( diff ) { + scrollContainer.scrollTop += diff; + } } - }; - }, [ triggerAnimationOnChange, adjustScrolling ] ); - - // Initialize SpringValue and Controller - const [ controller, setController ] = useState( null ); - - useLayoutEffect( () => { - const springConfig = { mass: 5, tension: 2000, friction: 200 }; - function onChange( { value } ) { - if ( ! ref.current ) { - return; - } - let { x, y } = value; - x = Math.round( x ); - y = Math.round( y ); - const finishedMoving = x === 0 && y === 0; - ref.current.style.transformOrigin = 'center center'; - ref.current.style.transform = finishedMoving - ? null // Set to `null` to explicitly remove the transform. - : `translate3d(${ x }px,${ y }px,0)`; - ref.current.style.zIndex = isSelected ? '1' : ''; - - preserveScrollPosition(); - } - setController( - new Controller( { - x: new SpringValue( 0, springConfig ), - y: new SpringValue( 0, springConfig ), - onChange, - } ) - ); - }, [ isSelected, preserveScrollPosition ] ); - - useLayoutEffect( () => { - if ( ! previous ) { - return; } if ( prefersReducedMotion ) { // If the animation is disabled and the scroll needs to be adjusted, // just move directly to the final scroll position. preserveScrollPosition(); - return; } + const controller = new Controller( { + x: 0, + y: 0, + config: { mass: 5, tension: 2000, friction: 200 }, + onChange( { value } ) { + if ( ! ref.current ) { + return; + } + let { x, y } = value; + x = Math.round( x ); + y = Math.round( y ); + const finishedMoving = x === 0 && y === 0; + ref.current.style.transformOrigin = 'center center'; + ref.current.style.transform = finishedMoving + ? null // Set to `null` to explicitly remove the transform. + : `translate3d(${ x }px,${ y }px,0)`; + ref.current.style.zIndex = isSelectedRef.current ? '1' : ''; + preserveScrollPosition(); + }, + } ); + ref.current.style.transform = undefined; const destination = getAbsolutePosition( ref.current ); const x = Math.round( previous.left - destination.left ); const y = Math.round( previous.top - destination.top ); - // ref.current.style.transformOrigin = 'center center'; - // ref.current.style.transform = `translate3d(${ x }px,${ y }px,0)`; + controller.start( { x: 0, y: 0, from: { x, y } } ); - controller.start( { x: 0, y: 0, from: { x, y } } ).then( () => {} ); - - // controller.update( { x, y } ); - // controller.start(); - }, [ triggerAnimationOnChange, controller ] ); + return () => { + controller.stop(); + }; + }, [ previous, prevRect, prefersReducedMotion ] ); return ref; } From 321b9631c42a6ee30a81f335d6af3aa4a5953ee0 Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 16 Dec 2023 19:37:16 +0100 Subject: [PATCH 3/4] Check reduced motion and call selectors in callback --- .../src/components/block-list/block.js | 23 ------ .../block-list/use-block-props/index.js | 10 +-- .../components/use-moving-animation/index.js | 78 ++++++++++++------- 3 files changed, 52 insertions(+), 59 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 0bd5d0b7e199f8..c053235c2d0d36 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -39,12 +39,6 @@ import { PrivateBlockContext } from './private-block-context'; import { unlock } from '../../lock-unlock'; -/** - * If the block count exceeds the threshold, we disable the reordering animation - * to avoid laginess. - */ -const BLOCK_ANIMATION_THRESHOLD = 200; - /** * Merges wrapper props with special handling for classNames and styles. * @@ -516,9 +510,7 @@ function BlockListBlockProvider( props ) { getBlockIndex, isTyping, - getGlobalBlockCount, isBlockMultiSelected, - isAncestorMultiSelected, isBlockSubtreeDisabled, isBlockHighlighted, __unstableIsFullySelected, @@ -550,9 +542,6 @@ function BlockListBlockProvider( props ) { const canRemove = canRemoveBlock( clientId, rootClientId ); const canMove = canMoveBlock( clientId, rootClientId ); const { name: blockName, attributes, isValid } = block; - const isPartOfMultiSelection = - isBlockMultiSelected( clientId ) || - isAncestorMultiSelected( clientId ); const blockType = getBlockType( blockName ); const match = getActiveBlockVariation( blockName, attributes ); const { outlineMode, supportsLayout } = getSettings(); @@ -600,12 +589,6 @@ function BlockListBlockProvider( props ) { index: getBlockIndex( clientId ), blockApiVersion: blockType?.apiVersion || 1, blockTitle: match?.title || blockType?.title, - isPartOfSelection: _isSelected || isPartOfMultiSelection, - adjustScrolling: - _isSelected || isFirstMultiSelectedBlock( clientId ), - enableAnimation: - ! typing && - getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), isOutlineEnabled: outlineMode, hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), @@ -662,9 +645,6 @@ function BlockListBlockProvider( props ) { index, blockApiVersion, blockTitle, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, @@ -699,9 +679,6 @@ function BlockListBlockProvider( props ) { blockApiVersion, blockTitle, isSelected, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index fea20506c28a1f..9c7ae30b5997a5 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -80,9 +80,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { blockApiVersion, blockTitle, isSelected, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, @@ -114,12 +111,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useNavModeExit( clientId ), useIsHovered( { isEnabled: isOutlineEnabled } ), useIntersectionObserver(), - useMovingAnimation( { - isSelected: isPartOfSelection, - adjustScrolling, - enableAnimation, - triggerAnimationOnChange: index, - } ), + useMovingAnimation( { triggerAnimationOnChange: index, clientId } ), useDisabled( { isDisabled: ! hasOverlay } ), ] ); diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index b8b9ed1d82db6a..0ba700761b40dc 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -7,8 +7,19 @@ import { Controller } from '@react-spring/web'; * WordPress dependencies */ import { useLayoutEffect, useMemo, useRef } from '@wordpress/element'; -import { useReducedMotion } from '@wordpress/compose'; import { getScrollContainer } from '@wordpress/dom'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; function getAbsolutePosition( element ) { return { @@ -28,30 +39,20 @@ function getAbsolutePosition( element ) { * - It uses the "resetAnimation" flag to reset the animation * from the beginning in order to animate to the new destination point. * - * @param {Object} $1 Options - * @param {boolean} $1.isSelected Whether it's the current block or not. - * @param {boolean} $1.adjustScrolling Adjust the scroll position to the current block. - * @param {boolean} $1.enableAnimation Enable/Disable animation. - * @param {*} $1.triggerAnimationOnChange Variable used to trigger the animation if it changes. + * @param {Object} $1 Options + * @param {*} $1.triggerAnimationOnChange Variable used to trigger the animation if it changes. + * @param {string} $1.clientId */ -function useMovingAnimation( { - isSelected, - adjustScrolling, - enableAnimation, - triggerAnimationOnChange, -} ) { +function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { const ref = useRef(); - const prefersReducedMotion = useReducedMotion() || ! enableAnimation; - - // We do not want to trigger the animation when a block gets selected, so we - // use a ref to keep track of the value for use later in a callback. - const isSelectedRef = useRef( isSelected ); - const adjustScrollingRef = useRef( adjustScrolling ); - - useLayoutEffect( () => { - isSelectedRef.current = isSelected; - adjustScrollingRef.current = adjustScrolling; - }, [ isSelected, adjustScrolling ] ); + const { + isTyping, + getGlobalBlockCount, + isBlockSelected, + isFirstMultiSelectedBlock, + isBlockMultiSelected, + isAncestorMultiSelected, + } = useSelect( blockEditorStore ); // Whenever the trigger changes, we need to take a snapshot of the current // position of the block to use it as a destination point for the animation. @@ -70,9 +71,12 @@ function useMovingAnimation( { } const scrollContainer = getScrollContainer( ref.current ); + const isSelected = isBlockSelected( clientId ); + const adjustScrolling = + isSelected || isFirstMultiSelectedBlock( clientId ); function preserveScrollPosition() { - if ( adjustScrollingRef.current && prevRect ) { + if ( adjustScrolling && prevRect ) { const blockRect = ref.current.getBoundingClientRect(); const diff = blockRect.top - prevRect.top; @@ -82,13 +86,23 @@ function useMovingAnimation( { } } - if ( prefersReducedMotion ) { + if ( + window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches || + isTyping() || + getGlobalBlockCount() > BLOCK_ANIMATION_THRESHOLD + ) { // If the animation is disabled and the scroll needs to be adjusted, // just move directly to the final scroll position. preserveScrollPosition(); return; } + const isPartOfSelection = + isSelected || + isBlockMultiSelected( clientId ) || + isAncestorMultiSelected( clientId ); + const zIndex = isPartOfSelection ? '1' : ''; + const controller = new Controller( { x: 0, y: 0, @@ -105,7 +119,7 @@ function useMovingAnimation( { ref.current.style.transform = finishedMoving ? null // Set to `null` to explicitly remove the transform. : `translate3d(${ x }px,${ y }px,0)`; - ref.current.style.zIndex = isSelectedRef.current ? '1' : ''; + ref.current.style.zIndex = zIndex; preserveScrollPosition(); }, } ); @@ -121,7 +135,17 @@ function useMovingAnimation( { return () => { controller.stop(); }; - }, [ previous, prevRect, prefersReducedMotion ] ); + }, [ + previous, + prevRect, + clientId, + isTyping, + getGlobalBlockCount, + isBlockSelected, + isFirstMultiSelectedBlock, + isBlockMultiSelected, + isAncestorMultiSelected, + ] ); return ref; } From 4bb81c408f0b9b651ecacb19ac237b38f42fde39 Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 16 Dec 2023 20:09:25 +0100 Subject: [PATCH 4/4] Comments --- .../src/components/use-moving-animation/index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index 0ba700761b40dc..4a66fe6fb6e637 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -86,11 +86,18 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { } } - if ( + // We disable the animation if the user has a preference for reduced + // motion, if the user is typing (insertion by Enter), or if the block + // count exceeds the threshold (insertion caused all the blocks that + // follow to animate). + // To do: consider enableing the _moving_ animation even for large + // posts, while only disabling the _insertion_ animation? + const disableAnimation = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches || isTyping() || - getGlobalBlockCount() > BLOCK_ANIMATION_THRESHOLD - ) { + getGlobalBlockCount() > BLOCK_ANIMATION_THRESHOLD; + + if ( disableAnimation ) { // If the animation is disabled and the scroll needs to be adjusted, // just move directly to the final scroll position. preserveScrollPosition(); @@ -101,6 +108,7 @@ function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { isSelected || isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ); + // Make sure the other blocks move under the selected block(s). const zIndex = isPartOfSelection ? '1' : ''; const controller = new Controller( {