Material-UI では、Material Design Guidelines で定められているドラッグ&ドロップで並べ替えられるリストが提供されていません。ドラッグ&ドロップの機能を提供するライブラリと組み合わせることで、これを実現できます。
ドラッグ&ドロップ
React でドラッグ&ドロップを実現するライブラリは数多ありますが、今回は以下の4つを比較・検討しました。箇条書きはあくまで個人的な感想です。
react-dnd
react-dnd/react-dnd: Drag and Drop for React
- 一番有名どころ。オリジナルの作者は、React や Redux の Dan Abramov (gaearon) 氏。
- 拡張性が高いので、その気になればいろんなことができそう。一方で、アニメーションなど自力で実装しなければならない部分が多い。
- 記述量は多め。
react-beautiful-dnd
atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React
- Atlassian 製。
- ソートのアニメーションが優れている。記述量は多め。
- “Not for everyone” だそうだ。たとえば、今回必要な「軸の固定 (axis locking) 」は、何度か機能提案されているが、UX を損なう (意訳) としてすべて却下されている。
- ということで、“Not for me” だった。
react-sortable-hoc
- 記述量が少なくて済むので、実装が楽。
- 現在非推奨な
findDOMNode
が使われていてレガシーな感じ。 - Material-UI で DragHandle をやろうとするとちょっとハマった (後述)。
react-smooth-dnd
kutlugsahin/react-smooth-dnd: react wrapper components for smooth-dnd
- Star の数では劣るが、特に問題はなさそう。
- 記述量は少なくて済む。ハマりポイントもなく、一番素直に実装できた。
- タッチデバイスだと難ありらしい (Issue が上がっている)。
実装例
react-sortable-hoc で実装してみた例と、react-smooth-dnd で実装してみた例を記載します。(手元で実装したものと CodeSandbox に上げたもので、使用したパッケージのバージョンが異なりますが、動作に問題はありません。)
react-sortable-hoc
使用したパッケージ
- React v16.8.5
- Material-UI v3.9.2
- react-sortable-hoc v1.8.3
- array-move v2.0.0
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';
const DragHandle = SortableHandle(() => (
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
));
const SortableItem = SortableElement(({ text }) => (
<ListItem ContainerComponent="div">
<ListItemText primary={text} />
<ListItemSecondaryAction>
<DragHandle />
</ListItemSecondaryAction>
</ListItem>
));
const SortableListContainer = SortableContainer(({ items }) => (
<List component="div">
{items.map(({ id, text }, index) => (
<SortableItem key={id} index={index} text={text} />
))}
</List>
));
const SortableList = () => {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
{ id: '4', text: 'Item 4' }
]);
const onSortEnd = ({ oldIndex, newIndex }) => {
setItems(items => arrayMove(items, oldIndex, newIndex));
};
return (
<SortableListContainer
items={items}
onSortEnd={onSortEnd}
useDragHandle={true}
lockAxis="y"
/>
);
};
ReactDOM.render(<SortableList />, document.getElementById('root'));
ハマった点ですが、ドラッグ中の li
要素が document.body
にクローンされるため、リストのマーカーが表示されてしまって、表示崩れを起こしてしまいました。これを回避するにはいくつかの方法があります。
-
body
にlist-style-type: none;
を適用する。 -
ListItemSecondaryAction
をSortableHandle
に含めてしまう。 -
ListItem
にContainerComponent="div"
を指定する。
実装例では3番目の方法を採用しています。ListItem
だけ変更すると、実際の DOM で ul
の直下に div
がきて気持ち悪いので、List
にも component="div"
を指定しています。
react-smooth-dnd
使用したパッケージ
- React v16.8.5
- Material-UI v3.9.2
- react-smooth-dnd v0.8.2
- array-move v2.0.0
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Container, Draggable } from 'react-smooth-dnd';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';
const SortableList = () => {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
{ id: '4', text: 'Item 4' }
]);
const onDrop = ({ removedIndex, addedIndex }) => {
setItems(items => arrayMove(items, removedIndex, addedIndex))
};
return (
<List>
<Container dragHandleSelector=".drag-handle" lockAxis="y" onDrop={onDrop}>
{items.map(({ id, text }) => (
<Draggable key={id}>
<ListItem>
<ListItemText primary={text} />
<ListItemSecondaryAction>
<ListItemIcon className="drag-handle">
<DragHandleIcon />
</ListItemIcon>
</ListItemSecondaryAction>
</ListItem>
</Draggable>
))}
</Container>
</List>
);
};
ReactDOM.render(<SortableList />, document.getElementById('root'));
先述の通り、現バージョンではタッチデバイスで難ありらしいですが、PC 向けにはこれで問題ないように思います。
余談
いずれ React Hooks を使って useDragAndDrop
みたいに書けるようになるかもしれないですね。実装が楽になって、見通しがよくなるといいですね。