このコミットが含まれているのは:
2026-10-26 18:45:28 +09:00
コミット b4139f8370
13個のファイルの変更、199行の追加、45行の削除
+6 -5
ファイルの表示
@@ -9,9 +9,9 @@ import type { ReactNode } from 'react'
type Name = { text: string; language: string }
const SortableItem = ({ id, name, fixed, children, moveGroup }: {
const SortableItem = ({ id, name, fixed, children, moveGroup, handleId }: {
id: string; name: Name; fixed?: boolean; children: ReactNode
moveGroup: (direction: number) => void },
moveGroup: (direction: number) => void; handleId: string },
) => {
const motion = useClientMotion ()
const item = useSortable ({ id, disabled: fixed,
@@ -25,15 +25,16 @@ const SortableItem = ({ id, name, fixed, children, moveGroup }: {
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">
{fixed ? <span className="w-6 shrink-0"/> : <button type="button"
ref={item.setActivatorNodeRef} {...item.attributes} {...item.listeners}
id={handleId} ref={item.setActivatorNodeRef} {...item.attributes} {...item.listeners}
aria-label={name.text} lang={name.language}
aria-keyshortcuts="Space ArrowUp ArrowDown ArrowLeft ArrowRight Enter Escape"
aria-keyshortcuts="Space ArrowUp ArrowDown Enter Escape Alt+ArrowLeft Alt+ArrowRight"
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 && ['ArrowLeft', 'ArrowRight'].includes (event.key))
if (event.altKey && !item.isDragging
&& ['ArrowLeft', 'ArrowRight'].includes (event.key))
{
event.preventDefault ()
event.stopPropagation ()
+43 -11
ファイルの表示
@@ -2,7 +2,7 @@ import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
closestCenter, useDroppable, useSensor, useSensors } from '@dnd-kit/core'
import { SortableContext, arrayMove, rectSortingStrategy,
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { useState } from 'react'
import { useId, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import SortableItem from '@/components/common/SortableItem'
@@ -30,6 +30,9 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
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 scope = useId ()
const [active, setActive] = useState<string | null> (null)
const motion = useClientMotion ()
const current = preview ?? groups
@@ -47,22 +50,33 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
if (source === target)
{
const from = value[source].indexOf (id)
const to = over.startsWith ('group:') ? value[target].length - 1 : value[target].indexOf (over)
const to = over.startsWith ('group:')
? value[target].length - 1 : value[target].indexOf (over)
if (from === to || to < 0) return value
return { ...value, [source]: arrayMove (value[source], from, to) }
}
const destination = value[target].filter (item => item !== id)
const position = destination.indexOf (over)
destination.splice (position < 0 ? destination.length : position, 0, id)
return { ...value, [source]: value[source].filter (item => item !== id), [target]: destination }
return { ...value, [source]: value[source].filter (item => item !== id),
[target]: destination }
}
const moveGroup = (id: string, direction: number) => {
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}`))
if (target != null)
{
onChange (relocate (groups, id, `group:${target}`))
requestAnimationFrame (() => document.getElementById (`${scope}-${id}`)?.focus ())
}
}
const reset = () => {
previewRef.current = null
lastOver.current = null
setPreview (null)
setActive (null)
}
const reset = () => { setPreview (null); setActive (null) }
return <DndContext sensors={sensors} collisionDetection={closestCenter}
accessibility={{ announcements: {
onDragStart: event => name (String (event.active.id)).text,
@@ -72,19 +86,37 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
onDragMove: () => undefined, onDragEnd: () => undefined,
onDragCancel: () => undefined },
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
onDragStart={event => { setPreview (groups); setActive (String (event.active.id)) }}
onDragOver={event => {
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
setPreview (previous => relocate (
previous ?? groups, String (event.active.id), String (event.over!.id)))
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)
}} onDragCancel={reset} onDragEnd={event => {
if (event.over != null && preview != null) onChange (preview)
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)
}
reset ()
}}>
{Object.entries (current).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)} moveGroup={direction => moveGroup (id, direction)}>
name={name (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
moveGroup={direction => moveGroup (id, direction)}>
{renderItem (id, group)}
</SortableItem>)}</Group>
</SortableContext>)}