fix(ui): a removed chip's focus outlives its neighbour; a scroll restore yields to the reader
A chip handed its focus to a Clear all or +N that went in the same removal, and the focus fell to the body. The restore now stops when the reader scrolls, and finishes the way once the Back-to-top spacer brings the saved spot within reach.
This commit is contained in:
@@ -31,7 +31,12 @@ function rememberScroll(key: string, top: number): void {
|
||||
// A spot the page can no longer reach — the last stretch of a page scrolled to its very end, whose
|
||||
// Back-to-top spacer only shows once scrolled — goes as far as the page does: it used to be left at
|
||||
// the top (round 2, F8). Only a spacer's worth short, and only once the height has held still, is
|
||||
// that answer early; a list still loading is further short than that, and gets the full wait.
|
||||
// that answer early; a list still loading is further short than that, and gets the full wait. And
|
||||
// landing there is not the end of it: that scroll is what shows Back to top, whose spacer then
|
||||
// makes the spot reachable, so the loop watches a few frames more and finishes the way (round 3).
|
||||
//
|
||||
// The reader scrolling meanwhile — a wheel, a finger, a key, the scrollbar — takes the page over:
|
||||
// the restore stops where they are, instead of pulling them back or dropping them at the end.
|
||||
const SETTLED_FRAMES = 10;
|
||||
const SPACER_REM = 6;
|
||||
function restoreScroll(el: HTMLElement, top: number): () => void {
|
||||
@@ -43,27 +48,42 @@ function restoreScroll(el: HTMLElement, top: number): () => void {
|
||||
let frames = 0;
|
||||
let still = 0;
|
||||
let lastMax = -1;
|
||||
let landedAt = -1;
|
||||
let cancelled = false;
|
||||
const stop = () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(raf);
|
||||
el.removeEventListener("wheel", stop);
|
||||
el.removeEventListener("touchstart", stop);
|
||||
el.removeEventListener("pointerdown", stop);
|
||||
document.removeEventListener("keydown", stop, true);
|
||||
};
|
||||
el.addEventListener("wheel", stop, { passive: true });
|
||||
el.addEventListener("touchstart", stop, { passive: true });
|
||||
el.addEventListener("pointerdown", stop);
|
||||
document.addEventListener("keydown", stop, true);
|
||||
const tick = () => {
|
||||
if (cancelled) return;
|
||||
const max = el.scrollHeight - el.clientHeight;
|
||||
if (max >= top) el.scrollTop = top;
|
||||
if (el.scrollTop >= top - 1) return; // reached
|
||||
still = max === lastMax ? still + 1 : 0;
|
||||
lastMax = max;
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
const nearEnd = max > 0 && top - max <= SPACER_REM * rem;
|
||||
if ((nearEnd && still >= SETTLED_FRAMES) || frames++ > 90) {
|
||||
el.scrollTop = max; // as near as the page reaches now
|
||||
return;
|
||||
if (el.scrollTop >= top - 1) return stop(); // reached
|
||||
if (landedAt >= 0) {
|
||||
if (frames - landedAt > SETTLED_FRAMES) return stop(); // no spacer came: the end it is
|
||||
} else {
|
||||
still = max === lastMax ? still + 1 : 0;
|
||||
lastMax = max;
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
const nearEnd = max > 0 && top - max <= SPACER_REM * rem;
|
||||
if ((nearEnd && still >= SETTLED_FRAMES) || frames > 90) {
|
||||
el.scrollTop = max; // as near as the page reaches now
|
||||
landedAt = frames;
|
||||
}
|
||||
}
|
||||
frames++;
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(raf);
|
||||
};
|
||||
return stop;
|
||||
}
|
||||
|
||||
// The page's scroll container — the ONE scroller in the app, owned by PageShell. The feed, the
|
||||
|
||||
Reference in New Issue
Block a user