このコミットが含まれているのは:
2026-10-28 19:28:11 +09:00
コミット 8ea3d211dc
49個のファイルの変更、967行の追加、356行の削除
+15 -26
ファイルの表示
@@ -14,10 +14,8 @@ 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 drag = useRef<{ id: string; ids: string[] } | null> (null)
const motion = useClientMotion ()
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
@@ -27,12 +25,11 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
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)
return source < 0 || target < 0 || source === target
? current : arrayMove (current, source, target)
}
const reset = () => {
latest.current = null
lastOver.current = null
setPreview (null)
drag.current = null
setActive (null)
}
return <DndContext sensors={sensors} collisionDetection={closestCenter}
@@ -43,28 +40,20 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
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)
const id = String (event.active.id)
drag.current = { id, ids }
setActive (id)
}} 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))
}
const session = drag.current
reset ()
if (session == null || event.over == null || session.id !== String (event.active.id)
|| session.ids.length !== ids.length
|| session.ids.some ((id, index) => id !== ids[index])) return
const next = move (ids, session.id, String (event.over.id))
if (next.some ((id, index) => id !== ids[index])) onChange (next)
}}>
<SortableContext items={preview ?? ids} strategy={horizontalListSortingStrategy}>
{children (preview ?? ids)}
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
{children (ids)}
</SortableContext>
{typeof document !== 'undefined' && createPortal (<DragOverlay
dropAnimation={motion.reduced ? null : {
+2
ファイルの表示
@@ -46,6 +46,8 @@ const SortableItem = ({ id, name, fixed, children, moveGroup, handleId, renderSh
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}
+24 -31
ファイルの表示
@@ -15,6 +15,12 @@ type Groups = Record<string, string[]>
type Name = { text: string; language: string }
const sameGroups = (left: Groups, right: Groups): boolean =>
Object.keys (left).length === Object.keys (right).length
&& Object.keys (left).every (key => left[key].length === right[key]?.length
&& left[key].every ((id, index) => id === right[key][index]))
const Group = ({ id, children }: { id: string; children: ReactNode }) => {
const drop = useDroppable ({ id: `group:${id}` })
return <div ref={drop.setNodeRef} className={cn (
@@ -29,13 +35,10 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
renderGroup: (id: string, content: ReactNode) => ReactNode
onChange: (groups: Groups) => void; fixed?: string[] },
) => {
const [preview, setPreview] = useState<Groups | null> (null)
const previewRef = useRef<Groups | null> (null)
const lastOver = useRef<string | null> (null)
const drag = useRef<{ id: string; groups: Groups } | null> (null)
const scope = useId ()
const [active, setActive] = useState<string | null> (null)
const motion = useClientMotion ()
const current = preview ?? groups
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
@@ -45,7 +48,8 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
const relocate = (value: Groups, id: string, over: string): Groups => {
const source = owner (id, value)
const target = over.startsWith ('group:') ? over.slice (6) : owner (over, value)
if (source == null || target == null || fixed.includes (id) || fixed.includes (over))
if (source == null || target == null || value[target] == null
|| fixed.includes (id) || fixed.includes (over))
return value
if (source === target)
{
@@ -62,19 +66,20 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
[target]: destination }
}
const moveGroup = (id: string, direction: number) => {
if (drag.current != null) return
const keys = Object.keys (groups)
const group = owner (id, groups)
const target = keys[keys.indexOf (group ?? '') + direction]
if (target != null)
{
onChange (relocate (groups, id, `group:${target}`))
const next = relocate (groups, id, `group:${target}`)
if (sameGroups (groups, next)) return
onChange (next)
requestAnimationFrame (() => document.getElementById (`${scope}-${id}`)?.focus ())
}
}
const reset = () => {
previewRef.current = null
lastOver.current = null
setPreview (null)
drag.current = null
setActive (null)
}
return <DndContext sensors={sensors} collisionDetection={closestCenter}
@@ -87,32 +92,20 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
onDragCancel: () => undefined },
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
onDragStart={event => {
previewRef.current = groups
lastOver.current = null
setPreview (groups)
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 (lastOver.current === target) return
const next = relocate (previewRef.current ?? groups, String (event.active.id), target)
previewRef.current = next
lastOver.current = target
setPreview (next)
const id = String (event.active.id)
drag.current = { id, groups }
setActive (id)
}} onDragCancel={reset} onDragEnd={event => {
if (event.over != null)
{
const id = String (event.active.id)
const target = String (event.over.id)
const latest = previewRef.current ?? groups
const next = (target === id || target === lastOver.current)
? latest : relocate (latest, id, target)
onChange (next)
}
const session = drag.current
reset ()
if (session == null || event.over == null || session.id !== String (event.active.id)
|| !sameGroups (session.groups, groups)) return
const next = relocate (groups, session.id, String (event.over.id))
if (!sameGroups (groups, next)) onChange (next)
}}>
{Object.entries (current).map (([group, ids]) => <div key={group}>
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
{Object.entries (groups).map (([group, ids]) => <div key={group}>
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
<Group id={group}>{ids.map (id => <SortableItem key={id} id={id}
name={name (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}