nandi/oripublic Fork 0
db7446cc6b52b6bd9196294c1e66e5eb56754d5e
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

ResizeHandle.tsx · 147 lines · 3.8 KBTypeScript Blame HistoryRaw
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday1/**
2 * ResizeHandle is the thin vertical grip between two columns. Dragging it
3 * with the pointer resizes the column on its left; the arrow keys resize it
4 * step by step, Home / End jump to the bounds, and a double-click restores
5 * the default width. It exposes the ARIA separator pattern so the current
6 * width is readable by assistive technology.
7 *
8 * The handle owns no width itself: it reports the wanted width through
9 * `onResize` and the parent decides (and clamps) what to apply.
10 *
11 * @example
12 * <ResizeHandle
13 * label="Resize the file tree"
14 * width={width}
15 * min={120}
16 * max={800}
17 * onResize={setFileTreeWidth}
18 * onReset={resetFileTreeWidth}
19 * />
20 */
21
22import {
23 type KeyboardEvent,
24 type PointerEvent as ReactPointerEvent,
25 useRef,
26 useState,
27} from "react";
28
29/** Pixels moved per arrow-key press. */
30export const KEYBOARD_RESIZE_STEP = 16;
31
32export interface ResizeHandleProps {
33 /** Accessible name of the separator. */
34 label: string;
35 /** Current width of the resized column, in pixels. */
36 width: number;
37 /** Narrowest width the parent accepts, in pixels. */
38 min: number;
39 /** Widest width the parent accepts, in pixels. */
40 max: number;
41 /** Called with the wanted width whenever the user drags or presses a key. */
42 onResize: (width: number) => void;
43 /** Called on double-click to restore the default width. */
44 onReset: () => void;
45}
46
47interface DragOrigin {
48 pointerX: number;
49 width: number;
50}
51
52export function ResizeHandle({
53 label,
54 width,
55 min,
56 max,
57 onResize,
58 onReset,
59}: ResizeHandleProps) {
60 // The origin of the current drag; null when the pointer is up.
61 const dragOrigin = useRef<DragOrigin | null>(null);
62 const [dragging, setDragging] = useState(false);
63
64 const startDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
65 // Prevents the browser from starting a text selection while dragging.
66 event.preventDefault();
67 dragOrigin.current = { pointerX: event.clientX, width };
68 // Capture keeps the move/up events coming even when the pointer
69 // leaves the thin handle (jsdom lacks the API, hence the guard).
70 event.currentTarget.setPointerCapture?.(event.pointerId);
71 setDragging(true);
72 };
73
74 const moveDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
75 const origin = dragOrigin.current;
76 if (origin === null) {
77 return;
78 }
79 onResize(origin.width + event.clientX - origin.pointerX);
80 };
81
82 const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
83 if (dragOrigin.current === null) {
84 return;
85 }
86 dragOrigin.current = null;
87 event.currentTarget.releasePointerCapture?.(event.pointerId);
88 setDragging(false);
89 };
90
91 const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {
92 const wanted = widthAfterKey(event.key, width, min, max);
93 if (wanted === null) {
94 return;
95 }
96 event.preventDefault();
97 onResize(wanted);
98 };
99
100 return (
101 <div
102 role="separator"
103 aria-label={label}
104 aria-orientation="vertical"
105 aria-valuenow={width}
106 aria-valuemin={min}
107 aria-valuemax={max}
108 tabIndex={0}
109 className={`resize-handle${dragging ? " resize-handle-active" : ""}`}
110 onPointerDown={startDrag}
111 onPointerMove={moveDrag}
112 onPointerUp={endDrag}
113 onPointerCancel={endDrag}
114 onKeyDown={handleKey}
115 onDoubleClick={onReset}
116 />
117 );
118}
119
120/**
121 * widthAfterKey maps a key press to the wanted width, or null when the key
122 * is not a resize key so the event keeps its default behaviour.
123 *
124 * @example
125 * widthAfterKey("ArrowRight", 200, 120, 800); // 216
126 * widthAfterKey("Home", 200, 120, 800); // 120
127 * widthAfterKey("Enter", 200, 120, 800); // null
128 */
129export function widthAfterKey(
130 key: string,
131 width: number,
132 min: number,
133 max: number,
134): number | null {
135 switch (key) {
136 case "ArrowLeft":
137 return width - KEYBOARD_RESIZE_STEP;
138 case "ArrowRight":
139 return width + KEYBOARD_RESIZE_STEP;
140 case "Home":
141 return min;
142 case "End":
143 return max;
144 default:
145 return null;
146 }
147}