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;