I'm trying to create a simple calculator with React and dnd-kit. Elements of calculator can be dragged to droppable area and can be sorted inside of it. You can see a problem on the gif: when I drag element from left side to droppable area, there is no animation of dragging but element can be dropped to area. And inside of droppable area elements can be beautifly sorted with animation of dragging.
So, I need drag animation to work when I drag elements from left side to droppable area.
Code for App component:
const App: FC = () => {
const [selected, setSelected] = useState('Constructor')
const [droppedElems, setDroppedElems] = useState<CalcElemListInterface[]>([])
const handleActiveSwitcher = (id: string) => {
setSelected(id)
}
const deleteDroppedElem = (item: CalcElemListInterface) => {
const filtered = [...droppedElems].filter(elem => elem.id !== item.id)
setDroppedElems(filtered)
}
const leftFieldStyles = cn(styles.left, {
[styles.hidden]: selected === 'Runtime'
})
const calcElementsList = calcElemListArray.map((item) => {
const index = droppedElems.findIndex(elem => elem.id === item.id)
const layoutDisabledStyle = index !== -1
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
layoutDisabledStyle={layoutDisabledStyle}
/>
)
})
const handleDragEnd = (event: DragEndEvent) => {
const { id, list } = event.active.data.current as CalcElemListInterface
const elem = {id, list}
if (event.over && event.over.id === 'droppable') {
setDroppedElems((prev) => {
return [...prev, elem]
})
}
}
return (
<div className={styles.layout}>
<div className={styles.top}>
<Switcher
selected={selected}
handleActiveSwitcher={handleActiveSwitcher}
/>
</div>
<DndContext
onDragEnd={handleDragEnd}
>
<div className={styles.content}>
<div className={leftFieldStyles}>
{calcElementsList}
</div>
<DropElemLayout
deleteDroppedElem={deleteDroppedElem}
selected={selected}
droppedElems={droppedElems}
setDroppedElems={setDroppedElems}
/>
</div>
</DndContext>
</div>
)
}
Code for droppable area:
const DropElemLayout: FC<DropElemLayoutInterface> = ({ selected, droppedElems, deleteDroppedElem, setDroppedElems }) => {
const { isOver, setNodeRef } = useDroppable({
id: 'droppable'
})
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const style = {
backgroundColor: (isOver && !droppedElems.length) ? '#F0F9FF' : undefined,
}
const droppedRuntimeElemList = droppedElems.map((item) => {
const layoutEnabledStyle = droppedElems.length ? true : false
return (
<CalcElemLayout
key={item.id}
id={item.id}
item={item}
deleteDroppedElem={deleteDroppedElem}
selected={selected}
layoutEnabledStyle={layoutEnabledStyle}
/>
)
})
const droppedElemList = !droppedElems.length
?
<div className={styles.rightContent}>
<Icon name="#drop"/>
<p>Перетащите сюда</p>
<span>любой элемент</span>
<span>из левой панели</span>
</div>
:
droppedRuntimeElemList
const className = !droppedElems.length ? styles.right : styles.left
const handleDragEnd = (event: DragEndEvent) => {
if (event.active.id !== event.over?.id) {
setDroppedElems((items: CalcElemListInterface[]) => {
const oldIndex = items.findIndex(item => item.id === event.active?.id)
const newIndex = items.findIndex(item => item.id === event.over?.id)
return arrayMove(items, oldIndex, newIndex)
})
}
}
return (
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
>
<div
ref={setNodeRef}
className={className}
style={style}
>
<SortableContext
items={droppedElems}
strategy={verticalListSortingStrategy}
>
{droppedElemList}
</SortableContext>
</div>
</DndContext>
)
}
Code for Element itself:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
// const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
// id: id,
// data: {...item},
// disabled: selected === 'Runtime'
// })
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime'
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
})
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const overlayStyle = {
opacity: '0.5',
}
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
</>
)
}
All you need to do is to add DragOverlay
component properly in Element like so:
const CalcElemLayout: FC<CalcElemLayoutInterface> = ({ item, id, deleteDroppedElem, selected, layoutDisabledStyle, layoutEnabledStyle }) => {
const { current } = useAppSelector(state => state.calculator)
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: id,
data: {...item},
disabled: selected === 'Runtime',
})
const handleDeleteDroppedElem = () => {
deleteDroppedElem?.(item)
}
const doubleClickCondition = selected === 'Constructor' ? handleDeleteDroppedElem : undefined
const layoutStyle = cn(styles.elemLayout, {
[styles.operators]: item.id === 'operators',
[styles.digits]: item.id === 'digits',
[styles.equal]: item.id === 'equal',
[styles.disabled]: layoutDisabledStyle,
[styles.enabled]: layoutEnabledStyle,
[styles.transparent]: isDragging
})
const style = {
transform: CSS.Translate.toString(transform),
transition: transition
}
const buttonList = item.list?.map(elem => (
<Button
key={elem.name}
elem={elem.name}
selected={selected!}
/>
))
const resultStyle = cn(styles.result, {
[styles.minified]: current.length >= 10
})
const elemList = item.id === 'result'
?
<div className={resultStyle}>{current}</div>
:
buttonList
const dragOverlayContent = isDragging
?
<div
className={layoutStyle}
style={{
opacity: isDragging ? '1' : '',
boxShadow: isDragging ? '0px 2px 4px rgba(0, 0, 0, 0.06), 0px 4px 6px rgba(0, 0, 0, 0.1)' : ''
}}
>
{elemList}
</div>
:
null
return (
<>
<div
ref={setNodeRef}
className={layoutStyle}
onDoubleClick={doubleClickCondition}
style={style}
{...attributes}
{...listeners}
>
{elemList}
</div>
<DragOverlay dropAnimation={null}>
{dragOverlayContent}
</DragOverlay>
</>
)
}