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;

Result recording