Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RAC): Tree drag and drop #7692

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
903ba60
[5574] - add moveBefore and moveAfter to useTreeData
rob-clayburn Jan 30, 2025
8f14593
add docs
rob-clayburn Jan 30, 2025
768e253
remove onlys
rob-clayburn Jan 30, 2025
aa2d362
remove console logs
rob-clayburn Jan 30, 2025
d9b4d57
[wip] add intial tree drag and drop
rob-clayburn Jan 30, 2025
5112433
Merge branch 'main' into tree-dnd
rob-clayburn Jan 30, 2025
e6becc6
Merge branch 'main' into tree-dnd
rob-clayburn Jan 31, 2025
22b88d5
useTreeData - add getDescendantKeys method which is used to determine…
rob-clayburn Jan 31, 2025
121712a
revert packlog json change
rob-clayburn Jan 31, 2025
7c81ea2
Merge remote-tracking branch 'upstream/main' into tree-dnd
rob-clayburn Feb 19, 2025
02e07de
updates + story
reidbarber Mar 11, 2025
5be35b6
Merge remote-tracking branch 'origin/main' into tree-dnd
reidbarber Mar 11, 2025
d7506e9
remove RSP TreeView dnd for now
reidbarber Mar 12, 2025
34073ba
cleanup
reidbarber Mar 12, 2025
efd5259
fix story
reidbarber Mar 12, 2025
5b38175
lint
reidbarber Mar 12, 2025
e0731ed
lint
reidbarber Mar 12, 2025
f7272d9
Merge remote-tracking branch 'origin/main' into tree-dnd
reidbarber Mar 12, 2025
2cbccd9
lint
reidbarber Mar 12, 2025
8ba1b42
ts
reidbarber Mar 12, 2025
0504991
fix listMapData destructure
reidbarber Mar 12, 2025
a0aa836
allow expanding during dragging
reidbarber Mar 19, 2025
8be4765
lint
reidbarber Mar 19, 2025
5e54c62
review comments
reidbarber Mar 19, 2025
b9d0746
Merge remote-tracking branch 'upstream/main' into tree-dnd
reidbarber Mar 19, 2025
074a3f6
fixes
reidbarber Mar 19, 2025
bb82173
lint
reidbarber Mar 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/@react-aria/dnd/src/useDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export interface DropOptions {
/**
* Handler that is called after a valid drag is held over the drop target for a period of time.
* This typically opens the item so that the user can drop within it.
* @private
*/
onDropActivate?: (e: DropActivateEvent) => void,
/** Handler that is called when a valid drag exits the drop target. */
Expand Down
7 changes: 5 additions & 2 deletions packages/@react-aria/dnd/src/useDroppableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DropTargetDelegate,
Key,
KeyboardDelegate,
KeyboardEvents,
Node,
RefObject
} from '@react-types/shared';
Expand All @@ -46,7 +47,8 @@ export interface DroppableCollectionOptions extends DroppableCollectionProps {
/** A delegate object that implements behavior for keyboard focus movement. */
keyboardDelegate: KeyboardDelegate,
/** A delegate object that provides drop targets for pointer coordinates within the collection. */
dropTargetDelegate: DropTargetDelegate
dropTargetDelegate: DropTargetDelegate,
onKeyDown?: KeyboardEvents['onKeyDown']
}

export interface DroppableCollectionResult {
Expand Down Expand Up @@ -201,7 +203,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
autoScroll.stop();
},
onDropActivate(e) {
if (state.target?.type === 'item' && state.target?.dropPosition === 'on' && typeof props.onDropActivate === 'function') {
if (state.target?.type === 'item' && typeof props.onDropActivate === 'function') {
props.onDropActivate({
type: 'dropactivate',
x: e.x, // todo
Expand Down Expand Up @@ -741,6 +743,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
break;
}
}
localState.props.onKeyDown?.(e as any);
}
});
}, [localState, ref, onDrop, direction]);
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt

// let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/gridlist');
let {direction} = useLocale();
let {onAction, linkBehavior, keyboardNavigationBehavior} = listMap.get(state)!;
let listMapData = listMap.get(state);
let {onAction, linkBehavior = 'action', keyboardNavigationBehavior = 'arrow'} = listMapData || {};
let descriptionId = useSlotId();

// We need to track the key of the item at the time it was last focused so that we force
Expand Down
20 changes: 20 additions & 0 deletions packages/@react-stately/data/src/useTreeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface TreeData<T extends object> {
*/
getItem(key: Key): TreeNode<T> | undefined,

getDescendantKeys(node?: TreeNode<T>): Key[],
/**
* Inserts an item into a parent node as a child.
* @param parentKey - The key of the parent item to insert into. `null` for the root.
Expand Down Expand Up @@ -234,10 +235,29 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
}
}

function getDescendantKeys(node: TreeNode<T>): Key[] {
let descendantKeys: Key[] = [];
if (!node) {
return descendantKeys;
}
function recurse(currentNode: TreeNode<T>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the traversal order matter? preorder, in order, postorder

should descendantKeys be a Set if it doesn't matter?

if (currentNode.children) {
for (let child of currentNode.children) {
descendantKeys.push(child.key);
recurse(child);
}
}
}

recurse(node);
return descendantKeys;
}

return {
items,
selectedKeys,
setSelectedKeys,
getDescendantKeys,
getItem(key: Key) {
return nodeMap.get(key);
},
Expand Down
57 changes: 56 additions & 1 deletion packages/@react-stately/data/test/useTreeData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/

import {actHook as act, renderHook} from '@react-spectrum/test-utils-internal';
import React from 'react';
import {useTreeData} from '../src/useTreeData';

const initial = [
Expand Down Expand Up @@ -745,4 +744,60 @@ describe('useTreeData', function () {
expect(result.current.items[1].key).toEqual('Emily');
expect(result.current.items.length).toEqual(2);
});

it('gets the descendants of a node', function () {
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
let {result} = renderHook(() =>
useTreeData({initialItems, getChildren, getKey})
);
let descendants;
const top = result.current.getItem('David');
descendants = result.current.getDescendantKeys(top);
expect(descendants).toEqual([
'John',
'Suzie',
'Sam',
'Stacy',
'Brad',
'Jane'
]);
});

it('gets the descendants of a child node', function () {
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
let {result} = renderHook(() =>
useTreeData({initialItems, getChildren, getKey})
);
let descendants;
act(() => {
const top = result.current.getItem('Sam');
descendants = result.current.getDescendantKeys(top);
});
expect(descendants).toEqual(['Stacy', 'Brad']);
});

it('returns an empty array when getting the descendant keys for a leaf node', function () {
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
let {result} = renderHook(() =>
useTreeData({initialItems, getChildren, getKey})
);
let descendants;
act(() => {
const top = result.current.getItem('Eli');
descendants = result.current.getDescendantKeys(top);
});
expect(descendants).toEqual([]);
});

it('returns an empty array when an undefined key is supplied', function () {
const initialItems = [...initial, {name: 'Emily'}, {name: 'Eli'}];
let {result} = renderHook(() =>
useTreeData({initialItems, getChildren, getKey})
);
let descendants;
act(() => {
descendants = result.current.getDescendantKeys(undefined);
});
expect(descendants).toEqual([]);
});
});
Loading