Skip to content

Commit

Permalink
[RNMobile] Refactor draggable logic and introduce DraggableTrigger
Browse files Browse the repository at this point in the history
…component (#40406)

* Present block mover picker only after state updates

* Refactor draggable component

* Use DraggableTrigger in BlockDraggable

* Move BlockDraggable render to BlockListBlock component

* Fix long-press gesture when editing a text on iOS

* Memoize draggable provider value to prevent re-renders

* Fix dragging not being disabled after scrolling

* Reduce calls to event handlers of pan and long-press gestures

* Prevent onDragEnd event to be called upon mounting

* Add DEFAULT_IOS_LONG_PRESS_MIN_DURATION constant
  • Loading branch information
fluiddot authored Apr 20, 2022
1 parent 951103c commit 3996c65
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 139 deletions.
112 changes: 63 additions & 49 deletions packages/block-editor/src/components/block-draggable/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import Animated, {
withDelay,
withTiming,
} from 'react-native-reanimated';
import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';

/**
* WordPress dependencies
*/
import { Draggable } from '@wordpress/components';
import { Draggable, DraggableTrigger } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
import { useEffect, useRef, useState, Platform } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -33,6 +33,9 @@ import styles from './style.scss';
const CHIP_OFFSET_TO_TOUCH_POSITION = 32;
const BLOCK_OPACITY_ANIMATION_CONFIG = { duration: 350 };
const BLOCK_OPACITY_ANIMATION_DELAY = 250;
const DEFAULT_LONG_PRESS_MIN_DURATION = 500;
const DEFAULT_IOS_LONG_PRESS_MIN_DURATION =
DEFAULT_LONG_PRESS_MIN_DURATION - 50;

/**
* Block draggable wrapper component
Expand All @@ -51,22 +54,15 @@ const BLOCK_OPACITY_ANIMATION_DELAY = 250;
* @return {Function} Render function that passes `onScroll` event handler.
*/
const BlockDraggableWrapper = ( { children } ) => {
const currentBlockLayout = useRef();

const wrapperStyles = usePreferredColorSchemeStyle(
styles[ 'draggable-wrapper__container' ],
styles[ 'draggable-wrapper__container--dark' ]
);

const { startDraggingBlocks, stopDraggingBlocks } = useDispatch(
blockEditorStore
);
const [ currentClientId, setCurrentClientId ] = useState();

const {
blocksLayouts,
scrollRef,
findBlockLayoutByPosition,
} = useBlockListContext();
selectBlock,
startDraggingBlocks,
stopDraggingBlocks,
} = useDispatch( blockEditorStore );

const { scrollRef } = useBlockListContext();
const animatedScrollRef = useAnimatedRef();
animatedScrollRef( scrollRef );

Expand Down Expand Up @@ -113,16 +109,10 @@ const BlockDraggableWrapper = ( { children } ) => {
};
}, [] );

const onStartDragging = ( position ) => {
const blockLayout = findBlockLayoutByPosition( blocksLayouts.current, {
x: position.x,
y: position.y + scroll.offsetY.value,
} );

const foundClientId = blockLayout?.clientId;
currentBlockLayout.current = blockLayout;
if ( foundClientId ) {
startDraggingBlocks( [ foundClientId ] );
const onStartDragging = ( { clientId, position } ) => {
if ( clientId ) {
startDraggingBlocks( [ clientId ] );
setCurrentClientId( clientId );
runOnUI( startScrolling )( position.y );
} else {
// We stop dragging if no block is found.
Expand All @@ -131,14 +121,15 @@ const BlockDraggableWrapper = ( { children } ) => {
};

const onStopDragging = () => {
const currentClientId = currentBlockLayout.current?.clientId;
if ( currentClientId ) {
onBlockDrop( {
// Dropping is only allowed at root level
srcRootClientId: '',
srcClientIds: [ currentClientId ],
type: 'block',
} );
selectBlock( currentClientId );
setCurrentClientId( undefined );
}
onBlockDragEnd();
stopDraggingBlocks();
Expand All @@ -149,7 +140,7 @@ const BlockDraggableWrapper = ( { children } ) => {
chip.height.value = layout.height;
};

const startDragging = ( { x, y } ) => {
const startDragging = ( { x, y, id } ) => {
'worklet';
const dragPosition = { x, y };
chip.x.value = dragPosition.x;
Expand All @@ -158,7 +149,7 @@ const BlockDraggableWrapper = ( { children } ) => {
isDragging.value = true;

chip.scale.value = withTiming( 1 );
runOnJS( onStartDragging )( dragPosition );
runOnJS( onStartDragging )( { clientId: id, position: dragPosition } );
};

const updateDragging = ( { x, y } ) => {
Expand Down Expand Up @@ -209,12 +200,10 @@ const BlockDraggableWrapper = ( { children } ) => {
isDragging={ isDragging }
targetBlockIndex={ targetBlockIndex }
/>

<Draggable
onDragStart={ startDragging }
onDragOver={ updateDragging }
onDragEnd={ stopDragging }
wrapperAnimatedStyles={ wrapperStyles }
>
{ children( { onScroll: scrollHandler } ) }
</Draggable>
Expand All @@ -235,14 +224,14 @@ const BlockDraggableWrapper = ( { children } ) => {
* This component serves for animating the block when it is being dragged.
* Hence, it should be wrapped around the rendering of a block.
*
* @param {Object} props Component props.
* @param {JSX.Element} props.children Children to be rendered.
* @param {string[]} props.clientId Client id of the block.
* @param {Object} props Component props.
* @param {JSX.Element} props.children Children to be rendered.
* @param {string[]} props.clientId Client id of the block.
* @param {boolean} [props.enabled] Enables the draggable trigger.
*
* @return {Function} Render function which includes the parameter `isDraggable` to determine if the block can be dragged.
*/
const BlockDraggable = ( { clientId, children } ) => {
const { selectBlock } = useDispatch( blockEditorStore );
const BlockDraggable = ( { clientId, children, enabled = true } ) => {
const wasBeingDragged = useRef( false );

const draggingAnimation = {
Expand All @@ -261,53 +250,78 @@ const BlockDraggable = ( { clientId, children } ) => {
BLOCK_OPACITY_ANIMATION_DELAY,
withTiming( 1, BLOCK_OPACITY_ANIMATION_CONFIG )
);
runOnJS( selectBlock )( clientId );
};

const { isDraggable, isBeingDragged } = useSelect(
const { isDraggable, isBeingDragged, canDragBlock } = useSelect(
( select ) => {
const {
getBlockRootClientId,
getTemplateLock,
isBlockBeingDragged,
hasSelectedBlock,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
const templateLock = rootClientId
? getTemplateLock( rootClientId )
: null;
const isAnyTextInputFocused =
TextInputState.currentlyFocusedInput() !== null;

return {
isBeingDragged: isBlockBeingDragged( clientId ),
isDraggable: 'all' !== templateLock,
canDragBlock: hasSelectedBlock()
? ! isAnyTextInputFocused
: true,
};
},
[ clientId ]
);

useEffect( () => {
if ( isBeingDragged ) {
startDraggingBlock();
wasBeingDragged.current = true;
} else if ( wasBeingDragged.current ) {
stopDraggingBlock();
wasBeingDragged.current = false;
if ( isBeingDragged !== wasBeingDragged.current ) {
if ( isBeingDragged ) {
startDraggingBlock();
} else {
stopDraggingBlock();
}
}
wasBeingDragged.current = isBeingDragged;
}, [ isBeingDragged ] );

const wrapperStyles = useAnimatedStyle( () => {
const animatedWrapperStyles = useAnimatedStyle( () => {
return {
opacity: draggingAnimation.opacity.value,
};
} );
const wrapperStyles = [
animatedWrapperStyles,
styles[ 'draggable-wrapper__container' ],
];

if ( ! isDraggable ) {
return children( { isDraggable: false } );
}

return (
<Animated.View style={ wrapperStyles }>
{ children( { isDraggable: true } ) }
</Animated.View>
<DraggableTrigger
id={ clientId }
enabled={ enabled && canDragBlock }
minDuration={ Platform.select( {
// On iOS, using a lower min duration than the default
// value prevents the long-press gesture from being
// triggered in underneath elements. This is required to
// prevent enabling text editing when dragging is available.
ios: canDragBlock
? DEFAULT_IOS_LONG_PRESS_MIN_DURATION
: DEFAULT_LONG_PRESS_MIN_DURATION,
android: DEFAULT_LONG_PRESS_MIN_DURATION,
} ) }
>
<Animated.View style={ wrapperStyles }>
{ children( { isDraggable: true } ) }
</Animated.View>
</DraggableTrigger>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { useEffect, useCallback } from '@wordpress/element';
* Internal dependencies
*/
import { useBlockListContext } from './block-list-context';
import BlockDraggable from '../block-draggable';

function BlockListItemCell( { children, clientId, rootClientId } ) {
const { blocksLayouts, updateBlocksLayouts } = useBlockListContext();
Expand All @@ -37,13 +36,7 @@ function BlockListItemCell( { children, clientId, rootClientId } ) {
[ clientId, rootClientId, updateBlocksLayouts ]
);

return (
<View onLayout={ onLayout }>
<BlockDraggable clientId={ clientId }>
{ () => children }
</BlockDraggable>
</View>
);
return <View onLayout={ onLayout }>{ children }</View>;
}

export default BlockListItemCell;
26 changes: 18 additions & 8 deletions packages/block-editor/src/components/block-list/block.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import BlockEdit from '../block-edit';
import BlockInvalidWarning from './block-invalid-warning';
import BlockMobileToolbar from '../block-mobile-toolbar';
import { store as blockEditorStore } from '../../store';
import BlockDraggable from '../block-draggable';

const emptyArray = [];
function BlockForType( {
Expand Down Expand Up @@ -189,6 +190,7 @@ class BlockListBlock extends Component {
marginHorizontal,
isInnerBlockSelected,
name,
rootClientId,
} = this.props;

if ( ! attributes || ! blockType ) {
Expand All @@ -207,6 +209,7 @@ class BlockListBlock extends Component {
const isScreenWidthEqual = blockWidth === screenWidth;
const isScreenWidthWider = blockWidth < screenWidth;
const isFullWidthToolbar = isFullWidth( align ) || isScreenWidthEqual;
const hasParent = !! rootClientId;

return (
<TouchableWithoutFeedback
Expand Down Expand Up @@ -256,14 +259,21 @@ class BlockListBlock extends Component {
] }
/>
) }
{ isValid ? (
this.getBlockForType()
) : (
<BlockInvalidWarning
blockTitle={ title }
icon={ icon }
/>
) }
<BlockDraggable
enabled={ ! hasParent }
clientId={ clientId }
>
{ () =>
isValid ? (
this.getBlockForType()
) : (
<BlockInvalidWarning
blockTitle={ title }
icon={ icon }
/>
)
}
</BlockDraggable>
<View
style={ styles.neutralToolbar }
ref={ this.anchorNodeRef }
Expand Down
28 changes: 22 additions & 6 deletions packages/block-editor/src/components/block-mover/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { __ } from '@wordpress/i18n';
import { Picker, ToolbarButton } from '@wordpress/components';
import { withInstanceId, compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';
import { useRef, useState } from '@wordpress/element';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -36,6 +36,7 @@ export const BlockMover = ( {
isStackedHorizontally,
} ) => {
const pickerRef = useRef();
const [ shouldPresentPicker, setShouldPresentPicker ] = useState( false );
const [ blockPageMoverState, setBlockPageMoverState ] = useState(
undefined
);
Expand All @@ -46,9 +47,17 @@ export const BlockMover = ( {
}

setBlockPageMoverState( direction );
pickerRef.current.presentPicker();
setShouldPresentPicker( true );
};

// Ensure that the picker is only presented after state updates.
useEffect( () => {
if ( shouldPresentPicker ) {
pickerRef.current?.presentPicker();
setShouldPresentPicker( false );
}
}, [ shouldPresentPicker ] );

const {
description: {
backwardButtonHint,
Expand Down Expand Up @@ -86,6 +95,15 @@ export const BlockMover = ( {
if ( option && option.onSelect ) option.onSelect();
};

const onLongPressMoveUp = useCallback(
showBlockPageMover( BLOCK_MOVER_DIRECTION_TOP ),
[]
);
const onLongPressMoveDown = useCallback(
showBlockPageMover( BLOCK_MOVER_DIRECTION_BOTTOM ),
[]
);

if ( ! canMove || ( isFirst && isLast && ! rootClientId ) ) {
return null;
}
Expand All @@ -96,7 +114,7 @@ export const BlockMover = ( {
title={ ! isFirst ? backwardButtonTitle : firstBlockTitle }
isDisabled={ isFirst }
onClick={ onMoveUp }
onLongPress={ showBlockPageMover( BLOCK_MOVER_DIRECTION_TOP ) }
onLongPress={ onLongPressMoveUp }
icon={ backwardButtonIcon }
extraProps={ { hint: backwardButtonHint } }
/>
Expand All @@ -105,9 +123,7 @@ export const BlockMover = ( {
title={ ! isLast ? forwardButtonTitle : lastBlockTitle }
isDisabled={ isLast }
onClick={ onMoveDown }
onLongPress={ showBlockPageMover(
BLOCK_MOVER_DIRECTION_BOTTOM
) }
onLongPress={ onLongPressMoveDown }
icon={ forwardButtonIcon }
extraProps={ {
hint: forwardButtonHint,
Expand Down
Loading

0 comments on commit 3996c65

Please sign in to comment.