| 1 | import { useEffect, useMemo, useRef } from "react"; |
| 2 | import { View } from "react-native"; |
| 3 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; |
| 4 | |
| 5 | import { SymbolView } from "../../../components/AppSymbol"; |
| 6 | |
| 7 | /** Pan recognition wins over the settings scroll view only inside the handle. */ |
| 8 | export function SettingsDragHandle(props: { |
| 9 | readonly title: string; |
| 10 | readonly canMoveUp: boolean; |
| 11 | readonly canMoveDown: boolean; |
| 12 | readonly onStart: () => void; |
| 13 | readonly onMove: (translation: number) => void; |
| 14 | readonly onEnd: (translation: number, cancelled: boolean) => void; |
| 15 | readonly onStep: (direction: "up" | "down") => void; |
| 16 | }) { |
| 17 | const latest = useRef(props); |
| 18 | useEffect(() => { |
| 19 | latest.current = props; |
| 20 | }); |
| 21 | const translation = useRef(0); |
| 22 | const gesture = useMemo( |
| 23 | () => |
| 24 | Gesture.Pan() |
| 25 | .minDistance(0) |
| 26 | .shouldCancelWhenOutside(false) |
| 27 | .runOnJS(true) |
| 28 | .onStart(() => { |
| 29 | translation.current = 0; |
| 30 | latest.current.onStart(); |
| 31 | }) |
| 32 | .onUpdate((event) => { |
| 33 | translation.current = event.translationY; |
| 34 | latest.current.onMove(event.translationY); |
| 35 | }) |
| 36 | .onFinalize((_, success) => latest.current.onEnd(translation.current, !success)), |
| 37 | [], |
| 38 | ); |
| 39 | return ( |
| 40 | <GestureDetector gesture={gesture}> |
| 41 | <View |
| 42 | collapsable={false} |
| 43 | accessible |
| 44 | accessibilityRole="adjustable" |
| 45 | accessibilityLabel={`Reorder ${props.title}`} |
| 46 | accessibilityActions={[ |
| 47 | ...(props.canMoveUp ? [{ name: "decrement", label: "Move up" }] : []), |
| 48 | ...(props.canMoveDown ? [{ name: "increment", label: "Move down" }] : []), |
| 49 | ]} |
| 50 | onAccessibilityAction={({ nativeEvent }) => { |
| 51 | if (nativeEvent.actionName === "decrement" && props.canMoveUp) props.onStep("up"); |
| 52 | if (nativeEvent.actionName === "increment" && props.canMoveDown) props.onStep("down"); |
| 53 | }} |
| 54 | style={{ width: 48, alignSelf: "stretch", alignItems: "center", justifyContent: "center" }} |
| 55 | > |
| 56 | <SymbolView |
| 57 | name="line.3.horizontal" |
| 58 | size={20} |
| 59 | tintColorClassName="accent-foreground-muted" |
| 60 | /> |
| 61 | </View> |
| 62 | </GestureDetector> |
| 63 | ); |
| 64 | } |