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:
@@ -4,12 +4,18 @@ import { BookmarkPlus, Check, Play, Save, Star, Trash2 } from "lucide-react";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api";
|
||||
import Modal from "./ui/Modal";
|
||||
@@ -22,15 +28,17 @@ import { useUndoable } from "../lib/useUndoable";
|
||||
import UndoToolbar from "./ui/UndoToolbar";
|
||||
import { useSortableA11y, useSortableItem } from "./ui/sortableA11y";
|
||||
import { DragHandle } from "./ui/DragHandle";
|
||||
import { OrderButtons } from "./ui/OrderButtons";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { DismissibleChip } from "./ui/Chip";
|
||||
import { Input } from "./ui/Input";
|
||||
|
||||
// Master–detail manager for saved ("personal") views: the left column lists the views (drag to
|
||||
// reorder) plus a "save the current filters" input; the right column shows the selected view's name
|
||||
// (renamable), its captured filters as REMOVABLE chips (pruning writes back to the stored view,
|
||||
// undoable), its default flag, apply, save-current, and delete. Opened from the filter panel's title
|
||||
// or the saved-views popover; all mutations flow through SavedViewsProvider.
|
||||
// reorder, or the selected one's order buttons on the right) plus a "save the current filters" input;
|
||||
// the right column shows the selected view's name (renamable), its captured filters as REMOVABLE
|
||||
// chips (pruning writes back to the stored view, undoable), its default flag, its place in the order,
|
||||
// apply, save-current, and delete. Opened from the filter panel's title or the saved-views popover;
|
||||
// all mutations flow through SavedViewsProvider.
|
||||
function ViewRow({
|
||||
view,
|
||||
selected,
|
||||
@@ -83,7 +91,12 @@ export default function PersonalViewsModal() {
|
||||
reorder,
|
||||
closeModal,
|
||||
} = useSavedViews();
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));
|
||||
// The grip's keyboard pick-up (space, the arrows, space) is what its screen-reader instructions
|
||||
// promise; without the sensor they described a gesture the list did not have.
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
const [selectedId, setSelectedId] = useState<number | null>(activeViewId ?? views[0]?.id ?? null);
|
||||
const selected = views.find((v) => v.id === selectedId) ?? null;
|
||||
@@ -154,6 +167,12 @@ export default function PersonalViewsModal() {
|
||||
const next = arrayMove(ids, ids.indexOf(active.id as number), ids.indexOf(over.id as number));
|
||||
await reorder(next);
|
||||
}
|
||||
// The non-drag way (WCAG 2.5.7): the selected view's Top / Up / Down / Bottom, beside its editor.
|
||||
const selectedAt = views.findIndex((v) => v.id === selectedId);
|
||||
function moveSelected(to: number) {
|
||||
const ids = views.map((v) => v.id);
|
||||
void reorder(arrayMove(ids, selectedAt, to));
|
||||
}
|
||||
|
||||
async function onCreate() {
|
||||
const name = createName.trim();
|
||||
@@ -273,6 +292,9 @@ export default function PersonalViewsModal() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{views.length > 1 && (
|
||||
<OrderButtons from={selectedAt} count={views.length} onMove={moveSelected} />
|
||||
)}
|
||||
<div className="mt-auto flex flex-wrap items-center gap-2 border-t border-border/60 pt-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
Reference in New Issue
Block a user