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:
2026-09-26 18:14:28 +02:00
parent 7c2b22bafa
commit 4d0abc61d5
22 changed files with 512 additions and 91 deletions
+10 -3
View File
@@ -5,12 +5,13 @@ import { CSS } from "@dnd-kit/utilities";
import { useSortableItem } from "./sortableA11y";
import { DragHandle } from "./DragHandle";
import { IconButton } from "./IconButton";
import { OrderButtons } from "./OrderButtons";
import { Pressable } from "./Pressable";
import { SURFACE } from "./surfaces";
// One "island" card inside a SidePanel: a titled glass-card, and — in the panel's edit mode — a drag
// handle (reorder) and a hide toggle. Generalized from the feed sidebar's WidgetCard so all three
// panels share the same island look and behavior. `collapsible` groups (the default) get a per-group
// handle and an Up / Down pair (reorder) and a hide toggle. Generalized from the feed sidebar's
// WidgetCard so all three panels share the same island look and behavior. `collapsible` groups (the default) get a per-group
// collapse chevron; non-collapsible groups render as a plain always-open label + control row, and
// their title can instead act as a trigger via `onTitleClick` (e.g. feed "Your tags" -> tag manager).
export default function PanelGroup({
@@ -24,6 +25,7 @@ export default function PanelGroup({
onToggleHidden,
onTitleClick,
titleActionLabel,
order,
children,
}: {
id: string;
@@ -36,6 +38,8 @@ export default function PanelGroup({
onToggleHidden: () => void;
onTitleClick?: () => void;
titleActionLabel?: string;
/** Edit mode's non-drag reorder (WCAG 2.5.7): this group's place and how to move it. */
order?: { from: number; count: number; onMove: (to: number) => void };
children: ReactNode;
}) {
const { t } = useTranslation();
@@ -89,9 +93,12 @@ export default function PanelGroup({
</Pressable>
) : (
<span className={`${titleCls} text-muted`}>
<span className="truncate">{title}</span>
{/* In edit mode the name wraps instead of truncating: the grip, the Up / Down pair and
the eye leave a long one ("Feltöltés dátuma") too little room on one line. */}
<span className={editing ? "break-words" : "truncate"}>{title}</span>
</span>
)}
{editing && order && <OrderButtons {...order} compact name={title} />}
{editing ? (
<IconButton
onClick={onToggleHidden}