fix(ui): Back finds the item again past the focus keeper; nested drags stay in their list
Deep review of the debt burn, each measured in Chrome: - Back from a sub-view whose focused control the swap removed: the focus keeper put the focus on a stand-in (the returning list's first item) and `restoreNavPosition`, finding the focus "already somewhere", gave up — Plex show → Back landed on tile 0, not the tile it was opened from. A stand-in the keeper placed now counts as untouched, like <body>. The keeper's walk up also stops at its container: a slot left with nothing focusable no longer sends the focus to the first control of the whole document. - DataTable enabled both layouts' keyboard lists with the same flag, and `useListNav` reads its ref only when the flag changes: after a resize across `md`, the layout that mounted was never registered (no j/k) and the one that left stayed registered. Each is enabled with its layout. - A Plex playlist nests blocks and each show's episodes in one DndContext; closestCenter and the keyboard coordinates weighed every droppable, so a movie landed "over" an episode — announced as a place in the other list, dropped somewhere else or nowhere. `sameListCollision` and `sameListKeyboardCoordinates` keep a drag among its siblings; onDragEnd's episode → block resolution is gone with the case. - dnd-kit reports the item over itself in the pick-up's own tick; announced, "moved to position 1" replaced "picked up … use the arrow keys" before a screen reader read it.
This commit is contained in:
@@ -46,6 +46,29 @@ describe("useFocusKeeper", () => {
|
||||
expect(document.activeElement).toBe(btn("after the list"));
|
||||
});
|
||||
|
||||
// A page swapped for its loading fallback leaves nothing focusable in the slot. The walk up used to
|
||||
// run past the container and hand the focus to the first control of the whole document.
|
||||
it("never puts the focus outside its own container", async () => {
|
||||
function Swapped() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useFocusKeeper(ref);
|
||||
const [loaded, setLoaded] = useState(true);
|
||||
return (
|
||||
<>
|
||||
<button>rail</button>
|
||||
<div ref={ref}>
|
||||
{loaded ? <button onClick={() => setLoaded(false)}>go</button> : <p>Loading…</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Swapped />);
|
||||
btn("go").focus();
|
||||
act(() => btn("go").click());
|
||||
await flush();
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
});
|
||||
|
||||
it("leaves a keyboard list's rows to listNav", async () => {
|
||||
render(<Rows nav />);
|
||||
btn("approve b").focus();
|
||||
|
||||
Reference in New Issue
Block a user