Let one pointer drag the Discover top card at a time
A second primary pointer pressing mid-drag replaced the drag record, so neither release cleared the first pointer's transform and the card stayed displaced. A press while a drag is in progress is now ignored, and a new top card ends a drag left on the old one so it cannot block the next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1040,6 +1040,45 @@ describe('DiscoverPage', () => {
|
||||
expect(card.style.transform).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['without moving', null],
|
||||
['after moving', 160],
|
||||
])('ignores a second primary pointer pressing and releasing %s mid-drag, then springs the card back clean', async (_, moves) => {
|
||||
const server = stubServer([BOWLS, TIKKA])
|
||||
const animations = stubAnimate({ autoFinish: false })
|
||||
await renderPage()
|
||||
const card = await screen.findByRole('article', { name: 'Korean beef bowls' })
|
||||
const [tryStamp, passStamp] = stamps(card)
|
||||
// A touch (id 7) drags; a mouse (id 1), also primary, comes and goes.
|
||||
const mouse = (x: number) => ({ pointerId: 1, pointerType: 'mouse', isPrimary: true, button: 0, clientX: 400 + x, clientY: 300 })
|
||||
|
||||
press(card)
|
||||
drag(card, 72, 0)
|
||||
fireEvent.pointerDown(card, mouse(0))
|
||||
if (moves !== null) fireEvent.pointerMove(card, mouse(moves))
|
||||
fireEvent.pointerUp(card, mouse(moves ?? 0))
|
||||
fireEvent.lostPointerCapture(card, mouse(moves ?? 0))
|
||||
expect(capture).toHaveBeenCalledExactlyOnceWith(7)
|
||||
expect(card.style.transform).toBe('translate(72px,0px) rotate(4deg)')
|
||||
expect(animations).toEqual([])
|
||||
|
||||
release(card)
|
||||
expect(animations.map((a) => [a.target, a.options])).toEqual([[card, { duration: 420, easing: SPRING }]])
|
||||
expect(card.style.transform).toBe('')
|
||||
expect(tryStamp!.style.opacity).toBe('')
|
||||
expect(passStamp!.style.opacity).toBe('')
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(screen.getByRole('button', { name: 'Try it' })).toBeEnabled()
|
||||
expect(sent(server.fetchMock)).toEqual([])
|
||||
|
||||
// The card can be dragged again.
|
||||
press(card)
|
||||
drag(card, 36, 0)
|
||||
expect(card.style.transform).toBe('translate(36px,0px) rotate(2deg)')
|
||||
})
|
||||
|
||||
it('a key decision mid-drag ends the drag and throws the card from rest', async () => {
|
||||
const server = stubServer([BOWLS, TIKKA], {
|
||||
'POST /api/v1/suggestions/s1/accept': () => {
|
||||
|
||||
@@ -221,8 +221,9 @@ export function DiscoverPage() {
|
||||
// press, until the release decides past the threshold or springs it back
|
||||
// short of it. The release decides from where the pointer let go, which
|
||||
// may differ from its last move. A cancel, or losing the capture before
|
||||
// the release, springs it back from its last move. Input while busy is
|
||||
// ignored, and a drag held into a busy spell (a Retry) is put back on
|
||||
// the release, springs it back from its last move. One pointer drags at
|
||||
// a time: a press while a drag is in progress is ignored, as is input
|
||||
// while busy, and a drag held into a busy spell (a Retry) is put back on
|
||||
// release.
|
||||
const swipe = useMemo<SwipeHandlers>(() => {
|
||||
function track(card: HTMLElement, current: Drag, event: PointerEvent<HTMLElement>) {
|
||||
@@ -245,7 +246,7 @@ export function DiscoverPage() {
|
||||
}
|
||||
return {
|
||||
onPointerDown: (event) => {
|
||||
if (deciding.current || !event.isPrimary || event.button !== 0) return
|
||||
if (drag.current !== null || deciding.current || !event.isPrimary || event.button !== 0) return
|
||||
stopSpring()
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
drag.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, dx: 0, transform: null }
|
||||
@@ -266,9 +267,14 @@ export function DiscoverPage() {
|
||||
// Leaving the page stops an exit before its decision is sent.
|
||||
useEffect(() => () => exit.current?.cancel(), [])
|
||||
|
||||
const topId = top?.id
|
||||
// A drag belongs to the card it started on, which a new top card ends.
|
||||
useLayoutEffect(() => {
|
||||
drag.current = null
|
||||
}, [topId])
|
||||
|
||||
// Once the deck moves past the card a decision was sent for, the cards
|
||||
// behind it move up a depth.
|
||||
const topId = top?.id
|
||||
useLayoutEffect(() => {
|
||||
if (leaving.current === null || leaving.current === topId) return
|
||||
leaving.current = null
|
||||
|
||||
Reference in New Issue
Block a user