fix(ui): focus, paging and state that a key or a finger can reach

- A key landing on a side panel's tab steps into the card it peeks open, not onto the hidden tab.
- The Channels cards keep their status / pager row at the list's end while the band is folded.
- A menu row's note shows on an enabled row too: the download state under 'Download…', not a title.
- A removal chip hands the focus to its neighbour before it goes, for every caller (the feed's and
  Plex's active filters); the hidden-filter cue's own copy is gone.
- Back to a spot within a page's end spacer lands at the page's end, not its top.
- A running push / revert is tracked per list: another list's push is no longer refused unseen.
This commit is contained in:
2026-09-27 19:43:04 +02:00
parent e31a588f19
commit 740602b9bf
15 changed files with 231 additions and 48 deletions
@@ -81,6 +81,50 @@ describe("PageScroller scroll memory", () => {
expect(main.scrollTop).toBe(200);
});
// Round 2 (F8): the page's very end, whose Back-to-top spacer only shows once scrolled, is out of
// reach when Back rebuilds the page — and the restore gave up with the page at the top.
it("Back to a spot within the end's spacer lands at the page's end, not its top", () => {
setEntry(1);
const { container } = render(<PageScroller scrollKey="e:k1">x</PageScroller>);
const main = sizedMain(container);
main.scrollTop = 500; // the very end, the spacer showing
fireEvent.scroll(main);
setEntry(2);
notifyNav();
Object.defineProperty(main, "scrollHeight", { configurable: true, value: 928 }); // no spacer
setEntry(1);
notifyNav();
expect(main.scrollTop).toBe(428);
});
it("a list still far short of the spot waits for it rather than settling early", () => {
let frames: FrameRequestCallback[] = [];
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => frames.push(cb));
const flush = (n: number) => {
for (let i = 0; i < n; i++) {
const run = frames;
frames = [];
for (const cb of run) cb(0);
}
};
setEntry(1);
const { container } = render(<PageScroller scrollKey="e:k1">x</PageScroller>);
const main = sizedMain(container);
main.scrollTop = 450;
fireEvent.scroll(main);
setEntry(2);
notifyNav();
flush(1);
Object.defineProperty(main, "scrollHeight", { configurable: true, value: 600 }); // loading
setEntry(1);
notifyNav();
flush(20); // held still, but 350px short: no answer yet
expect(main.scrollTop).toBe(0);
Object.defineProperty(main, "scrollHeight", { configurable: true, value: 1000 }); // loaded
flush(1);
expect(main.scrollTop).toBe(450);
});
it("restores per content key within one entry: a new key resets to top, returning restores", () => {
setEntry(10);
const { container, rerender } = render(<PageScroller scrollKey="e10:a">x</PageScroller>);