Skip to content

useMove refactor to useLayoutEffect breaks Playwright/synthetic drag events on Slider #9744

@rastersysteme

Description

@rastersysteme

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

  1. Render a <Slider> with <SliderThumb> using react-aria-components@1.16.0
  2. 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,
})
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions