Skip to content

Commit 8ba032d

Browse files
LFDanLureidbarber
andauthored
useDragHooks/useDropHooks docs (#3012)
* initial content for react-spectrum useDragHooks * adding drop target and adding more copy * adding after version * updating example * adding types and updating docs * updating type descriptions per review * adding example blurb * making it a bit more readable * fixing grammer * addressing some review comments * small reorg * simplifying drag and drop example * some outlining * updating accessbility section * first draft of concepts section * typo * proof reading * adding sections for api props * addressing review comments * review comments * forgot one update * renaming file * fix link * addressing review comments * fix plural copy * addressing review comments * Fixing typo Co-authored-by: Reid Barber <[email protected]> Co-authored-by: Reid Barber <[email protected]>
1 parent ad7b386 commit 8ba032d

File tree

4 files changed

+347
-2
lines changed

4 files changed

+347
-2
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
{/* Copyright 2022 Adobe. All rights reserved.
2+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License. You may obtain a copy
4+
of the License at http://www.apache.org/licenses/LICENSE-2.0
5+
Unless required by applicable law or agreed to in writing, software distributed under
6+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
7+
OF ANY KIND, either express or implied. See the License for the specific language
8+
governing permissions and limitations under the License. */}
9+
10+
import {Layout} from '@react-spectrum/docs';
11+
export default Layout;
12+
13+
import ChevronRight from '@spectrum-icons/workflow/ChevronRight';
14+
import docs from 'docs:@react-spectrum/dnd';
15+
import dndDocs from 'docs:@react-types/shared/src/dnd.d.ts';
16+
import {FunctionAPI, PageDescription, TypeContext, InterfaceType} from '@react-spectrum/docs';
17+
import {Keyboard} from '@react-spectrum/text';
18+
19+
---
20+
category: Concepts
21+
keywords: [drag and drop, dnd]
22+
after_version: 3.0.0
23+
order: 6
24+
---
25+
26+
# Drag and Drop
27+
28+
This page describes how to enable drag and drop functionality for the various React Spectrum components that support it.
29+
30+
## Introduction
31+
32+
Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen.
33+
It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems.
34+
In addition to the standard mouse and touch interactions, React Spectrum also implements keyboard and screen reader accessible alternatives for drag and drop to enable all users to perform these tasks.
35+
36+
## Drag and Drop Concepts
37+
38+
Before we dive into how to enable drag and drop in React Spectrum, let's touch briefly on the terminology and concepts of drag and drop. In a drag and drop operation, there is
39+
a **drag source** and a **drop target**. The drag source is the starting location of your dragged data and the drop target is its intended destination. The dragged data
40+
is made up of one or more **drag items**, each of which contains information specific to their original item within the drag source.
41+
42+
A drag item contains several pieces of information: the **type** of the data, the item's **kind**, and the actual data itself. The type of a drag item can be one of the
43+
[common mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) or a custom type specific to your application. Multiple types can be attached to a
44+
single drag item so that the item's data can be provided in different formats for interoperability with various drop targets. For example, an image could be represented by an
45+
`image/jpeg` type and thus be recognized as a JPEG by a file upload drop target but also have a `plain/text` type that allows the image's file name to be communicated to a
46+
text input drop target. In addition, there are two kinds of items: string items include inline data in the form of a Unicode string, and file items include a reference to a file from
47+
the user's computer.
48+
49+
There are several **drop operations** that can be performed in a drag and drop operation: `"move"`, `"copy"`, `"link"`, and `"cancel"`. A `"move"` operation indicates that the dragged data will be
50+
moved from its source location to the target location. A `"copy"` operation indicates that the dragged data will be copied to the target destination. A `"link"` operation indicates that
51+
there will be a relationship established between the source and target locations. Finally, a `"cancel"` operation indicates that the drag and drop operation will be canceled, resulting in
52+
no changes made to the source or target. The drag source can specify what drop operations are allowed for its data, allowing the drop source to decide what operation to perform on drop by using the restrictions
53+
set by the drag source as a guideline.
54+
55+
Collection components, such as ListView, support multiple **drop positions**. The component may support a `"root"` drop position, allowing items to be dropped on the collection as a whole.
56+
It may also support `"on"` drop positions, such as when dropping into a folder in a list. If the collection allows reordering of its items, it could support `"between"` drop positions, allowing the
57+
user to insert or move items between other items. Any number of these drop positions can be allowed at the same time and the component can use the types of the dragged items to selectively allow or disallow certain positions.
58+
59+
### Interaction modes
60+
61+
There are several interaction modes that need to be considered for drag and drop. When using a mouse, you can click an item and drag by holding the mouse button down and moving the pointer. A drop can
62+
be performed by releasing the mouse button or canceled by the <Keyboard>Esc</Keyboard> key. A similar interaction can be performed via touch, with a drag initiated via a long press and a drop performed by
63+
removing your finger from the screen. In both cases, selecting and dragging an item is often accompanied by a **drag preview**. The drag preview is a smaller version of the dragged item that follows the cursor
64+
or touch point. When multiple items are dragged at once, the drag preview displays a stack of items instead, accompanied by a badge reflecting the total number of dragged items. Drop targets are visually highlighted
65+
when dragged over, and the desired drop operation can be controlled via modifier key presses or drop activations via hovering over the drop target for a period of time.
66+
67+
For keyboards, copy and paste shortcuts have traditionally been the alternative method to drag and drop. This comes with many limitations as it is often hard to know where pasting is allowed and difficult to control the
68+
exact positioning of the pasted items. Touch screen readers are even more limited in their ability to perform these operations since they often lack access to a keyboard and thus cannot copy paste in the same manner.
69+
70+
React Spectrum attempts to resolve the above limitations by providing interactive drag affordances that bring a user into drag and drop mode when triggered via keyboard or screen reader virtual click. To perform a drag and drop operation via a keyboard,
71+
first select the items to be dragged by focusing the row and pressing <Keyboard>Space</Keyboard>. You can then start the drag operation by moving focus to the drag handle on any of the selected rows via the arrow keys and hitting <Keyboard>Enter</Keyboard>
72+
or <Keyboard>Space</Keyboard>. Once a drag operation is started, you will be automatically brought to the first valid drop target. <Keyboard>Tab</Keyboard> can then be used to cycle through other valid drop targets. For collection components like the ListView above,
73+
<Keyboard>Tab</Keyboard> will move you on or off the overall component itself whereas <Keyboard>ArrowUp</Keyboard> and <Keyboard>ArrowDown</Keyboard> will cycle through the valid drop targets within the component itself. Hitting <Keyboard>Enter</Keyboard>
74+
will then confirm the drop operation on the focused drop target. To cancel a drag operation, you can hit <Keyboard>Esc</Keyboard> at any time.
75+
76+
For screen readers, please follow the custom instructions announced when focusing the row's drag handle to begin a drag operation. For screen readers on mobile devices, swiping left and right will
77+
move you between valid drop targets and double tapping will confirm a drop operation. Go ahead and try out drag and drop in the example below!
78+
79+
## Example
80+
81+
```tsx snippet
82+
import {Flex} from '@react-spectrum/layout';
83+
84+
<Flex gap="size-300">
85+
<DraggableList />
86+
<DroppableList />
87+
</Flex>
88+
```
89+
90+
### Creating the draggable list
91+
92+
For the first ListView in the example above, we want to make the rows draggable and have the dragged rows removed from the list upon a successful drop. To accomplish this,
93+
we first want to setup the initial list of items for our draggable ListView via [useListData](/react-stately/useListData.html) so that we have access to some helper methods
94+
to modify the list of items on the fly. Note that this is completely optional and is not required to enable drag and drop in React Spectrum. You may substitute `useListData` with
95+
[useAsyncList](/react-stately/useAsyncList.html) or with any other state management solution.
96+
97+
```tsx
98+
let list = useListData({
99+
initialItems: [
100+
{id: 'a', textValue: 'Adobe Photoshop'},
101+
{id: 'b', textValue: 'Adobe XD'},
102+
{id: 'c', textValue: 'Adobe Dreamweaver'},
103+
{id: 'd', textValue: 'Adobe InDesign'},
104+
{id: 'e', textValue: 'Adobe Connect'}
105+
]
106+
});
107+
```
108+
109+
Next, we need to specify the data associated with each dragged item by returning an array from the `getItems` function. As described above in the [concepts section](#drag-and-drop-concepts), each item includes a mapping of drag types to serialized data.
110+
In this case, we look up the information for each dragged item and serialize it, mapping it to a custom item type. This information will be processed and provided to the drop target's drop handlers on drop.
111+
112+
```tsx
113+
function getItems(keys) {
114+
return [...keys].map(key => {
115+
let item = list.getItem(key);
116+
return {
117+
'adobe-app': JSON.stringify(item)
118+
};
119+
})
120+
}
121+
```
122+
123+
We also create an `onDragEnd` event handler for `useDragHooks` that handles removing the dragged items from the draggable list upon a successful drop operation. Note how we use
124+
the `.remove` method provided by `useListData` to remove the dropped items from our list.
125+
126+
```tsx
127+
function onDragEnd(e) {
128+
if (e.dropOperation === 'move') {
129+
list.remove(...e.keys);
130+
}
131+
}
132+
```
133+
134+
Finally, we provide our `getItems` and `onDragEnd` functions as options to `useDragHooks`, providing us with a set of `dragHooks` that we can pass to our ListView directly.
135+
Below is what our draggable ListView would look like after combining everything together. For more info on `getItems` and `onDragEnd`, see the [API section](#usedraghooks-props) below.
136+
137+
```tsx example export=true render=false
138+
import {Item, ListView} from '@react-spectrum/list';
139+
import {useDragHooks} from '@react-spectrum/dnd';
140+
import {useListData} from '@react-stately/data';
141+
142+
function DraggableList(props) {
143+
let {
144+
items,
145+
...otherProps
146+
} = props;
147+
let list = useListData({
148+
initialItems: items || [
149+
{id: 'a', textValue: 'Adobe Photoshop'},
150+
{id: 'b', textValue: 'Adobe XD'},
151+
{id: 'c', textValue: 'Adobe Dreamweaver'},
152+
{id: 'd', textValue: 'Adobe InDesign'},
153+
{id: 'e', textValue: 'Adobe Connect'}
154+
]
155+
});
156+
157+
let dragHooks = useDragHooks({
158+
getItems: (keys) => [...keys].map(key => {
159+
let item = list.getItem(key);
160+
return {
161+
'adobe-app': JSON.stringify(item)
162+
};
163+
}),
164+
onDragEnd: (e) => {
165+
if (e.dropOperation === 'move') {
166+
list.remove(...e.keys);
167+
}
168+
}
169+
});
170+
171+
return (
172+
<ListView
173+
aria-label="Draggable list view example"
174+
width="size-3600"
175+
height="size-3600"
176+
selectionMode="multiple"
177+
items={list.items}
178+
dragHooks={dragHooks}
179+
{...otherProps}>
180+
{item => (
181+
<Item key={item.key}>
182+
{item.textValue}
183+
</Item>
184+
)}
185+
</ListView>
186+
);
187+
}
188+
```
189+
190+
### Creating the droppable list
191+
192+
For the second ListView, we want to make it droppable but have it only accept root level drops. Similar to the draggable ListView, we'll start by initializing the default item list
193+
via [useListData](/react-stately/useListData.html). As a reminder, `useListData` is completely optional here and can be replaced by [useAsyncList](/react-stately/useAsyncList.html) or any other state management solution.
194+
195+
```tsx
196+
let list = useListData({
197+
initialItems: [
198+
{id: 'f', textValue: 'Adobe AfterEffects'},
199+
{id: 'g', textValue: 'Adobe Illustrator'},
200+
{id: 'h', textValue: 'Adobe Lightroom'},
201+
{id: 'i', textValue: 'Adobe Premiere Pro'},
202+
{id: 'j', textValue: 'Adobe Fresco'}
203+
]
204+
});
205+
```
206+
207+
To implement root level only drops, we create a `getDropOperation` function that returns `"cancel"` for any drop targets other than `root` or if the dragged item types doesn't include
208+
`'adobe-app'`.
209+
210+
```tsx
211+
function getDropOperation(target, types) {
212+
if (target.type !== 'root' || !types.has('adobe-app')) {
213+
return 'cancel';
214+
}
215+
216+
return 'move';
217+
}
218+
```
219+
220+
Next, we create an `onDrop` event handler to process the successfully dropped items. This `onDrop` handler first checks each dropped item's kind and type, extracting the item's
221+
relevant information if it detects it has the expected types. This information is used to construct an array of items to insert to the end of the droppable list via `list.append`.
222+
223+
```tsx
224+
async function onDrop(e) {
225+
let itemsToAdd = await Promise.all(
226+
e.items
227+
.filter(item => item.kind === 'text' && item.types.has('adobe-app'))
228+
.map(async item => JSON.parse(await item.getText('adobe-app')))
229+
);
230+
list.append(...itemsToAdd);
231+
}
232+
```
233+
234+
Finally, we provide our `getDropOperation` and `onDrop` functions as options to `useDropHooks`, providing us with a set of `dropHooks` that we can pass to our ListView directly.
235+
Below is what our droppable ListView would look like after combining everything together. For more info on `getDropOperation` and `onDrop`, see the [API section](#usedrophook-props) below.
236+
237+
```tsx example export=true render=false
238+
import {useDropHooks} from '@react-spectrum/dnd';
239+
240+
function DroppableList(props) {
241+
let {
242+
items,
243+
...otherProps
244+
} = props;
245+
let list = useListData({
246+
initialItems: items || [
247+
{id: 'f', textValue: 'Adobe AfterEffects'},
248+
{id: 'g', textValue: 'Adobe Illustrator'},
249+
{id: 'h', textValue: 'Adobe Lightroom'},
250+
{id: 'i', textValue: 'Adobe Premiere Pro'},
251+
{id: 'j', textValue: 'Adobe Fresco'}
252+
]
253+
});
254+
255+
let dropHooks = useDropHooks({
256+
getDropOperation: (target, types) => {
257+
if (target.type !== 'root' || !types.has('adobe-app')) {
258+
return 'cancel';
259+
}
260+
261+
return 'move';
262+
},
263+
onDrop: async (e) => {
264+
let itemsToAdd = await Promise.all(
265+
e.items
266+
.filter(item => item.kind === 'text' && item.types.has('adobe-app'))
267+
.map(async item => JSON.parse(await item.getText('adobe-app')))
268+
);
269+
list.append(...itemsToAdd);
270+
}
271+
});
272+
273+
return (
274+
<ListView
275+
aria-label="Droppable list view example"
276+
width="size-3600"
277+
height="size-3600"
278+
selectionMode="multiple"
279+
items={list.items}
280+
dropHooks={dropHooks}
281+
{...otherProps}>
282+
{item => (
283+
<Item key={item.key}>
284+
{item.textValue}
285+
</Item>
286+
)}
287+
</ListView>
288+
);
289+
}
290+
```
291+
292+
## API
293+
294+
As seen in the [example](#example) above, enabling drag and drop for a supported React Spectrum component differs slightly from the typical event handler prop pattern that you may be familiar with.
295+
Instead of providing each event handler directly to the component, you must first import `useDragHooks` and `useDropHooks` from the `@react-spectrum/dnd` package
296+
and provide those hooks with your desired options. `useDragHooks` will then provide you with a set of drag hooks that you can pass to the component via its `dragHooks` prop, thus enabling drag operations for the component.
297+
Drop operations are then enabled in the same way by passing the hooks from `useDropHooks` to the component's `dropHooks` prop. This approach allows the drag and drop implementation to be included only when used,
298+
keeping bundle sizes small when unused by an application.
299+
300+
### useDragHooks
301+
302+
<FunctionAPI function={docs.exports.useDragHooks} links={docs.links} />
303+
<TypeContext.Provider value={docs.links}>
304+
<InterfaceType properties={docs.exports.DragHookOptions.properties} />
305+
</TypeContext.Provider>
306+
307+
### useDropHooks
308+
309+
<FunctionAPI function={docs.exports.useDropHooks} links={docs.links} />
310+
<TypeContext.Provider value={dndDocs.links}>
311+
<InterfaceType properties={dndDocs.exports.DroppableCollectionProps.properties} />
312+
</TypeContext.Provider>
313+
314+
Of the various hook options above, `getAllowedDropOperations` and `getDropOperation` may be of particular interest since it allows you to specify what kinds of drag and drop operations you want to allow.
315+
When the dragged items are dropped on a drop target created using the React Aria drag and drop hooks, the allowed drop operations you return in `getAllowedDropOperations`
316+
are provided to the drop target's `getDropOperation`, giving the drop target extra information to use when deciding what drop operation to execute. This in turn provides
317+
the `onDragEnd` and `onDrop` handlers with the executed drop operation, allowing you to decide what to do with the dragged items in your original collection and in the dropped collection.
318+
319+
For instance, you may have a draggable collection of items that allows `"move"` and `"copy"` operations but you need a way to know whether or not you should be removing the dragged items
320+
from the list after a drop operation. A drop target that only allows `"copy"` operations, such as a file upload drop zone, would be able to return `"copy"` from its `getDropOperation` and communicate that
321+
to your draggable collection's `onDragEnd` handler, letting the draggable collection know that it shouldn't remove the dragged items from its list. Alternatively, a drop target that allows `"move"` operations, like in the
322+
[example](#example) above, would return `move` from its `getDropOperation` and thus inform your draggable collection to remove the dragged items from its list.
323+
324+
325+
## Supported components
326+
327+
The following list shows which components currently support drag and drop. Common drag and drop implementations are included in each component's documentation so definitely take a look!
328+
- [ListView](ListView.html#drag-and-drop)

packages/@react-spectrum/dnd/src/useDragHooks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export interface DragHooks {
99
DragPreview: typeof DragPreview
1010
}
1111

12-
export function useDragHooks(options: DraggableCollectionProps): DragHooks {
12+
export interface DragHookOptions extends Omit<DraggableCollectionProps, 'preview'> {}
13+
14+
export function useDragHooks(options: DragHookOptions): DragHooks {
1315
return useMemo(() => ({
1416
useDraggableCollectionState(props: DraggableCollectionOptions) {
1517
return useDraggableCollectionState({...props, ...options});

packages/@react-spectrum/list/docs/ListView.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ This behavior is slightly different in the highlight selection style, where sing
341341
342342
To enable drag and drop in a ListView, you must provide the respective drag and drop hooks sourced from `useDragHooks` and `useDropHooks` to the ListView's `dragHooks` and `dropHooks` props respectively.
343343
See the examples below for various common drag and drop use cases. For more information on the hooks themselves and the various supported ways to perform a drag and drop interaction,
344-
please see the [drag and drop documentation](dragAndDrop.html).
344+
please see the [drag and drop documentation](dnd.html).
345345
346346
The example below demonstrates how to create a draggable ListView and a droppable ListView.
347347

0 commit comments

Comments
 (0)