Repository navigation
Conversation
🦋 Changeset detectedLatest commit: eae5371 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## v1-base #581 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 78 78
Lines 2563 2567 +4
Branches 820 827 +7
=========================================
+ Hits 2563 2567 +4 🚀 New features to boost your workflow:
|
fb9ffe3 to
b0f8b5c
Compare
|
Thanks for catching that! I switched the base to |
|
@rjsgud49 please rebase 🙏 |
b0f8b5c to
f435c93
Compare
|
Rebased onto the latest |
Keep delayMs from v1-base. A press is one pointer stream, so emulated mouse events after a tap no longer call onClick again, and pointercancel cancels the press when the browser scrolls.
f435c93 to
abd64b2
Compare
hyesungoh
left a comment
There was a problem hiding this comment.
Thanks! This matches #580 well, and the tests are thorough.
One fix: per the Pointer Events spec ("Chorded button interactions"), pointerup fires only when the last button is released. Other button changes fire pointermove. So for left down → right down → left up → right up, the only pointerup has button: 2. The release check skips it, the press never ends, and the next click calls onLongPressEnd instead of onClick. Suggestions below.
| // A mouse chord can release the right button while the left button is still down. | ||
| // Touch and pen releases are not mouse buttons; ending those on pointerup is the whole press. | ||
| if (outcome === 'release' && isNonPrimaryMouseButton(event)) { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
| // A mouse chord can release the right button while the left button is still down. | |
| // Touch and pen releases are not mouse buttons; ending those on pointerup is the whole press. | |
| if (outcome === 'release' && isNonPrimaryMouseButton(event)) { | |
| return; | |
| } |
This case can't happen: a chord never fires pointerup until the last button is up.
|
|
||
| function isNonPrimaryMouseButton(event: PointerEvent<HTMLElement>) { | ||
| return event.pointerType === 'mouse' && event.button !== 0; | ||
| } |
There was a problem hiding this comment.
| function isNonPrimaryMouseButton(event: PointerEvent<HTMLElement>) { | |
| return event.pointerType === 'mouse' && event.button !== 0; | |
| } |
No longer used.
| it('does not end a mouse press when another mouse button is released', async () => { | ||
| const callbacks = createCallbacks(); | ||
| render(<PressTarget {...callbacks} />); | ||
| const button = screen.getByTestId('test-button'); | ||
|
|
||
| fireEvent.pointerDown(button, pointerInit({ button: 0 })); | ||
| await advance(500); | ||
| fireEvent.pointerUp(button, pointerInit({ button: 2 })); | ||
|
|
||
| expect(callbacks.onLongPressEnd).not.toHaveBeenCalled(); | ||
|
|
||
| fireEvent.pointerUp(button, pointerInit({ button: 0 })); | ||
| expect(callbacks.onLongPressEnd).toHaveBeenCalledTimes(1); | ||
| expect(callbacks.onClick).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
| it('does not end a mouse press when another mouse button is released', async () => { | |
| const callbacks = createCallbacks(); | |
| render(<PressTarget {...callbacks} />); | |
| const button = screen.getByTestId('test-button'); | |
| fireEvent.pointerDown(button, pointerInit({ button: 0 })); | |
| await advance(500); | |
| fireEvent.pointerUp(button, pointerInit({ button: 2 })); | |
| expect(callbacks.onLongPressEnd).not.toHaveBeenCalled(); | |
| fireEvent.pointerUp(button, pointerInit({ button: 0 })); | |
| expect(callbacks.onLongPressEnd).toHaveBeenCalledTimes(1); | |
| expect(callbacks.onClick).not.toHaveBeenCalled(); | |
| }); | |
| it('ends a mouse press on the single pointerup after a button chord', async () => { | |
| const callbacks = createCallbacks(); | |
| render(<PressTarget {...callbacks} />); | |
| const button = screen.getByTestId('test-button'); | |
| fireEvent.pointerDown(button, pointerInit({ button: 0, buttons: 1 })); | |
| await advance(500); | |
| // Chord changes fire pointermove, not pointerdown or pointerup. | |
| fireEvent.pointerMove(button, pointerInit({ button: 2, buttons: 3 })); | |
| fireEvent.pointerMove(button, pointerInit({ button: 0, buttons: 2 })); | |
| fireEvent.pointerUp(button, pointerInit({ button: 2, buttons: 0 })); | |
| expect(callbacks.onLongPressEnd).toHaveBeenCalledTimes(1); | |
| fireEvent.pointerDown(button, pointerInit({ button: 0, buttons: 1 })); | |
| fireEvent.pointerUp(button, pointerInit({ button: 0, buttons: 0 })); | |
| expect(callbacks.onClick).toHaveBeenCalledTimes(1); | |
| }); |
The old test sent a pointerup that browsers don't send. This one follows the spec order.
|
Thanks, you are right about the chord order. Applied all three suggestions in eae5371: removed the non-primary button check on release and the unused helper, and replaced the test with the spec order. The new test fails on the previous commit and passes now. test:coverage, test:compiled, typecheck and lint pass locally. |
Overview
useLongPresslistens to both touch and mouse events. After a tap, browsers fire emulatedmousedown/mouseup, so one tap is handled as two presses. Onmain, #576 ignores mouse presses within 1 second of a touch. That commit is not onv1-base, so this branch fixes the same double handling by listening to pointer events instead.pointerdown/pointerup/pointercancelgive one event stream per press, so the emulated mouse events never reach the hook.pointercancelalso cancels the press when the browser takes the pointer to scroll.delayMsis unchanged.This is a breaking change. The returned handlers are
onPointerDown,onPointerUp,onPointerLeave, andonPointerCancel, plusonPointerMovewhenmoveThresholdis set.onMouseDown,onMouseUp,onMouseLeave,onTouchStart,onTouchEnd,onMouseMove, andonTouchMoveare no longer returned. Callbacks receiveReact.PointerEventinstead ofMouseEvent | TouchEvent.Repro (Chromium via Playwright,
hasTouch/isMobile, hook callbacks fromuseLongPress(onLongPress, { onClick, onLongPressEnd })). Thev1-basecolumn is the mouse-and-touch hook, which does not have #576:v1-baseclick, clickclicklongPress, longPressEnd, clicklongPress, longPressEndclickclickpointercancel)The event order Chromium dispatches for the tap is
pointerdown, pointerup, touchend, mousedown, mouseup, click. For a scroll it ispointerdown, pointercancel, pointerleave, and the scroller moves.Fix: the hook tracks one
pointerIdand ignores a non-primary pointer and any mouse button other than the primary button.pointerupends that press (onClickbefore the delay,onLongPressEndafter).pointercancelandpointerleavecancel it without callingonClick; if the long press already fired they callonLongPressEndonce.Tests added:
mousedown/mouseupcallsonClickonceonClickpointercancelbefore the delay calls nothing, and after a long press callsonLongPressEndoncepointerId0PointerEventpolyfill in the spec fileAdded a major changeset, matching the other breaking changes already on
v1-base.Closes #580
Checklist
yarn run fixto format and lint the code and docs?yarn run test:coverageto make sure there is no uncovered line? (100%;test:compiled,test:docs, andtest:skillalso pass)