fix(web): use global pointer listeners for sidebar resize handle#497
fix(web): use global pointer listeners for sidebar resize handle#497GeT-LeFt wants to merge 3 commits intotiann:mainfrom
Conversation
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.
There was a problem hiding this comment.
Findings
- [Major] Drag state is no longer scoped to the initiating pointer. With the new document-level listeners, any unrelated
pointermovecan resize the sidebar and any unrelatedpointerup/pointercancelcan stop the drag whileisDraggingis true. Evidence:web/src/hooks/useSidebarResize.ts:38,web/src/hooks/useSidebarResize.ts:43,web/src/hooks/useSidebarResize.ts:45.
Summary
Review mode: initial
One correctness issue found in the new resize listener flow: the drag now follows every pointer event on the page instead of only the pointer that started the resize. Residual risk: this hook has no automated coverage in the repo, so multi-pointer/touch regressions depend on manual verification.
Testing
- Not run (automation)
HAPI Bot
|
|
||
| const onUp = () => setIsDragging(false) | ||
|
|
||
| document.addEventListener('pointermove', onMove) |
There was a problem hiding this comment.
[MAJOR] These document listeners are not filtered by pointerId, so once a resize starts, any other pointer on the page can move or end it. That is a regression from setPointerCapture, which kept the drag bound to the initiating pointer only.
Suggested fix:
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)
}, [width])
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)
}There was a problem hiding this comment.
Findings
- [Major] Document-level drag handling is still not scoped to the pointer that started the resize. Any second finger/stylus/mouse
pointermovecan resize the sidebar, and any unrelatedpointerup/pointercancelcan end the drag, which regresses the oldsetPointerCapturebehavior. Evidence:web/src/hooks/useSidebarResize.ts:38.
Suggested fix: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) }, [width]) 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) }
Summary
- Review mode: follow-up after new commits
- The follow-up commit only retriggers CI, so the previously reported current-diff correctness issue remains unresolved. Residual risk: this hook has no automated coverage for multi-pointer/touch interruption behavior.
Testing
- Not run (automation)
HAPI Bot
| const onPointerUp = useCallback(() => { | ||
| if (!isDragging) return | ||
| setIsDragging(false) | ||
| const onMove = (e: PointerEvent) => { |
There was a problem hiding this comment.
[MAJOR] The document listeners still accept every PointerEvent while isDragging is true. That means a second finger/stylus/mouse can move the sidebar, and an unrelated pointerup/pointercancel can stop the drag, instead of keeping ownership with the pointer that began resizing.
Suggested fix:
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)
}, [width])
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)
}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.
There was a problem hiding this comment.
Findings
- No new findings on the current diff.
Summary
- Review mode: follow-up after new commits
- The follow-up commit addresses the previously reported pointer-ownership issue by tracking the active
pointerIdin the document-level handlers atweb/src/hooks/useSidebarResize.ts:26andweb/src/hooks/useSidebarResize.ts:40. Residual risk: there is still no automated coverage foruseSidebarResize, so regressions aroundpointercanceland multi-pointer interactions would rely on manual testing.
Testing
- Not run (automation); no dedicated test for
web/src/hooks/useSidebarResize.tsfound in repo.
HAPI Bot
Problem
The sidebar resize handle uses
setPointerCaptureon the 4px-wide handle element. When dragging fast, the cursor easily leaves the handle area, and despite pointer capture, some browsers (especially on Linux/Wayland and touch devices) stop delivering events to the element. This causes:col-resizeafter releasing the mouse buttonThis is a known limitation of element-level pointer capture on narrow drag targets (related chromium issue).
Solution
Replace element-level
setPointerCapture+ React event props with document-levelpointermove/pointerup/pointercancellisteners, managed viauseEffect:isDraggingbecomes truepointercancelis also handled (covers touch interruptions, browser tab switches, etc.)The hook's public API simplifies from
{ onPointerDown, onPointerMove, onPointerUp }to just{ onPointerDown }— the rest is handled internally.Changes
web/src/hooks/useSidebarResize.tsweb/src/router.tsxonPointerMove/onPointerUpprops from resize handleTesting
Fixes #500