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:
@@ -6,17 +6,11 @@ import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { SortableContext, arrayMove, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -40,7 +34,12 @@ import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { usePlexPlaylistEdits } from "./usePlexPlaylistEdits";
|
||||
import { useSortableA11y, useSortableItem } from "./ui/sortableA11y";
|
||||
import {
|
||||
sameListCollision,
|
||||
sameListKeyboardCoordinates,
|
||||
useSortableA11y,
|
||||
useSortableItem,
|
||||
} from "./ui/sortableA11y";
|
||||
|
||||
// A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the
|
||||
// same show collapses into a season/show group so a whole series doesn't sprawl into an endless flat
|
||||
@@ -455,7 +454,7 @@ export default function PlexPlaylistView({
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sameListKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
// A whole block (a movie, a show) sorts among the blocks; an episode only within its own show.
|
||||
@@ -481,20 +480,16 @@ export default function PlexPlaylistView({
|
||||
const aId = String(active.id);
|
||||
const oId = String(over.id);
|
||||
const blockIds = new Set(blocks.map((b) => b.id));
|
||||
// `over` is always a sibling of `active` (sameListCollision): a block over a block, an episode
|
||||
// over an episode of the same show. The checks below only refuse a stale id.
|
||||
if (blockIds.has(aId)) {
|
||||
// Moving a whole block (movie or show). Resolve the drop target to a block.
|
||||
let oBlockId = oId;
|
||||
if (!blockIds.has(oId)) {
|
||||
const b = blocks.find((bl) => bl.kind === "show" && bl.items.some((it) => it.id === oId));
|
||||
if (!b) return;
|
||||
oBlockId = b.id;
|
||||
}
|
||||
// Moving a whole block (movie or show) among the blocks.
|
||||
const from = blocks.findIndex((b) => b.id === aId);
|
||||
const to = blocks.findIndex((b) => b.id === oBlockId);
|
||||
const to = blocks.findIndex((b) => b.id === oId);
|
||||
if (from < 0 || to < 0) return;
|
||||
commit(flatten(arrayMove(blocks, from, to)));
|
||||
} else {
|
||||
// Moving an episode — only within its own show block.
|
||||
// Moving an episode — within its own show block.
|
||||
const b = blocks.find((bl) => bl.kind === "show" && bl.items.some((it) => it.id === aId));
|
||||
if (!b || b.kind !== "show" || !b.items.some((it) => it.id === oId)) return;
|
||||
const from = b.items.findIndex((it) => it.id === aId);
|
||||
@@ -600,7 +595,7 @@ export default function PlexPlaylistView({
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
collisionDetection={sameListCollision}
|
||||
onDragEnd={onDragEnd}
|
||||
accessibility={dndA11y}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user