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:
2026-09-27 21:56:23 +02:00
parent 5ab6778b97
commit 25f2d9d0e4
5 changed files with 152 additions and 19 deletions
+21
View File
@@ -49,6 +49,27 @@ export function focusNeighbour(el: HTMLElement): boolean {
return false;
}
/** Run `remove`, which takes `el` off the page, without dropping `el`'s focus: it goes to the
* neighbour first (`focusNeighbour`). That neighbour may go in the same removal — the "Clear all"
* that only shows beside two filters, the "+1" of the last hidden one (E28 review round 3) — so
* once the removal has rendered, a focus left on <body> goes to what preceded `el`, else to the
* first control left in its row. Only when the focus was lost: a focus the reader moved stays. */
export function handOffFocus(el: HTMLElement, remove: () => void): void {
const before = firstFocusable(el.previousSibling, true);
const row = el.parentElement;
focusNeighbour(el);
remove();
// A timer, not a frame: a page the browser is not painting runs no requestAnimationFrame.
window.setTimeout(() => {
const now = document.activeElement;
if (now && now !== document.body && now.isConnected) return;
const stayed = before?.isConnected && shown(before) ? before : null;
(stayed ?? (row?.isConnected ? firstFocusable(row.firstChild) : null))?.focus({
preventScroll: true,
});
}, 0);
}
// The element the keeper last put the focus on, while it still holds it. A Back that swaps the page
// or a sub-view removes the held control too, and the keeper's stand-in then sat where
// `restoreNavPosition` looks for an untouched <body>: the restore gave up and Back left the focus on