PanelGroups registered only a PointerSensor, so its focusable, "reorderable" group grips did nothing from the keyboard. Added KeyboardSensor + sortableKeyboardCoordinates, matching the other sortable lists (Playlists/PlexPlaylistView already had it).
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { type ReactNode } from "react";
|
|
import {
|
|
closestCenter,
|
|
DndContext,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
type DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
verticalListSortingStrategy,
|
|
} from "@dnd-kit/sortable";
|
|
import type { PanelLayout } from "../lib/panelLayout";
|
|
import PanelGroup from "./PanelGroup";
|
|
|
|
export type PanelItem = { id: string; title: string; body: ReactNode };
|
|
|
|
// Renders a panel's islands in the user's saved order, with drag-to-reorder + hide + per-group
|
|
// collapse driven by `layout` (see lib/panelLayout). `items` holds only the currently AVAILABLE
|
|
// groups (a caller drops facet-gated / empty ones); the layout order is filtered to them so a
|
|
// stored order that references a now-absent group is tolerated. Reordering/hiding is active only
|
|
// in `editing` mode. Shared by all three side panels.
|
|
export default function PanelGroups({
|
|
items,
|
|
layout,
|
|
setLayout,
|
|
editing,
|
|
}: {
|
|
items: PanelItem[];
|
|
layout: PanelLayout;
|
|
setLayout: (l: PanelLayout) => void;
|
|
editing: boolean;
|
|
}) {
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
// The group grips are focusable and call themselves reorderable; without this only a pointer
|
|
// could actually move them (keyboard drag matched the other sortable lists).
|
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
);
|
|
const byId = new Map(items.map((it) => [it.id, it]));
|
|
|
|
const orderedAvailable = layout.order.filter((id) => byId.has(id));
|
|
// Append any available id missing from the stored order (e.g. a group that became available
|
|
// after the layout was saved) so it never silently disappears.
|
|
for (const it of items) if (!orderedAvailable.includes(it.id)) orderedAvailable.push(it.id);
|
|
const renderedIds = editing
|
|
? orderedAvailable
|
|
: orderedAvailable.filter((id) => !layout.hidden[id]);
|
|
|
|
function toggleCollapse(id: string) {
|
|
setLayout({ ...layout, collapsed: { ...layout.collapsed, [id]: !layout.collapsed[id] } });
|
|
}
|
|
function toggleHidden(id: string) {
|
|
setLayout({ ...layout, hidden: { ...layout.hidden, [id]: !layout.hidden[id] } });
|
|
}
|
|
function onDragEnd(e: DragEndEvent) {
|
|
const { active, over } = e;
|
|
if (!over || active.id === over.id) return;
|
|
const from = layout.order.indexOf(active.id as string);
|
|
const to = layout.order.indexOf(over.id as string);
|
|
if (from < 0 || to < 0) return;
|
|
setLayout({ ...layout, order: arrayMove(layout.order, from, to) });
|
|
}
|
|
|
|
return (
|
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
<SortableContext items={renderedIds} strategy={verticalListSortingStrategy}>
|
|
<div className="space-y-3">
|
|
{renderedIds.map((id) => (
|
|
<PanelGroup
|
|
key={id}
|
|
id={id}
|
|
title={byId.get(id)!.title}
|
|
editing={editing}
|
|
collapsed={!!layout.collapsed[id]}
|
|
hidden={!!layout.hidden[id]}
|
|
onToggleCollapse={() => toggleCollapse(id)}
|
|
onToggleHidden={() => toggleHidden(id)}
|
|
>
|
|
{byId.get(id)!.body}
|
|
</PanelGroup>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
);
|
|
}
|