Skip to content

Commit 7215206

Browse files
committed
Update dnd API for DragPreview without ReactDOM.render
1 parent b3a27a5 commit 7215206

File tree

13 files changed

+234
-176
lines changed

13 files changed

+234
-176
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2020 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {DragItem, DragPreviewRenderer} from '@react-types/shared';
14+
import {flushSync} from 'react-dom';
15+
import React, {RefObject, useImperativeHandle, useRef, useState} from 'react';
16+
17+
export interface DragPreviewProps {
18+
children: (items: DragItem[]) => JSX.Element
19+
}
20+
21+
function DragPreview(props: DragPreviewProps, ref: RefObject<DragPreviewRenderer>) {
22+
let render = props.children;
23+
let [children, setChildren] = useState<JSX.Element>(null);
24+
let domRef = useRef(null);
25+
26+
useImperativeHandle(ref, () => (items: DragItem[], callback: (node: HTMLElement) => void) => {
27+
// This will be called during the onDragStart event by useDrag. We need to render the
28+
// preview synchronously before this event returns so we can call event.dataTransfer.setDragImage.
29+
flushSync(() => {
30+
setChildren(render(items));
31+
});
32+
33+
// Yield back to useDrag to set the drag image.
34+
callback(domRef.current);
35+
36+
// Remove the preview from the DOM after a frame so the browser has time to paint.
37+
requestAnimationFrame(() => {
38+
setChildren(null);
39+
});
40+
}, [render]);
41+
42+
if (!children) {
43+
return null;
44+
}
45+
46+
return (
47+
<div style={{zIndex: -100, position: 'absolute', top: 0, left: -100000}} ref={domRef}>
48+
{children}
49+
</div>
50+
);
51+
}
52+
53+
let _DragPreview = React.forwardRef(DragPreview);
54+
export {_DragPreview as DragPreview};

packages/@react-aria/dnd/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export * from './useDroppableItem';
1717
export * from './useDropIndicator';
1818
export * from './useDraggableItem';
1919
export * from './useClipboard';
20+
export {DragPreview} from './DragPreview';
21+
22+
export type {DragPreviewProps} from './DragPreview';

packages/@react-aria/dnd/src/useDrag.ts

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
*/
1212

1313
import {AriaButtonProps} from '@react-types/button';
14-
import {DragEndEvent, DragItem, DragMoveEvent, DragStartEvent, DropOperation, PressEvent} from '@react-types/shared';
15-
import {DragEvent, HTMLAttributes, useRef, useState} from 'react';
14+
import {DragEndEvent, DragItem, DragMoveEvent, DragPreviewRenderer, DragStartEvent, DropOperation, PressEvent} from '@react-types/shared';
15+
import {DragEvent, HTMLAttributes, RefObject, useRef, useState} from 'react';
1616
import * as DragManager from './DragManager';
1717
import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, EFFECT_ALLOWED} from './constants';
1818
// @ts-ignore
1919
import intlMessages from '../intl/*.json';
20-
import ReactDOM from 'react-dom';
2120
import {useDescription, useGlobalListeners} from '@react-aria/utils';
2221
import {useDragModality} from './utils';
2322
import {useMessageFormatter} from '@react-aria/i18n';
@@ -28,7 +27,7 @@ interface DragOptions {
2827
onDragMove?: (e: DragMoveEvent) => void,
2928
onDragEnd?: (e: DragEndEvent) => void,
3029
getItems: () => DragItem[],
31-
renderPreview?: (items: DragItem[]) => JSX.Element,
30+
preview?: RefObject<DragPreviewRenderer>,
3231
getAllowedDropOperations?: () => DropOperation[]
3332
}
3433

@@ -65,6 +64,14 @@ export function useDrag(options: DragOptions): DragResult {
6564
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
6665

6766
let onDragStart = (e: DragEvent) => {
67+
if (typeof options.onDragStart === 'function') {
68+
options.onDragStart({
69+
type: 'dragstart',
70+
x: e.clientX,
71+
y: e.clientY
72+
});
73+
}
74+
6875
let items = options.getItems();
6976
writeToDataTransfer(e.dataTransfer, items);
7077

@@ -78,22 +85,10 @@ export function useDrag(options: DragOptions): DragResult {
7885
e.dataTransfer.effectAllowed = EFFECT_ALLOWED[allowed] || 'none';
7986
}
8087

81-
// If there is a renderPreview function, use it to render a custom preview image that will
88+
// If there is a preview option, use it to render a custom preview image that will
8289
// appear under the pointer while dragging. If not, the element itself is dragged by the browser.
83-
if (typeof options.renderPreview === 'function') {
84-
let preview = options.renderPreview(items);
85-
if (preview) {
86-
// Create an off-screen div to render the preview into.
87-
let node = document.createElement('div');
88-
node.style.zIndex = '-100';
89-
node.style.position = 'absolute';
90-
node.style.top = '0';
91-
node.style.left = '-100000px';
92-
document.body.appendChild(node);
93-
94-
// Call renderPreview to get a JSX element, and render it into the div with React DOM.
95-
ReactDOM.render(preview, node);
96-
90+
if (typeof options.preview?.current === 'function') {
91+
options.preview.current(items, node => {
9792
// Compute the offset that the preview will appear under the mouse.
9893
// If possible, this is based on the point the user clicked on the target.
9994
// If the preview is much smaller, then just use the center point of the preview.
@@ -109,14 +104,9 @@ export function useDrag(options: DragOptions): DragResult {
109104
// Rounding height to an even number prevents blurry preview seen on some screens
110105
let height = 2 * Math.round(rect.height / 2);
111106
node.style.height = `${height}px`;
112-
113-
e.dataTransfer.setDragImage(node, x, y);
114107

115-
// Remove the preview from the DOM after a frame so the browser has time to paint.
116-
requestAnimationFrame(() => {
117-
document.body.removeChild(node);
118-
});
119-
}
108+
e.dataTransfer.setDragImage(node, x, y);
109+
});
120110
}
121111

122112
// Enforce that drops are handled by useDrop.
@@ -128,14 +118,6 @@ export function useDrag(options: DragOptions): DragResult {
128118
}
129119
}, {capture: true, once: true});
130120

131-
if (typeof options.onDragStart === 'function') {
132-
options.onDragStart({
133-
type: 'dragstart',
134-
x: e.clientX,
135-
y: e.clientY
136-
});
137-
}
138-
139121
state.x = e.clientX;
140122
state.y = e.clientY;
141123

packages/@react-aria/dnd/src/useDraggableItem.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
3333
getItems() {
3434
return state.getItems(props.key);
3535
},
36-
renderPreview() {
37-
return state.renderPreview(props.key);
38-
},
36+
preview: state.preview,
3937
onDragStart(e) {
4038
state.startDrag(props.key, e);
4139
},

packages/@react-aria/dnd/stories/DraggableCollection.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
import {chain, useId} from '@react-aria/utils';
1313
import {classNames} from '@react-spectrum/utils';
1414
import dndStyles from './dnd.css';
15+
import {DragPreview} from '../src';
1516
import {FocusRing} from '@react-aria/focus';
1617
import Folder from '@spectrum-icons/workflow/Folder';
1718
import {GridCollection, useGridState} from '@react-stately/grid';
1819
import {Item} from '@react-stately/collections';
1920
import {mergeProps} from '@react-aria/utils';
20-
import {Provider, useProvider} from '@react-spectrum/provider';
21-
import React from 'react';
21+
import React, {useRef} from 'react';
2222
import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
2323
import {useButton} from '@react-aria/button';
2424
import {useDraggableCollectionState} from '@react-stately/dnd';
@@ -78,7 +78,7 @@ function DraggableCollection(props) {
7878
})
7979
});
8080

81-
let provider = useProvider();
81+
let preview = useRef(null);
8282
let dragState = useDraggableCollectionState({
8383
collection: gridState.collection,
8484
selectionManager: gridState.selectionManager,
@@ -93,22 +93,7 @@ function DraggableCollection(props) {
9393
};
9494
});
9595
},
96-
renderPreview(selectedKeys, draggedKey) {
97-
let item = state.collection.getItem(draggedKey);
98-
return (
99-
<Provider {...provider}>
100-
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': selectedKeys.size > 1})}>
101-
<div className={classNames(dndStyles, 'drag-handle')}>
102-
<ShowMenu size="XS" />
103-
</div>
104-
<span>{item.rendered}</span>
105-
{selectedKeys.size > 1 &&
106-
<div className={classNames(dndStyles, 'badge')}>{selectedKeys.size}</div>
107-
}
108-
</div>
109-
</Provider>
110-
);
111-
},
96+
preview,
11297
onDragStart: props.onDragStart,
11398
onDragMove: props.onDragMove,
11499
onDragEnd: props.onDragEnd
@@ -135,6 +120,22 @@ function DraggableCollection(props) {
135120
state={gridState}
136121
dragState={dragState} />
137122
))}
123+
<DragPreview ref={preview}>
124+
{() => {
125+
let item = state.collection.getItem(dragState.draggedKey);
126+
return (
127+
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': dragState.draggingKeys.size > 1})}>
128+
<div className={classNames(dndStyles, 'drag-handle')}>
129+
<ShowMenu size="XS" />
130+
</div>
131+
<span>{item.rendered}</span>
132+
{dragState.draggingKeys.size > 1 &&
133+
<div className={classNames(dndStyles, 'badge')}>{dragState.draggingKeys.size}</div>
134+
}
135+
</div>
136+
);
137+
}}
138+
</DragPreview>
138139
</div>
139140
);
140141
}

packages/@react-aria/dnd/stories/Reorderable.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {action} from '@storybook/addon-actions';
1414
import {chain} from '@react-aria/utils';
1515
import {classNames} from '@react-spectrum/utils';
1616
import dndStyles from './dnd.css';
17+
import {DragPreview} from '../src';
1718
import dropIndicatorStyles from '@adobe/spectrum-css-temp/components/dropindicator/vars.css';
1819
import {FocusRing} from '@react-aria/focus';
1920
import Folder from '@spectrum-icons/workflow/Folder';
@@ -22,8 +23,7 @@ import {Item} from '@react-stately/collections';
2223
import {ItemDropTarget} from '@react-types/shared';
2324
import {ListKeyboardDelegate} from '@react-aria/selection';
2425
import {mergeProps} from '@react-aria/utils';
25-
import {Provider, useProvider} from '@react-spectrum/provider';
26-
import React from 'react';
26+
import React, {useRef} from 'react';
2727
import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
2828
import {useButton} from '@react-aria/button';
2929
import {useDraggableCollectionState, useDroppableCollectionState} from '@react-stately/dnd';
@@ -91,10 +91,9 @@ function ReorderableGrid(props) {
9191
})
9292
});
9393

94-
let provider = useProvider();
95-
9694
// Use a random drag type so the items can only be reordered within this list and not dragged elsewhere.
9795
let dragType = React.useMemo(() => `keys-${Math.random().toString(36).slice(2)}`, []);
96+
let preview = useRef(null);
9897
let dragState = useDraggableCollectionState({
9998
collection: gridState.collection,
10099
selectionManager: gridState.selectionManager,
@@ -103,22 +102,7 @@ function ReorderableGrid(props) {
103102
[dragType]: JSON.stringify(key)
104103
}));
105104
},
106-
renderPreview(selectedKeys, draggedKey) {
107-
let item = gridState.collection.getItem(draggedKey);
108-
return (
109-
<Provider {...provider}>
110-
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': selectedKeys.size > 1})}>
111-
<div className={classNames(dndStyles, 'drag-handle')}>
112-
<ShowMenu size="XS" />
113-
</div>
114-
<span>{item.rendered}</span>
115-
{selectedKeys.size > 1 &&
116-
<div className={classNames(dndStyles, 'badge')}>{selectedKeys.size}</div>
117-
}
118-
</div>
119-
</Provider>
120-
);
121-
},
105+
preview,
122106
onDragStart: action('onDragStart'),
123107
onDragEnd: chain(action('onDragEnd'), props.onDragEnd)
124108
});
@@ -251,6 +235,22 @@ function ReorderableGrid(props) {
251235
}
252236
</>
253237
))}
238+
<DragPreview ref={preview}>
239+
{() => {
240+
let item = state.collection.getItem(dragState.draggedKey);
241+
return (
242+
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': dragState.draggingKeys.size > 1})}>
243+
<div className={classNames(dndStyles, 'drag-handle')}>
244+
<ShowMenu size="XS" />
245+
</div>
246+
<span>{item.rendered}</span>
247+
{dragState.draggingKeys.size > 1 &&
248+
<div className={classNames(dndStyles, 'badge')}>{dragState.draggingKeys.size}</div>
249+
}
250+
</div>
251+
);
252+
}}
253+
</DragPreview>
254254
</div>
255255
);
256256
}

packages/@react-aria/dnd/stories/dnd.stories.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Copy from '@spectrum-icons/workflow/Copy';
2020
import Cut from '@spectrum-icons/workflow/Cut';
2121
import {Dialog, DialogTrigger} from '@react-spectrum/dialog';
2222
import dndStyles from './dnd.css';
23+
import {DragPreview} from '../src/DragPreview';
2324
import {DroppableGridExample} from './DroppableGrid';
2425
import {DroppableListBox, DroppableListBoxExample} from './DroppableListBox';
2526
import dropzoneStyles from '@adobe/spectrum-css-temp/components/dropzone/vars.css';
@@ -32,8 +33,7 @@ import {Item} from '@react-stately/collections';
3233
import {mergeProps} from '@react-aria/utils';
3334
import Paste from '@spectrum-icons/workflow/Paste';
3435
import {PressResponder} from '@react-aria/interactions';
35-
import {Provider, useProvider} from '@react-spectrum/provider';
36-
import React from 'react';
36+
import React, {useRef} from 'react';
3737
import {ReorderableGridExample} from './Reorderable';
3838
import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
3939
import {storiesOf} from '@storybook/react';
@@ -342,7 +342,7 @@ function DraggableCollection(props) {
342342
})
343343
});
344344

345-
let provider = useProvider();
345+
let preview = useRef(null);
346346
let dragState = useDraggableCollectionState({
347347
collection: gridState.collection,
348348
selectionManager: gridState.selectionManager,
@@ -357,22 +357,7 @@ function DraggableCollection(props) {
357357
};
358358
});
359359
},
360-
renderPreview(selectedKeys, draggedKey) {
361-
let item = state.collection.getItem(draggedKey);
362-
return (
363-
<Provider {...provider}>
364-
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': selectedKeys.size > 1})}>
365-
<div className={classNames(dndStyles, 'drag-handle')}>
366-
<ShowMenu size="XS" />
367-
</div>
368-
<span>{item.rendered}</span>
369-
{selectedKeys.size > 1 &&
370-
<div className={classNames(dndStyles, 'badge')}>{selectedKeys.size}</div>
371-
}
372-
</div>
373-
</Provider>
374-
);
375-
},
360+
preview,
376361
onDragStart: action('onDragStart'),
377362
onDragEnd: chain(action('onDragEnd'), props.onDragEnd)
378363
});
@@ -399,6 +384,24 @@ function DraggableCollection(props) {
399384
dragState={dragState}
400385
onCut={props.onCut} />
401386
))}
387+
<DragPreview ref={preview}>
388+
{() => {
389+
let selectedKeys = dragState.draggingKeys;
390+
let draggedKey = [...selectedKeys][0];
391+
let item = state.collection.getItem(draggedKey);
392+
return (
393+
<div className={classNames(dndStyles, 'draggable', 'is-drag-preview', {'is-dragging-multiple': selectedKeys.size > 1})}>
394+
<div className={classNames(dndStyles, 'drag-handle')}>
395+
<ShowMenu size="XS" />
396+
</div>
397+
<span>{item.rendered}</span>
398+
{selectedKeys.size > 1 &&
399+
<div className={classNames(dndStyles, 'badge')}>{selectedKeys.size}</div>
400+
}
401+
</div>
402+
);
403+
}}
404+
</DragPreview>
402405
</div>
403406
);
404407
}

0 commit comments

Comments
 (0)