fix(web): use global pointer listeners for sidebar resize handle (#497)

* fix(web): use global pointer listeners for sidebar resize handle

The current implementation attaches pointermove/pointerup to the
resize handle element via setPointerCapture. When the cursor moves
fast enough to leave the narrow 4px handle, the browser may not
deliver subsequent pointer events to the element, causing:

- Cursor stuck as col-resize even after releasing the mouse
- Sidebar stops tracking the pointer, requiring a page reload

Switch to document-level pointermove/pointerup/pointercancel listeners
that are added on drag start and cleaned up on drag end. This
guarantees events are captured regardless of cursor position.

Also removes onPointerMove and onPointerUp from the hook's return
value (and the JSX props in router.tsx) since they are no longer
needed — the hook manages everything internally via useEffect.

* ci: retrigger CI (flaky AcpSdkBackend test)

* fix: scope drag listeners to the initiating pointer ID

Address review feedback: filter pointermove/pointerup/pointercancel
by the pointer that started the drag, so a second finger or stylus
cannot interfere with the resize.

---------

Co-authored-by: huchenxi <huchenxi@lattebank.com>
This commit is contained in:
hu chenxi
2026-04-21 13:55:49 +08:00
committed by GitHub
co-authored by huchenxi
parent 32755f9056
commit 1cb353b7b3
2 changed files with 26 additions and 11 deletions
+26 -9
View File
@@ -23,24 +23,41 @@ export function useSidebarResize() {
const [isDragging, setIsDragging] = useState(false)
const startXRef = useRef(0)
const startWidthRef = useRef(0)
const activePointerIdRef = useRef<number | null>(null)
const onPointerDown = useCallback((e: React.PointerEvent) => {
e.preventDefault()
activePointerIdRef.current = e.pointerId
startXRef.current = e.clientX
startWidthRef.current = width
setIsDragging(true)
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
}, [width])
const onPointerMove = useCallback((e: React.PointerEvent) => {
// Global listeners ensure pointerup is always captured even if cursor leaves the handle
useEffect(() => {
if (!isDragging) return
const delta = e.clientX - startXRef.current
setWidth(clamp(startWidthRef.current + delta))
}, [isDragging])
const onPointerUp = useCallback(() => {
if (!isDragging) return
setIsDragging(false)
const onMove = (e: PointerEvent) => {
if (e.pointerId !== activePointerIdRef.current) return
const delta = e.clientX - startXRef.current
setWidth(clamp(startWidthRef.current + delta))
}
const onUp = (e: PointerEvent) => {
if (e.pointerId !== activePointerIdRef.current) return
activePointerIdRef.current = null
setIsDragging(false)
}
document.addEventListener('pointermove', onMove)
document.addEventListener('pointerup', onUp)
document.addEventListener('pointercancel', onUp)
return () => {
document.removeEventListener('pointermove', onMove)
document.removeEventListener('pointerup', onUp)
document.removeEventListener('pointercancel', onUp)
}
}, [isDragging])
// Persist width to localStorage when drag ends
@@ -65,5 +82,5 @@ export function useSidebarResize() {
}
}, [isDragging])
return { width, isDragging, onPointerDown, onPointerMove, onPointerUp }
return { width, isDragging, onPointerDown }
}