Skip to content

Commit 22ab86d

Browse files
author
PR Batch Tester
committed
fix(web): preserve content ownership across late scroll begin
1 parent 0e48fd7 commit 22ab86d

2 files changed

Lines changed: 54 additions & 9 deletions

File tree

‎apps/web/src/components/HistorySwipeNavigation.browser.test.tsx‎

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { describe, expect, it, vi } from "vite-plus/test";
22

3-
import { render } from "../../test/render";
3+
import { render, stubGlobal } from "../../test/render";
44
import { HistorySwipeNavigation } from "./HistorySwipeNavigation";
55

66
type Scroller = "shadow" | "light" | "none" | "shadow-dialog";
7-
// Each step is one wheel event, optionally followed by the scroll it caused or a remount.
8-
type Step = readonly [deltaX: number, after?: number | "remount" | "remove"];
7+
type Step = readonly [
8+
event: number | "begin" | "end" | "zoom",
9+
after?: number | "remount" | "remove",
10+
];
911

1012
function mountPanel(container: HTMLElement, scroller: Scroller, scrollLeft: number) {
1113
const panel = container.appendChild(document.createElement("div"));
@@ -43,19 +45,62 @@ describe("HistorySwipeNavigation", () => {
4345
null,
4446
],
4547
["a scroller removed after a small first event", "shadow", 400, [[1, "remove"], [130]], null],
48+
[
49+
"a scroll begin arriving after the first wheel and scroller removal",
50+
"shadow",
51+
400,
52+
[[1, "remove"], ["begin"], [130]],
53+
null,
54+
],
55+
[
56+
"a new history gesture after a content scroll ends",
57+
"shadow",
58+
400,
59+
[[1, "remove"], ["end"], ["begin"], [130]],
60+
"forward",
61+
],
62+
[
63+
"a scroll begin after an ignored zoom wheel",
64+
"none",
65+
0,
66+
[["zoom"], ["begin"], [130]],
67+
"forward",
68+
],
4669
["swiping inside a dialog in a shadow root", "shadow-dialog", 0, [[-130]], null],
4770
["swiping left over content without overflow", "none", 0, [[-130]], "back"],
4871
["swiping right over content without overflow", "none", 0, [[130]], "forward"],
4972
])("%s", async (_name, scroller, scrollLeft, steps, navigates) => {
73+
const listeners = { begin: new Set<() => void>(), end: new Set<() => void>() };
74+
using _bridge = stubGlobal("desktopBridge", {
75+
onTrackpadScrollBegin: (listener: () => void) => {
76+
listeners.begin.add(listener);
77+
return () => listeners.begin.delete(listener);
78+
},
79+
onTrackpadScrollEnd: (listener: () => void) => {
80+
listeners.end.add(listener);
81+
return () => listeners.end.delete(listener);
82+
},
83+
});
5084
await using screen = await render(<HistorySwipeNavigation />);
5185
using back = vi.spyOn(window.history, "back").mockImplementation(() => undefined);
5286
using forward = vi.spyOn(window.history, "forward").mockImplementation(() => undefined);
5387
using _canGoBack = vi.spyOn(window.navigation, "canGoBack", "get").mockReturnValue(true);
5488
using _canGoForward = vi.spyOn(window.navigation, "canGoForward", "get").mockReturnValue(true);
5589
let { viewport, content } = mountPanel(screen.container, scroller, scrollLeft);
5690

57-
for (const [deltaX, after] of [...steps, [0] as const]) {
58-
content.dispatchEvent(new WheelEvent("wheel", { deltaX, bubbles: true, composed: true }));
91+
for (const [event, after] of [...steps, [0] as const]) {
92+
if (typeof event === "number" || event === "zoom") {
93+
content.dispatchEvent(
94+
new WheelEvent("wheel", {
95+
deltaX: typeof event === "number" ? event : 0,
96+
ctrlKey: event === "zoom",
97+
bubbles: true,
98+
composed: true,
99+
}),
100+
);
101+
} else {
102+
for (const listener of listeners[event]) listener();
103+
}
59104
if (after === "remount") {
60105
const replacement = viewport.cloneNode(true) as HTMLDivElement;
61106
const { scrollLeft: previous } = viewport;

‎apps/web/src/components/HistorySwipeNavigation.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type SwipeGesture =
2121
bounds: SwipeBounds;
2222
}
2323
// Not a swipe, or one already decided: the rest of its events are ignored.
24-
| { kind: "ignored" };
24+
| { kind: "ignored"; scrollerOwned?: true };
2525
type SwipeIndicatorState = {
2626
direction: HistoryDirection;
2727
progress: number;
@@ -101,9 +101,9 @@ export function HistorySwipeNavigation() {
101101
gesture = { kind: "ignored" };
102102
hideIndicator();
103103
};
104-
// Chromium brackets each trackpad scroll, momentum included, with these.
104+
// Scroll-begin IPC can arrive after the first wheel already gave ownership to content.
105105
const unsubscribeScrollBegin = window.desktopBridge?.onTrackpadScrollBegin(() => {
106-
if (gesture?.kind === "ignored") gesture = null;
106+
if (gesture?.kind === "ignored" && !gesture.scrollerOwned) gesture = null;
107107
});
108108
const unsubscribeScrollEnd = window.desktopBridge?.onTrackpadScrollEnd(() => {
109109
if (gesture?.kind === "tracking") release();
@@ -121,7 +121,7 @@ export function HistorySwipeNavigation() {
121121
const path = event.composedPath();
122122
// Claim content scrolling while its layout still exists, before a possible remount.
123123
if (isInHorizontalScroller(path)) {
124-
gesture = { kind: "ignored" };
124+
gesture = { kind: "ignored", scrollerOwned: true };
125125
return;
126126
}
127127
gesture.sumX += event.deltaX;

0 commit comments

Comments
 (0)