@react-aria/dnd 3.9.2 → 3.10.0

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 (40) hide show
  1. package/dist/DragManager.main.js +79 -12
  2. package/dist/DragManager.main.js.map +1 -1
  3. package/dist/DragManager.mjs +79 -12
  4. package/dist/DragManager.module.js +79 -12
  5. package/dist/DragManager.module.js.map +1 -1
  6. package/dist/DropTargetKeyboardNavigation.main.js +201 -0
  7. package/dist/DropTargetKeyboardNavigation.main.js.map +1 -0
  8. package/dist/DropTargetKeyboardNavigation.mjs +196 -0
  9. package/dist/DropTargetKeyboardNavigation.module.js +196 -0
  10. package/dist/DropTargetKeyboardNavigation.module.js.map +1 -0
  11. package/dist/ListDropTargetDelegate.main.js.map +1 -1
  12. package/dist/ListDropTargetDelegate.module.js.map +1 -1
  13. package/dist/types.d.ts +7 -1
  14. package/dist/types.d.ts.map +1 -1
  15. package/dist/useDrop.main.js.map +1 -1
  16. package/dist/useDrop.module.js.map +1 -1
  17. package/dist/useDropIndicator.main.js +15 -7
  18. package/dist/useDropIndicator.main.js.map +1 -1
  19. package/dist/useDropIndicator.mjs +15 -7
  20. package/dist/useDropIndicator.module.js +15 -7
  21. package/dist/useDropIndicator.module.js.map +1 -1
  22. package/dist/useDroppableCollection.main.js +33 -103
  23. package/dist/useDroppableCollection.main.js.map +1 -1
  24. package/dist/useDroppableCollection.mjs +33 -103
  25. package/dist/useDroppableCollection.module.js +33 -103
  26. package/dist/useDroppableCollection.module.js.map +1 -1
  27. package/dist/useDroppableItem.main.js +4 -2
  28. package/dist/useDroppableItem.main.js.map +1 -1
  29. package/dist/useDroppableItem.mjs +4 -2
  30. package/dist/useDroppableItem.module.js +4 -2
  31. package/dist/useDroppableItem.module.js.map +1 -1
  32. package/package.json +17 -12
  33. package/src/.DS_Store +0 -0
  34. package/src/DragManager.ts +66 -17
  35. package/src/DropTargetKeyboardNavigation.ts +273 -0
  36. package/src/ListDropTargetDelegate.ts +1 -1
  37. package/src/useDrop.ts +0 -1
  38. package/src/useDropIndicator.ts +17 -11
  39. package/src/useDroppableCollection.ts +41 -134
  40. package/src/useDroppableItem.ts +7 -4
@@ -0,0 +1,273 @@
1
+ import {Collection, DropTarget, Key, KeyboardDelegate, Node} from '@react-types/shared';
2
+ import {getChildNodes} from '@react-stately/collections';
3
+
4
+ export function navigate(
5
+ keyboardDelegate: KeyboardDelegate,
6
+ collection: Collection<Node<unknown>>,
7
+ target: DropTarget | null | undefined,
8
+ direction: 'left' | 'right' | 'up' | 'down',
9
+ rtl = false,
10
+ wrap = false
11
+ ): DropTarget | null {
12
+ switch (direction) {
13
+ case 'left':
14
+ return rtl
15
+ ? nextDropTarget(keyboardDelegate, collection, target, wrap, 'left')
16
+ : previousDropTarget(keyboardDelegate, collection, target, wrap, 'left');
17
+ case 'right':
18
+ return rtl
19
+ ? previousDropTarget(keyboardDelegate, collection, target, wrap, 'right')
20
+ : nextDropTarget(keyboardDelegate, collection, target, wrap, 'right');
21
+ case 'up':
22
+ return previousDropTarget(keyboardDelegate, collection, target, wrap);
23
+ case 'down':
24
+ return nextDropTarget(keyboardDelegate, collection, target, wrap);
25
+ }
26
+ }
27
+
28
+ function nextDropTarget(
29
+ keyboardDelegate: KeyboardDelegate,
30
+ collection: Collection<Node<unknown>>,
31
+ target: DropTarget | null | undefined,
32
+ wrap = false,
33
+ horizontal: 'left' | 'right' | null = null
34
+ ): DropTarget | null {
35
+ if (!target) {
36
+ return {
37
+ type: 'root'
38
+ };
39
+ }
40
+
41
+ if (target.type === 'root') {
42
+ let nextKey = keyboardDelegate.getFirstKey?.() ?? null;
43
+ if (nextKey != null) {
44
+ return {
45
+ type: 'item',
46
+ key: nextKey,
47
+ dropPosition: 'before'
48
+ };
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ if (target.type === 'item') {
55
+ let nextKey: Key | null | undefined = null;
56
+ if (horizontal) {
57
+ nextKey = horizontal === 'right' ? keyboardDelegate.getKeyRightOf?.(target.key) : keyboardDelegate.getKeyLeftOf?.(target.key);
58
+ } else {
59
+ nextKey = keyboardDelegate.getKeyBelow?.(target.key);
60
+ }
61
+ let nextCollectionKey = collection.getKeyAfter(target.key);
62
+
63
+ // If the keyboard delegate did not move to the next key in the collection,
64
+ // jump to that key with the same drop position. Otherwise, try the other
65
+ // drop positions on the current key first.
66
+ if (nextKey != null && nextKey !== nextCollectionKey) {
67
+ return {
68
+ type: 'item',
69
+ key: nextKey,
70
+ dropPosition: target.dropPosition
71
+ };
72
+ }
73
+
74
+ switch (target.dropPosition) {
75
+ case 'before': {
76
+ return {
77
+ type: 'item',
78
+ key: target.key,
79
+ dropPosition: 'on'
80
+ };
81
+ }
82
+ case 'on': {
83
+ // If there are nested items, traverse to them prior to the "after" position of this target.
84
+ // If the next key is on the same level, then its "before" position is equivalent to this item's "after" position.
85
+ let targetNode = collection.getItem(target.key);
86
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
87
+ if (targetNode && nextNode && nextNode.level >= targetNode.level) {
88
+ return {
89
+ type: 'item',
90
+ key: nextNode.key,
91
+ dropPosition: 'before'
92
+ };
93
+ }
94
+
95
+ return {
96
+ type: 'item',
97
+ key: target.key,
98
+ dropPosition: 'after'
99
+ };
100
+ }
101
+ case 'after': {
102
+ // If this is the last sibling in a level, traverse to the parent.
103
+ let targetNode = collection.getItem(target.key);
104
+ if (targetNode && targetNode.nextKey == null && targetNode.parentKey != null) {
105
+ // If the parent item has an item after it, use the "before" position.
106
+ let parentNode = collection.getItem(targetNode.parentKey);
107
+ if (parentNode?.nextKey != null) {
108
+ return {
109
+ type: 'item',
110
+ key: parentNode.nextKey,
111
+ dropPosition: 'before'
112
+ };
113
+ }
114
+
115
+ if (parentNode) {
116
+ return {
117
+ type: 'item',
118
+ key: parentNode.key,
119
+ dropPosition: 'after'
120
+ };
121
+ }
122
+ }
123
+
124
+ if (targetNode?.nextKey != null) {
125
+ return {
126
+ type: 'item',
127
+ key: targetNode.nextKey,
128
+ dropPosition: 'on'
129
+ };
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (wrap) {
136
+ return {
137
+ type: 'root'
138
+ };
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ function previousDropTarget(
145
+ keyboardDelegate: KeyboardDelegate,
146
+ collection: Collection<Node<unknown>>,
147
+ target: DropTarget | null | undefined,
148
+ wrap = false,
149
+ horizontal: 'left' | 'right' | null = null
150
+ ): DropTarget | null {
151
+ // Start after the last root-level item.
152
+ if (!target || (wrap && target.type === 'root')) {
153
+ // Keyboard delegate gets the deepest item but we want the shallowest.
154
+ let prevKey: Key | null = null;
155
+ let lastKey = keyboardDelegate.getLastKey?.();
156
+ while (lastKey != null) {
157
+ prevKey = lastKey;
158
+ let node = collection.getItem(lastKey);
159
+ lastKey = node?.parentKey;
160
+ }
161
+
162
+ if (prevKey != null) {
163
+ return {
164
+ type: 'item',
165
+ key: prevKey,
166
+ dropPosition: 'after'
167
+ };
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ if (target.type === 'item') {
174
+ let prevKey: Key | null | undefined = null;
175
+ if (horizontal) {
176
+ prevKey = horizontal === 'left' ? keyboardDelegate.getKeyLeftOf?.(target.key) : keyboardDelegate.getKeyRightOf?.(target.key);
177
+ } else {
178
+ prevKey = keyboardDelegate.getKeyAbove?.(target.key);
179
+ }
180
+ let prevCollectionKey = collection.getKeyBefore(target.key);
181
+
182
+ // If the keyboard delegate did not move to the next key in the collection,
183
+ // jump to that key with the same drop position. Otherwise, try the other
184
+ // drop positions on the current key first.
185
+ if (prevKey != null && prevKey !== prevCollectionKey) {
186
+ return {
187
+ type: 'item',
188
+ key: prevKey,
189
+ dropPosition: target.dropPosition
190
+ };
191
+ }
192
+
193
+ switch (target.dropPosition) {
194
+ case 'before': {
195
+ // Move after the last child of the previous item.
196
+ let targetNode = collection.getItem(target.key);
197
+ if (targetNode && targetNode.prevKey != null) {
198
+ let lastChild = getLastChild(collection, targetNode.prevKey);
199
+ if (lastChild) {
200
+ return lastChild;
201
+ }
202
+ }
203
+
204
+ if (prevKey != null) {
205
+ return {
206
+ type: 'item',
207
+ key: prevKey,
208
+ dropPosition: 'on'
209
+ };
210
+ }
211
+
212
+ return {
213
+ type: 'root'
214
+ };
215
+ }
216
+ case 'on': {
217
+ return {
218
+ type: 'item',
219
+ key: target.key,
220
+ dropPosition: 'before'
221
+ };
222
+ }
223
+ case 'after': {
224
+ // Move after the last child of this item.
225
+ let lastChild = getLastChild(collection, target.key);
226
+ if (lastChild) {
227
+ return lastChild;
228
+ }
229
+
230
+ return {
231
+ type: 'item',
232
+ key: target.key,
233
+ dropPosition: 'on'
234
+ };
235
+ }
236
+ }
237
+ }
238
+
239
+ if (target.type !== 'root') {
240
+ return {
241
+ type: 'root'
242
+ };
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ function getLastChild(collection: Collection<Node<unknown>>, key: Key): DropTarget | null {
249
+ // getChildNodes still returns child tree items even when the item is collapsed.
250
+ // Checking if the next item has a greater level is a silly way to determine if the item is expanded.
251
+ let targetNode = collection.getItem(key);
252
+ let nextKey = collection.getKeyAfter(key);
253
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
254
+ if (targetNode && nextNode && nextNode.level > targetNode.level) {
255
+ let children = getChildNodes(targetNode, collection);
256
+ let lastChild: Node<unknown> | null = null;
257
+ for (let child of children) {
258
+ if (child.type === 'item') {
259
+ lastChild = child;
260
+ }
261
+ }
262
+
263
+ if (lastChild) {
264
+ return {
265
+ type: 'item',
266
+ key: lastChild.key,
267
+ dropPosition: 'after'
268
+ };
269
+ }
270
+ }
271
+
272
+ return null;
273
+ }
@@ -33,7 +33,7 @@ export class ListDropTargetDelegate implements DropTargetDelegate {
33
33
  private ref: RefObject<HTMLElement | null>;
34
34
  private layout: 'stack' | 'grid';
35
35
  private orientation: Orientation;
36
- private direction: Direction;
36
+ protected direction: Direction;
37
37
 
38
38
  constructor(collection: Iterable<Node<unknown>>, ref: RefObject<HTMLElement | null>, options?: ListDropTargetDelegateOptions) {
39
39
  this.collection = collection;
package/src/useDrop.ts CHANGED
@@ -36,7 +36,6 @@ export interface DropOptions {
36
36
  /**
37
37
  * Handler that is called after a valid drag is held over the drop target for a period of time.
38
38
  * This typically opens the item so that the user can drop within it.
39
- * @private
40
39
  */
41
40
  onDropActivate?: (e: DropActivateEvent) => void,
42
41
  /** Handler that is called when a valid drag exits the drop target. */
@@ -12,7 +12,7 @@
12
12
 
13
13
  import * as DragManager from './DragManager';
14
14
  import {DroppableCollectionState} from '@react-stately/dnd';
15
- import {DropTarget, Key, RefObject} from '@react-types/shared';
15
+ import {DropTarget, FocusableElement, Key, RefObject} from '@react-types/shared';
16
16
  import {getDroppableCollectionId} from './utils';
17
17
  import {HTMLAttributes} from 'react';
18
18
  // @ts-ignore
@@ -23,7 +23,9 @@ import {useLocalizedStringFormatter} from '@react-aria/i18n';
23
23
 
24
24
  export interface DropIndicatorProps {
25
25
  /** The drop target that the drop indicator represents. */
26
- target: DropTarget
26
+ target: DropTarget,
27
+ /** The ref to the activate button. */
28
+ activateButtonRef?: RefObject<FocusableElement | null>
27
29
  }
28
30
 
29
31
  export interface DropIndicatorAria {
@@ -69,28 +71,32 @@ export function useDropIndicator(props: DropIndicatorProps, state: DroppableColl
69
71
  } else {
70
72
  let before: Key | null | undefined;
71
73
  let after: Key | null | undefined;
72
- if (collection.getFirstKey() === target.key && target.dropPosition === 'before') {
73
- before = null;
74
+ if (target.dropPosition === 'before') {
75
+ let prevKey = collection.getItem(target.key)?.prevKey;
76
+ let prevNode = prevKey != null ? collection.getItem(prevKey) : null;
77
+ before = prevNode?.type === 'item' ? prevNode.key : null;
74
78
  } else {
75
- before = target.dropPosition === 'before' ? collection.getKeyBefore(target.key) : target.key;
79
+ before = target.key;
76
80
  }
77
81
 
78
- if (collection.getLastKey() === target.key && target.dropPosition === 'after') {
79
- after = null;
82
+ if (target.dropPosition === 'after') {
83
+ let nextKey = collection.getItem(target.key)?.nextKey;
84
+ let nextNode = nextKey != null ? collection.getItem(nextKey) : null;
85
+ after = nextNode?.type === 'item' ? nextNode.key : null;
80
86
  } else {
81
- after = target.dropPosition === 'after' ? collection.getKeyAfter(target.key) : target.key;
87
+ after = target.key;
82
88
  }
83
89
 
84
- if (before && after) {
90
+ if (before != null && after != null) {
85
91
  label = stringFormatter.format('insertBetween', {
86
92
  beforeItemText: getText(before),
87
93
  afterItemText: getText(after)
88
94
  });
89
- } else if (before) {
95
+ } else if (before != null) {
90
96
  label = stringFormatter.format('insertAfter', {
91
97
  itemText: getText(before)
92
98
  });
93
- } else if (after) {
99
+ } else if (after != null) {
94
100
  label = stringFormatter.format('insertBefore', {
95
101
  itemText: getText(after)
96
102
  });
@@ -37,6 +37,7 @@ import * as DragManager from './DragManager';
37
37
  import {DroppableCollectionState} from '@react-stately/dnd';
38
38
  import {HTMLAttributes, useCallback, useEffect, useRef} from 'react';
39
39
  import {mergeProps, useId, useLayoutEffect} from '@react-aria/utils';
40
+ import {navigate} from './DropTargetKeyboardNavigation';
40
41
  import {setInteractionModality} from '@react-aria/interactions';
41
42
  import {useAutoScroll} from './useAutoScroll';
42
43
  import {useDrop} from './useDrop';
@@ -46,7 +47,9 @@ export interface DroppableCollectionOptions extends DroppableCollectionProps {
46
47
  /** A delegate object that implements behavior for keyboard focus movement. */
47
48
  keyboardDelegate: KeyboardDelegate,
48
49
  /** A delegate object that provides drop targets for pointer coordinates within the collection. */
49
- dropTargetDelegate: DropTargetDelegate
50
+ dropTargetDelegate: DropTargetDelegate,
51
+ /** A custom keyboard event handler for drop targets. */
52
+ onKeyDown?: (e: KeyboardEvent) => void
50
53
  }
51
54
 
52
55
  export interface DroppableCollectionResult {
@@ -64,9 +67,6 @@ interface DroppingState {
64
67
  timeout: ReturnType<typeof setTimeout> | undefined
65
68
  }
66
69
 
67
- const DROP_POSITIONS: DropPosition[] = ['before', 'on', 'after'];
68
- const DROP_POSITIONS_RTL: DropPosition[] = ['after', 'on', 'before'];
69
-
70
70
  /**
71
71
  * Handles drop interactions for a collection component, with support for traditional mouse and touch
72
72
  * based drag and drop, in addition to full parity for keyboard and screen reader users.
@@ -92,6 +92,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
92
92
  onRootDrop,
93
93
  onItemDrop,
94
94
  onReorder,
95
+ onMove,
95
96
  acceptedDragTypes = 'all',
96
97
  shouldAcceptItemDrop
97
98
  } = localState.props;
@@ -137,6 +138,10 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
137
138
  await onItemDrop({items: filteredItems, dropOperation, isInternal, target});
138
139
  }
139
140
 
141
+ if (onMove && isInternal) {
142
+ await onMove({keys: draggingKeys, dropOperation, target});
143
+ }
144
+
140
145
  if (target.dropPosition !== 'on') {
141
146
  if (!isInternal && onInsert) {
142
147
  await onInsert({items: filteredItems, dropOperation, target});
@@ -201,7 +206,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
201
206
  autoScroll.stop();
202
207
  },
203
208
  onDropActivate(e) {
204
- if (state.target?.type === 'item' && state.target?.dropPosition === 'on' && typeof props.onDropActivate === 'function') {
209
+ if (state.target?.type === 'item' && typeof props.onDropActivate === 'function') {
205
210
  props.onDropActivate({
206
211
  type: 'dropactivate',
207
212
  x: e.x, // todo
@@ -258,18 +263,25 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
258
263
  // inserted item. If selection is disabled, then also show the focus ring so there
259
264
  // is some indication that items were added.
260
265
  if (state.selectionManager.focusedKey === prevFocusedKey) {
261
- let first = newKeys.keys().next().value;
262
- let item = state.collection.getItem(first);
263
-
264
- // If this is a cell, focus the parent row.
265
- if (item?.type === 'cell') {
266
- first = item.parentKey;
267
- }
266
+ let first: Key | null | undefined = newKeys.keys().next().value;
267
+ if (first != null) {
268
+ let item = state.collection.getItem(first);
269
+
270
+ // If this is a cell, focus the parent row.
271
+ // eslint-disable-next-line max-depth
272
+ if (item?.type === 'cell') {
273
+ first = item.parentKey;
274
+ }
268
275
 
269
- state.selectionManager.setFocusedKey(first);
276
+ // eslint-disable-next-line max-depth
277
+ if (first != null) {
278
+ state.selectionManager.setFocusedKey(first);
279
+ }
270
280
 
271
- if (state.selectionManager.selectionMode === 'none') {
272
- setInteractionModality('keyboard');
281
+ // eslint-disable-next-line max-depth
282
+ if (state.selectionManager.selectionMode === 'none') {
283
+ setInteractionModality('keyboard');
284
+ }
273
285
  }
274
286
  }
275
287
  } else if (
@@ -335,7 +347,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
335
347
  }, 50);
336
348
  }, [localState, defaultOnDrop, ref, updateFocusAfterDrop]);
337
349
 
338
-
350
+
339
351
  useEffect(() => {
340
352
  return () => {
341
353
  if (droppingState.current) {
@@ -357,118 +369,12 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
357
369
  return;
358
370
  }
359
371
 
360
- let getNextTarget = (target: DropTarget | null | undefined, wrap = true, horizontal = false): DropTarget | null => {
361
- if (!target) {
362
- return {
363
- type: 'root'
364
- };
365
- }
366
-
367
- let {keyboardDelegate} = localState.props;
368
- let nextKey: Key | null | undefined;
369
- if (target?.type === 'item') {
370
- nextKey = horizontal ? keyboardDelegate.getKeyRightOf?.(target.key) : keyboardDelegate.getKeyBelow?.(target.key);
371
- } else {
372
- nextKey = horizontal && direction === 'rtl' ? keyboardDelegate.getLastKey?.() : keyboardDelegate.getFirstKey?.();
373
- }
374
- let dropPositions = horizontal && direction === 'rtl' ? DROP_POSITIONS_RTL : DROP_POSITIONS;
375
- let dropPosition: DropPosition = dropPositions[0];
376
-
377
- if (target.type === 'item') {
378
- // If the the keyboard delegate returned the next key in the collection,
379
- // first try the other positions in the current key. Otherwise (e.g. in a grid layout),
380
- // jump to the same drop position in the new key.
381
- let nextCollectionKey = horizontal && direction === 'rtl' ? localState.state.collection.getKeyBefore(target.key) : localState.state.collection.getKeyAfter(target.key);
382
- if (nextKey == null || nextKey === nextCollectionKey) {
383
- let positionIndex = dropPositions.indexOf(target.dropPosition);
384
- let nextDropPosition = dropPositions[positionIndex + 1];
385
- if (positionIndex < dropPositions.length - 1 && !(nextDropPosition === dropPositions[2] && nextKey != null)) {
386
- return {
387
- type: 'item',
388
- key: target.key,
389
- dropPosition: nextDropPosition
390
- };
391
- }
392
-
393
- // If the last drop position was 'after', then 'before' on the next key is equivalent.
394
- // Switch to 'on' instead.
395
- if (target.dropPosition === dropPositions[2]) {
396
- dropPosition = 'on';
397
- }
398
- } else {
399
- dropPosition = target.dropPosition;
400
- }
401
- }
402
-
403
- if (nextKey == null) {
404
- if (wrap) {
405
- return {
406
- type: 'root'
407
- };
408
- }
409
-
410
- return null;
411
- }
412
-
413
- return {
414
- type: 'item',
415
- key: nextKey,
416
- dropPosition
417
- };
372
+ let getNextTarget = (target: DropTarget | null | undefined, wrap = true, key: 'left' | 'right' | 'up' | 'down' = 'down') => {
373
+ return navigate(localState.props.keyboardDelegate, localState.state.collection, target, key, direction === 'rtl', wrap);
418
374
  };
419
375
 
420
- let getPreviousTarget = (target: DropTarget | null | undefined, wrap = true, horizontal = false): DropTarget | null => {
421
- let {keyboardDelegate} = localState.props;
422
- let nextKey: Key | null | undefined;
423
- if (target?.type === 'item') {
424
- nextKey = horizontal ? keyboardDelegate.getKeyLeftOf?.(target.key) : keyboardDelegate.getKeyAbove?.(target.key);
425
- } else {
426
- nextKey = horizontal && direction === 'rtl' ? keyboardDelegate.getFirstKey?.() : keyboardDelegate.getLastKey?.();
427
- }
428
- let dropPositions = horizontal && direction === 'rtl' ? DROP_POSITIONS_RTL : DROP_POSITIONS;
429
- let dropPosition: DropPosition = !target || target.type === 'root' ? dropPositions[2] : 'on';
430
-
431
- if (target?.type === 'item') {
432
- // If the the keyboard delegate returned the previous key in the collection,
433
- // first try the other positions in the current key. Otherwise (e.g. in a grid layout),
434
- // jump to the same drop position in the new key.
435
- let prevCollectionKey = horizontal && direction === 'rtl' ? localState.state.collection.getKeyAfter(target.key) : localState.state.collection.getKeyBefore(target.key);
436
- if (nextKey == null || nextKey === prevCollectionKey) {
437
- let positionIndex = dropPositions.indexOf(target.dropPosition);
438
- let nextDropPosition = dropPositions[positionIndex - 1];
439
- if (positionIndex > 0 && nextDropPosition !== dropPositions[2]) {
440
- return {
441
- type: 'item',
442
- key: target.key,
443
- dropPosition: nextDropPosition
444
- };
445
- }
446
-
447
- // If the last drop position was 'before', then 'after' on the previous key is equivalent.
448
- // Switch to 'on' instead.
449
- if (target.dropPosition === dropPositions[0]) {
450
- dropPosition = 'on';
451
- }
452
- } else {
453
- dropPosition = target.dropPosition;
454
- }
455
- }
456
-
457
- if (nextKey == null) {
458
- if (wrap) {
459
- return {
460
- type: 'root'
461
- };
462
- }
463
-
464
- return null;
465
- }
466
-
467
- return {
468
- type: 'item',
469
- key: nextKey,
470
- dropPosition
471
- };
376
+ let getPreviousTarget = (target: DropTarget | null | undefined, wrap = true) => {
377
+ return getNextTarget(target, wrap, 'up');
472
378
  };
473
379
 
474
380
  let nextValidTarget = (
@@ -581,17 +487,17 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
581
487
  onDropTargetEnter(target) {
582
488
  localState.state.setTarget(target);
583
489
  },
584
- onDropActivate(e) {
490
+ onDropActivate(e, target) {
585
491
  if (
586
- localState.state.target?.type === 'item' &&
587
- localState.state.target?.dropPosition === 'on' &&
492
+ target?.type === 'item' &&
493
+ target?.dropPosition === 'on' &&
588
494
  typeof localState.props.onDropActivate === 'function'
589
495
  ) {
590
496
  localState.props.onDropActivate({
591
497
  type: 'dropactivate',
592
498
  x: e.x, // todo
593
499
  y: e.y,
594
- target: localState.state.target
500
+ target
595
501
  });
596
502
  }
597
503
  },
@@ -607,28 +513,28 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
607
513
  switch (e.key) {
608
514
  case 'ArrowDown': {
609
515
  if (keyboardDelegate.getKeyBelow) {
610
- let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, getNextTarget);
516
+ let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getNextTarget(target, wrap, 'down'));
611
517
  localState.state.setTarget(target);
612
518
  }
613
519
  break;
614
520
  }
615
521
  case 'ArrowUp': {
616
522
  if (keyboardDelegate.getKeyAbove) {
617
- let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, getPreviousTarget);
523
+ let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getNextTarget(target, wrap, 'up'));
618
524
  localState.state.setTarget(target);
619
525
  }
620
526
  break;
621
527
  }
622
528
  case 'ArrowLeft': {
623
529
  if (keyboardDelegate.getKeyLeftOf) {
624
- let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getPreviousTarget(target, wrap, true));
530
+ let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getNextTarget(target, wrap, 'left'));
625
531
  localState.state.setTarget(target);
626
532
  }
627
533
  break;
628
534
  }
629
535
  case 'ArrowRight': {
630
536
  if (keyboardDelegate.getKeyRightOf) {
631
- let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getNextTarget(target, wrap, true));
537
+ let target = nextValidTarget(localState.state.target, types, drag.allowedDropOperations, (target, wrap) => getNextTarget(target, wrap, 'right'));
632
538
  localState.state.setTarget(target);
633
539
  }
634
540
  break;
@@ -741,6 +647,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
741
647
  break;
742
648
  }
743
649
  }
650
+ localState.props.onKeyDown?.(e);
744
651
  }
745
652
  });
746
653
  }, [localState, ref, onDrop, direction]);
@@ -12,14 +12,16 @@
12
12
 
13
13
  import * as DragManager from './DragManager';
14
14
  import {DroppableCollectionState} from '@react-stately/dnd';
15
- import {DropTarget, RefObject} from '@react-types/shared';
15
+ import {DropTarget, FocusableElement, RefObject} from '@react-types/shared';
16
16
  import {getDroppableCollectionRef, getTypes, globalDndState, isInternalDropOperation} from './utils';
17
17
  import {HTMLAttributes, useEffect} from 'react';
18
18
  import {useVirtualDrop} from './useVirtualDrop';
19
19
 
20
20
  export interface DroppableItemOptions {
21
21
  /** The drop target represented by the item. */
22
- target: DropTarget
22
+ target: DropTarget,
23
+ /** The ref to the activate button. */
24
+ activateButtonRef?: RefObject<FocusableElement | null>
23
25
  }
24
26
 
25
27
  export interface DroppableItemResult {
@@ -50,10 +52,11 @@ export function useDroppableItem(options: DroppableItemOptions, state: Droppable
50
52
  isInternal,
51
53
  draggingKeys
52
54
  });
53
- }
55
+ },
56
+ activateButtonRef: options.activateButtonRef
54
57
  });
55
58
  }
56
- }, [ref, options.target, state, droppableCollectionRef]);
59
+ }, [ref, options.target, state, droppableCollectionRef, options.activateButtonRef]);
57
60
 
58
61
  let dragSession = DragManager.useDragSession();
59
62
  let {draggingKeys} = globalDndState;