このコミットが含まれているのは:
2026-10-26 22:57:39 +09:00
コミット 5493673996
60個のファイルの変更、1202行の追加、273行の削除
+79
ファイルの表示
@@ -0,0 +1,79 @@
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
closestCenter, useSensor, useSensors } from '@dnd-kit/core'
import { SortableContext, arrayMove, horizontalListSortingStrategy,
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useClientMotion } from '@/lib/useClientMotion'
import type { ReactNode } from 'react'
const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
ids: string[]; fixed?: string[]; name: (id: string) => { text: string; language: string }
onChange: (ids: string[]) => void; children: (ids: string[]) => ReactNode },
) => {
const [preview, setPreview] = useState<string[] | null> (null)
const [active, setActive] = useState<string | null> (null)
const latest = useRef<string[] | null> (null)
const lastOver = useRef<string | null> (null)
const motion = useClientMotion ()
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
useSensor (KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }))
const move = (current: string[], from: string, to: string) => {
if (fixed.includes (from) || fixed.includes (to)) return current
const source = current.indexOf (from)
const target = current.indexOf (to)
return source < 0 || target < 0 ? current : arrayMove (current, source, target)
}
const reset = () => {
latest.current = null
lastOver.current = null
setPreview (null)
setActive (null)
}
return <DndContext sensors={sensors} collisionDetection={closestCenter}
accessibility={{ announcements: {
onDragStart: event => name (String (event.active.id)).text,
onDragOver: event => event.over == null ? undefined : name (String (event.over.id)).text,
onDragMove: () => undefined, onDragEnd: () => undefined,
onDragCancel: () => undefined },
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
onDragStart={event => {
latest.current = ids
setPreview (ids)
setActive (String (event.active.id))
}} onDragOver={event => {
if (event.over == null || event.active.id === event.over.id) return
const target = String (event.over.id)
if (target === lastOver.current) return
latest.current = move (latest.current ?? ids, String (event.active.id), target)
lastOver.current = target
setPreview (latest.current)
}} onDragCancel={reset} onDragEnd={event => {
if (event.over != null)
{
const target = String (event.over.id)
const current = latest.current ?? ids
onChange (target === lastOver.current || target === String (event.active.id)
? current : move (current, String (event.active.id), target))
}
reset ()
}}>
<SortableContext items={preview ?? ids} strategy={horizontalListSortingStrategy}>
{children (preview ?? ids)}
</SortableContext>
{typeof document !== 'undefined' && createPortal (<DragOverlay
dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing }}>
{active != null && <div lang={name (active).language}
className="rounded border border-blue-500 bg-white p-2 text-slate-900 shadow-lg
dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">{name (active).text}</div>}
</DragOverlay>, document.body)}
</DndContext>
}
export default SortableColumns