| author | |
| committer | |
| log | ca40de081c3ab087cd6458ce9bef12e43c5a7233 |
| tree | 5d5f5e323d63883822f1245ccb375302af324fd9 |
| parent | d97060709ca7741c0c45d2ffad4e241b992259f2 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 101 insertions(+), 43 deletions(-)
apps/web/src/components/ThreadTerminalDrawer.tsx+22-18| ... | @@ -1212,6 +1212,7 @@ export default function ThreadTerminalDrawer({ | ... | @@ -1212,6 +1212,7 @@ export default function ThreadTerminalDrawer({ |
| 1212 | pointerId: number; | 1212 | pointerId: number; |
| 1213 | startY: number; | 1213 | startY: number; |
| 1214 | startHeight: number; | 1214 | startHeight: number; |
| 1215 | drawer: HTMLElement | null; | ||
| 1215 | } | null>(null); | 1216 | } | null>(null); |
| 1216 | const didResizeDuringDragRef = useRef(false); | 1217 | const didResizeDuringDragRef = useRef(false); |
| 1217 | 1218 | ||
| ... | @@ -1418,26 +1419,26 @@ export default function ThreadTerminalDrawer({ | ... | @@ -1418,26 +1419,26 @@ export default function ThreadTerminalDrawer({ |
| 1418 | pointerId: event.pointerId, | 1419 | pointerId: event.pointerId, |
| 1419 | startY: event.clientY, | 1420 | startY: event.clientY, |
| 1420 | startHeight: drawerHeightRef.current, | 1421 | startHeight: drawerHeightRef.current, |
| 1422 | drawer: event.currentTarget.parentElement, | ||
| 1421 | }; | 1423 | }; |
| 1422 | }, []); | 1424 | }, []); |
| 1423 | 1425 | ||
| 1424 | const handleResizePointerMove = useCallback( | 1426 | // Writes the height straight to the drawer while dragging so the edge lands in |
| 1425 | (event: ReactPointerEvent<HTMLDivElement>) => { | 1427 | // the pointer's frame; React state commits once on release. |
| 1426 | const resizeState = resizeStateRef.current; | 1428 | const handleResizePointerMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => { |
| 1427 | if (!resizeState || resizeState.pointerId !== event.pointerId) return; | 1429 | const resizeState = resizeStateRef.current; |
| 1428 | event.preventDefault(); | 1430 | if (!resizeState || resizeState.pointerId !== event.pointerId) return; |
| 1429 | const clampedHeight = clampDrawerHeight( | 1431 | event.preventDefault(); |
| 1430 | resizeState.startHeight + (resizeState.startY - event.clientY), | 1432 | const clampedHeight = clampDrawerHeight( |
| 1431 | ); | 1433 | resizeState.startHeight + (resizeState.startY - event.clientY), |
| 1432 | if (clampedHeight === drawerHeightRef.current) { | 1434 | ); |
| 1433 | return; | 1435 | if (clampedHeight === drawerHeightRef.current) { |
| 1434 | } | 1436 | return; |
| 1435 | didResizeDuringDragRef.current = true; | 1437 | } |
| 1436 | drawerHeightRef.current = clampedHeight; | 1438 | didResizeDuringDragRef.current = true; |
| 1437 | setDrawerHeight(clampedHeight); | 1439 | drawerHeightRef.current = clampedHeight; |
| 1438 | }, | 1440 | resizeState.drawer?.style.setProperty("height", `${clampedHeight}px`); |
| 1439 | [setDrawerHeight], | 1441 | }, []); |
| 1440 | ); | ||
| 1441 | 1442 | ||
| 1442 | const handleResizePointerEnd = useCallback( | 1443 | const handleResizePointerEnd = useCallback( |
| 1443 | (event: ReactPointerEvent<HTMLDivElement>) => { | 1444 | (event: ReactPointerEvent<HTMLDivElement>) => { |
| ... | @@ -1450,10 +1451,11 @@ export default function ThreadTerminalDrawer({ | ... | @@ -1450,10 +1451,11 @@ export default function ThreadTerminalDrawer({ |
| 1450 | if (!didResizeDuringDragRef.current) { | 1451 | if (!didResizeDuringDragRef.current) { |
| 1451 | return; | 1452 | return; |
| 1452 | } | 1453 | } |
| 1454 | setDrawerHeight(drawerHeightRef.current); | ||
| 1453 | syncHeight(drawerHeightRef.current); | 1455 | syncHeight(drawerHeightRef.current); |
| 1454 | setResizeEpoch((value) => value + 1); | 1456 | setResizeEpoch((value) => value + 1); |
| 1455 | }, | 1457 | }, |
| 1456 | [syncHeight], | 1458 | [setDrawerHeight, syncHeight], |
| 1457 | ); | 1459 | ); |
| 1458 | 1460 | ||
| 1459 | useEffect(() => { | 1461 | useEffect(() => { |
| ... | @@ -1510,6 +1512,7 @@ export default function ThreadTerminalDrawer({ | ... | @@ -1510,6 +1512,7 @@ export default function ThreadTerminalDrawer({ |
| 1510 | onPointerMove={handleResizePointerMove} | 1512 | onPointerMove={handleResizePointerMove} |
| 1511 | onPointerUp={handleResizePointerEnd} | 1513 | onPointerUp={handleResizePointerEnd} |
| 1512 | onPointerCancel={handleResizePointerEnd} | 1514 | onPointerCancel={handleResizePointerEnd} |
| 1515 | onLostPointerCapture={handleResizePointerEnd} | ||
| 1513 | /> | 1516 | /> |
| 1514 | ) : null} | 1517 | ) : null} |
| 1515 | <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-4 py-6 text-center text-sm text-muted-foreground"> | 1518 | <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-4 py-6 text-center text-sm text-muted-foreground"> |
| ... | @@ -1546,6 +1549,7 @@ export default function ThreadTerminalDrawer({ | ... | @@ -1546,6 +1549,7 @@ export default function ThreadTerminalDrawer({ |
| 1546 | onPointerMove={handleResizePointerMove} | 1549 | onPointerMove={handleResizePointerMove} |
| 1547 | onPointerUp={handleResizePointerEnd} | 1550 | onPointerUp={handleResizePointerEnd} |
| 1548 | onPointerCancel={handleResizePointerEnd} | 1551 | onPointerCancel={handleResizePointerEnd} |
| 1552 | onLostPointerCapture={handleResizePointerEnd} | ||
| 1549 | /> | 1553 | /> |
| 1550 | ) : null} | 1554 | ) : null} |
| 1551 | 1555 |
apps/web/src/components/preview/PreviewPanelShell.tsx+22-5| ... | @@ -1,4 +1,11 @@ | ... | @@ -1,4 +1,11 @@ |
| 1 | import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"; | 1 | import { |
| 2 | type CSSProperties, | ||
| 3 | type ReactNode, | ||
| 4 | type RefObject, | ||
| 5 | useLayoutEffect, | ||
| 6 | useRef, | ||
| 7 | useState, | ||
| 8 | } from "react"; | ||
| 2 | 9 | ||
| 3 | import { isElectron } from "~/env"; | 10 | import { isElectron } from "~/env"; |
| 4 | import { | 11 | import { |
| ... | @@ -6,6 +13,7 @@ import { | ... | @@ -6,6 +13,7 @@ import { |
| 6 | type PreviewPanelInlineSize, | 13 | type PreviewPanelInlineSize, |
| 7 | usePreviewPanelInlineSize, | 14 | usePreviewPanelInlineSize, |
| 8 | } from "~/hooks/usePreviewPanelInlineSize"; | 15 | } from "~/hooks/usePreviewPanelInlineSize"; |
| 16 | import { RESIZABLE_WIDTH_PROPERTY } from "~/hooks/useResizableWidth"; | ||
| 9 | 17 | ||
| 10 | export { getPreviewPanelMaxWidth }; | 18 | export { getPreviewPanelMaxWidth }; |
| 11 | import { cn } from "~/lib/utils"; | 19 | import { cn } from "~/lib/utils"; |
| ... | @@ -122,10 +130,15 @@ function PreviewPanelShellFrame( | ... | @@ -122,10 +130,15 @@ function PreviewPanelShellFrame( |
| 122 | )} | 130 | )} |
| 123 | style={ | 131 | style={ |
| 124 | isInline | 132 | isInline |
| 125 | ? { | 133 | ? ({ |
| 126 | width: maximized ? "100%" : collapsible && !open ? "0px" : `${width}px`, | 134 | [RESIZABLE_WIDTH_PROPERTY]: `${width}px`, |
| 135 | width: maximized | ||
| 136 | ? "100%" | ||
| 137 | : collapsible && !open | ||
| 138 | ? "0px" | ||
| 139 | : `var(${RESIZABLE_WIDTH_PROPERTY})`, | ||
| 127 | transitionDuration: suppressWidthTransition ? "0ms" : undefined, | 140 | transitionDuration: suppressWidthTransition ? "0ms" : undefined, |
| 128 | } | 141 | } as CSSProperties) |
| 129 | : undefined | 142 | : undefined |
| 130 | } | 143 | } |
| 131 | data-preview-panel-mode={props.mode} | 144 | data-preview-panel-mode={props.mode} |
| ... | @@ -135,7 +148,11 @@ function PreviewPanelShellFrame( | ... | @@ -135,7 +148,11 @@ function PreviewPanelShellFrame( |
| 135 | <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> | 148 | <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> |
| 136 | <div | 149 | <div |
| 137 | className="flex h-full min-h-0 min-w-0 flex-col" | 150 | className="flex h-full min-h-0 min-w-0 flex-col" |
| 138 | style={collapsible && !maximized ? { width: `calc(${width}px - 1px)` } : undefined} | 151 | style={ |
| 152 | collapsible && !maximized | ||
| 153 | ? { width: `calc(var(${RESIZABLE_WIDTH_PROPERTY}) - 1px)` } | ||
| 154 | : undefined | ||
| 155 | } | ||
| 139 | > | 156 | > |
| 140 | {props.children} | 157 | {props.children} |
| 141 | </div> | 158 | </div> |
apps/web/src/hooks/useResizableWidth.test.tsx+17-5| ... | @@ -2,12 +2,19 @@ import { act, useLayoutEffect, type PointerEvent } from "react"; | ... | @@ -2,12 +2,19 @@ import { act, useLayoutEffect, type PointerEvent } from "react"; |
| 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; | 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 4 | 4 | ||
| 5 | import { useResizableWidth } from "./useResizableWidth"; | 5 | import { RESIZABLE_WIDTH_PROPERTY, useResizableWidth } from "./useResizableWidth"; |
| 6 | 6 | ||
| 7 | let renderer: ReactTestRenderer; | 7 | let renderer: ReactTestRenderer; |
| 8 | let result: ReturnType<typeof useResizableWidth>; | 8 | let result: ReturnType<typeof useResizableWidth>; |
| 9 | let captured = false; | 9 | let captured = false; |
| 10 | const hostStyle = new Map<string, string>(); | ||
| 10 | const target = { | 11 | const target = { |
| 12 | parentElement: { | ||
| 13 | style: { | ||
| 14 | setProperty: (property: string, value: string) => hostStyle.set(property, value), | ||
| 15 | removeProperty: (property: string) => hostStyle.delete(property), | ||
| 16 | }, | ||
| 17 | }, | ||
| 11 | setPointerCapture: () => { | 18 | setPointerCapture: () => { |
| 12 | captured = true; | 19 | captured = true; |
| 13 | }, | 20 | }, |
| ... | @@ -65,6 +72,7 @@ function Panel({ | ... | @@ -65,6 +72,7 @@ function Panel({ |
| 65 | 72 | ||
| 66 | beforeEach(async () => { | 73 | beforeEach(async () => { |
| 67 | savedWidths.clear(); | 74 | savedWidths.clear(); |
| 75 | hostStyle.clear(); | ||
| 68 | captured = false; | 76 | captured = false; |
| 69 | frame = undefined; | 77 | frame = undefined; |
| 70 | style.cursor = ""; | 78 | style.cursor = ""; |
| ... | @@ -102,7 +110,9 @@ describe("panel resize cleanup", () => { | ... | @@ -102,7 +110,9 @@ describe("panel resize cleanup", () => { |
| 102 | result.handlers.onPointerMove(pointer(50)); | 110 | result.handlers.onPointerMove(pointer(50)); |
| 103 | }); | 111 | }); |
| 104 | await act(() => frame?.(0)); | 112 | await act(() => frame?.(0)); |
| 105 | expect(result.width).toBe(450); | 113 | expect(hostStyle.get(RESIZABLE_WIDTH_PROPERTY)).toBe("450px"); |
| 114 | expect(hostStyle.get("transition-property")).toBe("none"); | ||
| 115 | expect(result.width).toBe(400); | ||
| 106 | // Queue another move to check that interruption cancels pending work too. | 116 | // Queue another move to check that interruption cancels pending work too. |
| 107 | await act(() => result.handlers.onPointerMove(pointer(25))); | 117 | await act(() => result.handlers.onPointerMove(pointer(25))); |
| 108 | expect(style.cursor).toBe("col-resize"); | 118 | expect(style.cursor).toBe("col-resize"); |
| ... | @@ -116,6 +126,7 @@ describe("panel resize cleanup", () => { | ... | @@ -116,6 +126,7 @@ describe("panel resize cleanup", () => { |
| 116 | expect(style.cursor).toBe(""); | 126 | expect(style.cursor).toBe(""); |
| 117 | expect(style.userSelect).toBe(""); | 127 | expect(style.userSelect).toBe(""); |
| 118 | expect(captured).toBe(false); | 128 | expect(captured).toBe(false); |
| 129 | expect(hostStyle.has("transition-property")).toBe(false); | ||
| 119 | expect(cancelAnimationFrame).toHaveBeenCalledWith(42); | 130 | expect(cancelAnimationFrame).toHaveBeenCalledWith(42); |
| 120 | if (reason === "unmount") { | 131 | if (reason === "unmount") { |
| 121 | expect(setItem).not.toHaveBeenCalled(); | 132 | expect(setItem).not.toHaveBeenCalled(); |
| ... | @@ -151,11 +162,11 @@ describe("panel resize cleanup", () => { | ... | @@ -151,11 +162,11 @@ describe("panel resize cleanup", () => { |
| 151 | result.handlers.onPointerMove(pointer(-200)); | 162 | result.handlers.onPointerMove(pointer(-200)); |
| 152 | }); | 163 | }); |
| 153 | await act(() => frame?.(0)); | 164 | await act(() => frame?.(0)); |
| 154 | expect(result.width).toBe(Math.min(700, initialMax)); | 165 | expect(hostStyle.get(RESIZABLE_WIDTH_PROPERTY)).toBe(`${Math.min(700, initialMax)}px`); |
| 155 | await act(() => renderer.update(<Panel maxWidth={nextMax} />)); | 166 | await act(() => renderer.update(<Panel maxWidth={nextMax} />)); |
| 156 | await act(() => result.handlers.onPointerMove(pointer(-250))); | 167 | await act(() => result.handlers.onPointerMove(pointer(-250))); |
| 157 | await act(() => frame?.(0)); | 168 | await act(() => frame?.(0)); |
| 158 | expect(result.width).toBe(Math.min(750, nextMax)); | 169 | expect(hostStyle.get(RESIZABLE_WIDTH_PROPERTY)).toBe(`${Math.min(750, nextMax)}px`); |
| 159 | await act(() => result.handlers.onPointerUp(pointer(-300))); | 170 | await act(() => result.handlers.onPointerUp(pointer(-300))); |
| 160 | expect(result.width).toBe(nextMax); | 171 | expect(result.width).toBe(nextMax); |
| 161 | expect(setItem).toHaveBeenCalledExactlyOnceWith("test-panel-width", String(nextMax)); | 172 | expect(setItem).toHaveBeenCalledExactlyOnceWith("test-panel-width", String(nextMax)); |
| ... | @@ -225,10 +236,11 @@ describe("panel width storage changes", () => { | ... | @@ -225,10 +236,11 @@ describe("panel width storage changes", () => { |
| 225 | result.handlers.onPointerMove(pointer(50)); | 236 | result.handlers.onPointerMove(pointer(50)); |
| 226 | }); | 237 | }); |
| 227 | await act(() => frame?.(0)); | 238 | await act(() => frame?.(0)); |
| 228 | expect(result.width).toBe(450); | 239 | expect(hostStyle.get(RESIZABLE_WIDTH_PROPERTY)).toBe("450px"); |
| 229 | await act(() => result.handlers.onPointerMove(pointer(25))); | 240 | await act(() => result.handlers.onPointerMove(pointer(25))); |
| 230 | await act(() => renderer.update(<Panel storageKey="thread-b" />)); | 241 | await act(() => renderer.update(<Panel storageKey="thread-b" />)); |
| 231 | expect(result.width).toBe(650); | 242 | expect(result.width).toBe(650); |
| 243 | expect(hostStyle.get(RESIZABLE_WIDTH_PROPERTY)).toBe("650px"); | ||
| 232 | expect(captured).toBe(false); | 244 | expect(captured).toBe(false); |
| 233 | expect(style.cursor).toBe(""); | 245 | expect(style.cursor).toBe(""); |
| 234 | await act(() => { | 246 | await act(() => { |
apps/web/src/hooks/useResizableWidth.ts+34-12| ... | @@ -12,6 +12,12 @@ import { useResizeDrag } from "./useResizeDrag"; | ... | @@ -12,6 +12,12 @@ import { useResizeDrag } from "./useResizeDrag"; |
| 12 | 12 | ||
| 13 | const WidthSchema = Schema.Finite; | 13 | const WidthSchema = Schema.Finite; |
| 14 | 14 | ||
| 15 | /** | ||
| 16 | * Custom property the host element must size itself from. The drag handle sits | ||
| 17 | * directly inside the host, and a drag writes this property on it each frame. | ||
| 18 | */ | ||
| 19 | export const RESIZABLE_WIDTH_PROPERTY = "--resizable-width"; | ||
| 20 | |||
| 15 | export interface UseResizableWidthOptions { | 21 | export interface UseResizableWidthOptions { |
| 16 | /** localStorage key the persisted width is stored under. */ | 22 | /** localStorage key the persisted width is stored under. */ |
| 17 | readonly storageKey: string; | 23 | readonly storageKey: string; |
| ... | @@ -39,9 +45,10 @@ export interface ResizableWidthHandlers { | ... | @@ -39,9 +45,10 @@ export interface ResizableWidthHandlers { |
| 39 | * specified edge. Width is read on mount or storage-key changes and persisted on | 45 | * specified edge. Width is read on mount or storage-key changes and persisted on |
| 40 | * drag-end (not on every rAF tick — would otherwise be ~60 writes/sec). | 46 | * drag-end (not on every rAF tick — would otherwise be ~60 writes/sec). |
| 41 | * | 47 | * |
| 42 | * The hook updates an internal `width` state during drag (so the panel | 48 | * During a drag the hook writes `RESIZABLE_WIDTH_PROPERTY` on the handle's |
| 43 | * follows the cursor live) and only commits to localStorage when the user | 49 | * parent, so the panel follows the cursor in the same frame without |
| 44 | * lifts the pointer or the drag is interrupted. | 50 | * re-rendering the owner. `width` state and localStorage commit once when the |
| 51 | * user lifts the pointer or the drag is interrupted. | ||
| 45 | */ | 52 | */ |
| 46 | export function useResizableWidth(options: UseResizableWidthOptions): { | 53 | export function useResizableWidth(options: UseResizableWidthOptions): { |
| 47 | readonly width: number; | 54 | readonly width: number; |
| ... | @@ -75,21 +82,26 @@ export function useResizableWidth(options: UseResizableWidthOptions): { | ... | @@ -75,21 +82,26 @@ export function useResizableWidth(options: UseResizableWidthOptions): { |
| 75 | } | 82 | } |
| 76 | 83 | ||
| 77 | const clampedWidth = clamp(widthState.width); | 84 | const clampedWidth = clamp(widthState.width); |
| 78 | const latestOptions = useRef({ clamp, storageKey }); | 85 | const latestOptions = useRef({ clamp, storageKey, width: clampedWidth }); |
| 79 | useLayoutEffect(() => { | 86 | useLayoutEffect(() => { |
| 80 | latestOptions.current = { clamp, storageKey }; | 87 | latestOptions.current = { clamp, storageKey, width: clampedWidth }; |
| 81 | }, [clamp, storageKey]); | 88 | }, [clamp, clampedWidth, storageKey]); |
| 82 | 89 | ||
| 83 | const handlers = useResizeDrag<HTMLElement>( | 90 | const { refresh, ...handlers } = useResizeDrag<HTMLElement>((event) => { |
| 84 | () => ({ | 91 | const host = event.currentTarget.parentElement; |
| 92 | // A collapsible host animates width; live drag writes must not. The host | ||
| 93 | // renders its own transition-duration, so override a property it leaves alone. | ||
| 94 | host?.style.setProperty("transition-property", "none"); | ||
| 95 | return { | ||
| 85 | width: clampedWidth, | 96 | width: clampedWidth, |
| 86 | edge, | 97 | edge, |
| 87 | resize(value) { | 98 | resize(value) { |
| 88 | const nextWidth = latestOptions.current.clamp(value); | 99 | const nextWidth = latestOptions.current.clamp(value); |
| 89 | setWidthState({ storageKey, width: nextWidth }); | 100 | host?.style.setProperty(RESIZABLE_WIDTH_PROPERTY, `${nextWidth}px`); |
| 90 | return nextWidth; | 101 | return nextWidth; |
| 91 | }, | 102 | }, |
| 92 | finish(finalWidth) { | 103 | finish(finalWidth) { |
| 104 | setWidthState({ storageKey, width: finalWidth }); | ||
| 93 | // Commit once at drag-end to avoid 60Hz localStorage writes. | 105 | // Commit once at drag-end to avoid 60Hz localStorage writes. |
| 94 | try { | 106 | try { |
| 95 | setLocalStorageItem(latestOptions.current.storageKey, finalWidth, WidthSchema); | 107 | setLocalStorageItem(latestOptions.current.storageKey, finalWidth, WidthSchema); |
| ... | @@ -97,9 +109,19 @@ export function useResizableWidth(options: UseResizableWidthOptions): { | ... | @@ -97,9 +109,19 @@ export function useResizableWidth(options: UseResizableWidthOptions): { |
| 97 | console.error("Could not persist panel width.", error); | 109 | console.error("Could not persist panel width.", error); |
| 98 | } | 110 | } |
| 99 | }, | 111 | }, |
| 100 | }), | 112 | cleanup(committed) { |
| 101 | storageKey, | 113 | host?.style.removeProperty("transition-property"); |
| 102 | ); | 114 | // React skips the write when the rendered width did not change. |
| 115 | if (!committed) { | ||
| 116 | host?.style.setProperty(RESIZABLE_WIDTH_PROPERTY, `${latestOptions.current.width}px`); | ||
| 117 | } | ||
| 118 | }, | ||
| 119 | }; | ||
| 120 | }, storageKey); | ||
| 121 | |||
| 122 | // Bounds can change mid-drag (sidebar opens, window narrows) and the render | ||
| 123 | // rewrites the committed width, so re-apply the live pointer position. | ||
| 124 | useLayoutEffect(refresh, [clamp, clampedWidth, refresh]); | ||
| 103 | 125 | ||
| 104 | return { width: clampedWidth, handlers }; | 126 | return { width: clampedWidth, handlers }; |
| 105 | } | 127 | } |
apps/web/src/hooks/useResizeDrag.ts+6-3| ... | @@ -5,7 +5,8 @@ interface ResizeSession { | ... | @@ -5,7 +5,8 @@ interface ResizeSession { |
| 5 | edge: "left" | "right"; | 5 | edge: "left" | "right"; |
| 6 | resize: (width: number) => number; | 6 | resize: (width: number) => number; |
| 7 | finish: (width: number, moved: boolean) => void; | 7 | finish: (width: number, moved: boolean) => void; |
| 8 | cleanup?: () => void; | 8 | /** Runs on every end; `committed` is false when the drag was abandoned without `finish`. */ |
| 9 | cleanup?: (committed: boolean) => void; | ||
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | /** Shared pointer lifecycle for side panels, including interrupted and sub-frame drags. */ | 12 | /** Shared pointer lifecycle for side panels, including interrupted and sub-frame drags. */ |
| ... | @@ -49,7 +50,7 @@ export function useResizeDrag<T extends HTMLElement>( | ... | @@ -49,7 +50,7 @@ export function useResizeDrag<T extends HTMLElement>( |
| 49 | } | 50 | } |
| 50 | document.body.style.removeProperty("cursor"); | 51 | document.body.style.removeProperty("cursor"); |
| 51 | document.body.style.removeProperty("user-select"); | 52 | document.body.style.removeProperty("user-select"); |
| 52 | active.session.cleanup?.(); | 53 | active.session.cleanup?.(commit); |
| 53 | if (commit) active.session.finish(active.width, active.moved); | 54 | if (commit) active.session.finish(active.width, active.moved); |
| 54 | }, | 55 | }, |
| 55 | [flush], | 56 | [flush], |
| ... | @@ -81,6 +82,8 @@ export function useResizeDrag<T extends HTMLElement>( | ... | @@ -81,6 +82,8 @@ export function useResizeDrag<T extends HTMLElement>( |
| 81 | }; | 82 | }; |
| 82 | 83 | ||
| 83 | return { | 84 | return { |
| 85 | /** Re-applies the pointer position to an active drag, e.g. after its bounds change. */ | ||
| 86 | refresh: flush, | ||
| 84 | onPointerDown(event: PointerEvent<T>) { | 87 | onPointerDown(event: PointerEvent<T>) { |
| 85 | if (event.button !== 0 || drag.current) return; | 88 | if (event.button !== 0 || drag.current) return; |
| 86 | const session = start(event); | 89 | const session = start(event); |
| ... | @@ -88,7 +91,7 @@ export function useResizeDrag<T extends HTMLElement>( | ... | @@ -88,7 +91,7 @@ export function useResizeDrag<T extends HTMLElement>( |
| 88 | try { | 91 | try { |
| 89 | event.currentTarget.setPointerCapture(event.pointerId); | 92 | event.currentTarget.setPointerCapture(event.pointerId); |
| 90 | } catch { | 93 | } catch { |
| 91 | session.cleanup?.(); | 94 | session.cleanup?.(false); |
| 92 | return; | 95 | return; |
| 93 | } | 96 | } |
| 94 | event.preventDefault(); | 97 | event.preventDefault(); |