feat(design): reorder without a drag everywhere a grip drags (WCAG 2.5.7, E28 S3e4a)
One rule, two forms: useOrderItems (Top / Up / Down / Bottom rows of a row menu) and OrderButtons (four buttons with the place, or a compact Up / Down pair). A move that would go nowhere is shown off. - YouTube playlist rows: the four moves now come from useOrderItems. - Plex playlist rows: a block among the blocks, an episode within its show, off while a write is in flight; drag and menu share moveBlock/moveEpisode. - Saved views editor: the selected view's four buttons (owner: A), and the grip's keyboard pick-up (its instructions promised one it did not have). - Side panel groups in edit mode: an Up / Down pair (owner: A); a group the stored order does not hold yet moves too; the name wraps there. - The grip's touch-action: none is now test-held.
This commit is contained in:
@@ -41,6 +41,7 @@ import { useCopyItems } from "./copyItems";
|
||||
import { usePlexCardActions } from "./usePlexCardActions";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { DragHandle } from "./ui/DragHandle";
|
||||
import { useOrderItems } from "./ui/useOrderItems";
|
||||
import { usePlexPlaylistEdits } from "./usePlexPlaylistEdits";
|
||||
import {
|
||||
sameListCollision,
|
||||
@@ -55,9 +56,10 @@ import { PageHeading } from "./ui/Headings";
|
||||
|
||||
// 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
|
||||
// list. Reorder is drag & drop (a whole show-group moves as one block; episodes reorder within their
|
||||
// show). Two layouts — Accordion (A) and Tree (C) — are a per-account preference. Rendered as a
|
||||
// PlexBrowse subview; "Play all" opens the player with the whole ordered list as its queue.
|
||||
// list. Reorder is drag & drop, or a row menu's Top / Up / Down / Bottom (a whole show-group moves as
|
||||
// one block; episodes reorder within their show). Two layouts — Accordion (A) and Tree (C) — are a
|
||||
// per-account preference. Rendered as a PlexBrowse subview; "Play all" opens the player with the
|
||||
// whole ordered list as its queue.
|
||||
//
|
||||
// E28 C1: every row — a title, a show group, a season — has ONE menu, opened by its "…" (shown on
|
||||
// hover or focus, always on touch), a right-click, a long-press or Shift+F10. Removing needs no
|
||||
@@ -505,34 +507,40 @@ export default function PlexPlaylistView({
|
||||
return { title: eps[ep]?.title ?? "", pos: ep + 1, total: eps.length };
|
||||
});
|
||||
|
||||
// A move, by drag or by a row menu's Top / Up / Down / Bottom: a whole block (a movie, a show)
|
||||
// among the blocks, an episode only within its own show.
|
||||
function moveBlock(id: string, to: number) {
|
||||
const from = blocks.findIndex((b) => b.id === id);
|
||||
if (from < 0 || to < 0 || to >= blocks.length || from === to) return;
|
||||
commit(flatten(arrayMove(blocks, from, to)));
|
||||
}
|
||||
function moveEpisode(id: string, to: number) {
|
||||
const b = blocks.find((bl) => bl.kind === "show" && bl.items.some((it) => it.id === id));
|
||||
if (!b || b.kind !== "show") return;
|
||||
const from = b.items.findIndex((it) => it.id === id);
|
||||
if (to < 0 || to >= b.items.length || from === to) return;
|
||||
commit(
|
||||
flatten(
|
||||
blocks.map((bl) => (bl.id === b.id ? { ...bl, items: arrayMove(b.items, from, to) } : bl)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function onDragEnd(e: DragEndEvent) {
|
||||
const { active, over } = e;
|
||||
if (!over || active.id === over.id) return;
|
||||
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) among the blocks.
|
||||
const from = blocks.findIndex((b) => b.id === aId);
|
||||
const to = blocks.findIndex((b) => b.id === oId);
|
||||
if (from < 0 || to < 0) return;
|
||||
commit(flatten(arrayMove(blocks, from, to)));
|
||||
} else {
|
||||
// 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);
|
||||
const to = b.items.findIndex((it) => it.id === oId);
|
||||
commit(
|
||||
flatten(
|
||||
blocks.map((bl) =>
|
||||
bl.id === b.id ? { ...bl, items: arrayMove(b.items, from, to) } : bl,
|
||||
),
|
||||
),
|
||||
);
|
||||
// over an episode of the same show. The lookups only refuse a stale id.
|
||||
const toBlock = blocks.findIndex((b) => b.id === oId);
|
||||
if (blocks.some((b) => b.id === aId)) {
|
||||
if (toBlock >= 0) moveBlock(aId, toBlock);
|
||||
return;
|
||||
}
|
||||
const show = blocks.find((bl) => bl.kind === "show" && bl.items.some((it) => it.id === aId));
|
||||
const to = show?.kind === "show" ? show.items.findIndex((it) => it.id === oId) : -1;
|
||||
if (to >= 0) moveEpisode(aId, to);
|
||||
}
|
||||
|
||||
// --- The rows' ONE menu (E28 C1) -------------------------------------------------------------
|
||||
@@ -586,12 +594,26 @@ export default function PlexPlaylistView({
|
||||
},
|
||||
]
|
||||
: [];
|
||||
// The non-drag way (WCAG 2.5.7): what the row's grip drags, its menu moves — a block among the
|
||||
// blocks, an episode within its show. Off while a write is in flight, like the grip.
|
||||
const orderItems = useOrderItems();
|
||||
const off = busy ? {} : undefined;
|
||||
const orderRows = (target: Exclude<MenuTarget, { kind: "season" }>): ActionMenuItem[] => {
|
||||
const id = target.kind === "item" ? target.item.id : target.block.id;
|
||||
const at = blocks.findIndex((b) => b.id === id);
|
||||
if (at >= 0) return orderItems(at, blocks.length, (to) => moveBlock(id, to), off);
|
||||
const show = blocks.find((b) => b.kind === "show" && b.items.some((it) => it.id === id));
|
||||
if (show?.kind !== "show") return [];
|
||||
const ep = show.items.findIndex((it) => it.id === id);
|
||||
return orderItems(ep, show.items.length, (to) => moveEpisode(id, to), off);
|
||||
};
|
||||
const menuTarget = rowMenu.target;
|
||||
const menuItems: ActionMenuItem[] = !menuTarget
|
||||
? []
|
||||
: menuTarget.kind === "item"
|
||||
? [
|
||||
...titleActions.items(menuTarget.item),
|
||||
...orderRows(menuTarget),
|
||||
removeRow(menuTarget, t("plex.playlist.removeFromPlaylist")),
|
||||
]
|
||||
: menuTarget.kind === "show"
|
||||
@@ -601,6 +623,7 @@ export default function PlexPlaylistView({
|
||||
...it,
|
||||
separatorBefore: i === 0,
|
||||
})),
|
||||
...orderRows(menuTarget),
|
||||
removeRow(menuTarget, t("plex.playlist.removeShow")),
|
||||
]
|
||||
: [
|
||||
|
||||
Reference in New Issue
Block a user