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:
@@ -29,8 +29,22 @@ function firstFocusable(node: Node | null, fromEnd = false): HTMLElement | null
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Where the focus goes once `lost` (removed with the nodes of `record`) has left the page. */
|
||||
function slotFor(record: MutationRecord, lost: Node): HTMLElement | null {
|
||||
// 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
|
||||
// the first tile instead of the one the view was opened from (measured in Chrome). A stand-in is
|
||||
// not the reader's choice, so the restore may take the focus from it.
|
||||
let standIn: HTMLElement | null = null;
|
||||
|
||||
/** Whether `el` holds the focus only because the keeper put it there. */
|
||||
export function isStandInFocus(el: Element | null): boolean {
|
||||
return el !== null && el === standIn;
|
||||
}
|
||||
|
||||
/** Where the focus goes once `lost` (removed with the nodes of `record`) has left the page — never
|
||||
* outside `root`: a slot with nothing focusable left (a page swapped for its loading fallback) must
|
||||
* not send the focus to the first control of the whole document. */
|
||||
function slotFor(record: MutationRecord, lost: Node, root: HTMLElement): HTMLElement | null {
|
||||
const removed = [...record.removedNodes].find((n) => n === lost || n.contains(lost));
|
||||
if (!removed) return null;
|
||||
const after = firstFocusable(record.nextSibling);
|
||||
@@ -38,7 +52,7 @@ function slotFor(record: MutationRecord, lost: Node): HTMLElement | null {
|
||||
const before = firstFocusable(record.previousSibling, true);
|
||||
if (before) return before;
|
||||
let up: Node | null = record.target;
|
||||
while (up && up instanceof HTMLElement) {
|
||||
while (up && up instanceof HTMLElement && root.contains(up)) {
|
||||
const inside = firstFocusable(up.firstChild);
|
||||
if (inside) return inside;
|
||||
up = up.parentElement;
|
||||
@@ -53,6 +67,7 @@ export function useFocusKeeper(ref: RefObject<HTMLElement | null>): void {
|
||||
let held: HTMLElement | null = null;
|
||||
const onFocusIn = (e: FocusEvent) => {
|
||||
const t = e.target as HTMLElement | null;
|
||||
if (t !== standIn) standIn = null;
|
||||
held = t && root.contains(t) && !t.closest("[data-nav-index]") ? t : null;
|
||||
};
|
||||
const watch = new MutationObserver((records) => {
|
||||
@@ -62,8 +77,9 @@ export function useFocusKeeper(ref: RefObject<HTMLElement | null>): void {
|
||||
const active = document.activeElement;
|
||||
if (active && active !== document.body) return; // something already took the focus
|
||||
for (const r of records) {
|
||||
const target = slotFor(r, lost);
|
||||
const target = slotFor(r, lost, root);
|
||||
if (target) {
|
||||
standIn = target;
|
||||
target.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user