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 e6c888330fca71..4a66fe6fb6e637 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -1,35 +1,32 @@ /** * External dependencies */ -import { useSpring } from '@react-spring/web'; +import { Controller } from '@react-spring/web'; /** * WordPress dependencies */ -import { - useState, - useLayoutEffect, - useReducer, - useMemo, - useRef, -} from '@wordpress/element'; -import { useReducedMotion } from '@wordpress/compose'; +import { useLayoutEffect, useMemo, useRef } from '@wordpress/element'; import { getScrollContainer } from '@wordpress/dom'; +import { useSelect } from '@wordpress/data'; /** - * Simple reducer used to increment a counter. - * - * @param {number} state Previous counter value. - * @return {number} New state value. + * Internal dependencies */ -const counterReducer = ( state ) => state + 1; +import { store as blockEditorStore } from '../../store'; -const getAbsolutePosition = ( element ) => { +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; + +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,114 +39,121 @@ const 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; - 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 ), + 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. + 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 () => {}; - } - - const prevRect = ref.current.getBoundingClientRect(); - return () => { - const blockRect = ref.current.getBoundingClientRect(); - const diff = blockRect.top - prevRect.top; - - if ( diff ) { - scrollContainer.scrollTop += diff; + const isSelected = isBlockSelected( clientId ); + const adjustScrolling = + isSelected || isFirstMultiSelectedBlock( clientId ); + + function preserveScrollPosition() { + if ( adjustScrolling && prevRect ) { + const blockRect = ref.current.getBoundingClientRect(); + const diff = blockRect.top - prevRect.top; + + if ( diff ) { + scrollContainer.scrollTop += diff; + } } - }; - }, [ triggerAnimationOnChange, adjustScrolling ] ); - - useLayoutEffect( () => { - if ( triggeredAnimation ) { - endAnimation(); - } - }, [ triggeredAnimation ] ); - useLayoutEffect( () => { - if ( ! previous ) { - return; } - if ( prefersReducedMotion ) { + // 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; + + if ( disableAnimation ) { // 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 ); + // Make sure the other blocks move under the selected block(s). + const zIndex = isPartOfSelection ? '1' : ''; + + 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 = zIndex; + preserveScrollPosition(); + }, + } ); + 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, - } ); + controller.start( { x: 0, y: 0, from: { x, y } } ); + + return () => { + controller.stop(); + }; + }, [ + previous, + prevRect, + clientId, + isTyping, + getGlobalBlockCount, + isBlockSelected, + isFirstMultiSelectedBlock, + isBlockMultiSelected, + isAncestorMultiSelected, + ] ); return ref; }