-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Provide a general summary of the issue here
After upgrading react-aria-components from 1.13.0 to 1.16.0, Playwright's dragTo() no longer moves SliderThumb elements. The slider value remains unchanged after a simulated drag interaction.
The root cause is a refactor of the useMove hook in @react-aria/interactions. In v1.13.0, global pointermove/pointerup listeners were attached synchronously inside the onPointerDown handler. In v1.16.0, onPointerDown calls setPointerDown('pointer'), deferring listener attachment to a useLayoutEffect. Playwright dispatches its synthetic pointermove events before React completes the state update + layout effect cycle, so the events are missed entirely.
🤔 Expected Behavior?
Synthetic pointer events dispatched programmatically (e.g., by Playwright's dragTo() or manual page.mouse sequences) should move the slider thumb, as they did in v1.13.0.
😯 Current Behavior
The slider thumb does not move. The pointermove events are dispatched before the useLayoutEffect attaches the global listener, so they are never captured.
💁 Possible Solution
Attach the global listeners synchronously in onPointerDown as before, or use flushSync to ensure the layout effect runs before returning from the handler. The useLayoutEffect cleanup pattern could still be used for the teardown path.
🔦 Context
For real user interactions, the browser's event loop ensures useLayoutEffect runs before the next pointer event is dispatched from the queue. But test automation tools like Playwright dispatch synthetic events synchronously without going through the browser's event scheduling, so pointermove fires before the layout effect has a chance to attach the listener.
v1.13.0 (@react-aria/interactions/src/useMove.ts) — synchronous:
moveProps.onPointerDown = (e: React.PointerEvent) => {
if (e.button === 0 && state.current.id == null) {
start();
e.stopPropagation();
e.preventDefault();
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
state.current.id = e.pointerId;
addGlobalListener(window, 'pointermove', onPointerMove, false); // immediate
addGlobalListener(window, 'pointerup', onPointerUp, false); // immediate
addGlobalListener(window, 'pointercancel', onPointerUp, false);
}
};v1.16.0 — deferred via state + useLayoutEffect:
moveProps.onPointerDown = (e: React.PointerEvent) => {
if (e.button === 0 && state.current.id == null) {
start();
e.stopPropagation();
e.preventDefault();
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
state.current.id = e.pointerId;
setPointerDown('pointer'); // deferred — triggers re-render + useLayoutEffect
}
};
useLayoutEffect(() => {
if (pointerDown === 'pointer') {
addGlobalListener(window, 'pointermove', onPointerMove, false);
addGlobalListener(window, 'pointerup', onPointerUp, false);
addGlobalListener(window, 'pointercancel', onPointerUp, false);
// ...
}
}, [pointerDown, ...]);Versions used:
react-aria-components: 1.16.0 (broken), 1.13.0 (working)react: 19.2.4- Playwright: 1.52.0
- Browser: Chromium (Playwright default)
🖥️ Steps to Reproduce
- Render a
<Slider>with<SliderThumb>usingreact-aria-components@1.16.0 - Use Playwright to drag the thumb:
const thumb = page.getByTestId('my-slider-thumb')
await thumb.dragTo(page.locator('body'), {
targetPosition: { x: targetX, y: targetY },
force: true,
})- Read the slider value — it is unchanged
This works correctly with react-aria-components@1.13.0.
Version
1.16.0
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
Mac OS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response