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:
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDown, ArrowDownToLine, ArrowUp, ArrowUpToLine } from "lucide-react";
|
||||
import { MOVES, moveTarget, type MoveTo } from "../../lib/moveInList";
|
||||
import type { ActionMenuItem } from "./ActionMenu";
|
||||
|
||||
// The non-drag reorder (WCAG 2.5.7, docs/DESIGN.md "Reordering"), in its two forms: a row menu's four
|
||||
// rows (here: a playlist item, a Plex playlist row) and buttons (`OrderButtons`: four beside the
|
||||
// selected item of the saved views' editor, or an Up / Down pair on every group of a side panel in
|
||||
// edit mode). Same labels, same glyphs, same rule: a move that would go nowhere — the first item up,
|
||||
// the last down — is shown off, not hidden, so the moves always sit in the same place.
|
||||
|
||||
export const ORDER_ICON: Record<MoveTo, typeof ArrowUp> = {
|
||||
top: ArrowUpToLine,
|
||||
up: ArrowUp,
|
||||
down: ArrowDown,
|
||||
bottom: ArrowDownToLine,
|
||||
};
|
||||
|
||||
/** The four move rows of a menu, for the item at `from` in a list of `count`; `onMove` gets the index
|
||||
* it lands on. `off` turns all four off (the list is not in its stored order; a write in flight) and
|
||||
* its reason, if any, goes under the FIRST row only — one explanation for the group, not the same
|
||||
* line four times. */
|
||||
export function useOrderItems() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
from: number,
|
||||
count: number,
|
||||
onMove: (to: number) => void,
|
||||
off?: { reason?: string },
|
||||
): ActionMenuItem[] =>
|
||||
MOVES.map((to, i) => {
|
||||
const Icon = ORDER_ICON[to];
|
||||
const at = moveTarget(from, count, to);
|
||||
return {
|
||||
id: `order-${to}`,
|
||||
label: t(`common.order.${to}`),
|
||||
icon: <Icon className="w-4 h-4" />,
|
||||
separatorBefore: i === 0,
|
||||
disabled: !!off || at === null,
|
||||
note: i === 0 ? off?.reason : undefined,
|
||||
onSelect: () => {
|
||||
if (at !== null) onMove(at);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user