DnD for Transfer/Relink Holdings and/or Items
Overview
https://github.com/atlassian/react-beautiful-dnd library was reviewed to support items transfer between tables with DnD
Library provides mostly all functionality we need:
- multi select
- multiple drop zones
- drop zone disable
- accessibility (with multi drag support it should be implemented), but there are some limitations like DnD can be done only for sibling zones - would be nice to discuss expectations
Library doesn't provide a way to replace content of item is currently drugging, but additional components can be added to display how many items are dragging.
Code example
Row.js
import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; const getItemStyle = (draggableStyle, isSelected) => { return { userSelect: 'none', fontWeight: isSelected ? 600 : 300, ...draggableStyle }; }; const primaryButton = 0; const keyCodes = { enter: 13, escape: 27, arrowDown: 40, arrowUp: 38, tab: 9, }; const Row = ({ rowClass, cells, rowIndex, rowData, rowProps, }) => { const { getIsSelected, toggleSelection, } = rowProps; const onKeyDown = (event, provided, snapshot) => { if (event.defaultPrevented) { return; } if (snapshot.isDragging) { return; } if (event.keyCode !== keyCodes.enter) { return; } // we are using the event for selection event.preventDefault(); toggleSelection(rowData.id); }; const onClick = (event) => { if (event.defaultPrevented) { return; } if (event.button !== primaryButton) { return; } // marking the event as used event.preventDefault(); toggleSelection(rowData.id); }; const onTouchEnd = (event: TouchEvent) => { if (event.defaultPrevented) { return; } // marking the event as used // we would also need to add some extra logic to prevent the click // if this element was an anchor event.preventDefault(); toggleSelection(rowData.id); }; return ( <Draggable key={`${rowData.id}`} draggableId={`${rowData.id}`} index={rowIndex} > {(provided, snapshot) => ( <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} onTouchEnd={onTouchEnd} onKeyDown={(event) => onKeyDown(event, provided, snapshot)} onClick={onClick} className={rowClass} role="row" tabIndex="0" style={getItemStyle( provided.draggableProps.style, getIsSelected(rowData.id), )} > {cells} </div> )} </Draggable> ); }; export default Row;
List.js
import React from 'react';
import { Droppable } from 'react-beautiful-dnd';
import {
MultiColumnList,
} from '@folio/stripes/components';
import Row from './Row';
const List = ({
droppableId,
contentData,
visibleColumns,
isDroppable,
getIsSelected,
toggleSelection,
}) => {
return (
<Droppable
droppableId={droppableId}
isDropDisabled={!isDroppable}
>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<MultiColumnList
contentData={contentData}
visibleColumns={visibleColumns}
rowFormatter={Row}
height={300}
rowProps={{
getIsSelected,
toggleSelection,
}}
/>
{provided.placeholder}
</div>
)}
</Droppable>
);
};
export default List;
Pane.js
import React, {
useState,
} from 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import List from './List';
const visibleColumns = ['id', 'name'];
const contentData = {
'uid1': [
{
id: 1,
name: 'test 1',
},
{
id: 2,
name: 'test 2',
},
{
id: 3,
name: 'test 3',
},
],
'uid2': [
{
id: 4,
name: 'test 4',
},
{
id: 5,
name: 'test 5',
},
{
id: 6,
name: 'test 6',
},
],
};
const Pane = () => {
const [lists, setLists] = useState(contentData);
const [selectedIds, setSelectedIds] = useState({});
const [activeDroppable, setActiveDroppable] = useState();
const onDragStart = (result) => {
setActiveDroppable(result.source.droppableId);
};
const onDragEnd = (result) => {
if (!result.destination) return;
const selectedItemIds = Object.keys(selectedIds).filter(itemId => selectedIds[itemId]);
if (!selectedItemIds.length) {
selectedItemIds.push(result.draggableId);
}
setLists({
...lists,
[result.source.droppableId]:
lists[result.source.droppableId].filter(item => !selectedItemIds.includes(`${item.id}`)),
[result.destination.droppableId]: [
...lists[result.destination.droppableId],
...lists[result.source.droppableId].filter(item => selectedItemIds.includes(`${item.id}`))
],
});
setActiveDroppable(undefined);
};
const toggleSelection = (id) => {
setSelectedIds({
...selectedIds,
[id]: !selectedIds[id],
});
};
const getIsSelected = (id) => {
return selectedIds[id];
};
return (
<DragDropContext
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<div>
{
Object.keys(lists).map((listId) => (
<List
key={listId}
droppableId={listId}
contentData={lists[listId]}
visibleColumns={visibleColumns}
isDroppable={activeDroppable !== listId}
getIsSelected={getIsSelected}
toggleSelection={toggleSelection}
/>
))
}
</div>
</DragDropContext>
);
};
export default Pane;