add a dragging

This commit is contained in:
Nate Kelley 2025-03-13 13:05:30 -06:00
parent 0fed3fcac8
commit 96fe76cfe6
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
2 changed files with 132 additions and 9 deletions

View File

@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SidebarCollapsible } from './SidebarCollapsible';
//import { Home, Settings, User } from 'lucide-react';
import { HouseModern, MapSettings, User } from '../icons/NucleoIconOutlined';
import { BusterRoutes } from '@/routes';
import { BusterRoutes } from '../../../routes';
import React from 'react';
const meta: Meta<typeof SidebarCollapsible> = {
title: 'UI/Sidebar/SidebarCollapsible',
@ -47,3 +47,10 @@ export const Default: Story = {
]
}
};
export const Sortable: Story = {
args: {
...Default.args,
isSortable: true
}
};

View File

@ -10,6 +10,26 @@ import { type ISidebarGroup } from './interfaces';
import { SidebarItem } from './SidebarItem';
import { CaretDown } from '../icons/NucleoIconFilled';
import { cn } from '@/lib/classMerge';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
DragStartEvent,
DragEndEvent
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useMemoizedFn } from '@/hooks';
interface SidebarTriggerProps {
label: string;
@ -39,6 +59,47 @@ const SidebarTrigger: React.FC<SidebarTriggerProps> = React.memo(({ label, isOpe
SidebarTrigger.displayName = 'SidebarTrigger';
interface SortableSidebarItemProps {
item: ISidebarGroup['items'][0];
active?: boolean;
}
const SortableSidebarItem: React.FC<SortableSidebarItemProps> = React.memo(({ item, active }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: item.id,
disabled: item.disabled
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0 : 1
};
const handleClick = (e: React.MouseEvent) => {
if (isDragging) {
e.preventDefault();
e.stopPropagation();
}
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
onClick={handleClick}
className={cn(isDragging && 'pointer-events-none')}>
<div onClick={isDragging ? (e) => e.stopPropagation() : undefined}>
<SidebarItem {...item} active={active} />
</div>
</div>
);
});
SortableSidebarItem.displayName = 'SortableSidebarItem';
export const SidebarCollapsible: React.FC<ISidebarGroup & { activeItem?: string }> = React.memo(
({
label,
@ -50,6 +111,34 @@ export const SidebarCollapsible: React.FC<ISidebarGroup & { activeItem?: string
defaultOpen = true
}) => {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
const [sortedItems, setSortedItems] = React.useState(items);
const [draggingId, setDraggingId] = React.useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
);
const handleDragStart = useMemoizedFn((event: DragStartEvent) => {
setDraggingId(event.active.id as string);
});
const handleDragEnd = useMemoizedFn((event: DragEndEvent) => {
const { active, over } = event;
setDraggingId(null);
if (active.id !== over?.id) {
setSortedItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
}
});
const draggingItem = draggingId ? sortedItems.find((item) => item.id === draggingId) : null;
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-0.5">
@ -74,13 +163,40 @@ export const SidebarCollapsible: React.FC<ISidebarGroup & { activeItem?: string
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up pl-0">
<div className="space-y-0.5">
{items.map((item) => (
<SidebarItem
key={item.id + item.route}
{...item}
active={activeItem === item.id || item.active}
/>
))}
{isSortable ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}>
<SortableContext
items={sortedItems.map((item) => item.id)}
strategy={verticalListSortingStrategy}>
{sortedItems.map((item) => (
<SortableSidebarItem
key={item.id}
item={item}
active={activeItem === item.id || item.active}
/>
))}
</SortableContext>
<DragOverlay>
{draggingId && draggingItem ? (
<div className="opacity-70 shadow">
<SidebarItem {...draggingItem} active={draggingItem.active} />
</div>
) : null}
</DragOverlay>
</DndContext>
) : (
items.map((item) => (
<SidebarItem
key={item.id + item.route}
{...item}
active={activeItem === item.id || item.active}
/>
))
)}
</div>
</CollapsibleContent>
</Collapsible>