61 行
2.8 KiB
TypeScript
61 行
2.8 KiB
TypeScript
import { defaultAnimateLayoutChanges, useSortable } from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
|
|
import { useClientMotion } from '@/lib/useClientMotion'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
import type { CSSProperties, ReactNode } from 'react'
|
|
|
|
type Name = { text: string; language: string }
|
|
|
|
|
|
const SortableItem = ({ id, name, fixed, children, moveGroup, handleId, renderShell }: {
|
|
id: string; name: Name; fixed?: boolean; children: ReactNode
|
|
moveGroup: (direction: number) => void; handleId: string
|
|
renderShell?: (shell: { ref: (node: HTMLElement | null) => void
|
|
style: CSSProperties; handle: ReactNode; dragging: boolean }) => ReactNode },
|
|
) => {
|
|
const motion = useClientMotion ()
|
|
const item = useSortable ({ id, disabled: fixed,
|
|
transition: { duration: motion.duration ('reorder'), easing: motion.easing },
|
|
animateLayoutChanges: options => !motion.reduced
|
|
&& defaultAnimateLayoutChanges ({ ...options, wasDragging: true }) })
|
|
const handle = fixed ? <span className="w-6 shrink-0"/> : <button type="button"
|
|
id={handleId} ref={item.setActivatorNodeRef} {...item.attributes} {...item.listeners}
|
|
aria-label={name.text} lang={name.language}
|
|
aria-keyshortcuts={renderShell == null
|
|
? 'Space ArrowUp ArrowDown Enter Escape Alt+ArrowLeft Alt+ArrowRight'
|
|
: 'Space ArrowLeft ArrowRight Enter Escape'}
|
|
className="mt-1 w-6 shrink-0 cursor-grab touch-none rounded
|
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
|
|
active:cursor-grabbing dark:focus-visible:ring-blue-400"
|
|
onClick={event => event.stopPropagation ()}
|
|
onKeyDown={event => {
|
|
if (event.altKey && !item.isDragging
|
|
&& ['ArrowLeft', 'ArrowRight'].includes (event.key))
|
|
{
|
|
event.preventDefault ()
|
|
event.stopPropagation ()
|
|
moveGroup (event.key === 'ArrowLeft' ? -1 : 1)
|
|
}
|
|
else item.listeners?.onKeyDown?.(event)
|
|
}}><span aria-hidden>⠿</span></button>
|
|
const style = { transform: CSS.Translate.toString (item.transform), transition: item.transition }
|
|
if (renderShell != null)
|
|
return renderShell ({ ref: item.setNodeRef, style, handle, dragging: item.isDragging })
|
|
return <div ref={item.setNodeRef} style={style}
|
|
className={cn ('relative max-w-full rounded border border-slate-300 bg-white',
|
|
'text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100',
|
|
item.isOver && !item.isDragging
|
|
&& 'ring-2 ring-blue-500 dark:ring-blue-400',
|
|
item.isDragging && 'opacity-30 ring-2 ring-blue-500 dark:ring-blue-400')}>
|
|
<div className="flex min-w-0 items-start gap-1 px-1">
|
|
{handle}
|
|
<div className="min-w-0 flex-1 py-1">{children}</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
|
|
export default SortableItem
|