@react-aria/dnd 3.0.0-alpha.9 → 3.0.0-nightly-4980928d3-240906

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.
Files changed (275) hide show
  1. package/dist/DragManager.main.js +432 -0
  2. package/dist/DragManager.main.js.map +1 -0
  3. package/dist/DragManager.mjs +423 -0
  4. package/dist/DragManager.module.js +423 -0
  5. package/dist/DragManager.module.js.map +1 -0
  6. package/dist/DragPreview.main.js +59 -0
  7. package/dist/DragPreview.main.js.map +1 -0
  8. package/dist/DragPreview.mjs +50 -0
  9. package/dist/DragPreview.module.js +50 -0
  10. package/dist/DragPreview.module.js.map +1 -0
  11. package/dist/ListDropTargetDelegate.main.js +123 -0
  12. package/dist/ListDropTargetDelegate.main.js.map +1 -0
  13. package/dist/ListDropTargetDelegate.mjs +118 -0
  14. package/dist/ListDropTargetDelegate.module.js +118 -0
  15. package/dist/ListDropTargetDelegate.module.js.map +1 -0
  16. package/dist/ar-AE.main.js +44 -0
  17. package/dist/ar-AE.main.js.map +1 -0
  18. package/dist/ar-AE.mjs +46 -0
  19. package/dist/ar-AE.module.js +46 -0
  20. package/dist/ar-AE.module.js.map +1 -0
  21. package/dist/bg-BG.main.js +44 -0
  22. package/dist/bg-BG.main.js.map +1 -0
  23. package/dist/bg-BG.mjs +46 -0
  24. package/dist/bg-BG.module.js +46 -0
  25. package/dist/bg-BG.module.js.map +1 -0
  26. package/dist/constants.main.js +65 -0
  27. package/dist/constants.main.js.map +1 -0
  28. package/dist/constants.mjs +53 -0
  29. package/dist/constants.module.js +53 -0
  30. package/dist/constants.module.js.map +1 -0
  31. package/dist/cs-CZ.main.js +45 -0
  32. package/dist/cs-CZ.main.js.map +1 -0
  33. package/dist/cs-CZ.mjs +47 -0
  34. package/dist/cs-CZ.module.js +47 -0
  35. package/dist/cs-CZ.module.js.map +1 -0
  36. package/dist/da-DK.main.js +44 -0
  37. package/dist/da-DK.main.js.map +1 -0
  38. package/dist/da-DK.mjs +46 -0
  39. package/dist/da-DK.module.js +46 -0
  40. package/dist/da-DK.module.js.map +1 -0
  41. package/dist/de-DE.main.js +44 -0
  42. package/dist/de-DE.main.js.map +1 -0
  43. package/dist/de-DE.mjs +46 -0
  44. package/dist/de-DE.module.js +46 -0
  45. package/dist/de-DE.module.js.map +1 -0
  46. package/dist/el-GR.main.js +44 -0
  47. package/dist/el-GR.main.js.map +1 -0
  48. package/dist/el-GR.mjs +46 -0
  49. package/dist/el-GR.module.js +46 -0
  50. package/dist/el-GR.module.js.map +1 -0
  51. package/dist/en-US.main.js +44 -0
  52. package/dist/en-US.main.js.map +1 -0
  53. package/dist/en-US.mjs +46 -0
  54. package/dist/en-US.module.js +46 -0
  55. package/dist/en-US.module.js.map +1 -0
  56. package/dist/es-ES.main.js +44 -0
  57. package/dist/es-ES.main.js.map +1 -0
  58. package/dist/es-ES.mjs +46 -0
  59. package/dist/es-ES.module.js +46 -0
  60. package/dist/es-ES.module.js.map +1 -0
  61. package/dist/et-EE.main.js +44 -0
  62. package/dist/et-EE.main.js.map +1 -0
  63. package/dist/et-EE.mjs +46 -0
  64. package/dist/et-EE.module.js +46 -0
  65. package/dist/et-EE.module.js.map +1 -0
  66. package/dist/fi-FI.main.js +44 -0
  67. package/dist/fi-FI.main.js.map +1 -0
  68. package/dist/fi-FI.mjs +46 -0
  69. package/dist/fi-FI.module.js +46 -0
  70. package/dist/fi-FI.module.js.map +1 -0
  71. package/dist/fr-FR.main.js +44 -0
  72. package/dist/fr-FR.main.js.map +1 -0
  73. package/dist/fr-FR.mjs +46 -0
  74. package/dist/fr-FR.module.js +46 -0
  75. package/dist/fr-FR.module.js.map +1 -0
  76. package/dist/he-IL.main.js +44 -0
  77. package/dist/he-IL.main.js.map +1 -0
  78. package/dist/he-IL.mjs +46 -0
  79. package/dist/he-IL.module.js +46 -0
  80. package/dist/he-IL.module.js.map +1 -0
  81. package/dist/hr-HR.main.js +44 -0
  82. package/dist/hr-HR.main.js.map +1 -0
  83. package/dist/hr-HR.mjs +46 -0
  84. package/dist/hr-HR.module.js +46 -0
  85. package/dist/hr-HR.module.js.map +1 -0
  86. package/dist/hu-HU.main.js +44 -0
  87. package/dist/hu-HU.main.js.map +1 -0
  88. package/dist/hu-HU.mjs +46 -0
  89. package/dist/hu-HU.module.js +46 -0
  90. package/dist/hu-HU.module.js.map +1 -0
  91. package/dist/import.mjs +39 -0
  92. package/dist/intlStrings.main.js +108 -0
  93. package/dist/intlStrings.main.js.map +1 -0
  94. package/dist/intlStrings.mjs +110 -0
  95. package/dist/intlStrings.module.js +110 -0
  96. package/dist/intlStrings.module.js.map +1 -0
  97. package/dist/it-IT.main.js +44 -0
  98. package/dist/it-IT.main.js.map +1 -0
  99. package/dist/it-IT.mjs +46 -0
  100. package/dist/it-IT.module.js +46 -0
  101. package/dist/it-IT.module.js.map +1 -0
  102. package/dist/ja-JP.main.js +44 -0
  103. package/dist/ja-JP.main.js.map +1 -0
  104. package/dist/ja-JP.mjs +46 -0
  105. package/dist/ja-JP.module.js +46 -0
  106. package/dist/ja-JP.module.js.map +1 -0
  107. package/dist/ko-KR.main.js +44 -0
  108. package/dist/ko-KR.main.js.map +1 -0
  109. package/dist/ko-KR.mjs +46 -0
  110. package/dist/ko-KR.module.js +46 -0
  111. package/dist/ko-KR.module.js.map +1 -0
  112. package/dist/lt-LT.main.js +44 -0
  113. package/dist/lt-LT.main.js.map +1 -0
  114. package/dist/lt-LT.mjs +46 -0
  115. package/dist/lt-LT.module.js +46 -0
  116. package/dist/lt-LT.module.js.map +1 -0
  117. package/dist/lv-LV.main.js +44 -0
  118. package/dist/lv-LV.main.js.map +1 -0
  119. package/dist/lv-LV.mjs +46 -0
  120. package/dist/lv-LV.module.js +46 -0
  121. package/dist/lv-LV.module.js.map +1 -0
  122. package/dist/main.js +39 -1873
  123. package/dist/main.js.map +1 -1
  124. package/dist/module.js +24 -1853
  125. package/dist/module.js.map +1 -1
  126. package/dist/nb-NO.main.js +44 -0
  127. package/dist/nb-NO.main.js.map +1 -0
  128. package/dist/nb-NO.mjs +46 -0
  129. package/dist/nb-NO.module.js +46 -0
  130. package/dist/nb-NO.module.js.map +1 -0
  131. package/dist/nl-NL.main.js +44 -0
  132. package/dist/nl-NL.main.js.map +1 -0
  133. package/dist/nl-NL.mjs +46 -0
  134. package/dist/nl-NL.module.js +46 -0
  135. package/dist/nl-NL.module.js.map +1 -0
  136. package/dist/pl-PL.main.js +44 -0
  137. package/dist/pl-PL.main.js.map +1 -0
  138. package/dist/pl-PL.mjs +46 -0
  139. package/dist/pl-PL.module.js +46 -0
  140. package/dist/pl-PL.module.js.map +1 -0
  141. package/dist/pt-BR.main.js +44 -0
  142. package/dist/pt-BR.main.js.map +1 -0
  143. package/dist/pt-BR.mjs +46 -0
  144. package/dist/pt-BR.module.js +46 -0
  145. package/dist/pt-BR.module.js.map +1 -0
  146. package/dist/pt-PT.main.js +44 -0
  147. package/dist/pt-PT.main.js.map +1 -0
  148. package/dist/pt-PT.mjs +46 -0
  149. package/dist/pt-PT.module.js +46 -0
  150. package/dist/pt-PT.module.js.map +1 -0
  151. package/dist/ro-RO.main.js +44 -0
  152. package/dist/ro-RO.main.js.map +1 -0
  153. package/dist/ro-RO.mjs +46 -0
  154. package/dist/ro-RO.module.js +46 -0
  155. package/dist/ro-RO.module.js.map +1 -0
  156. package/dist/ru-RU.main.js +44 -0
  157. package/dist/ru-RU.main.js.map +1 -0
  158. package/dist/ru-RU.mjs +46 -0
  159. package/dist/ru-RU.module.js +46 -0
  160. package/dist/ru-RU.module.js.map +1 -0
  161. package/dist/sk-SK.main.js +44 -0
  162. package/dist/sk-SK.main.js.map +1 -0
  163. package/dist/sk-SK.mjs +46 -0
  164. package/dist/sk-SK.module.js +46 -0
  165. package/dist/sk-SK.module.js.map +1 -0
  166. package/dist/sl-SI.main.js +44 -0
  167. package/dist/sl-SI.main.js.map +1 -0
  168. package/dist/sl-SI.mjs +46 -0
  169. package/dist/sl-SI.module.js +46 -0
  170. package/dist/sl-SI.module.js.map +1 -0
  171. package/dist/sr-SP.main.js +44 -0
  172. package/dist/sr-SP.main.js.map +1 -0
  173. package/dist/sr-SP.mjs +46 -0
  174. package/dist/sr-SP.module.js +46 -0
  175. package/dist/sr-SP.module.js.map +1 -0
  176. package/dist/sv-SE.main.js +44 -0
  177. package/dist/sv-SE.main.js.map +1 -0
  178. package/dist/sv-SE.mjs +46 -0
  179. package/dist/sv-SE.module.js +46 -0
  180. package/dist/sv-SE.module.js.map +1 -0
  181. package/dist/tr-TR.main.js +44 -0
  182. package/dist/tr-TR.main.js.map +1 -0
  183. package/dist/tr-TR.mjs +46 -0
  184. package/dist/tr-TR.module.js +46 -0
  185. package/dist/tr-TR.module.js.map +1 -0
  186. package/dist/types.d.ts +177 -29
  187. package/dist/types.d.ts.map +1 -1
  188. package/dist/uk-UA.main.js +44 -0
  189. package/dist/uk-UA.main.js.map +1 -0
  190. package/dist/uk-UA.mjs +46 -0
  191. package/dist/uk-UA.module.js +46 -0
  192. package/dist/uk-UA.module.js.map +1 -0
  193. package/dist/useAutoScroll.main.js +89 -0
  194. package/dist/useAutoScroll.main.js.map +1 -0
  195. package/dist/useAutoScroll.mjs +84 -0
  196. package/dist/useAutoScroll.module.js +84 -0
  197. package/dist/useAutoScroll.module.js.map +1 -0
  198. package/dist/useClipboard.main.js +107 -0
  199. package/dist/useClipboard.main.js.map +1 -0
  200. package/dist/useClipboard.mjs +102 -0
  201. package/dist/useClipboard.module.js +102 -0
  202. package/dist/useClipboard.module.js.map +1 -0
  203. package/dist/useDrag.main.js +286 -0
  204. package/dist/useDrag.main.js.map +1 -0
  205. package/dist/useDrag.mjs +281 -0
  206. package/dist/useDrag.module.js +281 -0
  207. package/dist/useDrag.module.js.map +1 -0
  208. package/dist/useDraggableCollection.main.js +27 -0
  209. package/dist/useDraggableCollection.main.js.map +1 -0
  210. package/dist/useDraggableCollection.mjs +22 -0
  211. package/dist/useDraggableCollection.module.js +22 -0
  212. package/dist/useDraggableCollection.module.js.map +1 -0
  213. package/dist/useDraggableItem.main.js +128 -0
  214. package/dist/useDraggableItem.main.js.map +1 -0
  215. package/dist/useDraggableItem.mjs +123 -0
  216. package/dist/useDraggableItem.module.js +123 -0
  217. package/dist/useDraggableItem.module.js.map +1 -0
  218. package/dist/useDrop.main.js +299 -0
  219. package/dist/useDrop.main.js.map +1 -0
  220. package/dist/useDrop.mjs +294 -0
  221. package/dist/useDrop.module.js +294 -0
  222. package/dist/useDrop.module.js.map +1 -0
  223. package/dist/useDropIndicator.main.js +93 -0
  224. package/dist/useDropIndicator.main.js.map +1 -0
  225. package/dist/useDropIndicator.mjs +88 -0
  226. package/dist/useDropIndicator.module.js +88 -0
  227. package/dist/useDropIndicator.module.js.map +1 -0
  228. package/dist/useDroppableCollection.main.js +592 -0
  229. package/dist/useDroppableCollection.main.js.map +1 -0
  230. package/dist/useDroppableCollection.mjs +587 -0
  231. package/dist/useDroppableCollection.module.js +587 -0
  232. package/dist/useDroppableCollection.module.js.map +1 -0
  233. package/dist/useDroppableItem.main.js +79 -0
  234. package/dist/useDroppableItem.main.js.map +1 -0
  235. package/dist/useDroppableItem.mjs +74 -0
  236. package/dist/useDroppableItem.module.js +74 -0
  237. package/dist/useDroppableItem.module.js.map +1 -0
  238. package/dist/useVirtualDrop.main.js +56 -0
  239. package/dist/useVirtualDrop.main.js.map +1 -0
  240. package/dist/useVirtualDrop.mjs +51 -0
  241. package/dist/useVirtualDrop.module.js +51 -0
  242. package/dist/useVirtualDrop.module.js.map +1 -0
  243. package/dist/utils.main.js +283 -0
  244. package/dist/utils.main.js.map +1 -0
  245. package/dist/utils.mjs +255 -0
  246. package/dist/utils.module.js +255 -0
  247. package/dist/utils.module.js.map +1 -0
  248. package/dist/zh-CN.main.js +44 -0
  249. package/dist/zh-CN.main.js.map +1 -0
  250. package/dist/zh-CN.mjs +46 -0
  251. package/dist/zh-CN.module.js +46 -0
  252. package/dist/zh-CN.module.js.map +1 -0
  253. package/dist/zh-TW.main.js +44 -0
  254. package/dist/zh-TW.main.js.map +1 -0
  255. package/dist/zh-TW.mjs +46 -0
  256. package/dist/zh-TW.module.js +46 -0
  257. package/dist/zh-TW.module.js.map +1 -0
  258. package/package.json +20 -16
  259. package/src/DragManager.ts +87 -52
  260. package/src/DragPreview.tsx +3 -3
  261. package/src/ListDropTargetDelegate.ts +173 -0
  262. package/src/constants.ts +2 -0
  263. package/src/index.ts +55 -9
  264. package/src/useAutoScroll.ts +26 -6
  265. package/src/useClipboard.ts +65 -59
  266. package/src/useDrag.ts +179 -31
  267. package/src/useDraggableCollection.ts +29 -0
  268. package/src/useDraggableItem.ts +105 -12
  269. package/src/useDrop.ts +273 -93
  270. package/src/useDropIndicator.ts +47 -21
  271. package/src/useDroppableCollection.ts +315 -115
  272. package/src/useDroppableItem.ts +33 -17
  273. package/src/useVirtualDrop.ts +5 -5
  274. package/src/utils.ts +92 -17
  275. package/LICENSE +0 -201
@@ -11,55 +11,148 @@
11
11
  */
12
12
 
13
13
  import {AriaButtonProps} from '@react-types/button';
14
+ import {clearGlobalDnDState, isInternalDropOperation, setDraggingKeys, useDragModality} from './utils';
14
15
  import {DraggableCollectionState} from '@react-stately/dnd';
15
- import {HTMLAttributes, Key} from 'react';
16
+ import {HTMLAttributes} from 'react';
16
17
  // @ts-ignore
17
18
  import intlMessages from '../intl/*.json';
19
+ import {Key} from '@react-types/shared';
20
+ import {useDescription} from '@react-aria/utils';
18
21
  import {useDrag} from './useDrag';
19
- import {useMessageFormatter} from '@react-aria/i18n';
22
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
20
23
 
21
24
  export interface DraggableItemProps {
22
- key: Key
25
+ /** The key of the draggable item within the collection. */
26
+ key: Key,
27
+ /**
28
+ * Whether the item has an explicit focusable drag affordance to initiate accessible drag and drop mode.
29
+ * If true, the dragProps will omit these event handlers, and they will be applied to dragButtonProps instead.
30
+ */
31
+ hasDragButton?: boolean,
32
+ /**
33
+ * Whether the item has a primary action (e.g. Enter key or long press) that would
34
+ * conflict with initiating accessible drag and drop. If true, the Alt key must be held to
35
+ * start dragging with a keyboard, and long press is disabled until selection mode is entered.
36
+ * This should be passed from the associated collection item hook (e.g. useOption, useGridListItem, etc.).
37
+ */
38
+ hasAction?: boolean
23
39
  }
24
40
 
25
41
  export interface DraggableItemResult {
42
+ /** Props for the draggable item. */
26
43
  dragProps: HTMLAttributes<HTMLElement>,
44
+ /** Props for the explicit drag button affordance, if any. */
27
45
  dragButtonProps: AriaButtonProps
28
46
  }
29
47
 
48
+ const MESSAGES = {
49
+ keyboard: {
50
+ selected: 'dragSelectedKeyboard',
51
+ notSelected: 'dragDescriptionKeyboard'
52
+ },
53
+ touch: {
54
+ selected: 'dragSelectedLongPress',
55
+ notSelected: 'dragDescriptionLongPress'
56
+ },
57
+ virtual: {
58
+ selected: 'dragDescriptionVirtual',
59
+ notSelected: 'dragDescriptionVirtual'
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Handles drag interactions for an item within a draggable collection.
65
+ */
30
66
  export function useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult {
31
- let formatMessage = useMessageFormatter(intlMessages);
67
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/dnd');
68
+ let isDisabled = state.isDisabled || state.selectionManager.isDisabled(props.key);
32
69
  let {dragProps, dragButtonProps} = useDrag({
33
70
  getItems() {
34
71
  return state.getItems(props.key);
35
72
  },
36
73
  preview: state.preview,
74
+ getAllowedDropOperations: state.getAllowedDropOperations,
75
+ hasDragButton: props.hasDragButton,
37
76
  onDragStart(e) {
38
77
  state.startDrag(props.key, e);
78
+ // Track draggingKeys for useDroppableCollection's default onDrop handler and useDroppableCollectionState's default getDropOperation
79
+ setDraggingKeys(state.draggingKeys);
39
80
  },
40
81
  onDragMove(e) {
41
82
  state.moveDrag(e);
42
83
  },
43
84
  onDragEnd(e) {
44
- state.endDrag(e);
85
+ let {dropOperation} = e;
86
+ let isInternal = dropOperation === 'cancel' ? false : isInternalDropOperation();
87
+ state.endDrag({...e, keys: state.draggingKeys, isInternal});
88
+ clearGlobalDnDState();
45
89
  }
46
90
  });
47
91
 
48
92
  let item = state.collection.getItem(props.key);
49
93
  let numKeysForDrag = state.getKeysForDrag(props.key).size;
50
- let isSelected = state.selectionManager.isSelected(props.key);
51
- let message: string;
52
- if (isSelected && numKeysForDrag > 1) {
53
- message = formatMessage('dragSelectedItems', {count: numKeysForDrag});
94
+ let isSelected = numKeysForDrag > 1 && state.selectionManager.isSelected(props.key);
95
+ let dragButtonLabel: string | undefined;
96
+ let description: string | undefined;
97
+
98
+ // Override description to include selected item count.
99
+ let modality = useDragModality();
100
+ if (!props.hasDragButton && state.selectionManager.selectionMode !== 'none') {
101
+ let msg = MESSAGES[modality][isSelected ? 'selected' : 'notSelected'];
102
+ if (props.hasAction && modality === 'keyboard') {
103
+ msg += 'Alt';
104
+ }
105
+
106
+ if (isSelected) {
107
+ description = stringFormatter.format(msg, {count: numKeysForDrag});
108
+ } else {
109
+ description = stringFormatter.format(msg);
110
+ }
111
+
112
+ // Remove the onClick handler from useDrag. Long pressing will be required on touch devices,
113
+ // and NVDA/JAWS are always in forms mode within collection components.
114
+ delete dragProps.onClick;
54
115
  } else {
55
- message = formatMessage('dragItem', {itemText: item?.textValue ?? ''});
116
+ if (isSelected) {
117
+ dragButtonLabel = stringFormatter.format('dragSelectedItems', {count: numKeysForDrag});
118
+ } else {
119
+ let itemText = state.collection.getTextValue?.(props.key) ?? item?.textValue ?? '';
120
+ dragButtonLabel = stringFormatter.format('dragItem', {itemText});
121
+ }
122
+ }
123
+
124
+ let descriptionProps = useDescription(description);
125
+ if (description) {
126
+ Object.assign(dragProps, descriptionProps);
127
+ }
128
+
129
+ if (!props.hasDragButton && props.hasAction) {
130
+ let {onKeyDownCapture, onKeyUpCapture} = dragProps;
131
+ if (modality === 'touch') {
132
+ // Remove long press description if an action is present, because in that case long pressing selects the item.
133
+ delete dragProps['aria-describedby'];
134
+ }
135
+
136
+ // Require Alt key if there is a conflicting action.
137
+ dragProps.onKeyDownCapture = e => {
138
+ if (e.altKey) {
139
+ onKeyDownCapture?.(e);
140
+ }
141
+ };
142
+
143
+ dragProps.onKeyUpCapture = e => {
144
+ if (e.altKey) {
145
+ onKeyUpCapture?.(e);
146
+ }
147
+ };
56
148
  }
57
149
 
58
150
  return {
59
- dragProps,
151
+ dragProps: isDisabled ? {} : dragProps,
60
152
  dragButtonProps: {
61
153
  ...dragButtonProps,
62
- 'aria-label': message
154
+ isDisabled,
155
+ 'aria-label': dragButtonLabel
63
156
  }
64
157
  };
65
158
  }
package/src/useDrop.ts CHANGED
@@ -10,49 +10,109 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {DragEvent, HTMLAttributes, RefObject, useRef, useState} from 'react';
13
+ import {AriaButtonProps} from '@react-types/button';
14
+ import {DragEvent, HTMLAttributes, useRef, useState} from 'react';
14
15
  import * as DragManager from './DragManager';
15
- import {DragTypes, readFromDataTransfer} from './utils';
16
+ import {DragTypes, globalAllowedDropOperations, globalDndState, readFromDataTransfer, setGlobalDnDState, setGlobalDropEffect} from './utils';
16
17
  import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, DROP_OPERATION_ALLOWED, DROP_OPERATION_TO_DROP_EFFECT} from './constants';
17
- import {DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropMoveEvent, DropOperation, DragTypes as IDragTypes} from '@react-types/shared';
18
- import {useLayoutEffect} from '@react-aria/utils';
18
+ import {DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropMoveEvent, DropOperation, DragTypes as IDragTypes, RefObject} from '@react-types/shared';
19
+ import {isIPad, isMac, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
19
20
  import {useVirtualDrop} from './useVirtualDrop';
20
21
 
21
- interface DropOptions {
22
- ref: RefObject<HTMLElement>,
22
+ export interface DropOptions {
23
+ /** A ref for the droppable element. */
24
+ ref: RefObject<HTMLElement | null>,
25
+ /**
26
+ * A function returning the drop operation to be performed when items matching the given types are dropped
27
+ * on the drop target.
28
+ */
23
29
  getDropOperation?: (types: IDragTypes, allowedOperations: DropOperation[]) => DropOperation,
30
+ /** A function that returns the drop operation for a specific point within the target. */
24
31
  getDropOperationForPoint?: (types: IDragTypes, allowedOperations: DropOperation[], x: number, y: number) => DropOperation,
32
+ /** Handler that is called when a valid drag enters the drop target. */
25
33
  onDropEnter?: (e: DropEnterEvent) => void,
34
+ /** Handler that is called when a valid drag is moved within the drop target. */
26
35
  onDropMove?: (e: DropMoveEvent) => void,
27
- // When the user hovers over the drop target for a period of time.
28
- // typically opens that item. macOS/iOS call this "spring loading".
36
+ /**
37
+ * Handler that is called after a valid drag is held over the drop target for a period of time.
38
+ * This typically opens the item so that the user can drop within it.
39
+ * @private
40
+ */
29
41
  onDropActivate?: (e: DropActivateEvent) => void,
42
+ /** Handler that is called when a valid drag exits the drop target. */
30
43
  onDropExit?: (e: DropExitEvent) => void,
31
- onDrop?: (e: DropEvent) => void
44
+ /** Handler that is called when a valid drag is dropped on the drop target. */
45
+ onDrop?: (e: DropEvent) => void,
46
+ /**
47
+ * Whether the item has an explicit focusable drop affordance to initiate accessible drag and drop mode.
48
+ * If true, the dropProps will omit these event handlers, and they will be applied to dropButtonProps instead.
49
+ */
50
+ hasDropButton?: boolean,
51
+ /**
52
+ * Whether the drop target is disabled. If true, the drop target will not accept any drops.
53
+ */
54
+ isDisabled?: boolean
32
55
  }
33
56
 
34
- interface DropResult {
57
+ export interface DropResult {
58
+ /** Props for the droppable element. */
35
59
  dropProps: HTMLAttributes<HTMLElement>,
36
- isDropTarget: boolean // (??) whether the element is currently an active drop target
60
+ /** Whether the drop target is currently focused or hovered. */
61
+ isDropTarget: boolean,
62
+ /** Props for the explicit drop button affordance, if any. */
63
+ dropButtonProps?: AriaButtonProps
37
64
  }
38
65
 
39
66
  const DROP_ACTIVATE_TIMEOUT = 800;
40
67
 
68
+ /**
69
+ * Handles drop interactions for an element, with support for traditional mouse and touch
70
+ * based drag and drop, in addition to full parity for keyboard and screen reader users.
71
+ */
41
72
  export function useDrop(options: DropOptions): DropResult {
73
+ let {hasDropButton, isDisabled} = options;
42
74
  let [isDropTarget, setDropTarget] = useState(false);
43
75
  let state = useRef({
44
76
  x: 0,
45
77
  y: 0,
46
- dragEnterCount: 0,
78
+ dragOverElements: new Set<Element>(),
47
79
  dropEffect: 'none' as DataTransfer['dropEffect'],
80
+ allowedOperations: DROP_OPERATION.all,
48
81
  dropActivateTimer: null
49
82
  }).current;
50
83
 
84
+ let fireDropEnter = (e: DragEvent) => {
85
+ setDropTarget(true);
86
+
87
+ if (typeof options.onDropEnter === 'function') {
88
+ let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
89
+ options.onDropEnter({
90
+ type: 'dropenter',
91
+ x: e.clientX - rect.x,
92
+ y: e.clientY - rect.y
93
+ });
94
+ }
95
+ };
96
+
97
+ let fireDropExit = (e: DragEvent) => {
98
+ setDropTarget(false);
99
+
100
+ if (typeof options.onDropExit === 'function') {
101
+ let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
102
+ options.onDropExit({
103
+ type: 'dropexit',
104
+ x: e.clientX - rect.x,
105
+ y: e.clientY - rect.y
106
+ });
107
+ }
108
+ };
109
+
51
110
  let onDragOver = (e: DragEvent) => {
52
111
  e.preventDefault();
53
112
  e.stopPropagation();
54
113
 
55
- if (e.clientX === state.x && e.clientY === state.y) {
114
+ let allowedOperations = getAllowedOperations(e);
115
+ if (e.clientX === state.x && e.clientY === state.y && allowedOperations === state.allowedOperations) {
56
116
  e.dataTransfer.dropEffect = state.dropEffect;
57
117
  return;
58
118
  }
@@ -60,17 +120,40 @@ export function useDrop(options: DropOptions): DropResult {
60
120
  state.x = e.clientX;
61
121
  state.y = e.clientY;
62
122
 
123
+ let prevDropEffect = state.dropEffect;
124
+
125
+ // Update drop effect if allowed drop operations changed (e.g. user pressed modifier key).
126
+ if (allowedOperations !== state.allowedOperations) {
127
+ let allowedOps = allowedOperationsToArray(allowedOperations);
128
+ let dropOperation = allowedOps[0];
129
+ if (typeof options.getDropOperation === 'function') {
130
+ let types = new DragTypes(e.dataTransfer);
131
+ dropOperation = getDropOperation(allowedOperations, options.getDropOperation(types, allowedOps));
132
+ }
133
+ state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
134
+ }
135
+
63
136
  if (typeof options.getDropOperationForPoint === 'function') {
64
- let allowedOperations = effectAllowedToOperations(e.dataTransfer.effectAllowed);
65
137
  let types = new DragTypes(e.dataTransfer);
66
138
  let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
67
- let dropOperation = options.getDropOperationForPoint(types, allowedOperations, state.x - rect.x, state.y - rect.y);
139
+ let dropOperation = getDropOperation(
140
+ allowedOperations,
141
+ options.getDropOperationForPoint(types, allowedOperationsToArray(allowedOperations), state.x - rect.x, state.y - rect.y)
142
+ );
68
143
  state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
69
144
  }
70
145
 
146
+ state.allowedOperations = allowedOperations;
71
147
  e.dataTransfer.dropEffect = state.dropEffect;
72
148
 
73
- if (typeof options.onDropMove === 'function') {
149
+ // If the drop operation changes, update state and fire events appropriately.
150
+ if (state.dropEffect === 'none' && prevDropEffect !== 'none') {
151
+ fireDropExit(e);
152
+ } else if (state.dropEffect !== 'none' && prevDropEffect === 'none') {
153
+ fireDropEnter(e);
154
+ }
155
+
156
+ if (typeof options.onDropMove === 'function' && state.dropEffect !== 'none') {
74
157
  let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
75
158
  options.onDropMove({
76
159
  type: 'dropmove',
@@ -94,69 +177,78 @@ export function useDrop(options: DropOptions): DropResult {
94
177
  };
95
178
 
96
179
  let onDragEnter = (e: DragEvent) => {
180
+ e.preventDefault();
97
181
  e.stopPropagation();
98
- state.dragEnterCount++;
99
- if (state.dragEnterCount > 1) {
182
+ state.dragOverElements.add(e.target as Element);
183
+ if (state.dragOverElements.size > 1) {
100
184
  return;
101
185
  }
102
186
 
103
- let allowedOperations = effectAllowedToOperations(e.dataTransfer.effectAllowed);
187
+ let allowedOperationsBits = getAllowedOperations(e);
188
+ let allowedOperations = allowedOperationsToArray(allowedOperationsBits);
104
189
  let dropOperation = allowedOperations[0];
105
190
 
106
191
  if (typeof options.getDropOperation === 'function') {
107
192
  let types = new DragTypes(e.dataTransfer);
108
- dropOperation = options.getDropOperation(types, allowedOperations);
109
- }
110
-
111
- if (dropOperation !== 'cancel') {
112
- setDropTarget(true);
193
+ dropOperation = getDropOperation(allowedOperationsBits, options.getDropOperation(types, allowedOperations));
113
194
  }
114
195
 
115
196
  if (typeof options.getDropOperationForPoint === 'function') {
116
197
  let types = new DragTypes(e.dataTransfer);
117
198
  let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
118
- dropOperation = options.getDropOperationForPoint(types, allowedOperations, e.clientX - rect.x, e.clientY - rect.y);
199
+ dropOperation = getDropOperation(
200
+ allowedOperationsBits,
201
+ options.getDropOperationForPoint(types, allowedOperations, e.clientX - rect.x, e.clientY - rect.y)
202
+ );
119
203
  }
120
204
 
205
+ state.x = e.clientX;
206
+ state.y = e.clientY;
207
+ state.allowedOperations = allowedOperationsBits;
121
208
  state.dropEffect = DROP_OPERATION_TO_DROP_EFFECT[dropOperation] || 'none';
122
209
  e.dataTransfer.dropEffect = state.dropEffect;
123
210
 
124
- if (typeof options.onDropEnter === 'function' && dropOperation !== 'cancel') {
125
- let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
126
- options.onDropEnter({
127
- type: 'dropenter',
128
- x: e.clientX - rect.x,
129
- y: e.clientY - rect.y
130
- });
211
+ if (dropOperation !== 'cancel') {
212
+ fireDropEnter(e);
131
213
  }
132
-
133
- state.x = e.clientX;
134
- state.y = e.clientY;
135
214
  };
136
215
 
137
216
  let onDragLeave = (e: DragEvent) => {
217
+ e.preventDefault();
138
218
  e.stopPropagation();
139
- state.dragEnterCount--;
140
- if (state.dragEnterCount > 0) {
219
+
220
+ // We would use e.relatedTarget to detect if the drag is still inside the drop target,
221
+ // but it is always null in WebKit. https://bugs.webkit.org/show_bug.cgi?id=66547
222
+ // Instead, we track all of the targets of dragenter events in a set, and remove them
223
+ // in dragleave. When the set becomes empty, we've left the drop target completely.
224
+ // We must also remove any elements that are no longer in the DOM, because dragleave
225
+ // events will never be fired for these. This can happen, for example, with drop
226
+ // indicators between items, which disappear when the drop target changes.
227
+
228
+ state.dragOverElements.delete(e.target as Element);
229
+ for (let element of state.dragOverElements) {
230
+ if (!e.currentTarget.contains(element)) {
231
+ state.dragOverElements.delete(element);
232
+ }
233
+ }
234
+
235
+ if (state.dragOverElements.size > 0) {
141
236
  return;
142
237
  }
143
238
 
144
- if (typeof options.onDropExit === 'function' && state.dropEffect !== 'none') {
145
- let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
146
- options.onDropExit({
147
- type: 'dropexit',
148
- x: e.clientX - rect.x,
149
- y: e.clientY - rect.y
150
- });
239
+ if (state.dropEffect !== 'none') {
240
+ fireDropExit(e);
151
241
  }
152
242
 
153
- setDropTarget(false);
154
243
  clearTimeout(state.dropActivateTimer);
155
244
  };
156
245
 
157
246
  let onDrop = (e: DragEvent) => {
158
247
  e.preventDefault();
159
248
  e.stopPropagation();
249
+ // Track drop effect in global state for Chrome Android. https://bugs.chromium.org/p/chromium/issues/detail?id=1353951
250
+ // Android onDragEnd always returns "none" as its drop effect.
251
+ setGlobalDropEffect(state.dropEffect);
160
252
 
161
253
  if (typeof options.onDrop === 'function') {
162
254
  let dropOperation = DROP_EFFECT_TO_DROP_OPERATION[state.dropEffect];
@@ -171,75 +263,158 @@ export function useDrop(options: DropOptions): DropResult {
171
263
  dropOperation
172
264
  };
173
265
 
174
- // Wait a frame to dispatch the drop event so that we ensure the dragend event fires first.
175
- // Otherwise, if onDrop removes the original dragged element from the DOM, dragend will never be fired.
176
- // This behavior is consistent across browsers, but see this issue for details:
177
- // https://bugzilla.mozilla.org/show_bug.cgi?id=460801
178
- setTimeout(() => {
179
- options.onDrop(event);
180
- }, 0);
266
+ options.onDrop(event);
267
+ }
268
+
269
+ let dndStateSnapshot = {...globalDndState};
270
+ state.dragOverElements.clear();
271
+ fireDropExit(e);
272
+ clearTimeout(state.dropActivateTimer);
273
+ // If there wasn't a collection being tracked as a dragged collection, then we are in a case where a non RSP drag is dropped on a
274
+ // RSP collection and thus we don't need to preserve the global drop effect
275
+ if (dndStateSnapshot.draggingCollectionRef == null) {
276
+ setGlobalDropEffect(undefined);
277
+ } else {
278
+ // Otherwise we need to preserve the global dnd state for onDragEnd's isInternal check.
279
+ // At the moment fireDropExit may clear dropCollectionRef (i.e. useDroppableCollection's provided onDropExit, required to clear dropCollectionRef when exiting a valid drop target)
280
+ setGlobalDnDState(dndStateSnapshot);
281
+ }
282
+ };
283
+
284
+ let onDropEnter = useEffectEvent((e: DropEnterEvent) => {
285
+ if (typeof options.onDropEnter === 'function') {
286
+ options.onDropEnter(e);
181
287
  }
288
+ });
182
289
 
290
+ let onDropExit = useEffectEvent((e: DropExitEvent) => {
183
291
  if (typeof options.onDropExit === 'function') {
184
- let rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
185
- options.onDropExit({
186
- type: 'dropexit',
187
- x: e.clientX - rect.x,
188
- y: e.clientY - rect.y
189
- });
292
+ options.onDropExit(e);
190
293
  }
294
+ });
191
295
 
192
- state.dragEnterCount = 0;
193
- setDropTarget(false);
194
- clearTimeout(state.dropActivateTimer);
195
- };
296
+ let onDropActivate = useEffectEvent((e: DropActivateEvent) => {
297
+ if (typeof options.onDropActivate === 'function') {
298
+ options.onDropActivate(e);
299
+ }
300
+ });
196
301
 
197
- let optionsRef = useRef(options);
198
- optionsRef.current = options;
302
+ let onKeyboardDrop = useEffectEvent((e: DropEvent) => {
303
+ if (typeof options.onDrop === 'function') {
304
+ options.onDrop(e);
305
+ }
306
+ });
199
307
 
200
- useLayoutEffect(() => DragManager.registerDropTarget({
201
- element: optionsRef.current.ref.current,
202
- getDropOperation: optionsRef.current.getDropOperation,
203
- onDropEnter(e) {
204
- setDropTarget(true);
205
- if (typeof optionsRef.current.onDropEnter === 'function') {
206
- optionsRef.current.onDropEnter(e);
207
- }
208
- },
209
- onDropExit(e) {
210
- setDropTarget(false);
211
- if (typeof optionsRef.current.onDropExit === 'function') {
212
- optionsRef.current.onDropExit(e);
213
- }
214
- },
215
- onDrop(e) {
216
- if (typeof optionsRef.current.onDrop === 'function') {
217
- optionsRef.current.onDrop(e);
218
- }
219
- },
220
- onDropActivate(e) {
221
- if (typeof optionsRef.current.onDropActivate === 'function') {
222
- optionsRef.current.onDropActivate(e);
223
- }
308
+ let getDropOperationKeyboard = useEffectEvent((types: IDragTypes, allowedOperations: DropOperation[]) => {
309
+ if (options.getDropOperation) {
310
+ return options.getDropOperation(types, allowedOperations);
224
311
  }
225
- }), [optionsRef]);
226
312
 
227
- let {dropProps} = useVirtualDrop();
313
+ return allowedOperations[0];
314
+ });
315
+
316
+ let {ref} = options;
317
+ useLayoutEffect(() => {
318
+ if (isDisabled) {
319
+ return;
320
+ }
321
+ return DragManager.registerDropTarget({
322
+ element: ref.current,
323
+ getDropOperation: getDropOperationKeyboard,
324
+ onDropEnter(e) {
325
+ setDropTarget(true);
326
+ onDropEnter(e);
327
+ },
328
+ onDropExit(e) {
329
+ setDropTarget(false);
330
+ onDropExit(e);
331
+ },
332
+ onDrop: onKeyboardDrop,
333
+ onDropActivate
334
+ });
335
+ }, [isDisabled, ref, getDropOperationKeyboard, onDropEnter, onDropExit, onKeyboardDrop, onDropActivate]);
228
336
 
337
+ let {dropProps} = useVirtualDrop();
338
+ if (isDisabled) {
339
+ return {
340
+ dropProps: {},
341
+ dropButtonProps: {isDisabled: true},
342
+ isDropTarget: false
343
+ };
344
+ }
229
345
  return {
230
346
  dropProps: {
231
- ...dropProps,
347
+ ...(!hasDropButton && dropProps),
232
348
  onDragEnter,
233
349
  onDragOver,
234
350
  onDragLeave,
235
351
  onDrop
236
352
  },
353
+ dropButtonProps: {...(hasDropButton && dropProps)},
237
354
  isDropTarget
238
355
  };
239
356
  }
240
357
 
241
- function effectAllowedToOperations(effectAllowed: string) {
242
- let allowedOperationsBits = DROP_OPERATION_ALLOWED[effectAllowed];
358
+ function getAllowedOperations(e: DragEvent) {
359
+ let allowedOperations = DROP_OPERATION_ALLOWED[e.dataTransfer.effectAllowed];
360
+
361
+ // WebKit always sets effectAllowed to "copyMove" on macOS, and "all" on iOS, regardless of what was
362
+ // set during the dragstart event: https://bugs.webkit.org/show_bug.cgi?id=178058
363
+ //
364
+ // Android Chrome also sets effectAllowed to "copyMove" in all cases: https://bugs.chromium.org/p/chromium/issues/detail?id=1359182
365
+ //
366
+ // If the drag started within the page, we can use a global variable to get the real allowed operations.
367
+ // This needs to be intersected with the actual effectAllowed, which may have been filtered based on modifier keys.
368
+ // Unfortunately, this means that link operations do not work at all in Safari.
369
+ if (globalAllowedDropOperations) {
370
+ allowedOperations &= globalAllowedDropOperations;
371
+ }
372
+
373
+ // Chrome and Safari on macOS will automatically filter effectAllowed when pressing modifier keys,
374
+ // allowing the user to switch between move, link, and copy operations. Firefox on macOS and all
375
+ // Windows browsers do not do this, so do it ourselves instead. The exact keys are platform dependent.
376
+ // https://ux.stackexchange.com/questions/83748/what-are-the-most-common-modifier-keys-for-dragging-objects-with-a-mouse
377
+ //
378
+ // Note that none of these modifiers are ever set in WebKit due to a bug: https://bugs.webkit.org/show_bug.cgi?id=77465
379
+ // However, Safari does update effectAllowed correctly, so we can just rely on that.
380
+ let allowedModifiers = DROP_OPERATION.none;
381
+ if (isMac()) {
382
+ if (e.altKey) {
383
+ allowedModifiers |= DROP_OPERATION.copy;
384
+ }
385
+
386
+ // Chrome and Safari both use the Control key for link, even though Finder uses Command + Option.
387
+ // iPadOS doesn't support link operations and will not fire the drop event at all if dropEffect is set to link.
388
+ // https://bugs.webkit.org/show_bug.cgi?id=244701
389
+ if (e.ctrlKey && !isIPad()) {
390
+ allowedModifiers |= DROP_OPERATION.link;
391
+ }
392
+
393
+ if (e.metaKey) {
394
+ allowedModifiers |= DROP_OPERATION.move;
395
+ }
396
+ } else {
397
+ if (e.altKey) {
398
+ allowedModifiers |= DROP_OPERATION.link;
399
+ }
400
+
401
+ if (e.shiftKey) {
402
+ allowedModifiers |= DROP_OPERATION.move;
403
+ }
404
+
405
+ if (e.ctrlKey) {
406
+ allowedModifiers |= DROP_OPERATION.copy;
407
+ }
408
+ }
409
+
410
+ if (allowedModifiers) {
411
+ return allowedOperations & allowedModifiers;
412
+ }
413
+
414
+ return allowedOperations;
415
+ }
416
+
417
+ function allowedOperationsToArray(allowedOperationsBits: DROP_OPERATION) {
243
418
  let allowedOperations = [];
244
419
  if (allowedOperationsBits & DROP_OPERATION.move) {
245
420
  allowedOperations.push('move');
@@ -255,3 +430,8 @@ function effectAllowedToOperations(effectAllowed: string) {
255
430
 
256
431
  return allowedOperations;
257
432
  }
433
+
434
+ function getDropOperation(allowedOperations: DROP_OPERATION, operation: DropOperation) {
435
+ let op = DROP_OPERATION[operation];
436
+ return allowedOperations & op ? operation : 'cancel';
437
+ }