1 | # react-movable
|
2 |
|
3 | [![npm version](https://img.shields.io/npm/v/react-movable.svg?style=flat-square)](https://www.npmjs.com/package/react-movable)
|
4 | [![npm downloads](https://img.shields.io/npm/dm/react-movable.svg?style=flat-square)](https://www.npmjs.com/package/react-movable)
|
5 | [![Build Status](https://travis-ci.org/tajo/react-movable.svg?branch=master)](https://travis-ci.org/tajo/react-movable)
|
6 | [![size](https://img.shields.io/bundlephobia/minzip/react-movable.svg?style=flat)](https://bundlephobia.com/result?p=react-movable)
|
7 |
|
8 | ![Basic list](https://raw.githubusercontent.com/tajo/react-movable/master/assets/react-movable.gif?raw=true)
|
9 |
|
10 | [![Edit Basic react-movable](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/k1mrwyr9l3)
|
11 |
|
12 | [See all the other examples](https://react-movable.netlify.com) and [their source code](https://github.com/tajo/react-movable/tree/master/examples)!
|
13 |
|
14 | ## Installation
|
15 |
|
16 | ```
|
17 | yarn add react-movable
|
18 | ```
|
19 |
|
20 | ## Usage
|
21 |
|
22 | ```tsx
|
23 | import * as React from 'react';
|
24 | import { List, arrayMove } from 'react-movable';
|
25 |
|
26 | const SuperSimple: React.FC = () => {
|
27 | const [items, setItems] = React.useState(['Item 1', 'Item 2', 'Item 3']);
|
28 | return (
|
29 | <List
|
30 | values={items}
|
31 | onChange={({ oldIndex, newIndex }) =>
|
32 | setItems(arrayMove(items, oldIndex, newIndex))
|
33 | }
|
34 | renderList={({ children, props }) => <ul {...props}>{children}</ul>}
|
35 | renderItem={({ value, props }) => <li {...props}>{value}</li>}
|
36 | />
|
37 | );
|
38 | };
|
39 | ```
|
40 |
|
41 | ## Features
|
42 |
|
43 | - **Vertical drag and drop for your lists and tables**
|
44 | - No wrapping divs or additional markup
|
45 | - Simple single component, no providers or HoCs
|
46 | - Unopinionated styling, great for **CSS in JS** too
|
47 | - **Accessible**, made for keyboards and screen readers
|
48 | - **Touchable**, works on mobile devices
|
49 | - Full control over the dragged item, it's a portaled React component
|
50 | - **Autoscrolling** when dragging (both for containers and the window)
|
51 | - Scrolling with the mousewheel / trackpad when dragging
|
52 | - Works with semantic table rows too
|
53 | - **Smooth animations**, can be disabled
|
54 | - Varying heights of items supported
|
55 | - Optional lock of the horizontal axis when dragging
|
56 | - Typescript and Flow type definitions
|
57 | - **No dependencies, less than 4kB (gzipped)**
|
58 | - Coverage by [e2e puppeteer tests](#end-to-end-testing)
|
59 |
|
60 | ## Keyboard support
|
61 |
|
62 | - `tab` and `shift+tab` to focus items
|
63 | - `space` to lift or drop the item
|
64 | - `j` or `arrow down` to move the lifted item down
|
65 | - `k` or `arrow up` to move the lifted item up
|
66 | - `escape` to cancel the lift and return the item to its initial position
|
67 |
|
68 | ## `<List />` props
|
69 |
|
70 | ### renderList
|
71 |
|
72 | ```ts
|
73 | renderList: (props: {
|
74 | children: React.ReactNode;
|
75 | isDragged: boolean;
|
76 | props: {
|
77 | ref: React.RefObject<any>;
|
78 | };
|
79 | }) => React.ReactNode;
|
80 | ```
|
81 |
|
82 | `renderList` prop to define your list (root) element. **Your function gets three parameters and should return a React component**:
|
83 |
|
84 | - `props` containing `ref` - this needs to be spread over the root list element, note that items need to be direct children of the DOM element that's being set with this `ref`
|
85 | - `children` - the content of the list
|
86 | - `isDragged` - `true` if any item is being dragged
|
87 |
|
88 | ### renderItem
|
89 |
|
90 | ```ts
|
91 | renderItem: (params: {
|
92 | value: Value;
|
93 | index?: number;
|
94 | isDragged: boolean;
|
95 | isSelected: boolean;
|
96 | isOutOfBounds: boolean;
|
97 | props: {
|
98 | key?: number;
|
99 | tabIndex?: number;
|
100 | 'aria-roledescription'?: string;
|
101 | onKeyDown?: (e: React.KeyboardEvent) => void;
|
102 | onWheel?: (e: React.WheelEvent) => void;
|
103 | style?: React.CSSProperties;
|
104 | ref?: React.RefObject<any>;
|
105 | };
|
106 | }) => React.ReactNode;
|
107 | ```
|
108 |
|
109 | `renderItem` prop to define your item element. **Your function gets 5 parameters and should return a React component**:
|
110 |
|
111 | - `value` - an item of the array you passed into the `values` prop
|
112 | - `index` - the item index (order)
|
113 | - `isDragged` - `true` if the item is dragged, great for styling purposes
|
114 | - `isSelected` - `true` if the item is lifted with the `space`
|
115 | - `isOutOfBounds` - `true` if the item is dragged far left or right
|
116 | - `props` - it has multiple props that you need to spread over your item element. Since one of these is `ref`, if you're spreading over a custom component, it must be wrapped in `React.forwardRef` like in the "Custom component" example.
|
117 |
|
118 | ### values
|
119 |
|
120 | ```ts
|
121 | values: Value[]
|
122 | ```
|
123 |
|
124 | An array of values. The value can be a string or any more complex object. The length of the `values` array equals the number of rendered items.
|
125 |
|
126 | ### onChange
|
127 |
|
128 | ```ts
|
129 | onChange: (meta: { oldIndex: number; newIndex: number, targetRect: ClientRect }) => void
|
130 | ```
|
131 |
|
132 | Called when the item is dropped to a new location:
|
133 |
|
134 | - `oldIndex` - the initial position of the element (0 indexed)
|
135 | - `newIndex` - the new position of the element (0 indexed), -1 when `removableByMove` is set and item dropped out of bounds
|
136 | - `targetRect` - [getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of dropped item
|
137 |
|
138 | The List component is `stateless` and `controlled` so you need to implement this function to change the order of input `values`. Check the initial example.
|
139 |
|
140 | ### beforeDrag
|
141 |
|
142 | ```ts
|
143 | beforeDrag?: (params: { elements: Element[]; index: number }) => void;
|
144 | ```
|
145 |
|
146 | Called when a valid drag is initiated. It provides a direct access to all list DOM elements and the index of dragged item. This can be useful when you need to do some upfront measurements like when building a [table with variable column widths](https://react-movable.netlify.app/?story=list--table-auto-cell-widths).
|
147 |
|
148 | ### removableByMove
|
149 |
|
150 | ```ts
|
151 | removableByMove: boolean;
|
152 | ```
|
153 |
|
154 | Default is `false`. When set to `true` and an item is dragged far left or far right (out of bounds), the original gap disappears (animated) and following item drop will cause `onChange` being called with `newIndex = -1`. You can use that to remove the item from your `values` state. [Example](https://react-movable.netlify.com/?story=list--removable-by-move).
|
155 |
|
156 | ### transitionDuration
|
157 |
|
158 | ```ts
|
159 | transitionDuration: number;
|
160 | ```
|
161 |
|
162 | The duration of CSS transitions. By default it's **300ms**. You can set it to 0 to disable all animations.
|
163 |
|
164 | ### lockVertically
|
165 |
|
166 | ```ts
|
167 | lockVertically: boolean;
|
168 | ```
|
169 |
|
170 | If `true`, the dragged element can move only vertically when being dragged.
|
171 |
|
172 | ### voiceover
|
173 |
|
174 | ```ts
|
175 | voiceover: {
|
176 | item: (position: number) => string;
|
177 | lifted: (position: number) => string;
|
178 | dropped: (from: number, to: number) => string;
|
179 | moved: (position: number, up: boolean) => string;
|
180 | canceled: (position: number) => string;
|
181 | }
|
182 | ```
|
183 |
|
184 | In order to support screen reader users, `react-movable` is triggering different messages when user is interacting with the list. There is already a set of [English messages](https://github.com/tajo/react-movable/blob/master/src/List.tsx#L77-L89) included but you can override it with this prop.
|
185 |
|
186 | ## container
|
187 |
|
188 | ```ts
|
189 | container?: Element;
|
190 | ```
|
191 |
|
192 | Provide custom DOM element where moved item will be rendered.
|
193 |
|
194 | ## `arrayMove` and `arrayRemove`
|
195 |
|
196 | There are also additional two helper functions being exported:
|
197 |
|
198 | ```ts
|
199 | arrayMove: <T>(array: T[], from: number, to: number) => T[];
|
200 | arrayRemove: <T>(array: T[], index: number) => T[];
|
201 | ```
|
202 |
|
203 | They are useful when you need to manipulate the state of `values` when `onChange` is triggered.
|
204 |
|
205 | ## Motivation
|
206 |
|
207 | There are two main ways how you can implement drag and drop today:
|
208 |
|
209 | - **[HTML5 drag and drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)**. However, it has some [severe limitations](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html).
|
210 | - Mouse and touch events. It's very low level. You have the full control but it has no concept of DnD.
|
211 |
|
212 | There are multiple great libraries in React's ecosystem already. DnD can get pretty complicated so each one of them covers different use-cases and has different goals:
|
213 |
|
214 | [react-dnd](https://github.com/react-dnd/react-dnd) is a general purpose DnD library that makes amazing job abstracting quirky HTML5 API. It provides well thought out lower-level DnD primitives and let you build anything you want.
|
215 |
|
216 | [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) is a really beautiful DnD library for lists. It comes with a great support for accessibility and it's packed with awesome features. It doesn't use HTML5 API so it doesn't impose any of its limitations.
|
217 |
|
218 | [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) provides a set of higher order components to make your lists dnd-able. It has many features and approaches similar to `react-beautiful-dnd` but it's more minimalistic and lacks some features as accessibility or unopinionated styling.
|
219 |
|
220 | So why `react-movable` was created? There are two main goals:
|
221 |
|
222 | - **Small footprint**. It's about 10 times smaller than `react-dnd` or `react-beautiful-dnd` (~3kB vs ~30kB) and half of the size of `react-sortable-hoc` (~7kB). That's especially important when you intend to use `react-movable` as a dependency in your own library. However, that also means that some features are left out - for example, the horizontal DnD is not supported.
|
223 | - **Simple but not compromised**. - Every byte counts but not if it comes down to the support for accessibility, screen readers, keyboards and touch devices. The goal is to support a limited set of use cases but without compromises.
|
224 |
|
225 | ### Features that are not supported (and never will be)
|
226 |
|
227 | - Horizontal sorting.
|
228 | - DnD between multiple list.
|
229 | - Combining items / multi drag support.
|
230 | - Supporting older versions of React. The minimum required version is `16.3` since the new `createRef` and `createPortal` APIs are used.
|
231 |
|
232 | If you need the features above, please give a try to `react-beautiful-dnd`. It's a really well-designed library with all those features and gives you a lot of power to customize! If you are building an application heavy on DnD interactions, it might be your best bet! `react-movable`'s goal is not to be feature complete with `react-beautiful-dnd`.
|
233 |
|
234 | ### Planned features
|
235 |
|
236 | - Built-in virtualization / windowing.
|
237 |
|
238 | Other feature requests will be thoroughly vetted. Again, the primary goal is to keep the size down while supporting main use-cases!
|
239 |
|
240 | ## End to end testing
|
241 |
|
242 | **This library is tightly coupled to many DOM APIs**. It would be very hard to write unit tests that would not involve a lot of mocking. Or we could re-architect the library to better abstract all DOM interfaces but that would mean more code and bigger footprint.
|
243 |
|
244 | Instead of that, `react-movable` is thoroughly tested by end to end tests powered by [puppeteer](https://github.com/GoogleChrome/puppeteer). It tests all user interactions:
|
245 |
|
246 | - [drag and drop](https://github.com/tajo/react-movable/blob/master/e2e/basic.test.ts) of items by mouse (same and different heights)
|
247 | - [keyboard controls](https://github.com/tajo/react-movable/blob/master/e2e/basic.a11y.test.ts) (moving items around)
|
248 | - [auto scrolling for containers](https://github.com/tajo/react-movable/blob/master/e2e/scrolling.container.test.ts)
|
249 | - [auto scrolling for the window](https://github.com/tajo/react-movable/blob/master/e2e/scrolling.window.test.ts)
|
250 | - [visual regression testing](https://github.com/americanexpress/jest-image-snapshot)
|
251 |
|
252 | All tests are automatically ran in Travis CI with headless chromium. This way, the public API is well tested, including pixel-perfect positioning. Also, the tests are pretty fast, reliable and very descriptive.
|
253 |
|
254 | Do you want to run them in the `dev` mode (slows down operations, opens the browser)?
|
255 |
|
256 | ```bash
|
257 | yarn ladle serve #start the ladle server
|
258 | yarn test:e2e:dev #run the e2e tests
|
259 | ```
|
260 |
|
261 | ```bash
|
262 | yarn test:e2e
|
263 | ```
|
264 |
|
265 | ## Browser support
|
266 |
|
267 | - **Chrome** (latest, mac, windows, iOS, Android)
|
268 | - **Firefox** (latest, mac, windows)
|
269 | - **Safari** (latest, mac, iOS)
|
270 | - **Edge** (latest, windows)
|
271 |
|
272 | ## Users
|
273 |
|
274 | - [Uber Base UI](https://baseui.design/components/dnd-list/)
|
275 |
|
276 | > If you are using react-movable, please open a PR and add yourself to this list!
|
277 |
|
278 | ## Contributing
|
279 |
|
280 | This is how you can spin up the dev environment:
|
281 |
|
282 | ```
|
283 | git clone https://github.com/tajo/react-movable
|
284 | cd react-movable
|
285 | yarn
|
286 | yarn ladle serve
|
287 | ```
|
288 |
|
289 | ## Learning more
|
290 |
|
291 | I wrote an article about [Building a Drag and Drop List](https://baseweb.design/blog/drag-and-drop-list/).
|
292 |
|
293 | Also, gave a talk at React Advanced London: What a Drag (2019):
|
294 |
|
295 | [![React Advanced London: What a Drag](https://img.youtube.com/vi/y_XkQ2qMTSA/0.jpg)](https://www.youtube.com/watch?v=y_XkQ2qMTSA)
|
296 |
|
297 | ## Shoutouts 🙏
|
298 |
|
299 | The popular React DnD libraries were already mentioned in the motivation part. Big shoutout to `react-beautiful-dnd` ❤️ ️ for supporting multiple great features and adding first-class support for accessibility! It was strongly used as an inspiration for `react-movable`!
|
300 |
|
301 | <img src="https://raw.githubusercontent.com/tajo/react-movable/master/assets/browserstack-logo.png?raw=true" height="80" title="BrowserStack Logo" alt="BrowserStack Logo" />
|
302 |
|
303 | Big thanks to [BrowserStack](https://www.browserstack.com) for letting the maintainers use their service to debug browser issues.
|
304 |
|
305 | And [Netlify](https://www.netlify.com/) for free hosting.
|
306 |
|
307 | ## Author
|
308 |
|
309 | Vojtech Miksu 2019, [miksu.cz](https://miksu.cz), [@vmiksu](https://twitter.com/vmiksu)
|