-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
7e67812
85e2c15
43b684e
1266379
3a64f56
909c5f8
af4c9e5
8ead864
b68268c
0a42d31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 ); | ||
|
||
|
@@ -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. | ||
|
@@ -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(); | ||
|
@@ -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; | ||
|
@@ -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 } ) => { | ||
|
@@ -209,12 +200,10 @@ const BlockDraggableWrapper = ( { children } ) => { | |
isDragging={ isDragging } | ||
targetBlockIndex={ targetBlockIndex } | ||
/> | ||
|
||
<Draggable | ||
onDragStart={ startDragging } | ||
onDragOver={ updateDragging } | ||
onDragEnd={ stopDragging } | ||
wrapperAnimatedStyles={ wrapperStyles } | ||
> | ||
{ children( { onScroll: scrollHandler } ) } | ||
</Draggable> | ||
|
@@ -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 = { | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic of this side effect will only be executed if |
||
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> | ||
); | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( { | ||
|
@@ -189,6 +190,7 @@ class BlockListBlock extends Component { | |
marginHorizontal, | ||
isInnerBlockSelected, | ||
name, | ||
rootClientId, | ||
} = this.props; | ||
|
||
if ( ! attributes || ! blockType ) { | ||
|
@@ -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 | ||
|
@@ -256,14 +259,21 @@ class BlockListBlock extends Component { | |
] } | ||
/> | ||
) } | ||
{ isValid ? ( | ||
this.getBlockForType() | ||
) : ( | ||
<BlockInvalidWarning | ||
blockTitle={ title } | ||
icon={ icon } | ||
/> | ||
) } | ||
<BlockDraggable | ||
enabled={ ! hasParent } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
clientId={ clientId } | ||
> | ||
{ () => | ||
isValid ? ( | ||
this.getBlockForType() | ||
) : ( | ||
<BlockInvalidWarning | ||
blockTitle={ title } | ||
icon={ icon } | ||
/> | ||
) | ||
} | ||
</BlockDraggable> | ||
<View | ||
style={ styles.neutralToolbar } | ||
ref={ this.anchorNodeRef } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -36,6 +36,7 @@ export const BlockMover = ( { | |
isStackedHorizontally, | ||
} ) => { | ||
const pickerRef = useRef(); | ||
const [ shouldPresentPicker, setShouldPresentPicker ] = useState( false ); | ||
const [ blockPageMoverState, setBlockPageMoverState ] = useState( | ||
undefined | ||
); | ||
|
@@ -46,9 +47,17 @@ export const BlockMover = ( { | |
} | ||
|
||
setBlockPageMoverState( direction ); | ||
pickerRef.current.presentPicker(); | ||
setShouldPresentPicker( true ); | ||
Comment on lines
49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The options of the picker are controlled by the |
||
}; | ||
|
||
// Ensure that the picker is only presented after state updates. | ||
useEffect( () => { | ||
if ( shouldPresentPicker ) { | ||
pickerRef.current?.presentPicker(); | ||
setShouldPresentPicker( false ); | ||
} | ||
}, [ shouldPresentPicker ] ); | ||
|
||
const { | ||
description: { | ||
backwardButtonHint, | ||
|
@@ -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; | ||
} | ||
|
@@ -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 } } | ||
/> | ||
|
@@ -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, | ||
|
There was a problem hiding this comment.
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.