Skip to content

Commit e40d636

Browse files
authored
Custom drag preview support in TableView/ListView (#4396)
* support custom drag previews * improve preview styles in story * improve custom preview example * add support to listview * add tests * improve prop description * add story
1 parent 8a06099 commit e40d636

File tree

8 files changed

+106
-5
lines changed

8 files changed

+106
-5
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ interface DropHooks {
5858

5959
export interface DragAndDropHooks {
6060
/** Drag and drop hooks for the collection element. */
61-
dragAndDropHooks: DragHooks & DropHooks & {isVirtualDragging?: () => boolean}
61+
dragAndDropHooks: DragHooks & DropHooks & {isVirtualDragging?: () => boolean, renderPreview?: (keys: Set<Key>, draggedKey: Key) => JSX.Element}
6262
}
6363

6464
export interface DragAndDropOptions extends Omit<DraggableCollectionProps, 'preview' | 'getItems'>, DroppableCollectionProps {
6565
/**
6666
* A function that returns the items being dragged. If not specified, we assume that the collection is not draggable.
6767
* @default () => []
6868
*/
69-
getItems?: (keys: Set<Key>) => DragItem[]
69+
getItems?: (keys: Set<Key>) => DragItem[],
70+
/** Provide a custom drag preview. `draggedKey` represents the key of the item the user actually dragged. */
71+
renderPreview?: (keys: Set<Key>, draggedKey: Key) => JSX.Element
7072
}
7173

7274
/**
@@ -80,20 +82,22 @@ export function useDragAndDrop(options: DragAndDropOptions): DragAndDropHooks {
8082
onItemDrop,
8183
onReorder,
8284
onRootDrop,
83-
getItems
85+
getItems,
86+
renderPreview
8487
} = options;
8588

8689
let isDraggable = !!getItems;
8790
let isDroppable = !!(onDrop || onInsert || onItemDrop || onReorder || onRootDrop);
8891

89-
let hooks = {} as DragHooks & DropHooks & {isVirtualDragging?: () => boolean};
92+
let hooks = {} as DragHooks & DropHooks & {isVirtualDragging?: () => boolean, renderPreview?: (keys: Set<Key>, draggedKey: Key) => JSX.Element};
9093
if (isDraggable) {
9194
hooks.useDraggableCollectionState = function useDraggableCollectionStateOverride(props: DraggableCollectionStateOptions) {
9295
return useDraggableCollectionState({...props, ...options});
9396
};
9497
hooks.useDraggableCollection = useDraggableCollection;
9598
hooks.useDraggableItem = useDraggableItem;
9699
hooks.DragPreview = DragPreview;
100+
hooks.renderPreview = renderPreview;
97101
}
98102

99103
if (isDroppable) {

packages/@react-spectrum/list/src/ListView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ function ListView<T extends object>(props: SpectrumListViewProps<T>, ref: DOMRef
290290
{DragPreview && isListDraggable &&
291291
<DragPreview ref={preview}>
292292
{() => {
293+
if (dragAndDropHooks.renderPreview) {
294+
return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey);
295+
}
293296
let item = state.collection.getItem(dragState.draggedKey);
294297
let itemCount = dragState.draggingKeys.size;
295298
let itemHeight = layout.getLayoutInfo(dragState.draggedKey).rect.height;

packages/@react-spectrum/list/stories/ListView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default {
148148

149149
export type ListViewStory = ComponentStoryObj<typeof ListView>;
150150

151-
export const DefaultListBox: ListViewStory = {
151+
export const Default: ListViewStory = {
152152
render: (args) => (
153153
<ListView width="250px" aria-label="default ListView" {...args}>
154154
<Item textValue="Adobe Photoshop">Adobe Photoshop</Item>

packages/@react-spectrum/list/stories/ListViewDnD.stories.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Droppable} from '@react-aria/dnd/stories/dnd.stories';
55
import {Flex} from '@react-spectrum/layout';
66
import {ListView} from '../';
77
import React from 'react';
8+
import {View} from '@react-spectrum/view';
89

910
export default {
1011
title: 'ListView/Drag and Drop',
@@ -70,6 +71,28 @@ export const DragOut: ListViewStory = {
7071
name: 'Drag out of list'
7172
};
7273

74+
export const CustomDragPreview: ListViewStory = {
75+
render: (args) => (
76+
<Flex direction="row" wrap alignItems="center">
77+
<input />
78+
<Droppable />
79+
<DragExample
80+
dragHookOptions={{
81+
onDragStart: action('dragStart'),
82+
onDragEnd: action('dragEnd'),
83+
renderPreview: (keys, draggedKey) => (
84+
<View backgroundColor="gray-50" padding="size-100" borderRadius="medium" borderWidth="thin" borderColor="blue-500">
85+
<strong>Custom Preview</strong>
86+
<div>Keys: [{[...keys].join(', ')}]</div>
87+
<div>Dragged: {draggedKey}</div>
88+
</View>
89+
)}}
90+
listViewProps={args} />
91+
</Flex>
92+
),
93+
name: 'Custom drag preview'
94+
};
95+
7396
export const DragWithin: ListViewStory = {
7497
render: (args) => (
7598
<Flex direction="row" wrap alignItems="center">

packages/@react-spectrum/list/test/ListViewDnd.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ describe('ListView', function () {
166166
expect(cellText).toHaveLength(1);
167167
});
168168

169+
it('should render a custom drag preview', function () {
170+
let renderPreview = jest.fn().mockImplementation((keys, draggedKey) => <div>Custom preview for [{[...keys].join(', ')}] , while dragging {draggedKey}.</div>);
171+
let {getAllByRole} = render(
172+
<DraggableListView dragHookOptions={{renderPreview}} listViewProps={{selectedKeys: ['a', 'b']}} />
173+
);
174+
175+
let row = getAllByRole('row')[0];
176+
let cell = within(row).getByRole('gridcell');
177+
178+
let dataTransfer = new DataTransfer();
179+
180+
fireEvent.pointerDown(cell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 5, clientY: 5});
181+
fireEvent(cell, new DragEvent('dragstart', {dataTransfer, clientX: 5, clientY: 5}));
182+
expect(dataTransfer._dragImage.node.tagName).toBe('DIV');
183+
expect(dataTransfer._dragImage.node.textContent).toBe('Custom preview for [a, b] , while dragging a.');
184+
expect(dataTransfer._dragImage.x).toBe(5);
185+
expect(dataTransfer._dragImage.y).toBe(5);
186+
});
187+
169188
it('should allow drag and drop of a single row', async function () {
170189
let {getAllByRole, getByText} = render(
171190
<DraggableListView />

packages/@react-spectrum/table/src/TableView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,9 @@ function TableView<T extends object>(props: SpectrumTableProps<T>, ref: DOMRef<H
562562
{DragPreview && isTableDraggable &&
563563
<DragPreview ref={preview}>
564564
{() => {
565+
if (dragAndDropHooks.renderPreview) {
566+
return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey);
567+
}
565568
let itemCount = dragState.draggingKeys.size;
566569
let maxWidth = bodyRef.current.getBoundingClientRect().width;
567570
let height = ROW_HEIGHTS[density][scale];

packages/@react-spectrum/table/stories/TableDnD.stories.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
import {action} from '@storybook/addon-actions';
1414
import {ComponentMeta} from '@storybook/react';
1515
import defaultConfig from './Table.stories';
16+
import {Divider} from '@react-spectrum/divider';
1617
import {DragBetweenTablesExample, DragBetweenTablesRootOnlyExample, DragExample, DragOntoRowExample, ReorderExample} from './TableDnDExamples';
1718
import {Droppable} from '@react-aria/dnd/stories/dnd.stories';
1819
import {Flex} from '@react-spectrum/layout';
1920
import React from 'react';
2021
import {TableStory} from './Table.stories';
2122
import {TableView} from '../';
23+
import {View} from '@react-spectrum/view';
2224

2325
export default {
2426
...defaultConfig,
@@ -39,6 +41,30 @@ export const DragOutOfTable: TableStory = {
3941
),
4042
name: 'Drag out of table'
4143
};
44+
export const CustomDragPreview: TableStory = {
45+
args: {
46+
disabledKeys: ['Foo 2']
47+
},
48+
render: (args) => (
49+
<Flex direction="row" wrap alignItems="center" gap="size-200">
50+
<Droppable />
51+
<DragExample
52+
dragHookOptions={{
53+
onDragStart: action('dragStart'),
54+
onDragEnd: action('dragEnd'),
55+
renderPreview: (keys, draggedKey) => (
56+
<View backgroundColor="gray-50" padding="size-100" borderRadius="medium" borderWidth="thin" borderColor="blue-500">
57+
<strong>Custom Preview</strong>
58+
<Divider size="S" />
59+
<div>Keys: [{[...keys].join(', ')}]</div>
60+
<div>Dragged: {draggedKey}</div>
61+
</View>
62+
)}}
63+
tableViewProps={args} />
64+
</Flex>
65+
),
66+
storyName: 'Custom drag preview'
67+
};
4268

4369
export const DragWithinTable: TableStory = {
4470
args: {

packages/@react-spectrum/table/test/TableDnd.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,29 @@ describe('TableView', function () {
168168
expect(cellText).toHaveLength(1);
169169
});
170170

171+
it('should render a custom drag preview', function () {
172+
let renderPreview = jest.fn().mockImplementation((keys, draggedKey) => <div>Custom preview for [{[...keys].join(', ')}] , while dragging {draggedKey}.</div>);
173+
let {getByRole} = render(
174+
<DraggableTableView dragHookOptions={{renderPreview}} tableViewProps={{selectedKeys: ['a', 'b']}} />
175+
);
176+
177+
let grid = getByRole('grid');
178+
let rowgroups = within(grid).getAllByRole('rowgroup');
179+
let rows = within(rowgroups[1]).getAllByRole('row');
180+
let row = rows[0];
181+
let cell = within(row).getAllByRole('rowheader')[0];
182+
183+
let dataTransfer = new DataTransfer();
184+
185+
fireEvent.pointerDown(cell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 5, clientY: 5});
186+
fireEvent(cell, new DragEvent('dragstart', {dataTransfer, clientX: 5, clientY: 5}));
187+
expect(dataTransfer._dragImage.node.tagName).toBe('DIV');
188+
expect(dataTransfer._dragImage.node.textContent).toBe('Custom preview for [a, b] , while dragging a.');
189+
expect(dataTransfer._dragImage.x).toBe(5);
190+
expect(dataTransfer._dragImage.y).toBe(5);
191+
});
192+
193+
171194
it('should allow drag and drop of a single row', async function () {
172195
let {getByRole, getByText} = render(
173196
<DraggableTableView />

0 commit comments

Comments
 (0)