Skip to content

Commit 84844d8

Browse files
authored
Revert "fix: Revert "feat(RAC): Tree drag and drop (#7692)" (#8243)" (#8264)
This reverts commit b7f8ed1.
1 parent 0aeed71 commit 84844d8

File tree

12 files changed

+737
-129
lines changed

12 files changed

+737
-129
lines changed

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

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {DragEndEvent, DragItem, DropActivateEvent, DropEnterEvent, DropEvent, Dr
1616
import {getDragModality, getTypes} from './utils';
1717
import {isVirtualClick, isVirtualPointerEvent} from '@react-aria/utils';
1818
import type {LocalizedStringFormatter} from '@internationalized/string';
19-
import {useEffect, useState} from 'react';
19+
import {RefObject, useEffect, useState} from 'react';
2020

2121
let dropTargets = new Map<Element, DropTarget>();
2222
let dropItems = new Map<Element, DroppableItem>();
@@ -32,7 +32,8 @@ interface DropTarget {
3232
onDropTargetEnter?: (target: DroppableCollectionTarget | null) => void,
3333
onDropActivate?: (e: DropActivateEvent, target: DroppableCollectionTarget | null) => void,
3434
onDrop?: (e: DropEvent, target: DroppableCollectionTarget | null) => void,
35-
onKeyDown?: (e: KeyboardEvent, dragTarget: DragTarget) => void
35+
onKeyDown?: (e: KeyboardEvent, dragTarget: DragTarget) => void,
36+
activateButtonRef?: RefObject<FocusableElement | null>
3637
}
3738

3839
export function registerDropTarget(target: DropTarget) {
@@ -47,7 +48,8 @@ export function registerDropTarget(target: DropTarget) {
4748
interface DroppableItem {
4849
element: FocusableElement,
4950
target: DroppableCollectionTarget,
50-
getDropOperation?: (types: Set<string>, allowedOperations: DropOperation[]) => DropOperation
51+
getDropOperation?: (types: Set<string>, allowedOperations: DropOperation[]) => DropOperation,
52+
activateButtonRef?: RefObject<FocusableElement | null>
5153
}
5254

5355
export function registerDropItem(item: DroppableItem) {
@@ -241,15 +243,26 @@ class DragSession {
241243
this.cancelEvent(e);
242244

243245
if (e.key === 'Enter') {
244-
if (e.altKey) {
245-
this.activate();
246+
if (e.altKey || this.getCurrentActivateButton()?.contains(e.target as Node)) {
247+
this.activate(this.currentDropTarget, this.currentDropItem);
246248
} else {
247249
this.drop();
248250
}
249251
}
250252
}
251253

254+
getCurrentActivateButton(): FocusableElement | null {
255+
return this.currentDropItem?.activateButtonRef?.current ?? this.currentDropTarget?.activateButtonRef?.current ?? null;
256+
}
257+
252258
onFocus(e: FocusEvent) {
259+
let activateButton = this.getCurrentActivateButton();
260+
if (e.target === activateButton) {
261+
// TODO: canceling this breaks the focus ring. Revisit when we support tabbing.
262+
this.cancelEvent(e);
263+
return;
264+
}
265+
253266
// Prevent focus events, except to the original drag target.
254267
if (e.target !== this.dragTarget.element) {
255268
this.cancelEvent(e);
@@ -265,6 +278,9 @@ class DragSession {
265278
this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
266279

267280
if (!dropTarget) {
281+
// if (e.target === activateButton) {
282+
// activateButton.focus();
283+
// }
268284
if (this.currentDropTarget) {
269285
this.currentDropTarget.element.focus();
270286
} else {
@@ -274,10 +290,18 @@ class DragSession {
274290
}
275291

276292
let item = dropItems.get(e.target as HTMLElement);
277-
this.setCurrentDropTarget(dropTarget, item);
293+
if (dropTarget) {
294+
this.setCurrentDropTarget(dropTarget, item);
295+
}
278296
}
279297

280298
onBlur(e: FocusEvent) {
299+
let activateButton = this.getCurrentActivateButton();
300+
if (e.relatedTarget === activateButton) {
301+
this.cancelEvent(e);
302+
return;
303+
}
304+
281305
if (e.target !== this.dragTarget.element) {
282306
this.cancelEvent(e);
283307
}
@@ -296,14 +320,21 @@ class DragSession {
296320
onClick(e: MouseEvent) {
297321
this.cancelEvent(e);
298322
if (isVirtualClick(e) || this.isVirtualClick) {
323+
let dropElements = dropItems.values();
324+
let item = [...dropElements].find(item => item.element === e.target as HTMLElement || item.activateButtonRef?.current?.contains(e.target as HTMLElement));
325+
let dropTarget = this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
326+
let activateButton = item?.activateButtonRef?.current ?? dropTarget?.activateButtonRef?.current;
327+
if (activateButton?.contains(e.target as HTMLElement) && dropTarget) {
328+
this.activate(dropTarget, item);
329+
return;
330+
}
331+
299332
if (e.target === this.dragTarget.element) {
300333
this.cancel();
301334
return;
302335
}
303336

304-
let dropTarget = this.validDropTargets.find(target => target.element.contains(e.target as HTMLElement));
305337
if (dropTarget) {
306-
let item = dropItems.get(e.target as HTMLElement);
307338
this.setCurrentDropTarget(dropTarget, item);
308339
this.drop(item);
309340
}
@@ -319,7 +350,7 @@ class DragSession {
319350

320351
cancelEvent(e: Event) {
321352
// Allow focusin and focusout on the drag target so focus ring works properly.
322-
if ((e.type === 'focusin' || e.type === 'focusout') && e.target === this.dragTarget?.element) {
353+
if ((e.type === 'focusin' || e.type === 'focusout') && (e.target === this.dragTarget?.element || e.target === this.getCurrentActivateButton())) {
323354
return;
324355
}
325356

@@ -375,14 +406,23 @@ class DragSession {
375406

376407
this.restoreAriaHidden = ariaHideOutside([
377408
this.dragTarget.element,
378-
...validDropItems.map(item => item.element),
379-
...visibleDropTargets.map(target => target.element)
409+
...validDropItems.flatMap(item => item.activateButtonRef?.current ? [item.element, item.activateButtonRef?.current] : [item.element]),
410+
...visibleDropTargets.flatMap(target => target.activateButtonRef?.current ? [target.element, target.activateButtonRef?.current] : [target.element])
380411
]);
381412

382413
this.mutationObserver.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['aria-hidden']});
383414
}
384415

385416
next() {
417+
// TODO: Allow tabbing to the activate button. Revisit once we fix the focus ring.
418+
// For now, the activate button is reachable by screen readers and ArrowLeft/ArrowRight
419+
// is usable specifically by Tree. Will need tabbing for other components.
420+
// let activateButton = this.getCurrentActivateButton();
421+
// if (activateButton && document.activeElement !== activateButton) {
422+
// activateButton.focus();
423+
// return;
424+
// }
425+
386426
if (!this.currentDropTarget) {
387427
this.setCurrentDropTarget(this.validDropTargets[0]);
388428
return;
@@ -409,6 +449,15 @@ class DragSession {
409449
}
410450

411451
previous() {
452+
// let activateButton = this.getCurrentActivateButton();
453+
// if (activateButton && document.activeElement === activateButton) {
454+
// let target = this.currentDropItem ?? this.currentDropTarget;
455+
// if (target) {
456+
// target.element.focus();
457+
// return;
458+
// }
459+
// }
460+
412461
if (!this.currentDropTarget) {
413462
this.setCurrentDropTarget(this.validDropTargets[this.validDropTargets.length - 1]);
414463
return;
@@ -487,7 +536,6 @@ class DragSession {
487536
if (this.currentDropTarget && typeof this.currentDropTarget.onDropTargetEnter === 'function') {
488537
this.currentDropTarget.onDropTargetEnter(item.target);
489538
}
490-
491539
item.element.focus();
492540
this.currentDropItem = item;
493541

@@ -576,14 +624,15 @@ class DragSession {
576624
announce(this.stringFormatter.format('dropComplete'));
577625
}
578626

579-
activate() {
580-
if (this.currentDropTarget && typeof this.currentDropTarget.onDropActivate === 'function') {
581-
let rect = this.currentDropTarget.element.getBoundingClientRect();
582-
this.currentDropTarget.onDropActivate({
627+
activate(dropTarget: DropTarget | null, dropItem: DroppableItem | null | undefined) {
628+
if (dropTarget && typeof dropTarget.onDropActivate === 'function') {
629+
let target = dropItem?.target ?? null;
630+
let rect = dropTarget.element.getBoundingClientRect();
631+
dropTarget.onDropActivate({
583632
type: 'dropactivate',
584633
x: rect.left + (rect.width / 2),
585634
y: rect.top + (rect.height / 2)
586-
}, null);
635+
}, target);
587636
}
588637
}
589638
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export interface DropOptions {
3636
/**
3737
* Handler that is called after a valid drag is held over the drop target for a period of time.
3838
* This typically opens the item so that the user can drop within it.
39-
* @private
4039
*/
4140
onDropActivate?: (e: DropActivateEvent) => void,
4241
/** Handler that is called when a valid drag exits the drop target. */

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import * as DragManager from './DragManager';
1414
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';
1616
import {getDroppableCollectionId} from './utils';
1717
import {HTMLAttributes} from 'react';
1818
// @ts-ignore
@@ -23,7 +23,9 @@ import {useLocalizedStringFormatter} from '@react-aria/i18n';
2323

2424
export interface DropIndicatorProps {
2525
/** 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>
2729
}
2830

2931
export interface DropIndicatorAria {

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export interface DroppableCollectionOptions extends DroppableCollectionProps {
4646
/** A delegate object that implements behavior for keyboard focus movement. */
4747
keyboardDelegate: KeyboardDelegate,
4848
/** A delegate object that provides drop targets for pointer coordinates within the collection. */
49-
dropTargetDelegate: DropTargetDelegate
49+
dropTargetDelegate: DropTargetDelegate,
50+
/** A custom keyboard event handler for drop targets. */
51+
onKeyDown?: (e: KeyboardEvent) => void
5052
}
5153

5254
export interface DroppableCollectionResult {
@@ -92,6 +94,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
9294
onRootDrop,
9395
onItemDrop,
9496
onReorder,
97+
onMove,
9598
acceptedDragTypes = 'all',
9699
shouldAcceptItemDrop
97100
} = localState.props;
@@ -137,6 +140,10 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
137140
await onItemDrop({items: filteredItems, dropOperation, isInternal, target});
138141
}
139142

143+
if (onMove && isInternal) {
144+
await onMove({keys: draggingKeys, dropOperation, target});
145+
}
146+
140147
if (target.dropPosition !== 'on') {
141148
if (!isInternal && onInsert) {
142149
await onInsert({items: filteredItems, dropOperation, target});
@@ -201,7 +208,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
201208
autoScroll.stop();
202209
},
203210
onDropActivate(e) {
204-
if (state.target?.type === 'item' && state.target?.dropPosition === 'on' && typeof props.onDropActivate === 'function') {
211+
if (state.target?.type === 'item' && typeof props.onDropActivate === 'function') {
205212
props.onDropActivate({
206213
type: 'dropactivate',
207214
x: e.x, // todo
@@ -588,17 +595,17 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
588595
onDropTargetEnter(target) {
589596
localState.state.setTarget(target);
590597
},
591-
onDropActivate(e) {
598+
onDropActivate(e, target) {
592599
if (
593-
localState.state.target?.type === 'item' &&
594-
localState.state.target?.dropPosition === 'on' &&
600+
target?.type === 'item' &&
601+
target?.dropPosition === 'on' &&
595602
typeof localState.props.onDropActivate === 'function'
596603
) {
597604
localState.props.onDropActivate({
598605
type: 'dropactivate',
599606
x: e.x, // todo
600607
y: e.y,
601-
target: localState.state.target
608+
target
602609
});
603610
}
604611
},
@@ -748,6 +755,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
748755
break;
749756
}
750757
}
758+
localState.props.onKeyDown?.(e);
751759
}
752760
});
753761
}, [localState, ref, onDrop, direction]);

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212

1313
import * as DragManager from './DragManager';
1414
import {DroppableCollectionState} from '@react-stately/dnd';
15-
import {DropTarget, RefObject} from '@react-types/shared';
15+
import {DropTarget, FocusableElement, RefObject} from '@react-types/shared';
1616
import {getDroppableCollectionRef, getTypes, globalDndState, isInternalDropOperation} from './utils';
1717
import {HTMLAttributes, useEffect} from 'react';
1818
import {useVirtualDrop} from './useVirtualDrop';
1919

2020
export interface DroppableItemOptions {
2121
/** 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>
2325
}
2426

2527
export interface DroppableItemResult {
@@ -50,10 +52,11 @@ export function useDroppableItem(options: DroppableItemOptions, state: Droppable
5052
isInternal,
5153
draggingKeys
5254
});
53-
}
55+
},
56+
activateButtonRef: options.activateButtonRef
5457
});
5558
}
56-
}, [ref, options.target, state, droppableCollectionRef]);
59+
}, [ref, options.target, state, droppableCollectionRef, options.activateButtonRef]);
5760

5861
let dragSession = DragManager.useDragSession();
5962
let {draggingKeys} = globalDndState;

packages/@react-stately/data/test/useTreeData.test.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -920,24 +920,6 @@ describe('useTreeData', function () {
920920
expect(result.current.items.length).toEqual(2);
921921
});
922922

923-
it('should move an item to a different level after the target', function () {
924-
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
925-
let {result} = renderHook(() =>
926-
useTreeData({initialItems, getChildren, getKey})
927-
);
928-
929-
act(() => {
930-
result.current.move('Eli', 'David', 2);
931-
});
932-
expect(result.current.items[0].key).toEqual('David');
933-
934-
expect(result.current.items[0].children[0].key).toEqual('John');
935-
expect(result.current.items[0].children[1].key).toEqual('Sam');
936-
expect(result.current.items[0].children[2].key).toEqual('Eli');
937-
expect(result.current.items[1].key).toEqual('Emily');
938-
expect(result.current.items.length).toEqual(2);
939-
});
940-
941923
it('can move an item multiple times', function () {
942924
const initialItems = [...initial, {name: 'Eli'}];
943925

@@ -1097,6 +1079,24 @@ describe('useTreeData', function () {
10971079
expect(result.current.items[0].children[1].children[2].key).toEqual('project-2B');
10981080
});
10991081

1082+
it('should move an item to a different level after the target', function () {
1083+
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
1084+
let {result} = renderHook(() =>
1085+
useTreeData({initialItems, getChildren, getKey})
1086+
);
1087+
1088+
act(() => {
1089+
result.current.move('Eli', 'David', 2);
1090+
});
1091+
expect(result.current.items[0].key).toEqual('David');
1092+
1093+
expect(result.current.items[0].children[0].key).toEqual('John');
1094+
expect(result.current.items[0].children[1].key).toEqual('Sam');
1095+
expect(result.current.items[0].children[2].key).toEqual('Eli');
1096+
expect(result.current.items[1].key).toEqual('Emily');
1097+
expect(result.current.items.length).toEqual(2);
1098+
});
1099+
11001100
it('should move an item to a different level at the end when the index is greater than the node list length', function () {
11011101
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
11021102
let {result} = renderHook(() =>

0 commit comments

Comments
 (0)