Skip to content

Commit 5f58b9f

Browse files
committed
fixes and cleanup
1 parent b2bb88e commit 5f58b9f

File tree

8 files changed

+83
-122
lines changed

8 files changed

+83
-122
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,5 @@ export {useDraggableCollection} from './useDraggableCollection';
6464
export {useClipboard} from './useClipboard';
6565
export {DragPreview} from './DragPreview';
6666
export {ListDropTargetDelegate} from './ListDropTargetDelegate';
67-
export {TreeDropTargetDelegate} from './TreeDropTargetDelegate';
6867
export {isVirtualDragging} from './DragManager';
6968
export {isDirectoryDropItem, isFileDropItem, isTextDropItem} from './utils';

packages/@react-stately/dnd/src/useDroppableCollectionState.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ export function useDroppableCollectionState(props: DroppableCollectionStateOptio
7272

7373
let getOppositeTarget = (target: ItemDropTarget): ItemDropTarget | null => {
7474
if (target.dropPosition === 'before') {
75-
let key = collection.getKeyBefore(target.key);
76-
return key != null && collection.getItem(key)?.level === collection.getItem(target.key)?.level
77-
? {type: 'item', key, dropPosition: 'after'}
75+
let node = collection.getItem(target.key);
76+
return node && node.prevKey != null
77+
? {type: 'item', key: node.prevKey, dropPosition: 'after'}
7878
: null;
7979
} else if (target.dropPosition === 'after') {
80-
let key = collection.getKeyAfter(target.key);
81-
return key != null && collection.getItem(key)?.level === collection.getItem(target.key)?.level
82-
? {type: 'item', key, dropPosition: 'before'}
80+
let node = collection.getItem(target.key);
81+
return node && node.nextKey != null
82+
? {type: 'item', key: node.nextKey, dropPosition: 'before'}
8383
: null;
8484
}
8585
return null;

packages/react-aria-components/example/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ html {
1313
overflow: auto;
1414

1515
:global(.react-aria-DropIndicator) {
16-
margin-left: calc(var(--tree-item-level) * 15px);
16+
margin-inline-start: calc(var(--tree-item-level) * 15px);
1717
}
1818

1919
:global(.react-aria-DropIndicator[data-drop-target]) {

packages/react-aria-components/src/Tree.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {DragAndDropContext, DropIndicatorContext, useDndPersistedKeys, useRender
2121
import {DragAndDropHooks} from './useDragAndDrop';
2222
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, Node, SelectionBehavior, TreeState, useTreeState} from 'react-stately';
2323
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
24-
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
25-
import {TreeDropTargetDelegate} from '@react-aria/dnd';
24+
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef, useState} from 'react';
25+
import {TreeDropTargetDelegate} from './TreeDropTargetDelegate';
2626
import {useControlledState} from '@react-stately/utils';
2727

2828
class TreeCollection<T> implements ICollection<Node<T>> {
@@ -253,12 +253,15 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
253253
: null;
254254
}
255255

256+
let [treeDropTargetDelegate] = useState(() => new TreeDropTargetDelegate());
256257
if (hasDropHooks && dragAndDropHooks) {
257258
dropState = dragAndDropHooks.useDroppableCollectionState!({
258259
collection: state.collection,
259260
selectionManager: state.selectionManager
260261
});
261-
let dropTargetDelegate = dragAndDropHooks.dropTargetDelegate || ctxDropTargetDelegate || new TreeDropTargetDelegate(state, ref, {direction});
262+
let dropTargetDelegate = dragAndDropHooks.dropTargetDelegate || ctxDropTargetDelegate || new dragAndDropHooks.ListDropTargetDelegate(state.collection, ref, {direction});
263+
treeDropTargetDelegate.setup(dropTargetDelegate, state, direction);
264+
262265
let keyboardDelegate = props.keyboardDelegate ||
263266
new ListKeyboardDelegate({
264267
collection: state.collection,
@@ -272,7 +275,7 @@ function TreeInner<T extends object>({props, collection, treeRef: ref}: TreeInne
272275
droppableCollection = dragAndDropHooks.useDroppableCollection!(
273276
{
274277
keyboardDelegate,
275-
dropTargetDelegate,
278+
dropTargetDelegate: treeDropTargetDelegate,
276279
onDropActivate: (e) => {
277280
// Expand collapsed item when dragging over. For keyboard, allow collapsing.
278281
if (e.target.type === 'item') {

packages/@react-aria/dnd/src/TreeDropTargetDelegate.ts renamed to packages/react-aria-components/src/TreeDropTargetDelegate.ts

Lines changed: 62 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
import {Direction, DropTarget, ItemDropTarget, Key, Node, RefObject} from '@react-types/shared';
2-
import {ListDropTargetDelegate} from './ListDropTargetDelegate';
3-
4-
interface TreeDropTargetDelegateOptions {
5-
/**
6-
* The horizontal layout direction.
7-
* @default 'ltr'
8-
*/
9-
direction?: Direction
10-
}
1+
import {Direction, DropTarget, DropTargetDelegate, ItemDropTarget, Key, Node} from '@react-types/shared';
112

123
interface TreeCollection<T> extends Iterable<Node<T>> {
134
getItem(key: Key): Node<T> | null,
@@ -36,11 +27,12 @@ interface PointerTracking {
3627
} | null
3728
}
3829

39-
const X_SWITCH_THRESHOLD = 3;
40-
const Y_SWITCH_THRESHOLD = 2;
41-
42-
export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
43-
private state: TreeState<T>;
30+
const X_SWITCH_THRESHOLD = 5;
31+
const Y_SWITCH_THRESHOLD = 5;
32+
export class TreeDropTargetDelegate<T> {
33+
private delegate: DropTargetDelegate | null = null;
34+
private state: TreeState<T> | null = null;
35+
private direction: Direction = 'ltr';
4436
private pointerTracking: PointerTracking = {
4537
lastY: 0,
4638
lastX: 0,
@@ -49,26 +41,20 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
4941
boundaryContext: null
5042
};
5143

52-
constructor(state: TreeState<T>, ref: RefObject<HTMLElement | null>, options?: TreeDropTargetDelegateOptions) {
53-
super(state.collection as Iterable<Node<unknown>>, ref, {
54-
direction: options?.direction || 'ltr',
55-
orientation: 'vertical',
56-
layout: 'stack'
57-
});
44+
setup(delegate: DropTargetDelegate, state: TreeState<T>, direction: Direction): void {
45+
this.delegate = delegate;
5846
this.state = state;
47+
this.direction = direction;
5948
}
6049

61-
getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget {
62-
let baseTarget = super.getDropTargetFromPoint(x, y, isValidDropTarget);
50+
getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget | null {
51+
let baseTarget = this.delegate!.getDropTargetFromPoint(x, y, isValidDropTarget);
6352

6453
if (!baseTarget || baseTarget.type === 'root') {
6554
return baseTarget;
6655
}
6756

68-
let target = this.resolveDropTarget(baseTarget, x, y, isValidDropTarget);
69-
console.log(target);
70-
71-
return target;
57+
return this.resolveDropTarget(baseTarget, x, y, isValidDropTarget);
7258
}
7359

7460
private resolveDropTarget(
@@ -82,21 +68,33 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
8268
// Calculate movement directions
8369
let deltaY = y - tracking.lastY;
8470
let deltaX = x - tracking.lastX;
85-
let currentYMovement: 'up' | 'down' | null = null;
86-
let currentXMovement: 'left' | 'right' | null = null;
71+
let currentYMovement: 'up' | 'down' | null = tracking.yDirection;
72+
let currentXMovement: 'left' | 'right' | null = tracking.xDirection;
8773

88-
if (Math.abs(deltaY) > 2) {
74+
if (Math.abs(deltaY) > Y_SWITCH_THRESHOLD) {
8975
currentYMovement = deltaY > 0 ? 'down' : 'up';
9076
tracking.yDirection = currentYMovement;
9177
tracking.lastY = y;
9278
}
9379

94-
if (Math.abs(deltaX) > 2) {
80+
if (Math.abs(deltaX) > X_SWITCH_THRESHOLD) {
9581
currentXMovement = deltaX > 0 ? 'right' : 'left';
9682
tracking.xDirection = currentXMovement;
9783
tracking.lastX = x;
9884
}
9985

86+
// Normalize to 'after'
87+
if (target.dropPosition === 'before') {
88+
let keyBefore = this.state!.collection.getKeyBefore(target.key);
89+
if (keyBefore != null) {
90+
target = {
91+
type: 'item',
92+
key: keyBefore,
93+
dropPosition: 'after'
94+
} as const;
95+
}
96+
}
97+
10098
let potentialTargets = this.getPotentialTargets(target, isValidDropTarget);
10199

102100
if (potentialTargets.length > 1) {
@@ -118,32 +116,20 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
118116
}
119117

120118
let target = originalTarget;
119+
let collection = this.state!.collection;
121120

122-
// Normalize to 'after'
123-
if (originalTarget.dropPosition === 'before') {
124-
let keyBefore = this.state.collection.getKeyBefore(originalTarget.key);
125-
if (keyBefore == null) {
126-
return [originalTarget];
127-
}
128-
target = {
129-
type: 'item',
130-
key: keyBefore,
131-
dropPosition: 'after'
132-
} as const;
133-
}
134-
135-
let currentItem = this.state.collection.getItem(target.key);
121+
let currentItem = collection.getItem(target.key);
136122
while (currentItem && currentItem?.type !== 'item' && currentItem.nextKey != null) {
137123
target.key = currentItem.nextKey;
138-
currentItem = this.state.collection.getItem(currentItem.nextKey);
124+
currentItem = collection.getItem(currentItem.nextKey);
139125
}
140126

141127
let potentialTargets = [target];
142128

143129
// If target has children and is expanded, use "before first child"
144-
if (currentItem && currentItem.hasChildNodes && this.state.expandedKeys.has(currentItem.key) && this.state.collection.getChildren) {
130+
if (currentItem && currentItem.hasChildNodes && this.state!.expandedKeys.has(currentItem.key) && collection.getChildren) {
145131
let firstChildItemNode: Node<any> | null = null;
146-
for (let child of this.state.collection.getChildren(currentItem.key)) {
132+
for (let child of collection.getChildren(currentItem.key)) {
147133
if (child.type === 'item') {
148134
firstChildItemNode = child;
149135
break;
@@ -172,8 +158,8 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
172158
let ancestorTargets: ItemDropTarget[] = [];
173159

174160
while (parentKey) {
175-
let parentItem = this.state.collection.getItem(parentKey);
176-
let nextItem = parentItem?.nextKey ? this.state.collection.getItem(parentItem.nextKey) : null;
161+
let parentItem = collection.getItem(parentKey);
162+
let nextItem = parentItem?.nextKey ? collection.getItem(parentItem.nextKey) : null;
177163
let isLastChildAtLevel = !nextItem || nextItem.parentKey !== parentKey;
178164

179165
if (isLastChildAtLevel) {
@@ -200,8 +186,8 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
200186

201187
// Handle converting "after" to "before next" for non-ambiguous cases
202188
if (potentialTargets.length === 1) {
203-
let nextKey = this.state.collection.getKeyAfter(target.key);
204-
let nextNode = nextKey ? this.state.collection.getItem(nextKey) : null;
189+
let nextKey = collection.getKeyAfter(target.key);
190+
let nextNode = nextKey ? collection.getItem(nextKey) : null;
205191
if (nextKey != null && nextNode && currentItem && nextNode.level != null && currentItem.level != null && nextNode.level > currentItem.level) {
206192
let beforeTarget = {
207193
type: 'item',
@@ -230,67 +216,18 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
230216
}
231217

232218
let tracking = this.pointerTracking;
233-
let currentItem = this.state.collection.getItem(originalTarget.key);
219+
let currentItem = this.state!.collection.getItem(originalTarget.key);
234220
let parentKey = currentItem?.parentKey;
235221

236222
if (!parentKey) {
237223
return potentialTargets[0];
238224
}
239-
240-
// Case 1: Exactly 2 potential targets - use Y movement only
241-
if (potentialTargets.length === 2) {
242-
// Initialize boundary context if needed
243-
if (!tracking.boundaryContext || tracking.boundaryContext.parentKey !== parentKey) {
244-
let initialTargetIndex = currentYMovement === 'up' ? 1 : 0;
245-
246-
tracking.boundaryContext = {
247-
parentKey,
248-
lastChildKey: originalTarget.key,
249-
preferredTargetIndex: initialTargetIndex,
250-
lastSwitchY: y,
251-
lastSwitchX: x,
252-
entryDirection: tracking.yDirection
253-
};
254-
}
255-
256-
let boundaryContext = tracking.boundaryContext;
257-
let distanceFromLastYSwitch = Math.abs(y - boundaryContext.lastSwitchY);
258-
259-
// Toggle between targets based on Y movement
260-
if (distanceFromLastYSwitch > Y_SWITCH_THRESHOLD && currentYMovement) {
261-
let currentIndex = boundaryContext.preferredTargetIndex || 0;
262-
263-
if (currentYMovement === 'down' && currentIndex === 0) {
264-
// Moving down from inner-most, switch to outer-most
265-
boundaryContext.preferredTargetIndex = 1;
266-
boundaryContext.lastSwitchY = y;
267-
} else if (currentYMovement === 'down' && currentIndex === 1) {
268-
// Moving down from outer-most, switch back to inner-most
269-
boundaryContext.preferredTargetIndex = 0;
270-
boundaryContext.lastSwitchY = y;
271-
} else if (currentYMovement === 'up' && currentIndex === 1) {
272-
// Moving up from outer-most, switch to inner-most
273-
boundaryContext.preferredTargetIndex = 0;
274-
boundaryContext.lastSwitchY = y;
275-
} else if (currentYMovement === 'up' && currentIndex === 0) {
276-
// Moving up from inner-most, switch to outer-most
277-
boundaryContext.preferredTargetIndex = 1;
278-
boundaryContext.lastSwitchY = y;
279-
}
280-
}
281-
282-
return potentialTargets[boundaryContext.preferredTargetIndex || 0];
283-
}
284225

285-
// Case 2: More than 2 potential targets - use Y for initial target, then X for switching levels
226+
// More than 1 potential target - use Y for initial target, then X for switching levels
286227
// Initialize boundary context if needed
287228
if (!tracking.boundaryContext || tracking.boundaryContext.parentKey !== parentKey) {
288-
let initialTargetIndex = 0; // Default to inner-most
289-
if (tracking.yDirection === 'up') {
290-
// If entering from below, start with outer-most
291-
initialTargetIndex = potentialTargets.length - 1;
292-
}
293-
229+
// If entering from below, start with outer-most
230+
let initialTargetIndex = tracking.yDirection === 'up' ? potentialTargets.length - 1 : 0;
294231
tracking.boundaryContext = {
295232
parentKey,
296233
lastChildKey: originalTarget.key,
@@ -303,6 +240,23 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
303240

304241
let boundaryContext = tracking.boundaryContext;
305242
let distanceFromLastXSwitch = Math.abs(x - boundaryContext.lastSwitchX);
243+
let distanceFromLastYSwitch = Math.abs(y - boundaryContext.lastSwitchY);
244+
245+
// Toggle between targets based on Y movement
246+
if (distanceFromLastYSwitch > Y_SWITCH_THRESHOLD && currentYMovement) {
247+
let currentIndex = boundaryContext.preferredTargetIndex || 0;
248+
249+
if (currentYMovement === 'down' && currentIndex === 0) {
250+
// Moving down from inner-most, switch to outer-most
251+
boundaryContext.preferredTargetIndex = potentialTargets.length - 1;
252+
} else if (currentYMovement === 'up' && currentIndex === potentialTargets.length - 1) {
253+
// Moving up from outer-most, switch to inner-most
254+
boundaryContext.preferredTargetIndex = 0;
255+
}
256+
257+
// Reset x tracking so that moving diagonally doesn't cause flickering.
258+
tracking.xDirection = null;
259+
}
306260

307261
// X movement controls level selection
308262
if (distanceFromLastXSwitch > X_SWITCH_THRESHOLD && currentXMovement) {
@@ -337,6 +291,9 @@ export class TreeDropTargetDelegate<T> extends ListDropTargetDelegate {
337291
}
338292
}
339293
}
294+
295+
// Reset y tracking so that moving diagonally doesn't cause flickering.
296+
tracking.yDirection = null;
340297
}
341298

342299
let targetIndex = Math.max(0, Math.min(boundaryContext.preferredTargetIndex || 0, potentialTargets.length - 1));

packages/react-aria-components/src/useDragAndDrop.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
DropTarget,
2626
DropTargetDelegate,
2727
ListDropTargetDelegate,
28-
TreeDropTargetDelegate,
2928
useDraggableCollection,
3029
useDraggableItem,
3130
useDropIndicator,
@@ -62,8 +61,7 @@ interface DropHooks {
6261
useDropIndicator?: (props: AriaDropIndicatorProps, state: DroppableCollectionState, ref: RefObject<HTMLElement | null>) => DropIndicatorAria,
6362
renderDropIndicator?: (target: DropTarget) => JSX.Element,
6463
dropTargetDelegate?: DropTargetDelegate,
65-
ListDropTargetDelegate: typeof ListDropTargetDelegate,
66-
TreeDropTargetDelegate: typeof TreeDropTargetDelegate
64+
ListDropTargetDelegate: typeof ListDropTargetDelegate
6765
}
6866

6967
export type DragAndDropHooks = DragHooks & DropHooks
@@ -141,7 +139,6 @@ export function useDragAndDrop(options: DragAndDropOptions): DragAndDrop {
141139
hooks.renderDropIndicator = renderDropIndicator;
142140
hooks.dropTargetDelegate = dropTargetDelegate;
143141
hooks.ListDropTargetDelegate = ListDropTargetDelegate;
144-
hooks.TreeDropTargetDelegate = TreeDropTargetDelegate;
145142
}
146143

147144
return hooks;

packages/react-aria-components/stories/Tree.stories.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,11 @@ function TreeDragAndDropExample(args) {
582582
let {dragAndDropHooks} = useDragAndDrop({
583583
getItems,
584584
getAllowedDropOperations: () => ['move'],
585+
renderDragPreview(items) {
586+
return (
587+
<div style={{background: 'blue', color: 'white', padding: '4px'}}>{items.length} items</div>
588+
);
589+
},
585590
shouldAcceptItemDrop: (target) => {
586591
if (args.shouldAcceptItemDrop === 'folders') {
587592
let item = treeData.getItem(target.key);

packages/react-aria/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export {useComboBox} from '@react-aria/combobox';
1919
export {useDateField, useDatePicker, useDateRangePicker, useDateSegment, useTimeField} from '@react-aria/datepicker';
2020
export {useDialog} from '@react-aria/dialog';
2121
export {useDisclosure} from '@react-aria/disclosure';
22-
export {useDrag, useDrop, useDraggableCollection, useDroppableCollection, useDroppableItem, useDropIndicator, useDraggableItem, useClipboard, DragPreview, ListDropTargetDelegate, TreeDropTargetDelegate, DIRECTORY_DRAG_TYPE, isDirectoryDropItem, isFileDropItem, isTextDropItem} from '@react-aria/dnd';
22+
export {useDrag, useDrop, useDraggableCollection, useDroppableCollection, useDroppableItem, useDropIndicator, useDraggableItem, useClipboard, DragPreview, ListDropTargetDelegate, DIRECTORY_DRAG_TYPE, isDirectoryDropItem, isFileDropItem, isTextDropItem} from '@react-aria/dnd';
2323
export {FocusRing, FocusScope, useFocusManager, useFocusRing} from '@react-aria/focus';
2424
export {I18nProvider, useCollator, useDateFormatter, useFilter, useLocale, useLocalizedStringFormatter, useMessageFormatter, useNumberFormatter} from '@react-aria/i18n';
2525
export {useFocus, useFocusVisible, useFocusWithin, useHover, useInteractOutside, useKeyboard, useMove, usePress, useLongPress, useFocusable, Pressable, Focusable} from '@react-aria/interactions';

0 commit comments

Comments
 (0)