1import { useCallback, useMemo, useRef, useState } from "react";
2import { Platform, Pressable, StyleSheet, View, type AccessibilityActionEvent } from "react-native";
3import { Gesture, GestureDetector } from "react-native-gesture-handler";
4import { runOnJS } from "react-native-reanimated";
5import { cn } from "../../lib/cn";
6
7const ACCESSIBILITY_RESIZE_STEP = 24;
8
9interface WorkspacePaneDividerProps {
10 readonly accessibilityLabel: string;
11 readonly currentWidth: number;
12 /** 1 when dragging right grows the pane, -1 when dragging left grows it. */
13 readonly resizeDirection: 1 | -1;
14 readonly onResizeStart?: () => void;
15 readonly onResizeBy: (delta: number) => void;
16 readonly onResizeEnd?: () => void;
17}
18
19/** A forgiving divider target for touch, pointer, and VoiceOver users. */
20export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {
21 const latestProps = useRef(props);
22 latestProps.current = props;
23 const [dragging, setDragging] = useState(false);
24 const handleResizeStart = useCallback(() => {
25 setDragging(true);
26 latestProps.current.onResizeStart?.();
27 }, []);
28 const handleResize = useCallback((translationX: number) => {
29 latestProps.current.onResizeBy(translationX * latestProps.current.resizeDirection);
30 }, []);
31 const handleResizeEnd = useCallback(() => {
32 setDragging(false);
33 latestProps.current.onResizeEnd?.();
34 }, []);
35 const resizeGesture = useMemo(
36 () =>
37 Gesture.Pan()
38 .activeOffsetX([-4, 4])
39 .failOffsetY([-24, 24])
40 .onStart(() => {
41 runOnJS(handleResizeStart)();
42 })
43 .onUpdate((event) => {
44 runOnJS(handleResize)(event.translationX);
45 })
46 .onFinalize(() => {
47 runOnJS(handleResizeEnd)();
48 }),
49 [handleResize, handleResizeEnd, handleResizeStart],
50 );
51
52 const handleAccessibilityAction = (event: AccessibilityActionEvent) => {
53 props.onResizeStart?.();
54 if (event.nativeEvent.actionName === "increment") {
55 props.onResizeBy(ACCESSIBILITY_RESIZE_STEP);
56 } else if (event.nativeEvent.actionName === "decrement") {
57 props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP);
58 }
59 props.onResizeEnd?.();
60 };
61
62 return (
63 <GestureDetector gesture={resizeGesture}>
64 <Pressable
65 className="relative z-[100] -mx-[22px] w-11 self-stretch justify-center"
66 accessibilityActions={[
67 { name: "increment", label: "Make pane wider" },
68 { name: "decrement", label: "Make pane narrower" },
69 ]}
70 accessibilityLabel={props.accessibilityLabel}
71 accessibilityRole="adjustable"
72 accessibilityValue={{
73 now: Math.round(props.currentWidth),
74 text: `${Math.round(props.currentWidth)} points wide`,
75 }}
76 onAccessibilityAction={handleAccessibilityAction}
77 >
78 <View
79 className={cn(
80 "h-full self-center bg-border opacity-70",
81 dragging ? "w-0.5 bg-primary opacity-100" : "w-px",
82 Platform.OS === "android" && !dragging && "opacity-0",
83 )}
84 style={[styles.line, dragging && styles.activeLine]}
85 />
86 </Pressable>
87 </GestureDetector>
88 );
89}
90
91const styles = StyleSheet.create({
92 line: {
93 alignSelf: "center",
94 height: "100%",
95 width: StyleSheet.hairlineWidth,
96 },
97 activeLine: {
98 width: 2,
99 },
100});