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,59 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MOVES, moveTarget, type MoveTo } from "../../lib/moveInList";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { ORDER_ICON } from "./useOrderItems";
|
||||
|
||||
// The button form of the non-drag reorder; the rule and the menu form are in `useOrderItems`.
|
||||
|
||||
/** The moves as buttons. In full, the four with the item's place ("2 / 5") in front: for a list whose
|
||||
* selected item is edited beside it (the saved views). `compact`: only Up and Down, ghost, no place —
|
||||
* for a short list where every item carries its own pair (a side panel's groups in edit mode), named
|
||||
* by `name` so a screen reader hears WHICH item the pair moves. Focus-safe: the "Up" just pressed on
|
||||
* the item now first stays focused. */
|
||||
export function OrderButtons({
|
||||
from,
|
||||
count,
|
||||
onMove,
|
||||
compact = false,
|
||||
name,
|
||||
}: {
|
||||
from: number;
|
||||
count: number;
|
||||
onMove: (to: number) => void;
|
||||
compact?: boolean;
|
||||
name?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const place = t("common.order.place", { pos: from + 1, total: count });
|
||||
const moves: readonly MoveTo[] = compact ? ["up", "down"] : MOVES;
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={[t("common.order.label"), name, place].filter(Boolean).join(" ")}
|
||||
className={`flex items-center ${compact ? "" : "gap-1"}`}
|
||||
>
|
||||
{!compact && (
|
||||
<span className="mr-1 text-xs text-muted tabular-nums" aria-hidden="true">
|
||||
{t("common.order.label")} {place}
|
||||
</span>
|
||||
)}
|
||||
{moves.map((to) => {
|
||||
const Icon = ORDER_ICON[to];
|
||||
const at = moveTarget(from, count, to);
|
||||
return (
|
||||
<IconButton
|
||||
key={to}
|
||||
label={t(`common.order.${to}`)}
|
||||
variant={compact ? "ghost" : "outline"}
|
||||
disabled={at === null}
|
||||
onClick={() => {
|
||||
if (at !== null) onMove(at);
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user