From 369ae74dcdd11e18c8461866e7bd1a6b09046a4c Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 25 Feb 2026 15:43:52 -0800 Subject: [PATCH 1/5] Add pointer capture --- .../src/hooks/useMouseHandler.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/react-resize-handle/src/hooks/useMouseHandler.ts b/packages/react-resize-handle/src/hooks/useMouseHandler.ts index 6bbc1b3e..7a9c9711 100644 --- a/packages/react-resize-handle/src/hooks/useMouseHandler.ts +++ b/packages/react-resize-handle/src/hooks/useMouseHandler.ts @@ -111,6 +111,22 @@ export function useMouseHandler(params: UseMouseHandlerParams): { ); }); + // Heads up! + // + // Pointer capture ensures that all subsequent pointer events (and their compatibility + // mouse events) are routed to the capturing element, even when the cursor moves outside + // the element bounds. This prevents a "stuck drag" state that occurs when the user drags + // rapidly to a limit and the cursor leaves the handle before mouseup fires. + // Touch events already have implicit capture, so this is only needed for mouse/pen. + const onPointerCaptureStart = useEventCallback((event: PointerEvent) => { + if ( + event.pointerType !== 'touch' && + event.currentTarget instanceof HTMLElement + ) { + event.currentTarget.setPointerCapture(event.pointerId); + } + }); + const onPointerDown = useEventCallback((event: NativeTouchOrMouseEvent) => { dragStartOriginCoords.current = getEventClientCoords(event); // As we start dragging, save the current value otherwise the value increases, @@ -145,18 +161,20 @@ export function useMouseHandler(params: UseMouseHandlerParams): { const attachHandlers = React.useCallback( (node: HTMLElement) => { + node.addEventListener('pointerdown', onPointerCaptureStart); node.addEventListener('mousedown', onPointerDown); node.addEventListener('touchstart', onPointerDown); }, - [onPointerDown] + [onPointerCaptureStart, onPointerDown] ); const detachHandlers = React.useCallback( (node: HTMLElement) => { + node.removeEventListener('pointerdown', onPointerCaptureStart); node.removeEventListener('mousedown', onPointerDown); node.removeEventListener('touchstart', onPointerDown); }, - [onPointerDown] + [onPointerCaptureStart, onPointerDown] ); React.useEffect(() => { From 83b0dda3e8a568386825d4b8d78e7868dcbb83b5 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 25 Feb 2026 15:44:09 -0800 Subject: [PATCH 2/5] Ignore native drag events --- .../src/hooks/useMouseHandler.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/react-resize-handle/src/hooks/useMouseHandler.ts b/packages/react-resize-handle/src/hooks/useMouseHandler.ts index 7a9c9711..43f134c9 100644 --- a/packages/react-resize-handle/src/hooks/useMouseHandler.ts +++ b/packages/react-resize-handle/src/hooks/useMouseHandler.ts @@ -111,8 +111,6 @@ export function useMouseHandler(params: UseMouseHandlerParams): { ); }); - // Heads up! - // // Pointer capture ensures that all subsequent pointer events (and their compatibility // mouse events) are routed to the capturing element, even when the cursor moves outside // the element bounds. This prevents a "stuck drag" state that occurs when the user drags @@ -121,12 +119,20 @@ export function useMouseHandler(params: UseMouseHandlerParams): { const onPointerCaptureStart = useEventCallback((event: PointerEvent) => { if ( event.pointerType !== 'touch' && - event.currentTarget instanceof HTMLElement + event.currentTarget instanceof Element ) { event.currentTarget.setPointerCapture(event.pointerId); } }); + // Suppressing the native "dragstart" event prevents the browser's HTML5 drag-and-drop + // system from activating on the handle element. Without this, the browser can enter a + // native drag state (showing a 🚫 cursor) that swallows mousemove/mouseup events, + // leaving the custom drag in a permanently stuck state. + const onNativeDragStart = useEventCallback((event: Event) => { + event.preventDefault(); + }); + const onPointerDown = useEventCallback((event: NativeTouchOrMouseEvent) => { dragStartOriginCoords.current = getEventClientCoords(event); // As we start dragging, save the current value otherwise the value increases, @@ -164,8 +170,9 @@ export function useMouseHandler(params: UseMouseHandlerParams): { node.addEventListener('pointerdown', onPointerCaptureStart); node.addEventListener('mousedown', onPointerDown); node.addEventListener('touchstart', onPointerDown); + node.addEventListener('dragstart', onNativeDragStart); }, - [onPointerCaptureStart, onPointerDown] + [onPointerCaptureStart, onPointerDown, onNativeDragStart] ); const detachHandlers = React.useCallback( @@ -173,8 +180,9 @@ export function useMouseHandler(params: UseMouseHandlerParams): { node.removeEventListener('pointerdown', onPointerCaptureStart); node.removeEventListener('mousedown', onPointerDown); node.removeEventListener('touchstart', onPointerDown); + node.removeEventListener('dragstart', onNativeDragStart); }, - [onPointerCaptureStart, onPointerDown] + [onPointerCaptureStart, onPointerDown, onNativeDragStart] ); React.useEffect(() => { From dc03bd1a1392098e12900bd1d7b8efdc14759cc7 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 25 Feb 2026 15:46:09 -0800 Subject: [PATCH 3/5] Disable text selection during resize operation --- .../src/hooks/useMouseHandler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-resize-handle/src/hooks/useMouseHandler.ts b/packages/react-resize-handle/src/hooks/useMouseHandler.ts index 43f134c9..a1127b99 100644 --- a/packages/react-resize-handle/src/hooks/useMouseHandler.ts +++ b/packages/react-resize-handle/src/hooks/useMouseHandler.ts @@ -81,7 +81,18 @@ export function useMouseHandler(params: UseMouseHandlerParams): { } }); + // Heads up! + // + // Suppressing "selectstart" on the document during a drag prevents the browser from + // selecting text as the user moves the pointer. The listener is added on drag start + // and removed on drag end so normal text selection is unaffected outside of a resize. + const onSelectStart = useEventCallback((event: Event) => { + event.preventDefault(); + }); + const onDragEnd = useEventCallback((event: NativeTouchOrMouseEvent) => { + targetDocument?.removeEventListener('selectstart', onSelectStart); + if (isMouseEvent(event)) { targetDocument?.removeEventListener('mouseup', onDragEnd); targetDocument?.removeEventListener('mousemove', onDrag); @@ -148,11 +159,13 @@ export function useMouseHandler(params: UseMouseHandlerParams): { if (event.target !== event.currentTarget || event.button !== 0) { return; } + targetDocument?.addEventListener('selectstart', onSelectStart); targetDocument?.addEventListener('mouseup', onDragEnd); targetDocument?.addEventListener('mousemove', onDrag); } if (isTouchEvent(event)) { + targetDocument?.addEventListener('selectstart', onSelectStart); targetDocument?.addEventListener('touchend', onDragEnd); targetDocument?.addEventListener('touchmove', onDrag); } From f49b64b89f3f24638e46f8e41eacdcc0979cef92 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 25 Feb 2026 15:56:48 -0800 Subject: [PATCH 4/5] Update comment --- packages/react-resize-handle/src/hooks/useMouseHandler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-resize-handle/src/hooks/useMouseHandler.ts b/packages/react-resize-handle/src/hooks/useMouseHandler.ts index a1127b99..fa3c7449 100644 --- a/packages/react-resize-handle/src/hooks/useMouseHandler.ts +++ b/packages/react-resize-handle/src/hooks/useMouseHandler.ts @@ -81,8 +81,6 @@ export function useMouseHandler(params: UseMouseHandlerParams): { } }); - // Heads up! - // // Suppressing "selectstart" on the document during a drag prevents the browser from // selecting text as the user moves the pointer. The listener is added on drag start // and removed on drag end so normal text selection is unaffected outside of a resize. From efbf969c1e8931beac2e7a46faeb124c6ee7e6e1 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 26 Feb 2026 08:12:09 -0800 Subject: [PATCH 5/5] Add change file --- ...resize-handle-111de9f8-9f6d-4fd6-b671-8fa1fcfb736e.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-contrib-react-resize-handle-111de9f8-9f6d-4fd6-b671-8fa1fcfb736e.json diff --git a/change/@fluentui-contrib-react-resize-handle-111de9f8-9f6d-4fd6-b671-8fa1fcfb736e.json b/change/@fluentui-contrib-react-resize-handle-111de9f8-9f6d-4fd6-b671-8fa1fcfb736e.json new file mode 100644 index 00000000..0c28ea7d --- /dev/null +++ b/change/@fluentui-contrib-react-resize-handle-111de9f8-9f6d-4fd6-b671-8fa1fcfb736e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix UX for edge cases", + "packageName": "@fluentui-contrib/react-resize-handle", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +}