@react-aria/dnd 3.0.0-alpha.0 → 3.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +2725 -1839
- package/dist/main.js.map +1 -1
- package/dist/module.js +2717 -1783
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +34 -30
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -14
- package/src/DragManager.ts +91 -43
- package/src/DragPreview.tsx +54 -0
- package/src/constants.ts +3 -1
- package/src/index.ts +17 -7
- package/src/useClipboard.ts +5 -5
- package/src/useDrag.ts +40 -39
- package/src/useDraggableItem.ts +9 -11
- package/src/useDrop.ts +9 -4
- package/src/useDropIndicator.ts +10 -10
- package/src/useDroppableCollection.ts +170 -31
- package/src/useDroppableItem.ts +2 -2
- package/src/useVirtualDrop.ts +5 -5
- package/src/utils.ts +6 -24
package/src/useDrag.ts
CHANGED
|
@@ -11,28 +11,27 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
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';
|
|
16
16
|
import * as DragManager from './DragManager';
|
|
17
17
|
import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, EFFECT_ALLOWED} from './constants';
|
|
18
18
|
// @ts-ignore
|
|
19
19
|
import intlMessages from '../intl/*.json';
|
|
20
|
-
import
|
|
21
|
-
import {useDescription} from '@react-aria/utils';
|
|
20
|
+
import {useDescription, useGlobalListeners} from '@react-aria/utils';
|
|
22
21
|
import {useDragModality} from './utils';
|
|
23
|
-
import {
|
|
22
|
+
import {useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
24
23
|
import {writeToDataTransfer} from './utils';
|
|
25
24
|
|
|
26
|
-
interface DragOptions {
|
|
25
|
+
export interface DragOptions {
|
|
27
26
|
onDragStart?: (e: DragStartEvent) => void,
|
|
28
27
|
onDragMove?: (e: DragMoveEvent) => void,
|
|
29
28
|
onDragEnd?: (e: DragEndEvent) => void,
|
|
30
29
|
getItems: () => DragItem[],
|
|
31
|
-
|
|
30
|
+
preview?: RefObject<DragPreviewRenderer>,
|
|
32
31
|
getAllowedDropOperations?: () => DropOperation[]
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
interface DragResult {
|
|
34
|
+
export interface DragResult {
|
|
36
35
|
dragProps: HTMLAttributes<HTMLElement>,
|
|
37
36
|
dragButtonProps: AriaButtonProps,
|
|
38
37
|
isDragging: boolean
|
|
@@ -54,7 +53,7 @@ const MESSAGES = {
|
|
|
54
53
|
};
|
|
55
54
|
|
|
56
55
|
export function useDrag(options: DragOptions): DragResult {
|
|
57
|
-
let
|
|
56
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
58
57
|
let state = useRef({
|
|
59
58
|
options,
|
|
60
59
|
x: 0,
|
|
@@ -62,8 +61,21 @@ export function useDrag(options: DragOptions): DragResult {
|
|
|
62
61
|
}).current;
|
|
63
62
|
state.options = options;
|
|
64
63
|
let [isDragging, setDragging] = useState(false);
|
|
64
|
+
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
|
65
65
|
|
|
66
66
|
let onDragStart = (e: DragEvent) => {
|
|
67
|
+
if (e.defaultPrevented) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof options.onDragStart === 'function') {
|
|
72
|
+
options.onDragStart({
|
|
73
|
+
type: 'dragstart',
|
|
74
|
+
x: e.clientX,
|
|
75
|
+
y: e.clientY
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
let items = options.getItems();
|
|
68
80
|
writeToDataTransfer(e.dataTransfer, items);
|
|
69
81
|
|
|
@@ -77,22 +89,10 @@ export function useDrag(options: DragOptions): DragResult {
|
|
|
77
89
|
e.dataTransfer.effectAllowed = EFFECT_ALLOWED[allowed] || 'none';
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
// If there is a
|
|
92
|
+
// If there is a preview option, use it to render a custom preview image that will
|
|
81
93
|
// appear under the pointer while dragging. If not, the element itself is dragged by the browser.
|
|
82
|
-
if (typeof options.
|
|
83
|
-
|
|
84
|
-
if (preview) {
|
|
85
|
-
// Create an off-screen div to render the preview into.
|
|
86
|
-
let node = document.createElement('div');
|
|
87
|
-
node.style.zIndex = '-100';
|
|
88
|
-
node.style.position = 'absolute';
|
|
89
|
-
node.style.top = '0';
|
|
90
|
-
node.style.left = '-100000px';
|
|
91
|
-
document.body.appendChild(node);
|
|
92
|
-
|
|
93
|
-
// Call renderPreview to get a JSX element, and render it into the div with React DOM.
|
|
94
|
-
ReactDOM.render(preview, node);
|
|
95
|
-
|
|
94
|
+
if (typeof options.preview?.current === 'function') {
|
|
95
|
+
options.preview.current(items, node => {
|
|
96
96
|
// Compute the offset that the preview will appear under the mouse.
|
|
97
97
|
// If possible, this is based on the point the user clicked on the target.
|
|
98
98
|
// If the preview is much smaller, then just use the center point of the preview.
|
|
@@ -105,23 +105,23 @@ export function useDrag(options: DragOptions): DragResult {
|
|
|
105
105
|
y = size.height / 2;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
requestAnimationFrame(() => {
|
|
112
|
-
document.body.removeChild(node);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
108
|
+
// Rounding height to an even number prevents blurry preview seen on some screens
|
|
109
|
+
let height = 2 * Math.round(rect.height / 2);
|
|
110
|
+
node.style.height = `${height}px`;
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
options.onDragStart({
|
|
119
|
-
type: 'dragstart',
|
|
120
|
-
x: e.clientX,
|
|
121
|
-
y: e.clientY
|
|
112
|
+
e.dataTransfer.setDragImage(node, x, y);
|
|
122
113
|
});
|
|
123
114
|
}
|
|
124
115
|
|
|
116
|
+
// Enforce that drops are handled by useDrop.
|
|
117
|
+
addGlobalListener(window, 'drop', e => {
|
|
118
|
+
if (!DragManager.isValidDropTarget(e.target as Element)) {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
throw new Error('Drags initiated from the React Aria useDrag hook may only be dropped on a target created with useDrop. This ensures that a keyboard and screen reader accessible alternative is available.');
|
|
122
|
+
}
|
|
123
|
+
}, {capture: true, once: true});
|
|
124
|
+
|
|
125
125
|
state.x = e.clientX;
|
|
126
126
|
state.y = e.clientY;
|
|
127
127
|
|
|
@@ -160,6 +160,7 @@ export function useDrag(options: DragOptions): DragResult {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
setDragging(false);
|
|
163
|
+
removeAllGlobalListeners();
|
|
163
164
|
};
|
|
164
165
|
|
|
165
166
|
let onPress = (e: PressEvent) => {
|
|
@@ -188,14 +189,14 @@ export function useDrag(options: DragOptions): DragResult {
|
|
|
188
189
|
state.options.onDragEnd(e);
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
|
-
},
|
|
192
|
+
}, stringFormatter);
|
|
192
193
|
|
|
193
194
|
setDragging(true);
|
|
194
195
|
};
|
|
195
196
|
|
|
196
197
|
let modality = useDragModality();
|
|
197
198
|
let descriptionProps = useDescription(
|
|
198
|
-
|
|
199
|
+
stringFormatter.format(!isDragging ? MESSAGES[modality].start : MESSAGES[modality].end)
|
|
199
200
|
);
|
|
200
201
|
|
|
201
202
|
return {
|
package/src/useDraggableItem.ts
CHANGED
|
@@ -16,26 +16,24 @@ import {HTMLAttributes, Key} from 'react';
|
|
|
16
16
|
// @ts-ignore
|
|
17
17
|
import intlMessages from '../intl/*.json';
|
|
18
18
|
import {useDrag} from './useDrag';
|
|
19
|
-
import {
|
|
19
|
+
import {useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
20
20
|
|
|
21
|
-
interface DraggableItemProps {
|
|
21
|
+
export interface DraggableItemProps {
|
|
22
22
|
key: Key
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
interface DraggableItemResult {
|
|
25
|
+
export interface DraggableItemResult {
|
|
26
26
|
dragProps: HTMLAttributes<HTMLElement>,
|
|
27
27
|
dragButtonProps: AriaButtonProps
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult {
|
|
31
|
-
let
|
|
31
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
32
32
|
let {dragProps, dragButtonProps} = useDrag({
|
|
33
33
|
getItems() {
|
|
34
34
|
return state.getItems(props.key);
|
|
35
35
|
},
|
|
36
|
-
|
|
37
|
-
return state.renderPreview(props.key);
|
|
38
|
-
},
|
|
36
|
+
preview: state.preview,
|
|
39
37
|
onDragStart(e) {
|
|
40
38
|
state.startDrag(props.key, e);
|
|
41
39
|
},
|
|
@@ -48,13 +46,13 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
|
|
|
48
46
|
});
|
|
49
47
|
|
|
50
48
|
let item = state.collection.getItem(props.key);
|
|
51
|
-
let
|
|
49
|
+
let numKeysForDrag = state.getKeysForDrag(props.key).size;
|
|
52
50
|
let isSelected = state.selectionManager.isSelected(props.key);
|
|
53
51
|
let message: string;
|
|
54
|
-
if (isSelected &&
|
|
55
|
-
message =
|
|
52
|
+
if (isSelected && numKeysForDrag > 1) {
|
|
53
|
+
message = stringFormatter.format('dragSelectedItems', {count: numKeysForDrag});
|
|
56
54
|
} else {
|
|
57
|
-
message =
|
|
55
|
+
message = stringFormatter.format('dragItem', {itemText: item?.textValue ?? ''});
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
return {
|
package/src/useDrop.ts
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {DragEvent, HTMLAttributes, RefObject,
|
|
13
|
+
import {DragEvent, HTMLAttributes, RefObject, useRef, useState} from 'react';
|
|
14
14
|
import * as DragManager from './DragManager';
|
|
15
15
|
import {DragTypes, readFromDataTransfer} from './utils';
|
|
16
16
|
import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, DROP_OPERATION_ALLOWED, DROP_OPERATION_TO_DROP_EFFECT} from './constants';
|
|
17
17
|
import {DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropMoveEvent, DropOperation, DragTypes as IDragTypes} from '@react-types/shared';
|
|
18
|
+
import {useLayoutEffect} from '@react-aria/utils';
|
|
18
19
|
import {useVirtualDrop} from './useVirtualDrop';
|
|
19
20
|
|
|
20
|
-
interface DropOptions {
|
|
21
|
+
export interface DropOptions {
|
|
21
22
|
ref: RefObject<HTMLElement>,
|
|
22
23
|
getDropOperation?: (types: IDragTypes, allowedOperations: DropOperation[]) => DropOperation,
|
|
23
24
|
getDropOperationForPoint?: (types: IDragTypes, allowedOperations: DropOperation[], x: number, y: number) => DropOperation,
|
|
@@ -30,7 +31,7 @@ interface DropOptions {
|
|
|
30
31
|
onDrop?: (e: DropEvent) => void
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
interface DropResult {
|
|
34
|
+
export interface DropResult {
|
|
34
35
|
dropProps: HTMLAttributes<HTMLElement>,
|
|
35
36
|
isDropTarget: boolean // (??) whether the element is currently an active drop target
|
|
36
37
|
}
|
|
@@ -43,12 +44,13 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
43
44
|
x: 0,
|
|
44
45
|
y: 0,
|
|
45
46
|
dragEnterCount: 0,
|
|
46
|
-
dropEffect: 'none',
|
|
47
|
+
dropEffect: 'none' as DataTransfer['dropEffect'],
|
|
47
48
|
dropActivateTimer: null
|
|
48
49
|
}).current;
|
|
49
50
|
|
|
50
51
|
let onDragOver = (e: DragEvent) => {
|
|
51
52
|
e.preventDefault();
|
|
53
|
+
e.stopPropagation();
|
|
52
54
|
|
|
53
55
|
if (e.clientX === state.x && e.clientY === state.y) {
|
|
54
56
|
e.dataTransfer.dropEffect = state.dropEffect;
|
|
@@ -92,6 +94,7 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
let onDragEnter = (e: DragEvent) => {
|
|
97
|
+
e.stopPropagation();
|
|
95
98
|
state.dragEnterCount++;
|
|
96
99
|
if (state.dragEnterCount > 1) {
|
|
97
100
|
return;
|
|
@@ -132,6 +135,7 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
132
135
|
};
|
|
133
136
|
|
|
134
137
|
let onDragLeave = (e: DragEvent) => {
|
|
138
|
+
e.stopPropagation();
|
|
135
139
|
state.dragEnterCount--;
|
|
136
140
|
if (state.dragEnterCount > 0) {
|
|
137
141
|
return;
|
|
@@ -152,6 +156,7 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
152
156
|
|
|
153
157
|
let onDrop = (e: DragEvent) => {
|
|
154
158
|
e.preventDefault();
|
|
159
|
+
e.stopPropagation();
|
|
155
160
|
|
|
156
161
|
if (typeof options.onDrop === 'function') {
|
|
157
162
|
let dropOperation = DROP_EFFECT_TO_DROP_OPERATION[state.dropEffect];
|
package/src/useDropIndicator.ts
CHANGED
|
@@ -19,13 +19,13 @@ import {HTMLAttributes, Key, RefObject} from 'react';
|
|
|
19
19
|
import intlMessages from '../intl/*.json';
|
|
20
20
|
import {useDroppableItem} from './useDroppableItem';
|
|
21
21
|
import {useId} from '@react-aria/utils';
|
|
22
|
-
import {
|
|
22
|
+
import {useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
23
23
|
|
|
24
|
-
interface DropIndicatorProps {
|
|
24
|
+
export interface DropIndicatorProps {
|
|
25
25
|
target: DropTarget
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
interface DropIndicatorAria {
|
|
28
|
+
export interface DropIndicatorAria {
|
|
29
29
|
dropIndicatorProps: HTMLAttributes<HTMLElement>
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
33
33
|
let {target} = props;
|
|
34
34
|
let {collection} = state;
|
|
35
35
|
|
|
36
|
-
let
|
|
36
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
37
37
|
let dragSession = DragManager.useDragSession();
|
|
38
38
|
let {dropProps} = useDroppableItem(props, state, ref);
|
|
39
39
|
let id = useId();
|
|
@@ -42,10 +42,10 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
42
42
|
let label = '';
|
|
43
43
|
let labelledBy: string;
|
|
44
44
|
if (target.type === 'root') {
|
|
45
|
-
label =
|
|
45
|
+
label = stringFormatter.format('dropOnRoot');
|
|
46
46
|
labelledBy = `${id} ${getDroppableCollectionId(state)}`;
|
|
47
47
|
} else if (target.dropPosition === 'on') {
|
|
48
|
-
label =
|
|
48
|
+
label = stringFormatter.format('dropOnItem', {
|
|
49
49
|
itemText: getText(target.key)
|
|
50
50
|
});
|
|
51
51
|
} else {
|
|
@@ -57,16 +57,16 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
57
57
|
: target.key;
|
|
58
58
|
|
|
59
59
|
if (before && after) {
|
|
60
|
-
label =
|
|
60
|
+
label = stringFormatter.format('insertBetween', {
|
|
61
61
|
beforeItemText: getText(before),
|
|
62
62
|
afterItemText: getText(after)
|
|
63
63
|
});
|
|
64
64
|
} else if (before) {
|
|
65
|
-
label =
|
|
65
|
+
label = stringFormatter.format('insertAfter', {
|
|
66
66
|
itemText: getText(before)
|
|
67
67
|
});
|
|
68
68
|
} else if (after) {
|
|
69
|
-
label =
|
|
69
|
+
label = stringFormatter.format('insertBefore', {
|
|
70
70
|
itemText: getText(after)
|
|
71
71
|
});
|
|
72
72
|
}
|
|
@@ -76,7 +76,7 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
76
76
|
dropIndicatorProps: {
|
|
77
77
|
...dropProps,
|
|
78
78
|
id,
|
|
79
|
-
'aria-roledescription':
|
|
79
|
+
'aria-roledescription': stringFormatter.format('dropIndicator'),
|
|
80
80
|
'aria-label': label,
|
|
81
81
|
'aria-labelledby': labelledBy,
|
|
82
82
|
'aria-hidden': !dragSession ? 'true' : dropProps['aria-hidden'],
|
|
@@ -10,25 +10,33 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {Collection, DropEvent, DropOperation, DroppableCollectionProps, DropPosition, DropTarget, KeyboardDelegate, Node} from '@react-types/shared';
|
|
13
14
|
import * as DragManager from './DragManager';
|
|
14
|
-
import {DropOperation, DroppableCollectionProps, DropPosition, DropTarget, KeyboardDelegate} from '@react-types/shared';
|
|
15
15
|
import {DroppableCollectionState} from '@react-stately/dnd';
|
|
16
16
|
import {getTypes} from './utils';
|
|
17
|
-
import {HTMLAttributes, RefObject, useEffect, useRef} from 'react';
|
|
18
|
-
import {mergeProps} from '@react-aria/utils';
|
|
17
|
+
import {HTMLAttributes, Key, RefObject, useCallback, useEffect, useRef} from 'react';
|
|
18
|
+
import {mergeProps, useLayoutEffect} from '@react-aria/utils';
|
|
19
|
+
import {setInteractionModality} from '@react-aria/interactions';
|
|
19
20
|
import {useAutoScroll} from './useAutoScroll';
|
|
20
21
|
import {useDrop} from './useDrop';
|
|
21
22
|
import {useDroppableCollectionId} from './utils';
|
|
22
23
|
|
|
23
|
-
interface DroppableCollectionOptions extends DroppableCollectionProps {
|
|
24
|
+
export interface DroppableCollectionOptions extends DroppableCollectionProps {
|
|
24
25
|
keyboardDelegate: KeyboardDelegate,
|
|
25
26
|
getDropTargetFromPoint: (x: number, y: number) => DropTarget | null
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
interface DroppableCollectionResult {
|
|
29
|
+
export interface DroppableCollectionResult {
|
|
29
30
|
collectionProps: HTMLAttributes<HTMLElement>
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface DroppingState {
|
|
34
|
+
collection: Collection<Node<unknown>>,
|
|
35
|
+
focusedKey: Key,
|
|
36
|
+
selectedKeys: Set<Key>,
|
|
37
|
+
timeout: ReturnType<typeof setTimeout>
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
const DROP_POSITIONS: DropPosition[] = ['before', 'on', 'after'];
|
|
33
41
|
|
|
34
42
|
export function useDroppableCollection(props: DroppableCollectionOptions, state: DroppableCollectionState, ref: RefObject<HTMLElement>): DroppableCollectionResult {
|
|
@@ -60,11 +68,6 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
60
68
|
return 'cancel';
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
if (state.isDropTarget(target)) {
|
|
64
|
-
localState.nextTarget = target;
|
|
65
|
-
return localState.dropOperation;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
71
|
localState.dropOperation = state.getDropOperation(target, types, allowedOperations);
|
|
69
72
|
|
|
70
73
|
// If the target doesn't accept the drop, see if the root accepts it instead.
|
|
@@ -96,18 +99,103 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
96
99
|
},
|
|
97
100
|
onDrop(e) {
|
|
98
101
|
if (state.target && typeof props.onDrop === 'function') {
|
|
99
|
-
|
|
100
|
-
type: 'drop',
|
|
101
|
-
x: e.x, // todo
|
|
102
|
-
y: e.y,
|
|
103
|
-
target: state.target,
|
|
104
|
-
items: e.items,
|
|
105
|
-
dropOperation: e.dropOperation
|
|
106
|
-
});
|
|
102
|
+
onDrop(e, state.target);
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
});
|
|
110
106
|
|
|
107
|
+
let droppingState = useRef<DroppingState>(null);
|
|
108
|
+
let onDrop = useCallback((e: DropEvent, target: DropTarget) => {
|
|
109
|
+
let {state} = localState;
|
|
110
|
+
|
|
111
|
+
// Focus the collection.
|
|
112
|
+
state.selectionManager.setFocused(true);
|
|
113
|
+
|
|
114
|
+
// Save some state of the collection/selection before the drop occurs so we can compare later.
|
|
115
|
+
let focusedKey = state.selectionManager.focusedKey;
|
|
116
|
+
droppingState.current = {
|
|
117
|
+
timeout: null,
|
|
118
|
+
focusedKey,
|
|
119
|
+
collection: state.collection,
|
|
120
|
+
selectedKeys: state.selectionManager.selectedKeys
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
localState.props.onDrop({
|
|
124
|
+
type: 'drop',
|
|
125
|
+
x: e.x, // todo
|
|
126
|
+
y: e.y,
|
|
127
|
+
target,
|
|
128
|
+
items: e.items,
|
|
129
|
+
dropOperation: e.dropOperation
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Wait for a short time period after the onDrop is called to allow the data to be read asynchronously
|
|
133
|
+
// and for React to re-render. If an insert occurs during this time, it will be selected/focused below.
|
|
134
|
+
// If items are not "immediately" inserted by the onDrop handler, the application will need to handle
|
|
135
|
+
// selecting and focusing those items themselves.
|
|
136
|
+
droppingState.current.timeout = setTimeout(() => {
|
|
137
|
+
// If focus didn't move already (e.g. due to an insert), and the user dropped on an item,
|
|
138
|
+
// focus that item and show the focus ring to give the user feedback that the drop occurred.
|
|
139
|
+
// Also show the focus ring if the focused key is not selected, e.g. in case of a reorder.
|
|
140
|
+
let {state} = localState;
|
|
141
|
+
if (state.selectionManager.focusedKey === focusedKey) {
|
|
142
|
+
if (target.type === 'item' && target.dropPosition === 'on' && state.collection.getItem(target.key) != null) {
|
|
143
|
+
state.selectionManager.setFocusedKey(target.key);
|
|
144
|
+
state.selectionManager.setFocused(true);
|
|
145
|
+
setInteractionModality('keyboard');
|
|
146
|
+
} else if (!state.selectionManager.isSelected(focusedKey)) {
|
|
147
|
+
setInteractionModality('keyboard');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
droppingState.current = null;
|
|
152
|
+
}, 50);
|
|
153
|
+
}, [localState]);
|
|
154
|
+
|
|
155
|
+
// eslint-disable-next-line arrow-body-style
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
return () => {
|
|
158
|
+
if (droppingState.current) {
|
|
159
|
+
clearTimeout(droppingState.current.timeout);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
// If an insert occurs during a drop, we want to immediately select these items to give
|
|
166
|
+
// feedback to the user that a drop occurred. Only do this if the selection didn't change
|
|
167
|
+
// since the drop started so we don't override if the user or application did something.
|
|
168
|
+
if (
|
|
169
|
+
droppingState.current &&
|
|
170
|
+
state.selectionManager.isFocused &&
|
|
171
|
+
state.collection.size > droppingState.current.collection.size &&
|
|
172
|
+
state.selectionManager.isSelectionEqual(droppingState.current.selectedKeys)
|
|
173
|
+
) {
|
|
174
|
+
let newKeys = new Set<Key>();
|
|
175
|
+
for (let key of state.collection.getKeys()) {
|
|
176
|
+
if (!droppingState.current.collection.getItem(key)) {
|
|
177
|
+
newKeys.add(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
state.selectionManager.setSelectedKeys(newKeys);
|
|
182
|
+
|
|
183
|
+
// If the focused item didn't change since the drop occurred, also focus the first
|
|
184
|
+
// inserted item. If selection is disabled, then also show the focus ring so there
|
|
185
|
+
// is some indication that items were added.
|
|
186
|
+
if (state.selectionManager.focusedKey === droppingState.current.focusedKey) {
|
|
187
|
+
let first = newKeys.keys().next().value;
|
|
188
|
+
state.selectionManager.setFocusedKey(first);
|
|
189
|
+
|
|
190
|
+
if (state.selectionManager.selectionMode === 'none') {
|
|
191
|
+
setInteractionModality('keyboard');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
droppingState.current = null;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
111
199
|
useEffect(() => {
|
|
112
200
|
let getNextTarget = (target: DropTarget, wrap = true): DropTarget => {
|
|
113
201
|
if (!target) {
|
|
@@ -120,6 +208,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
120
208
|
let nextKey = target.type === 'item'
|
|
121
209
|
? keyboardDelegate.getKeyBelow(target.key)
|
|
122
210
|
: keyboardDelegate.getFirstKey();
|
|
211
|
+
let dropPosition: DropPosition = 'before';
|
|
123
212
|
|
|
124
213
|
if (target.type === 'item') {
|
|
125
214
|
let positionIndex = DROP_POSITIONS.indexOf(target.dropPosition);
|
|
@@ -131,6 +220,12 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
131
220
|
dropPosition: nextDropPosition
|
|
132
221
|
};
|
|
133
222
|
}
|
|
223
|
+
|
|
224
|
+
// If the last drop position was 'after', then 'before' on the next key is equivalent.
|
|
225
|
+
// Switch to 'on' instead.
|
|
226
|
+
if (target.dropPosition === 'after') {
|
|
227
|
+
dropPosition = 'on';
|
|
228
|
+
}
|
|
134
229
|
}
|
|
135
230
|
|
|
136
231
|
if (nextKey == null) {
|
|
@@ -146,7 +241,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
146
241
|
return {
|
|
147
242
|
type: 'item',
|
|
148
243
|
key: nextKey,
|
|
149
|
-
dropPosition
|
|
244
|
+
dropPosition
|
|
150
245
|
};
|
|
151
246
|
};
|
|
152
247
|
|
|
@@ -155,6 +250,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
155
250
|
let nextKey = target?.type === 'item'
|
|
156
251
|
? keyboardDelegate.getKeyAbove(target.key)
|
|
157
252
|
: keyboardDelegate.getLastKey();
|
|
253
|
+
let dropPosition: DropPosition = !target || target.type === 'root' ? 'after' : 'on';
|
|
158
254
|
|
|
159
255
|
if (target?.type === 'item') {
|
|
160
256
|
let positionIndex = DROP_POSITIONS.indexOf(target.dropPosition);
|
|
@@ -166,6 +262,12 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
166
262
|
dropPosition: nextDropPosition
|
|
167
263
|
};
|
|
168
264
|
}
|
|
265
|
+
|
|
266
|
+
// If the last drop position was 'before', then 'after' on the previous key is equivalent.
|
|
267
|
+
// Switch to 'on' instead.
|
|
268
|
+
if (target.dropPosition === 'before') {
|
|
269
|
+
dropPosition = 'on';
|
|
270
|
+
}
|
|
169
271
|
}
|
|
170
272
|
|
|
171
273
|
if (nextKey == null) {
|
|
@@ -181,7 +283,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
181
283
|
return {
|
|
182
284
|
type: 'item',
|
|
183
285
|
key: nextKey,
|
|
184
|
-
dropPosition
|
|
286
|
+
dropPosition
|
|
185
287
|
};
|
|
186
288
|
};
|
|
187
289
|
|
|
@@ -205,7 +307,6 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
205
307
|
seenRoot++;
|
|
206
308
|
}
|
|
207
309
|
} while (
|
|
208
|
-
target &&
|
|
209
310
|
operation === 'cancel' &&
|
|
210
311
|
!localState.state.isDropTarget(target) &&
|
|
211
312
|
seenRoot < 2
|
|
@@ -232,7 +333,52 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
232
333
|
},
|
|
233
334
|
onDropEnter(e, drag) {
|
|
234
335
|
let types = getTypes(drag.items);
|
|
235
|
-
let
|
|
336
|
+
let selectionManager = localState.state.selectionManager;
|
|
337
|
+
let target: DropTarget;
|
|
338
|
+
|
|
339
|
+
// When entering the droppable collection for the first time, the default drop target
|
|
340
|
+
// is after the focused key.
|
|
341
|
+
let key = selectionManager.focusedKey;
|
|
342
|
+
let dropPosition: DropPosition = 'after';
|
|
343
|
+
|
|
344
|
+
// If the focused key is a cell, get the parent item instead.
|
|
345
|
+
// For now, we assume that individual cells cannot be dropped on.
|
|
346
|
+
let item = localState.state.collection.getItem(key);
|
|
347
|
+
if (item?.type === 'cell') {
|
|
348
|
+
key = item.parentKey;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If the focused item is also selected, the default drop target is after the last selected item.
|
|
352
|
+
// But if the focused key is the first selected item, then default to before the first selected item.
|
|
353
|
+
// This is to make reordering lists slightly easier. If you select top down, we assume you want to
|
|
354
|
+
// move the items down. If you select bottom up, we assume you want to move the items up.
|
|
355
|
+
if (selectionManager.isSelected(key)) {
|
|
356
|
+
if (selectionManager.selectedKeys.size > 1 && selectionManager.firstSelectedKey === key) {
|
|
357
|
+
dropPosition = 'before';
|
|
358
|
+
} else {
|
|
359
|
+
key = selectionManager.lastSelectedKey;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (key != null) {
|
|
364
|
+
target = {
|
|
365
|
+
type: 'item',
|
|
366
|
+
key,
|
|
367
|
+
dropPosition
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// If the default target is not valid, find the next one that is.
|
|
371
|
+
if (localState.state.getDropOperation(target, types, drag.allowedDropOperations) === 'cancel') {
|
|
372
|
+
target = nextValidTarget(target, types, drag.allowedDropOperations, getNextTarget, false)
|
|
373
|
+
?? nextValidTarget(target, types, drag.allowedDropOperations, getPreviousTarget, false);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// If no focused key, then start from the root.
|
|
378
|
+
if (!target) {
|
|
379
|
+
target = nextValidTarget(null, types, drag.allowedDropOperations, getNextTarget);
|
|
380
|
+
}
|
|
381
|
+
|
|
236
382
|
localState.state.setTarget(target);
|
|
237
383
|
},
|
|
238
384
|
onDropExit() {
|
|
@@ -257,14 +403,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
257
403
|
},
|
|
258
404
|
onDrop(e, target) {
|
|
259
405
|
if (localState.state.target && typeof localState.props.onDrop === 'function') {
|
|
260
|
-
localState.
|
|
261
|
-
type: 'drop',
|
|
262
|
-
x: e.x, // todo
|
|
263
|
-
y: e.y,
|
|
264
|
-
target: target || localState.state.target,
|
|
265
|
-
items: e.items,
|
|
266
|
-
dropOperation: e.dropOperation
|
|
267
|
-
});
|
|
406
|
+
onDrop(e, target || localState.state.target);
|
|
268
407
|
}
|
|
269
408
|
},
|
|
270
409
|
onKeyDown(e, drag) {
|
|
@@ -382,7 +521,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
|
|
|
382
521
|
}
|
|
383
522
|
}
|
|
384
523
|
});
|
|
385
|
-
}, [localState, ref]);
|
|
524
|
+
}, [localState, ref, onDrop]);
|
|
386
525
|
|
|
387
526
|
let id = useDroppableCollectionId(state);
|
|
388
527
|
return {
|
package/src/useDroppableItem.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {getTypes} from './utils';
|
|
|
17
17
|
import {HTMLAttributes, RefObject, useEffect} from 'react';
|
|
18
18
|
import {useVirtualDrop} from './useVirtualDrop';
|
|
19
19
|
|
|
20
|
-
interface DroppableItemOptions {
|
|
20
|
+
export interface DroppableItemOptions {
|
|
21
21
|
target: DropTarget
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
interface DroppableItemResult {
|
|
24
|
+
export interface DroppableItemResult {
|
|
25
25
|
dropProps: HTMLAttributes<HTMLElement>
|
|
26
26
|
}
|
|
27
27
|
|