Skip to content

Conversation

yihuiliao
Copy link
Member

Closes

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot
Copy link

rspbot commented Oct 10, 2025

@rspbot
Copy link

rspbot commented Oct 10, 2025

@rspbot
Copy link

rspbot commented Oct 10, 2025

@rspbot
Copy link

rspbot commented Oct 10, 2025

if (parent) {
// siblings must exist because our original node exists
let siblings = state.collection.getChildren?.(parent.key)!;
let siblings = getDirectChildren(parent as CollectionNode<T>, state.collection as Collection<CollectionNode<T>>);
Copy link
Member Author

Choose a reason for hiding this comment

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

we used to use the getChildren method from TreeCollection but now that we've changed it to get ALL the descendants of a parent node instead of its immediate descendants, we can't use that anymore. i decided to make a new function called getDirectChildren which is essentially the same code as what getChildren was before

let {flattenedRows, keyMap, itemCount} = flattenTree<T>(collection, {expandedKeys});
this.flattenedRows = flattenedRows;
let {collection, lastExpandedKeys, expandedKeys} = opts;
let {keyMap, itemCount} = flattenTree<T>(collection, {expandedKeys});
Copy link
Member Author

Choose a reason for hiding this comment

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

i've removed flattenedRows from TreeCollection since we've moved away from the flattened structure but i didn't feel entirely comfortable removing flattenTree entirely since we use it to build the keyMap and modify the nodes (like the levels and index). but if we do wanna rework it, im happy to do it in a follow-up unless we'd like the changes in this PR

return this.keyMap.get(key) || null;
}

at(idx: number) {
Copy link
Member Author

Choose a reason for hiding this comment

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

obviously, this was a lot easier when we had flattenedRows because we could just index it but now we don't have a nice array to index. getKeyAfter will get the next visible row from the top and we just iterate however many times idx is

if you're wondering why we don't just use getKeyAfter that's already built into TreeCollection...continue reading...

Copy link
Member

Choose a reason for hiding this comment

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

Do we actually use the at method anywhere? In BaseCollection we throw an error that it isn't supported. Maybe we don't need this?

Copy link
Member Author

Choose a reason for hiding this comment

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

no it doesn't look like we use it anywhere and it's not defined in any other collection either so we can probably just remove it

let [expandedKeys, setExpandedKeys] = useControlledState(
propExpandedKeys ? convertExpanded(propExpandedKeys) : undefined,
propDefaultExpandedKeys ? convertExpanded(propDefaultExpandedKeys) : new Set(),
propExpandedKeys ? new Set(propExpandedKeys) : undefined,
Copy link
Member Author

Choose a reason for hiding this comment

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

ended up getting rid of convertExpanded because i was running into a type error where expandedKeys could either be a Set or string but since propExpandedKeys/propDefaultExpandedKeys are type Iterable<Key> and cannot be set to all, there wasn't any way expandedKeys would ever be a string

}

if ((this.expandedKeys.has(node.key) || node.type !== 'item') && node.firstChildKey != null) {
return node.firstChildKey;
Copy link
Member Author

Choose a reason for hiding this comment

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

should getKeyAfter include content nodes or should it skip them?

in this case, if the node is included in expandedKeys, we'll look at its firstChildKey which will be a TreeItemContent node. however, this caused issues in DropTargetKeyboardNavigation so we check if the node is a content node and if it is, we check its nextKey. however, we could deal with that here but depends on what we want to do

@yihuiliao yihuiliao changed the title wip: tree section feat: add support for tree sections Oct 10, 2025
@yihuiliao yihuiliao marked this pull request as ready for review October 10, 2025 23:20
@rspbot
Copy link

rspbot commented Oct 10, 2025

@rspbot
Copy link

rspbot commented Oct 10, 2025

## API Changes

react-aria-components

/react-aria-components:GridListSectionProps

-GridListSectionProps <T> {
-  aria-label?: string
-  children?: ReactNode | (T) => ReactElement
-  className?: string = 'react-aria-GridListSection'
-  dependencies?: ReadonlyArray<any>
-  id?: Key
-  items?: Iterable<T>
-  style?: CSSProperties
-  value?: T
-}

/react-aria-components:TreeHeader

+TreeHeader {
+  UNTYPED
+}

/react-aria-components:TreeSection

+TreeSection <T extends {}> {
+  aria-label?: string
+  children?: ReactNode | ({}) => ReactElement
+  className?: string
+  dependencies?: ReadonlyArray<any>
+  id?: Key
+  items?: Iterable<{}>
+  style?: CSSProperties
+  value?: {}
+}

Copy link
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

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

Let's discuss what we want the division of responsibilties to be between collections/keyboard delegates/drop delegates/etc.

}

function getDirectChildren<T>(parent: CollectionNode<T>, collection: Collection<CollectionNode<T>>) {
let node = parent?.firstChildKey != null ? collection.getItem(parent.firstChildKey) : null;
Copy link
Member

Choose a reason for hiding this comment

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

I think this only works with new collections (that's why it required a type cast above)? Maybe ok because we currently only implement Tree in RAC, not directly via hooks, but if someone did that this wouldn't work.

Copy link
Member Author

Choose a reason for hiding this comment

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

oh true. i guess i could maybe do some type guarding and have some different logic if it's Node and not a CollectionNode? kinda unfortunate that we can't use getChildren here anymore tho

return this.keyMap.get(key) || null;
}

at(idx: number) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we actually use the at method anywhere? In BaseCollection we throw an error that it isn't supported. Maybe we don't need this?

return null;
}

// Skip over any nodes that aren't an item node (e.g. section or header node)
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this should be the job of the collection or the keyboard delegate? In BaseCollection we don't skip any nodes here. I kinda think collections should traverse everything and everything downstream (keyboard delegate, dnd, etc.) should skip over things it doesn't care about.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with this sentiment

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah i think that would make sense. daniel and i kinda talked about that regarding getKeyAfter. either we could skip the content node inside of getKeyAfter or handle it inside of dnd. we ended up doing the latter but i can see applying that logic elsewhere

let expandedKeys = this.expandedKeys;
return {
*[Symbol.iterator]() {
function* traverseDepthFirst(node: CollectionNode<T> | null, expandedKeys: Set<Key>) {
Copy link
Member

Choose a reason for hiding this comment

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

In theory, this should be equivalent to starting at key and traversing by iterating through getKeyAfter until it returns null. Wonder if we could implement it that way and avoid duplicating the logic here? Same could probably be done for the iterator over the whole collection above (but starting at getFirstKey()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants