|
4 | 4 | import ShortcutWrapper from '$lib/ShortcutWrapper.svelte' |
5 | 5 | import TooltipContent from '$lib/tooltip/tooltip-content.svelte' |
6 | 6 | import { cn, type WithElementRef } from '$lib/utils.js' |
7 | | - import { SIDEBAR_DRAG_THRESHOLD_PX, SIDEBAR_MIN_WIDTH_PX } from './constants.js' |
| 7 | + import { SIDEBAR_DRAG_THRESHOLD_PX, SIDEBAR_WIDTH_ICON_PX } from './constants.js' |
8 | 8 | import { useSidebar } from './context.svelte.js' |
9 | 9 |
|
10 | 10 | let { |
|
20 | 20 | let dragStartWidthPx = 0 |
21 | 21 | let dragMoved = false |
22 | 22 | let dragDirection: 1 | -1 = 1 |
| 23 | + let activePointerId: number | null = null |
23 | 24 |
|
24 | 25 | let isDragging = $state(false) |
25 | 26 | let tooltipOpen = $state(false) |
|
29 | 30 | const TOOLTIP_HOVER_DELAY_MS = 700 |
30 | 31 | const TOOLTIP_CURSOR_OFFSET = 12 |
31 | 32 |
|
| 33 | + let tooltipDelay = $derived(isDragging ? Number.MAX_SAFE_INTEGER : TOOLTIP_HOVER_DELAY_MS) |
| 34 | +
|
32 | 35 | let cursorAnchor = $derived.by(() => { |
33 | 36 | const x = cursorX |
34 | 37 | const y = cursorY |
|
37 | 40 | } |
38 | 41 | }) |
39 | 42 |
|
| 43 | + const POST_DRAG_CLICK_GUARD_MS = 250 |
| 44 | + let dragEndTime = 0 |
| 45 | +
|
40 | 46 | function onPointerEnter(e: PointerEvent) { |
41 | 47 | const sidebarRoot = (e.currentTarget as HTMLElement).closest('[data-slot="sidebar"]') |
42 | 48 | dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1 |
43 | 49 | cursorX = e.clientX |
44 | 50 | cursorY = e.clientY |
45 | 51 | } |
46 | 52 |
|
47 | | - const COLLAPSE_DRAG_OVERSHOOT_PX = 100 |
48 | | - const POST_DRAG_CLICK_GUARD_MS = 250 |
49 | | - let dragEndTime = 0 |
| 53 | + function onPointerMoveOnTrigger(e: PointerEvent) { |
| 54 | + if (activePointerId !== null) return |
| 55 | + cursorX = e.clientX |
| 56 | + cursorY = e.clientY |
| 57 | + } |
| 58 | +
|
| 59 | + function endDrag() { |
| 60 | + if (activePointerId !== null) { |
| 61 | + window.removeEventListener('pointermove', onWindowPointerMove) |
| 62 | + window.removeEventListener('pointerup', onWindowPointerUp) |
| 63 | + window.removeEventListener('pointercancel', onWindowPointerUp) |
| 64 | + activePointerId = null |
| 65 | + } |
| 66 | + document.body.style.cursor = '' |
| 67 | + document.body.style.userSelect = '' |
| 68 | + isDragging = false |
| 69 | + sidebar.isResizing = false |
| 70 | + if (dragMoved) { |
| 71 | + dragEndTime = Date.now() |
| 72 | + dragMoved = false |
| 73 | + } |
| 74 | + } |
50 | 75 |
|
51 | | - function onPointerMove(e: PointerEvent) { |
| 76 | + function onWindowPointerMove(e: PointerEvent) { |
| 77 | + if (e.pointerId !== activePointerId) return |
52 | 78 | cursorX = e.clientX |
53 | 79 | cursorY = e.clientY |
54 | | - const button = e.currentTarget as HTMLButtonElement |
55 | | - if (!button.hasPointerCapture(e.pointerId)) return |
56 | 80 | const delta = (e.clientX - dragStartX) * dragDirection |
57 | 81 | if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) { |
58 | 82 | dragMoved = true |
|
63 | 87 | if (!dragMoved) return |
64 | 88 | if (sidebar.state === 'collapsed') { |
65 | 89 | if (delta > 0) { |
66 | | - button.releasePointerCapture(e.pointerId) |
67 | | - document.body.style.cursor = '' |
68 | | - document.body.style.userSelect = '' |
69 | | - isDragging = false |
70 | | - sidebar.isResizing = false |
71 | | - dragMoved = false |
72 | | - dragEndTime = Date.now() |
| 90 | + endDrag() |
73 | 91 | sidebar.setOpen(true) |
74 | 92 | } |
75 | 93 | return |
76 | 94 | } |
77 | 95 | const targetWidth = dragStartWidthPx + delta |
78 | | - if (targetWidth < SIDEBAR_MIN_WIDTH_PX - COLLAPSE_DRAG_OVERSHOOT_PX) { |
79 | | - button.releasePointerCapture(e.pointerId) |
80 | | - document.body.style.cursor = '' |
81 | | - document.body.style.userSelect = '' |
82 | | - isDragging = false |
83 | | - sidebar.isResizing = false |
84 | | - dragMoved = false |
85 | | - dragEndTime = Date.now() |
| 96 | + if (targetWidth < SIDEBAR_WIDTH_ICON_PX) { |
| 97 | + endDrag() |
86 | 98 | sidebar.resetWidth() |
87 | 99 | sidebar.setOpen(false) |
88 | 100 | return |
89 | 101 | } |
90 | 102 | sidebar.setWidth(targetWidth) |
91 | 103 | } |
92 | 104 |
|
| 105 | + function onWindowPointerUp(e: PointerEvent) { |
| 106 | + if (e.pointerId !== activePointerId) return |
| 107 | + endDrag() |
| 108 | + } |
| 109 | +
|
93 | 110 | function onPointerDown(e: PointerEvent) { |
94 | 111 | if (sidebar.isMobile) return |
95 | | - const button = e.currentTarget as HTMLButtonElement |
96 | | - const sidebarRoot = button.closest('[data-slot="sidebar"]') |
| 112 | + if (e.button !== 0) return |
| 113 | + const target = e.currentTarget as HTMLElement |
| 114 | + const sidebarRoot = target.closest('[data-slot="sidebar"]') |
97 | 115 | dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1 |
98 | 116 |
|
99 | 117 | const container = sidebarRoot?.querySelector('[data-slot="sidebar-container"]') |
100 | 118 | dragStartWidthPx = container instanceof HTMLElement ? container.offsetWidth : 256 |
101 | 119 | dragStartX = e.clientX |
102 | 120 | dragMoved = false |
103 | | - button.setPointerCapture(e.pointerId) |
| 121 | + activePointerId = e.pointerId |
104 | 122 | document.body.style.cursor = 'col-resize' |
105 | 123 | document.body.style.userSelect = 'none' |
| 124 | + window.addEventListener('pointermove', onWindowPointerMove) |
| 125 | + window.addEventListener('pointerup', onWindowPointerUp) |
| 126 | + window.addEventListener('pointercancel', onWindowPointerUp) |
106 | 127 | } |
107 | 128 |
|
108 | | - function onPointerUp(e: PointerEvent) { |
109 | | - const button = e.currentTarget as HTMLButtonElement |
110 | | - if (button.hasPointerCapture(e.pointerId)) button.releasePointerCapture(e.pointerId) |
111 | | - document.body.style.cursor = '' |
112 | | - document.body.style.userSelect = '' |
113 | | - isDragging = false |
114 | | - sidebar.isResizing = false |
115 | | - if (dragMoved) { |
116 | | - dragEndTime = Date.now() |
117 | | - dragMoved = false |
118 | | - } |
119 | | - } |
120 | | -
|
121 | | - const DOUBLE_CLICK_DELAY_MS = 300 |
| 129 | + const DOUBLE_CLICK_DELAY_MS = 150 |
122 | 130 | let pendingClickTimer: ReturnType<typeof setTimeout> | undefined |
123 | 131 |
|
124 | 132 | function onClick(e: MouseEvent) { |
|
145 | 153 |
|
146 | 154 | <TooltipPrimitive.Root |
147 | 155 | bind:open={tooltipOpen} |
148 | | - delayDuration={TOOLTIP_HOVER_DELAY_MS} |
| 156 | + delayDuration={tooltipDelay} |
149 | 157 | disableHoverableContent |
150 | 158 | > |
151 | | - <TooltipPrimitive.Trigger disabled={isDragging}> |
| 159 | + <TooltipPrimitive.Trigger> |
152 | 160 | {#snippet child({ props })} |
153 | 161 | {@const buttonProps = props as HTMLButtonAttributes} |
154 | 162 | <button |
|
160 | 168 | }} |
161 | 169 | onpointermove={(e) => { |
162 | 170 | buttonProps.onpointermove?.(e) |
163 | | - onPointerMove(e) |
| 171 | + onPointerMoveOnTrigger(e) |
164 | 172 | }} |
165 | 173 | onpointerdown={(e) => { |
166 | 174 | buttonProps.onpointerdown?.(e) |
167 | 175 | onPointerDown(e) |
168 | 176 | }} |
169 | | - onpointerup={(e) => { |
170 | | - buttonProps.onpointerup?.(e) |
171 | | - onPointerUp(e) |
172 | | - }} |
173 | | - onpointercancel={onPointerUp} |
174 | 177 | onclick={(e) => { |
175 | 178 | buttonProps.onclick?.(e) |
176 | 179 | onClick(e) |
|
203 | 206 | side="bottom" |
204 | 207 | align="center" |
205 | 208 | sideOffset={TOOLTIP_CURSOR_OFFSET} |
206 | | - class="px-3 py-2" |
207 | 209 | > |
208 | | - <div class="flex flex-col gap-1.5"> |
| 210 | + <div class="flex flex-col gap-1"> |
209 | 211 | {#if sidebar.state === 'expanded'} |
210 | | - <div>Drag to resize</div> |
| 212 | + <div class="flex w-full items-center justify-between gap-3"> |
| 213 | + <span>Drag to resize</span> |
| 214 | + <div class="flex items-center gap-0.5 opacity-0"> |
| 215 | + <ShortcutWrapper size="sm" theme="navigation">⌘</ShortcutWrapper> |
| 216 | + <ShortcutWrapper size="sm" theme="navigation">.</ShortcutWrapper> |
| 217 | + </div> |
| 218 | + </div> |
211 | 219 | {/if} |
212 | | - <div class="flex items-center justify-between gap-3"> |
| 220 | + <div class="flex w-full items-center justify-between gap-3"> |
213 | 221 | <span> |
214 | 222 | {sidebar.state === 'expanded' ? 'Click to collapse' : 'Click to expand'} |
215 | 223 | </span> |
216 | | - <div class="flex items-center gap-1"> |
217 | | - <ShortcutWrapper size="md" theme="navigation">⌘</ShortcutWrapper> |
218 | | - <ShortcutWrapper size="md" theme="navigation">.</ShortcutWrapper> |
| 224 | + <div class="flex items-center gap-0.5"> |
| 225 | + <ShortcutWrapper size="sm" theme="navigation">⌘</ShortcutWrapper> |
| 226 | + <ShortcutWrapper size="sm" theme="navigation">.</ShortcutWrapper> |
219 | 227 | </div> |
220 | 228 | </div> |
221 | 229 | </div> |
|
0 commit comments