mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): retire prepend mode on every reader wheel (#2913)
## Summary - retire Virtua prepend reconciliation on every ordinary reader wheel event, including events suppressed from its separate wheel-timing heuristic - keep Ctrl+wheel browser zoom excluded from reader ownership - arm the Ctrl+wheel prepend probe before pagination begins so it cannot miss the commit - preserve ESM/CJS patch parity and update the patch lock hash ## Why main was red PR #2855 added the reader-wheel retirement action after Virtua's existing suppression guard. Once the first event set that guard, later events in the same wheel burst returned before retiring prepend mode. A late ResizeObserver correction could then pull the viewport backward by 20–40px. The same test had failed twice on #2855 but passed its final retry, so the PR job appeared green; the merge commit lost all three retries. The separate Ctrl+wheel failure was a test race: its MutationObserver was registered after the request had already been triggered and could miss the prepend commit. ## Validation - desktop full unit suite: 3,515 passed - pre-push desktop check: passed - `git diff --check`: passed - CI is the E2E verification; no local E2E was run Fixes the main-branch failure in CI run 30180099007. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -21,7 +21,17 @@ test("reader wheel retires Virtua shift mode without publishing scroll end", ()
|
|||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
assert.deepEqual(addedActionBodies, [["I = 0;"], ["w = 0;"]]);
|
assert.deepEqual(addedActionBodies, [["I = 0;"], ["w = 0;"]]);
|
||||||
assert.match(patch, /\+\s+e\.q\(9\);/);
|
assert.match(
|
||||||
assert.match(patch, /\+\s+e\.B\(9\);/);
|
patch,
|
||||||
|
/if \(!e\.M\(\) \|\| t\.ctrlKey\) return;\n\+\s+e\.q\(9\);\n\+\s+if \(f\) return;/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
patch,
|
||||||
|
/if \(!e\.M\(\) \|\| t\.ctrlKey\) return;\n\+\s+e\.B\(9\);\n\+\s+if \(c\) return;/,
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
patch,
|
||||||
|
/\+\s+if \((?:f|c) \|\| !e\.M\(\) \|\| t\.ctrlKey\) return;/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(patch, /\+\s+e\.(?:q|B)\(2\);/);
|
assert.doesNotMatch(patch, /\+\s+e\.(?:q|B)\(2\);/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -252,6 +252,29 @@ test.describe("list virtualization", () => {
|
|||||||
});
|
});
|
||||||
await page.waitForTimeout(150);
|
await page.waitForTimeout(150);
|
||||||
const before = await sampleVisibleAnchor();
|
const before = await sampleVisibleAnchor();
|
||||||
|
const ctrlWheelPromise =
|
||||||
|
pageIndex === 0
|
||||||
|
? timeline.evaluate(
|
||||||
|
(scroller) =>
|
||||||
|
new Promise<boolean>((resolve) => {
|
||||||
|
const s = scroller as HTMLElement;
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
// Ctrl+wheel is browser zoom, not reader scroll intent. Fire
|
||||||
|
// it synchronously with the prepend DOM commit, before the
|
||||||
|
// ResizeObserver measurement batch reconciles estimated rows.
|
||||||
|
s.dispatchEvent(
|
||||||
|
new WheelEvent("wheel", { ctrlKey: true, deltaY: -100 }),
|
||||||
|
);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
observer.observe(s.firstElementChild ?? s, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: Promise.resolve(false);
|
||||||
const wheelTracePromise = timeline.evaluate(async (scroller) => {
|
const wheelTracePromise = timeline.evaluate(async (scroller) => {
|
||||||
const s = scroller as HTMLElement;
|
const s = scroller as HTMLElement;
|
||||||
let previousScrollTop = s.scrollTop;
|
let previousScrollTop = s.scrollTop;
|
||||||
@@ -297,25 +320,8 @@ test.describe("list virtualization", () => {
|
|||||||
});
|
});
|
||||||
const committedAnchor = await sampleVisibleAnchor(before.id);
|
const committedAnchor = await sampleVisibleAnchor(before.id);
|
||||||
const motion = await timeline.evaluate(
|
const motion = await timeline.evaluate(
|
||||||
async (scroller, { anchorId, anchorTop, oldHeight, testCtrlWheel }) => {
|
async (scroller, { anchorId, anchorTop, oldHeight }) => {
|
||||||
const s = scroller as HTMLElement;
|
const s = scroller as HTMLElement;
|
||||||
let ctrlWheelDispatched = false;
|
|
||||||
const mutationObserver = testCtrlWheel
|
|
||||||
? new MutationObserver(() => {
|
|
||||||
if (ctrlWheelDispatched) return;
|
|
||||||
ctrlWheelDispatched = true;
|
|
||||||
// Ctrl+wheel is browser zoom, not reader scroll intent. Fire it
|
|
||||||
// synchronously with the prepend DOM commit, before the
|
|
||||||
// ResizeObserver measurement batch reconciles estimated rows.
|
|
||||||
s.dispatchEvent(
|
|
||||||
new WheelEvent("wheel", { ctrlKey: true, deltaY: -100 }),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
mutationObserver?.observe(s.firstElementChild ?? s, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
let maxDrift = 0;
|
let maxDrift = 0;
|
||||||
let sawPrepend = false;
|
let sawPrepend = false;
|
||||||
let sawAnchorAfterPrepend = false;
|
let sawAnchorAfterPrepend = false;
|
||||||
@@ -343,20 +349,16 @@ test.describe("list virtualization", () => {
|
|||||||
if (sawAnchorAfterPrepend && stableFrames >= 8) break;
|
if (sawAnchorAfterPrepend && stableFrames >= 8) break;
|
||||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
}
|
}
|
||||||
mutationObserver?.disconnect();
|
return { maxDrift, sawPrepend };
|
||||||
return { ctrlWheelDispatched, maxDrift, sawPrepend };
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: committedAnchor.id,
|
anchorId: committedAnchor.id,
|
||||||
anchorTop: committedAnchor.top,
|
anchorTop: committedAnchor.top,
|
||||||
oldHeight: before.scrollHeight,
|
oldHeight: before.scrollHeight,
|
||||||
// Exercise browser-zoom input once while a real prepend transaction
|
|
||||||
// still owns its measurement reconciliation.
|
|
||||||
testCtrlWheel: pageIndex === 0,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(motion.sawPrepend).toBe(true);
|
expect(motion.sawPrepend).toBe(true);
|
||||||
if (pageIndex === 0) expect(motion.ctrlWheelDispatched).toBe(true);
|
if (pageIndex === 0) expect(await ctrlWheelPromise).toBe(true);
|
||||||
expect(motion.maxDrift).toBeLessThan(5);
|
expect(motion.maxDrift).toBeLessThan(5);
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
|
|||||||
@@ -54,11 +54,13 @@ index e02dfd0b3db60faff2cc705d7ce0aa51cb958f5a..b12677a356f748ebccf6e16cb781efe1
|
|||||||
}
|
}
|
||||||
l && (d = 1 + (2147483647 & d), o && z && ($ += z, z = 0), O.forEach(([e, t]) => {
|
l && (d = 1 + (2147483647 & d), o && z && ($ += z, z = 0), O.forEach(([e, t]) => {
|
||||||
l & e && t(n);
|
l & e && t(n);
|
||||||
@@ -186,6 +198,7 @@ const r = null, {min: n, max: s, abs: i, floor: l} = Math, c = (e, t, o) => n(o,
|
@@ -186,6 +198,8 @@ const r = null, {min: n, max: s, abs: i, floor: l} = Math, c = (e, t, o) => n(o,
|
||||||
c = l(), d && (a = !0), i && e.q(6, i()), e.q(1, n()), h();
|
c = l(), d && (a = !0), i && e.q(6, i()), e.q(1, n()), h();
|
||||||
}, p = t => {
|
}, p = t => {
|
||||||
if (f || !e.M() || t.ctrlKey) return;
|
- if (f || !e.M() || t.ctrlKey) return;
|
||||||
|
+ if (!e.M() || t.ctrlKey) return;
|
||||||
+ e.q(9);
|
+ e.q(9);
|
||||||
|
+ if (f) return;
|
||||||
const r = l() - c;
|
const r = l() - c;
|
||||||
150 > r && 50 < r && (o ? t.deltaX : t.deltaY) && (f = !0);
|
150 > r && 50 < r && (o ? t.deltaX : t.deltaY) && (f = !0);
|
||||||
}, v = () => {
|
}, v = () => {
|
||||||
@@ -150,11 +152,13 @@ index 110ac3858a002a6cdb698da2b56350bc1bf609d2..db83efc83fb3b6d7aa07d83b7463d6ed
|
|||||||
}
|
}
|
||||||
d && (i = 1 + (2147483647 & i), o && p && (g += p, p = 0), O.forEach(([e, t]) => {
|
d && (i = 1 + (2147483647 & i), o && p && (g += p, p = 0), O.forEach(([e, t]) => {
|
||||||
d & e && t(n);
|
d & e && t(n);
|
||||||
@@ -190,6 +202,7 @@ const u = null, {min: d, max: a, abs: h, floor: g} = Math, p = (e, t, o) => d(o,
|
@@ -190,6 +202,8 @@ const u = null, {min: d, max: a, abs: h, floor: g} = Math, p = (e, t, o) => d(o,
|
||||||
l = i(), d && (a = !0), s && e.B(6, s()), e.B(1, n()), h();
|
l = i(), d && (a = !0), s && e.B(6, s()), e.B(1, n()), h();
|
||||||
}, p = t => {
|
}, p = t => {
|
||||||
if (c || !e.M() || t.ctrlKey) return;
|
- if (c || !e.M() || t.ctrlKey) return;
|
||||||
|
+ if (!e.M() || t.ctrlKey) return;
|
||||||
+ e.B(9);
|
+ e.B(9);
|
||||||
|
+ if (c) return;
|
||||||
const n = i() - l;
|
const n = i() - l;
|
||||||
150 > n && 50 < n && (o ? t.deltaX : t.deltaY) && (c = !0);
|
150 > n && 50 < n && (o ? t.deltaX : t.deltaY) && (c = !0);
|
||||||
}, v = () => {
|
}, v = () => {
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ overrides:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
isomorphic-git: e9b414a60d4cf1d8aa18f7a779483984e821989967c235662566e94ef0238d3f
|
isomorphic-git: e9b414a60d4cf1d8aa18f7a779483984e821989967c235662566e94ef0238d3f
|
||||||
virtua@0.49.3: 63923c1f0c73f6fd487c788159fe2f1bd6930e5aeebe815cc511c1c4747294bc
|
virtua@0.49.3: 30a7a92b94800ec37be8d36546ea98c04a05dcd0637f9fac27a06f016ba2eff4
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ importers:
|
|||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
virtua:
|
virtua:
|
||||||
specifier: 0.49.3
|
specifier: 0.49.3
|
||||||
version: 0.49.3(patch_hash=63923c1f0c73f6fd487c788159fe2f1bd6930e5aeebe815cc511c1c4747294bc)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 0.49.3(patch_hash=30a7a92b94800ec37be8d36546ea98c04a05dcd0637f9fac27a06f016ba2eff4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.8.3
|
specifier: ^2.8.3
|
||||||
version: 2.9.0
|
version: 2.9.0
|
||||||
@@ -6926,7 +6926,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
virtua@0.49.3(patch_hash=63923c1f0c73f6fd487c788159fe2f1bd6930e5aeebe815cc511c1c4747294bc)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
virtua@0.49.3(patch_hash=30a7a92b94800ec37be8d36546ea98c04a05dcd0637f9fac27a06f016ba2eff4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|||||||
Reference in New Issue
Block a user