Skip to content

fix(useLongPress): ignore emulated mouse events after a touch - #576

Open
rjsgud49 wants to merge 2 commits into
toss:mainfrom
rjsgud49:fix/use-long-press-emulated-mouse
Open

rjsgud49 wants to merge 2 commits into
toss:mainfrom
rjsgud49:fix/use-long-press-emulated-mouse

Conversation

@rjsgud49

Copy link
Copy Markdown
Contributor

Overview

On touch devices, useLongPress calls onClick twice for one tap.

After touchend, browsers fire emulated mousedown/mouseup (then click) on the same element. The hook attaches both touch and mouse handlers, so the emulated mousedown starts a second press and the emulated mouseup calls onClick again. After a long press from touch, the same emulated events call onClick right after onLongPressEnd.

Repro (Chromium via Playwright with hasTouch/isMobile, page.tap() on a button using useLongPress(onLongPress, { onClick, onLongPressEnd })):

main this PR
tap click, click click
touch held 700 ms longPress, longPressEnd, click longPress, longPressEnd
mouse click click click

The event order Chromium dispatches for the tap is touchstart, touchend, mousedown, mouseup, click.

Fix: the hook records the time of the last touchstart/touchend it handled, and a mouse press that starts less than 1 second after that is ignored. Ignoring the mousedown is enough: the following mouseup then finds no pending press and does nothing. Mouse input more than 1 second after a touch works as before, so hybrid touch/mouse devices keep both inputs.

Tests added:

  • tap followed by emulated mousedown/mouseup → onClick once (fails on main: called 2 times)
  • touch long press followed by emulated mouse events → no onClick (fails on main: called once)
  • mouse press 1 second after a touch → handled again

Added a patch changeset.

Checklist

  • Did you write the test code?
  • Have you run yarn run fix to format and lint the code and docs? (ran yarn lint and prettier --list-different on the changed files; no new warnings)
  • Have you run yarn run test:coverage to make sure there is no uncovered line? (100%; test:compiled also passes)
  • Did you write the JSDoc? (no public API change, JSDoc unchanged)

@changeset-bot

changeset-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1067826

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
react-simplikit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov-commenter

codecov-commenter commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (402995e) to head (1067826).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #576   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           70        70           
  Lines         2288      2304   +16     
  Branches       738       746    +8     
=========================================
+ Hits          2288      2304   +16     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hyesungoh hyesungoh left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

Could you strengthen one test? In "calls onClick once for a tap followed by emulated mouse events", onLongPress is asserted without advancing the timers, so it passes even if the emulated mousedown starts the long-press timer. Advancing past delay first catches that:

await act(async () => {
  vi.advanceTimersByTime(600);
});

Longer term, Pointer Events seem a better fit than a 1-second window, so I opened #580 for v1. Would you be interested in picking that up as well? The issue has the notes and references.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants