From cb561942b33e00d643525d6094cffe41449d5c51 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 13 Apr 2022 19:09:52 +0200 Subject: [PATCH] Refactor draggable component --- .../components/src/draggable/index.native.js | 198 ++++++++++++------ .../src/draggable/style.native.scss | 3 + packages/components/src/index.native.js | 2 +- 3 files changed, 137 insertions(+), 66 deletions(-) create mode 100644 packages/components/src/draggable/style.native.scss diff --git a/packages/components/src/draggable/index.native.js b/packages/components/src/draggable/index.native.js index 35c5ad8c99095e..58545592e96ae3 100644 --- a/packages/components/src/draggable/index.native.js +++ b/packages/components/src/draggable/index.native.js @@ -1,103 +1,171 @@ /** * External dependencies */ -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { useSharedValue, runOnJS } from 'react-native-reanimated'; -import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; +import { + Gesture, + GestureDetector, + LongPressGestureHandler, +} from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + runOnJS, + useAnimatedReaction, + useAnimatedGestureHandler, +} from 'react-native-reanimated'; /** * WordPress dependencies */ -import { Platform } from '@wordpress/element'; +import { createContext, useContext, useRef } from '@wordpress/element'; /** - * Draggable component + * Internal dependencies + */ +import styles from './style.scss'; + +const Context = createContext( {} ); +const { Provider } = Context; + +/** + * Draggable component. * - * @param {Object} props Component props. - * @param {JSX.Element} props.children Children to be rendered. - * @param {number} [props.maxDistance] Maximum distance, that defines how far the finger is allowed to travel during a long press gesture. - * @param {number} [props.minDuration] Minimum time, that a finger must remain pressed on the corresponding view. - * @param {Function} [props.onDragEnd] Callback when dragging ends. - * @param {Function} [props.onDragOver] Callback when dragging happens over an element. - * @param {Function} [props.onDragStart] Callback when dragging starts. - * @param {import('react-native-reanimated').StyleProp} [props.wrapperAnimatedStyles] Animated styles for the wrapper component. + * @param {Object} props Component props. + * @param {JSX.Element} props.children Children to be rendered. + * @param {Function} [props.onDragEnd] Callback when dragging ends. + * @param {Function} [props.onDragOver] Callback when dragging happens over an element. + * @param {Function} [props.onDragStart] Callback when dragging starts. * * @return {JSX.Element} The component to be rendered. */ -export default function Draggable( { - children, - maxDistance = 1000, - minDuration = 500, - onDragEnd, - onDragOver, - onDragStart, - wrapperAnimatedStyles, -} ) { +const Draggable = ( { children, onDragEnd, onDragOver, onDragStart } ) => { const isDragging = useSharedValue( false ); - const isAnyTextInputFocused = useSharedValue( false ); + const draggingId = useSharedValue( '' ); + const panGestureRef = useRef(); - const checkTextInputFocus = () => { - isAnyTextInputFocused.value = - TextInputState.currentlyFocusedInput() !== null; + const initialPosition = { + x: useSharedValue( 0 ), + y: useSharedValue( 0 ), + }; + const lastPosition = { + x: useSharedValue( 0 ), + y: useSharedValue( 0 ), }; - const longPressGesture = Gesture.LongPress() - .onBegin( () => { - 'worklet'; - runOnJS( checkTextInputFocus )(); - } ) - .onStart( ( ev ) => { - 'worklet'; - if ( isAnyTextInputFocused.value ) { - return; - } - - isDragging.value = true; - - if ( onDragStart ) { - onDragStart( ev ); - } - } ) - .onEnd( () => { - 'worklet'; - if ( isAnyTextInputFocused.value ) { + useAnimatedReaction( + () => isDragging.value, + ( result, previous ) => { + if ( result === previous ) { return; } - isDragging.value = false; - if ( onDragEnd ) { - onDragEnd(); + if ( result ) { + if ( onDragStart ) { + onDragStart( { + x: initialPosition.x.value, + y: initialPosition.y.value, + id: draggingId.value, + } ); + } + } else if ( onDragEnd ) { + onDragEnd( { + x: lastPosition.x.value, + y: lastPosition.y.value, + id: draggingId.value, + } ); } - } ) - .maxDistance( maxDistance ) - .minDuration( minDuration ) - .shouldCancelWhenOutside( false ); + } + ); const panGesture = Gesture.Pan() .manualActivation( true ) + .onTouchesDown( ( event ) => { + const { x = 0, y = 0 } = event.allTouches[ 0 ]; + initialPosition.x.value = x; + initialPosition.y.value = y; + } ) .onTouchesMove( ( _, state ) => { - 'worklet'; if ( isDragging.value ) { state.activate(); - } else if ( Platform.isIOS || isAnyTextInputFocused.value ) { - state.fail(); } } ) - .onUpdate( ( ev ) => { - 'worklet'; + .onUpdate( ( event ) => { + lastPosition.x.value = event.x; + lastPosition.y.value = event.y; + if ( onDragOver ) { - onDragOver( ev ); + onDragOver( event ); } } ) + .withRef( panGestureRef ) .shouldCancelWhenOutside( false ); - const dragHandler = Gesture.Simultaneous( panGesture, longPressGesture ); - return ( - - - { children } + + + + { children } + ); -} +}; + +/** + * Draggable trigger component. + * + * This component acts as the trigger for the dragging functionality. + * + * @param {Object} props Component props. + * @param {JSX.Element} props.children Children to be rendered. + * @param {*} props.id Identifier passed within the event callbacks. + * @param {boolean} [props.enabled] Enables the long-press gesture. + * @param {number} [props.maxDistance] Maximum distance, that defines how far the finger is allowed to travel during a long press gesture. + * @param {number} [props.minDuration] Minimum time, that a finger must remain pressed on the corresponding view. + * @param {Function} [props.onLongPress] Callback when long-press gesture is triggered over an element. + * @param {Function} [props.onLongPressEnd] Callback when long-press gesture ends. + * + * @return {JSX.Element} The component to be rendered. + */ +const DraggableTrigger = ( { + children, + enabled = true, + id, + maxDistance = 1000, + minDuration = 500, + onLongPress, + onLongPressEnd, +} ) => { + const { panGestureRef, isDragging, draggingId } = useContext( Context ); + + const gestureHandler = useAnimatedGestureHandler( { + onActive: () => { + isDragging.value = true; + draggingId.value = id; + if ( onLongPress ) { + runOnJS( onLongPress )( id ); + } + }, + onEnd: () => { + isDragging.value = false; + if ( onLongPressEnd ) { + runOnJS( onLongPressEnd )( id ); + } + }, + } ); + + return ( + + { children } + + ); +}; + +export { DraggableTrigger }; +export default Draggable; diff --git a/packages/components/src/draggable/style.native.scss b/packages/components/src/draggable/style.native.scss new file mode 100644 index 00000000000000..c7b3fbcd21fe4e --- /dev/null +++ b/packages/components/src/draggable/style.native.scss @@ -0,0 +1,3 @@ +.draggable__container { + flex: 1,; +} diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 108fb95b094be3..140ad940ace6eb 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -61,7 +61,7 @@ export { filterUnitsWithSettings as filterUnitsWithSettings, } from './unit-control/utils'; export { default as Disabled } from './disabled'; -export { default as Draggable } from './draggable'; +export { default as Draggable, DraggableTrigger } from './draggable'; // Higher-Order Components. export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing';