Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RNMobile] Refactor draggable logic and introduce DraggableTrigger component #40406

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 );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The dragged block gets automatically selected after it's dropped.

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,
Comment on lines +273 to +275
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We determine if the dragging can be enabled by checking if any text input is focused while a block is selected.

};
},
[ clientId ]
);

useEffect( () => {
if ( isBeingDragged ) {
startDraggingBlock();
wasBeingDragged.current = true;
} else if ( wasBeingDragged.current ) {
stopDraggingBlock();
wasBeingDragged.current = false;
if ( isBeingDragged !== wasBeingDragged.current ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic of this side effect will only be executed if isBeingDragged value has changed.

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( {
geriux marked this conversation as resolved.
Show resolved Hide resolved
// 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 }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The drag & drop only works for root-level blocks, for this reason, we disable the BlockDraggable component when the block has a parent. In the future, once the drag & drop functionality support nested blocks, we could enable this.

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 );
Comment on lines 49 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The options of the picker are controlled by the blockPageMoverState state value. We're presenting the picker right after updating the value, however, it's not guaranteed that the picker will present the right options since it might not have been rendered. For this reason, I updated this logic and now the presentation of the picker is controlled with another state value.

};

// 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