diff --git a/src/platform/reordered-tap-click.test.ts b/src/platform/reordered-tap-click.test.ts index e415ac99..103ec26d 100644 --- a/src/platform/reordered-tap-click.test.ts +++ b/src/platform/reordered-tap-click.test.ts @@ -87,6 +87,35 @@ describe("installReorderedTapClick", () => { expect(log.filter((l) => l.startsWith("click"))).toEqual(["click:1", "click:1"]); }); + it("leaves a quick double-click alone when its second press shares the first release's timestamp", () => { + // Fast input (automation, a quick double-click) stamps both at the same time step. + el.dispatchEvent(ev("pointerdown", 5900.9)); + el.dispatchEvent(ev("mousedown", 5900.9, { detail: 1 })); + el.dispatchEvent(ev("pointerup", 5901)); + el.dispatchEvent(ev("mouseup", 5901)); + el.dispatchEvent(ev("click", 5901, { detail: 1 })); + el.dispatchEvent(ev("pointerdown", 5901)); + el.dispatchEvent(ev("mousedown", 5901, { detail: 2 })); + el.dispatchEvent(ev("pointerup", 5901)); + el.dispatchEvent(ev("mouseup", 5901)); + el.dispatchEvent(ev("click", 5901, { detail: 2 })); + el.dispatchEvent(ev("dblclick", 5912, { detail: 2 })); + vi.runAllTimers(); + expect(log.filter((l) => /^(click|dblclick)/.test(l))).toEqual(["click:1", "click:2", "dblclick:2"]); + }); + + it("finishes nothing when the engine releases the press before the tick", () => { + el.dispatchEvent(ev("pointerup", 1393)); + el.dispatchEvent(ev("pointerdown", 1386)); + el.dispatchEvent(ev("mousedown", 1386, { detail: 1 })); + el.dispatchEvent(ev("pointerup", 1400)); + el.dispatchEvent(ev("mouseup", 1400)); + el.dispatchEvent(ev("click", 1400, { detail: 1 })); + vi.runAllTimers(); + expect(log.filter((l) => l.startsWith("click"))).toEqual(["click:1"]); + expect(log.filter((l) => l.startsWith("pointerup"))).toHaveLength(2); + }); + it("repeats the double-click detail of the stale press", () => { reversedTap(1393, 1386, 2); expect(log).toContain("click:2"); diff --git a/src/platform/reordered-tap-click.ts b/src/platform/reordered-tap-click.ts index b2c2d048..ac78e00d 100644 --- a/src/platform/reordered-tap-click.ts +++ b/src/platform/reordered-tap-click.ts @@ -3,8 +3,8 @@ * focus, and with a third-party input method it can hand the page a tap as `pointerup` -> * `pointerdown` (WebKit bug 219670). The press then has no release: the tap's click is lost, * and the engine, which still considers the button down, starts the next tap with a bare - * `mousedown` (no `pointerdown`). `pointerdown.timeStamp` stays native, so a press stamped no - * later than the `pointerup` just before it is a reordered tap: finish it with a synthetic + * `mousedown` (no `pointerdown`). `pointerdown.timeStamp` stays native, so a press stamped before + * the `pointerup` just before it is a reordered tap: finish it with a synthetic * `pointerup`/`mouseup`/`click`, and give the next press the `pointerdown` the engine skips. * The next tap's own click lands on its own target, so it is left alone. * @@ -47,7 +47,10 @@ export function installReorderedTapClick({ const onPointerUp = (e: PointerEvent) => { if (!isTrusted(e)) return; down = false; - if (primaryMouse(e)) lastUp = e.timeStamp; + if (!primaryMouse(e)) return; + lastUp = e.timeStamp; + // The engine released the press itself, so it was not a stale one: there is nothing to finish. + pending = null; }; const onPointerCancel = (e: PointerEvent) => { @@ -60,7 +63,8 @@ export function installReorderedTapClick({ const before = lastUp; lastUp = null; pending = null; - if (!primaryMouse(e) || before === null || e.timeStamp > before) return; + // A quick in-order click can stamp its press with its last release's time: only an earlier press is stale. + if (!primaryMouse(e) || before === null || e.timeStamp >= before) return; if (!(e.target instanceof Element)) return; pending = { target: e.target, detail: 1, x: e.clientX, y: e.clientY, mods: mods(e) }; // After this tick the page's own pointerdown/mousedown handlers have run, as for an in-order tap.