@react-aria/dnd 3.0.0-alpha.1 → 3.0.0-alpha.12
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 +2853 -1965
- package/dist/main.js.map +1 -1
- package/dist/module.js +2844 -1908
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +55 -31
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -14
- package/src/DragManager.ts +109 -43
- package/src/DragPreview.tsx +54 -0
- package/src/ListDropTargetDelegate.ts +90 -0
- package/src/constants.ts +3 -1
- package/src/index.ts +19 -7
- package/src/useClipboard.ts +5 -5
- package/src/useDrag.ts +40 -39
- package/src/useDraggableItem.ts +10 -11
- package/src/useDrop.ts +116 -49
- package/src/useDropIndicator.ts +22 -13
- package/src/useDroppableCollection.ts +11 -16
- package/src/useDroppableItem.ts +6 -4
- package/src/useVirtualDrop.ts +5 -5
- package/src/utils.ts +1 -20
package/src/index.ts
CHANGED
|
@@ -10,10 +10,22 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
17
|
-
export
|
|
18
|
-
export
|
|
19
|
-
export
|
|
13
|
+
export type {DroppableCollectionOptions, DroppableCollectionResult} from './useDroppableCollection';
|
|
14
|
+
export type {DroppableItemOptions, DroppableItemResult} from './useDroppableItem';
|
|
15
|
+
export type {DropIndicatorProps, DropIndicatorAria} from './useDropIndicator';
|
|
16
|
+
export type {DraggableItemProps, DraggableItemResult} from './useDraggableItem';
|
|
17
|
+
export type {DragPreviewProps} from './DragPreview';
|
|
18
|
+
export type {DragOptions, DragResult} from './useDrag';
|
|
19
|
+
export type {DropOptions, DropResult} from './useDrop';
|
|
20
|
+
export type {ClipboardProps, ClipboardResult} from './useClipboard';
|
|
21
|
+
export type {DropTargetDelegate} from '@react-types/shared';
|
|
22
|
+
|
|
23
|
+
export {useDrag} from './useDrag';
|
|
24
|
+
export {useDrop} from './useDrop';
|
|
25
|
+
export {useDroppableCollection} from './useDroppableCollection';
|
|
26
|
+
export {useDroppableItem} from './useDroppableItem';
|
|
27
|
+
export {useDropIndicator} from './useDropIndicator';
|
|
28
|
+
export {useDraggableItem} from './useDraggableItem';
|
|
29
|
+
export {useClipboard} from './useClipboard';
|
|
30
|
+
export {DragPreview} from './DragPreview';
|
|
31
|
+
export {ListDropTargetDelegate} from './ListDropTargetDelegate';
|
package/src/useClipboard.ts
CHANGED
|
@@ -11,20 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {chain} from '@react-aria/utils';
|
|
14
|
-
import {DragItem, DropItem} from '@react-types/shared';
|
|
15
|
-
import {HTMLAttributes, useEffect, useRef} from 'react';
|
|
14
|
+
import {DOMAttributes, DragItem, DropItem} from '@react-types/shared';
|
|
16
15
|
import {readFromDataTransfer, writeToDataTransfer} from './utils';
|
|
16
|
+
import {useEffect, useRef} from 'react';
|
|
17
17
|
import {useFocus} from '@react-aria/interactions';
|
|
18
18
|
|
|
19
|
-
interface ClipboardProps {
|
|
19
|
+
export interface ClipboardProps {
|
|
20
20
|
getItems?: () => DragItem[],
|
|
21
21
|
onCopy?: () => void,
|
|
22
22
|
onCut?: () => void,
|
|
23
23
|
onPaste?: (items: DropItem[]) => void
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface ClipboardResult {
|
|
27
|
-
clipboardProps:
|
|
26
|
+
export interface ClipboardResult {
|
|
27
|
+
clipboardProps: DOMAttributes
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const globalEvents = new Map();
|
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,25 @@ 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
|
-
|
|
38
|
-
},
|
|
36
|
+
preview: state.preview,
|
|
37
|
+
getAllowedDropOperations: state.getAllowedDropOperations,
|
|
39
38
|
onDragStart(e) {
|
|
40
39
|
state.startDrag(props.key, e);
|
|
41
40
|
},
|
|
@@ -48,13 +47,13 @@ export function useDraggableItem(props: DraggableItemProps, state: DraggableColl
|
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
let item = state.collection.getItem(props.key);
|
|
51
|
-
let
|
|
50
|
+
let numKeysForDrag = state.getKeysForDrag(props.key).size;
|
|
52
51
|
let isSelected = state.selectionManager.isSelected(props.key);
|
|
53
52
|
let message: string;
|
|
54
|
-
if (isSelected &&
|
|
55
|
-
message =
|
|
53
|
+
if (isSelected && numKeysForDrag > 1) {
|
|
54
|
+
message = stringFormatter.format('dragSelectedItems', {count: numKeysForDrag});
|
|
56
55
|
} else {
|
|
57
|
-
message =
|
|
56
|
+
message = stringFormatter.format('dragItem', {itemText: item?.textValue ?? ''});
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
return {
|
package/src/useDrop.ts
CHANGED
|
@@ -10,27 +10,38 @@
|
|
|
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>,
|
|
23
|
+
/**
|
|
24
|
+
* A function returning the drop operation to be performed when items matching the given types are dropped
|
|
25
|
+
* on the drop target.
|
|
26
|
+
*/
|
|
22
27
|
getDropOperation?: (types: IDragTypes, allowedOperations: DropOperation[]) => DropOperation,
|
|
23
28
|
getDropOperationForPoint?: (types: IDragTypes, allowedOperations: DropOperation[], x: number, y: number) => DropOperation,
|
|
29
|
+
/** Handler that is called when a valid drag enters the drop target. */
|
|
24
30
|
onDropEnter?: (e: DropEnterEvent) => void,
|
|
31
|
+
/** Handler that is called when a valid drag is moved within the drop target. */
|
|
25
32
|
onDropMove?: (e: DropMoveEvent) => void,
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Handler that is called after a valid drag is held over the drop target for a period of time.
|
|
35
|
+
* This typically opens the item so that the user can drop within it.
|
|
36
|
+
*/
|
|
28
37
|
onDropActivate?: (e: DropActivateEvent) => void,
|
|
38
|
+
/** Handler that is called when a valid drag exits the drop target. */
|
|
29
39
|
onDropExit?: (e: DropExitEvent) => void,
|
|
40
|
+
/** Handler that is called when a valid drag is dropped on the drop target. */
|
|
30
41
|
onDrop?: (e: DropEvent) => void
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
interface DropResult {
|
|
44
|
+
export interface DropResult {
|
|
34
45
|
dropProps: HTMLAttributes<HTMLElement>,
|
|
35
46
|
isDropTarget: boolean // (??) whether the element is currently an active drop target
|
|
36
47
|
}
|
|
@@ -42,15 +53,43 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
42
53
|
let state = useRef({
|
|
43
54
|
x: 0,
|
|
44
55
|
y: 0,
|
|
45
|
-
|
|
46
|
-
dropEffect: 'none',
|
|
56
|
+
dragOverElements: new Set<Element>(),
|
|
57
|
+
dropEffect: 'none' as DataTransfer['dropEffect'],
|
|
58
|
+
effectAllowed: 'none' as DataTransfer['effectAllowed'],
|
|
47
59
|
dropActivateTimer: null
|
|
48
60
|
}).current;
|
|
49
61
|
|
|
62
|
+
let fireDropEnter = (e: DragEvent) => {
|
|
63
|
+
setDropTarget(true);
|
|
64
|
+
|
|
65
|
+
if (typeof options.onDropEnter === 'function') {
|
|
66
|
+
let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
67
|
+
options.onDropEnter({
|
|
68
|
+
type: 'dropenter',
|
|
69
|
+
x: e.clientX - rect.x,
|
|
70
|
+
y: e.clientY - rect.y
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let fireDropExit = (e: DragEvent) => {
|
|
76
|
+
setDropTarget(false);
|
|
77
|
+
|
|
78
|
+
if (typeof options.onDropExit === 'function') {
|
|
79
|
+
let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
80
|
+
options.onDropExit({
|
|
81
|
+
type: 'dropexit',
|
|
82
|
+
x: e.clientX - rect.x,
|
|
83
|
+
y: e.clientY - rect.y
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
50
88
|
let onDragOver = (e: DragEvent) => {
|
|
51
89
|
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
52
91
|
|
|
53
|
-
if (e.clientX === state.x && e.clientY === state.y) {
|
|
92
|
+
if (e.clientX === state.x && e.clientY === state.y && e.dataTransfer.effectAllowed === state.effectAllowed) {
|
|
54
93
|
e.dataTransfer.dropEffect = state.dropEffect;
|
|
55
94
|
return;
|
|
56
95
|
}
|
|
@@ -58,17 +97,42 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
58
97
|
state.x = e.clientX;
|
|
59
98
|
state.y = e.clientY;
|
|
60
99
|
|
|
100
|
+
let prevDropEffect = state.dropEffect;
|
|
101
|
+
|
|
102
|
+
// Update drop effect if allowed drop operations changed (e.g. user pressed modifier key).
|
|
103
|
+
if (e.dataTransfer.effectAllowed !== state.effectAllowed) {
|
|
104
|
+
let allowedOperations = effectAllowedToOperations(e.dataTransfer.effectAllowed);
|
|
105
|
+
let dropOperation = allowedOperations[0];
|
|
106
|
+
if (typeof options.getDropOperation === 'function') {
|
|
107
|
+
let types = new DragTypes(e.dataTransfer);
|
|
108
|
+
dropOperation = getDropOperation(e.dataTransfer.effectAllowed, options.getDropOperation(types, allowedOperations));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
if (typeof options.getDropOperationForPoint === 'function') {
|
|
62
115
|
let allowedOperations = effectAllowedToOperations(e.dataTransfer.effectAllowed);
|
|
63
116
|
let types = new DragTypes(e.dataTransfer);
|
|
64
117
|
let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
65
|
-
let dropOperation =
|
|
118
|
+
let dropOperation = getDropOperation(
|
|
119
|
+
e.dataTransfer.effectAllowed,
|
|
120
|
+
options.getDropOperationForPoint(types, allowedOperations, state.x - rect.x, state.y - rect.y)
|
|
121
|
+
);
|
|
66
122
|
state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
|
|
67
123
|
}
|
|
68
124
|
|
|
125
|
+
state.effectAllowed = e.dataTransfer.effectAllowed;
|
|
69
126
|
e.dataTransfer.dropEffect = state.dropEffect;
|
|
70
127
|
|
|
71
|
-
|
|
128
|
+
// If the drop operation changes, update state and fire events appropriately.
|
|
129
|
+
if (state.dropEffect === 'none' && prevDropEffect !== 'none') {
|
|
130
|
+
fireDropExit(e);
|
|
131
|
+
} else if (state.dropEffect !== 'none' && prevDropEffect === 'none') {
|
|
132
|
+
fireDropEnter(e);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof options.onDropMove === 'function' && state.dropEffect !== 'none') {
|
|
72
136
|
let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
73
137
|
options.onDropMove({
|
|
74
138
|
type: 'dropmove',
|
|
@@ -92,8 +156,9 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
92
156
|
};
|
|
93
157
|
|
|
94
158
|
let onDragEnter = (e: DragEvent) => {
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
state.dragOverElements.add(e.target as Element);
|
|
161
|
+
if (state.dragOverElements.size > 1) {
|
|
97
162
|
return;
|
|
98
163
|
}
|
|
99
164
|
|
|
@@ -102,56 +167,61 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
102
167
|
|
|
103
168
|
if (typeof options.getDropOperation === 'function') {
|
|
104
169
|
let types = new DragTypes(e.dataTransfer);
|
|
105
|
-
dropOperation = options.getDropOperation(types, allowedOperations);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (dropOperation !== 'cancel') {
|
|
109
|
-
setDropTarget(true);
|
|
170
|
+
dropOperation = getDropOperation(e.dataTransfer.effectAllowed, options.getDropOperation(types, allowedOperations));
|
|
110
171
|
}
|
|
111
172
|
|
|
112
173
|
if (typeof options.getDropOperationForPoint === 'function') {
|
|
113
174
|
let types = new DragTypes(e.dataTransfer);
|
|
114
175
|
let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
115
|
-
dropOperation =
|
|
176
|
+
dropOperation = getDropOperation(
|
|
177
|
+
e.dataTransfer.effectAllowed,
|
|
178
|
+
options.getDropOperationForPoint(types, allowedOperations, e.clientX - rect.x, e.clientY - rect.y)
|
|
179
|
+
);
|
|
116
180
|
}
|
|
117
181
|
|
|
182
|
+
state.x = e.clientX;
|
|
183
|
+
state.y = e.clientY;
|
|
184
|
+
state.effectAllowed = e.dataTransfer.effectAllowed;
|
|
118
185
|
state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
|
|
119
186
|
e.dataTransfer.dropEffect = state.dropEffect;
|
|
120
187
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
options.onDropEnter({
|
|
124
|
-
type: 'dropenter',
|
|
125
|
-
x: e.clientX - rect.x,
|
|
126
|
-
y: e.clientY - rect.y
|
|
127
|
-
});
|
|
188
|
+
if (dropOperation !== 'cancel') {
|
|
189
|
+
fireDropEnter(e);
|
|
128
190
|
}
|
|
129
|
-
|
|
130
|
-
state.x = e.clientX;
|
|
131
|
-
state.y = e.clientY;
|
|
132
191
|
};
|
|
133
192
|
|
|
134
193
|
let onDragLeave = (e: DragEvent) => {
|
|
135
|
-
|
|
136
|
-
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
|
|
196
|
+
// We would use e.relatedTarget to detect if the drag is still inside the drop target,
|
|
197
|
+
// but it is always null in WebKit. https://bugs.webkit.org/show_bug.cgi?id=66547
|
|
198
|
+
// Instead, we track all of the targets of dragenter events in a set, and remove them
|
|
199
|
+
// in dragleave. When the set becomes empty, we've left the drop target completely.
|
|
200
|
+
// We must also remove any elements that are no longer in the DOM, because dragleave
|
|
201
|
+
// events will never be fired for these. This can happen, for example, with drop
|
|
202
|
+
// indicators between items, which disappear when the drop target changes.
|
|
203
|
+
|
|
204
|
+
state.dragOverElements.delete(e.target as Element);
|
|
205
|
+
for (let element of state.dragOverElements) {
|
|
206
|
+
if (!e.currentTarget.contains(element)) {
|
|
207
|
+
state.dragOverElements.delete(element);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (state.dragOverElements.size > 0) {
|
|
137
212
|
return;
|
|
138
213
|
}
|
|
139
214
|
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
options.onDropExit({
|
|
143
|
-
type: 'dropexit',
|
|
144
|
-
x: e.clientX - rect.x,
|
|
145
|
-
y: e.clientY - rect.y
|
|
146
|
-
});
|
|
215
|
+
if (state.dropEffect !== 'none') {
|
|
216
|
+
fireDropExit(e);
|
|
147
217
|
}
|
|
148
218
|
|
|
149
|
-
setDropTarget(false);
|
|
150
219
|
clearTimeout(state.dropActivateTimer);
|
|
151
220
|
};
|
|
152
221
|
|
|
153
222
|
let onDrop = (e: DragEvent) => {
|
|
154
223
|
e.preventDefault();
|
|
224
|
+
e.stopPropagation();
|
|
155
225
|
|
|
156
226
|
if (typeof options.onDrop === 'function') {
|
|
157
227
|
let dropOperation = DROP_EFFECT_TO_DROP_OPERATION[state.dropEffect];
|
|
@@ -175,17 +245,8 @@ export function useDrop(options: DropOptions): DropResult {
|
|
|
175
245
|
}, 0);
|
|
176
246
|
}
|
|
177
247
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
options.onDropExit({
|
|
181
|
-
type: 'dropexit',
|
|
182
|
-
x: e.clientX - rect.x,
|
|
183
|
-
y: e.clientY - rect.y
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
state.dragEnterCount = 0;
|
|
188
|
-
setDropTarget(false);
|
|
248
|
+
state.dragOverElements.clear();
|
|
249
|
+
fireDropExit(e);
|
|
189
250
|
clearTimeout(state.dropActivateTimer);
|
|
190
251
|
};
|
|
191
252
|
|
|
@@ -250,3 +311,9 @@ function effectAllowedToOperations(effectAllowed: string) {
|
|
|
250
311
|
|
|
251
312
|
return allowedOperations;
|
|
252
313
|
}
|
|
314
|
+
|
|
315
|
+
function getDropOperation(effectAllowed: string, operation: DropOperation) {
|
|
316
|
+
let allowedOperationsBits = DROP_OPERATION_ALLOWED[effectAllowed];
|
|
317
|
+
let op = DROP_OPERATION[operation];
|
|
318
|
+
return allowedOperationsBits & op ? operation : 'cancel';
|
|
319
|
+
}
|
package/src/useDropIndicator.ts
CHANGED
|
@@ -19,21 +19,23 @@ 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 {
|
|
29
|
-
dropIndicatorProps: HTMLAttributes<HTMLElement
|
|
28
|
+
export interface DropIndicatorAria {
|
|
29
|
+
dropIndicatorProps: HTMLAttributes<HTMLElement>,
|
|
30
|
+
isDropTarget: boolean,
|
|
31
|
+
isHidden: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function useDropIndicator(props: DropIndicatorProps, state: DroppableCollectionState, ref: RefObject<HTMLElement>): DropIndicatorAria {
|
|
33
35
|
let {target} = props;
|
|
34
36
|
let {collection} = state;
|
|
35
37
|
|
|
36
|
-
let
|
|
38
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
37
39
|
let dragSession = DragManager.useDragSession();
|
|
38
40
|
let {dropProps} = useDroppableItem(props, state, ref);
|
|
39
41
|
let id = useId();
|
|
@@ -42,10 +44,10 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
42
44
|
let label = '';
|
|
43
45
|
let labelledBy: string;
|
|
44
46
|
if (target.type === 'root') {
|
|
45
|
-
label =
|
|
47
|
+
label = stringFormatter.format('dropOnRoot');
|
|
46
48
|
labelledBy = `${id} ${getDroppableCollectionId(state)}`;
|
|
47
49
|
} else if (target.dropPosition === 'on') {
|
|
48
|
-
label =
|
|
50
|
+
label = stringFormatter.format('dropOnItem', {
|
|
49
51
|
itemText: getText(target.key)
|
|
50
52
|
});
|
|
51
53
|
} else {
|
|
@@ -57,30 +59,37 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
|
|
|
57
59
|
: target.key;
|
|
58
60
|
|
|
59
61
|
if (before && after) {
|
|
60
|
-
label =
|
|
62
|
+
label = stringFormatter.format('insertBetween', {
|
|
61
63
|
beforeItemText: getText(before),
|
|
62
64
|
afterItemText: getText(after)
|
|
63
65
|
});
|
|
64
66
|
} else if (before) {
|
|
65
|
-
label =
|
|
67
|
+
label = stringFormatter.format('insertAfter', {
|
|
66
68
|
itemText: getText(before)
|
|
67
69
|
});
|
|
68
70
|
} else if (after) {
|
|
69
|
-
label =
|
|
71
|
+
label = stringFormatter.format('insertBefore', {
|
|
70
72
|
itemText: getText(after)
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
let isDropTarget = state.isDropTarget(target);
|
|
78
|
+
let ariaHidden = !dragSession ? 'true' : dropProps['aria-hidden'];
|
|
75
79
|
return {
|
|
76
80
|
dropIndicatorProps: {
|
|
77
81
|
...dropProps,
|
|
78
82
|
id,
|
|
79
|
-
'aria-roledescription':
|
|
83
|
+
'aria-roledescription': stringFormatter.format('dropIndicator'),
|
|
80
84
|
'aria-label': label,
|
|
81
85
|
'aria-labelledby': labelledBy,
|
|
82
|
-
'aria-hidden':
|
|
86
|
+
'aria-hidden': ariaHidden,
|
|
83
87
|
tabIndex: -1
|
|
84
|
-
}
|
|
88
|
+
},
|
|
89
|
+
isDropTarget,
|
|
90
|
+
// If aria-hidden, we are either not in a drag session or the drop target is invalid.
|
|
91
|
+
// In that case, there's no need to render anything at all unless we need to show the indicator visually.
|
|
92
|
+
// This can happen when dragging using the native DnD API as opposed to keyboard dragging.
|
|
93
|
+
isHidden: !isDropTarget && !!ariaHidden
|
|
85
94
|
};
|
|
86
95
|
}
|