-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
137 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<GestureDetector gesture={ dragHandler }> | ||
<Animated.View style={ wrapperAnimatedStyles }> | ||
{ children } | ||
<GestureDetector gesture={ panGesture }> | ||
<Animated.View style={ styles.draggable__container }> | ||
<Provider value={ { panGestureRef, isDragging, draggingId } }> | ||
{ children } | ||
</Provider> | ||
</Animated.View> | ||
</GestureDetector> | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* 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 ( | ||
<LongPressGestureHandler | ||
enabled={ enabled } | ||
minDurationMs={ minDuration } | ||
maxDist={ maxDistance } | ||
simultaneousHandlers={ panGestureRef } | ||
shouldCancelWhenOutside={ false } | ||
onGestureEvent={ gestureHandler } | ||
> | ||
{ children } | ||
</LongPressGestureHandler> | ||
); | ||
}; | ||
|
||
export { DraggableTrigger }; | ||
export default Draggable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.draggable__container { | ||
flex: 1,; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters