Skip to content

refactor: Centralize expandedKeys logic in TreeCollection#9711

Merged
devongovett merged 2 commits intomainfrom
refactor-tree-collection
Feb 28, 2026
Merged

refactor: Centralize expandedKeys logic in TreeCollection#9711
devongovett merged 2 commits intomainfrom
refactor-tree-collection

Conversation

@devongovett
Copy link
Member

I noticed during release audit that we were handling expandedKeys in both TreeCollection and ListKeyboardDelegate. Also, we are still doing a full traversal over the entire collection on the initial render to adjust some indices. I have now centralized all of this within TreeCollection.

  • Updated getLastKey in TreeCollection to traverse to the deepest expanded descendant, and removed this from ListKeyboardDelegate.
  • Updated TreeCollection to extend BaseCollection instead of wrapping one. This allows us to access some of the internal properties by making them protected instead of private (avoiding making them externally public). Updated CollectionBuilder to construct it via createCollection, and set expandedKeys later.
  • Moving adjustments to index and level properties into useGridListItem during rendering instead of updating the collection nodes themselves.

@rspbot
Copy link

rspbot commented Feb 27, 2026

get level(): number {
if (this.parentNode instanceof ElementNode) {
return this.parentNode.level + (this.node?.type === 'item' ? 1 : 0);
return this.parentNode.level + (this.parentNode.node?.type === 'item' ? 1 : 0);
Copy link
Member Author

Choose a reason for hiding this comment

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

The logic here was incorrect: we should increment the level if the parent is an item, not if the child is. This means that loaders and other elements have the same level as their siblings.

@rspbot
Copy link

rspbot commented Feb 27, 2026

Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

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

LGTM, tested Tree and GridList components.

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

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

Behavior looks good to me so far, nothing amiss that I can tell about the collection structure or any of the behaviors/attributes applied from querying the collection.

let [lastCollection, setLastCollection] = useState(collection);
let [lastExpandedKeys, setLastExpandedKeys] = useState(expandedKeys);
let [flattenedCollection, setFlattenedCollection] = useState(() => new TreeCollection<object>({collection, lastExpandedKeys: new Set(), expandedKeys}));
let [flattenedCollection, setFlattenedCollection] = useState(() => collection.withExpandedKeys(lastExpandedKeys, expandedKeys));
Copy link
Member

Choose a reason for hiding this comment

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

I was initially concerned about this since now the "flattened" collection isn't actually flattened any more and thus things like itemCount always include rows that are in collapsed parents, but so far it doesn't seem like there are any issues (it helps that most of the collection size checks are if the collection is empty).

The only change in behavior I've found so far is with DnD where starting a keyboard drag on the first child of a parent and pressing ArrowUp actually loops you back to the drop position after the last child of the parent. You need to then traverse up through all of the children before escaping the parent, but the previous behavior was also broken but in a different way so we'll have to revisit

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm I guess aria-rowcount would be affected for virtualized trees. Seems like we don't have any tests for that? That's very unfortunate. I wonder what is correct in that case?

Copy link
Member

Choose a reason for hiding this comment

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

yeah I'm surprised I don't have a test for that either ugh. Testing https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/examples/treegrid-1/, it seems to omit the rows in collapsed sections in the announcement in VO (note this treegrid doesn't use aria-rowcount) so it seems like we'll still have to somehow account for that. Maybe we can address this via the eventual "number of item of type" change we've always talked about adding to collection nodes

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 it's an interesting case because the rows do still exist, they are just collapsed. So I could see it both ways.

Copy link
Member

Choose a reason for hiding this comment

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

yeah I can definitely see that being correct too, I assume a screen reader user would pass by collapsed rows and could be clued in that the row count is the total row count, not what is available immediately. The blurb about aria-rowcount in https://www.w3.org/WAI/ARIA/apg/practices/grid-and-table-properties/ could also be read in favor of being the absolute total of rows:

When the number of rows represented by the DOM structure is not the total number of rows available for a table, grid, or treegrid, the aria-rowcount property is used to communicate the total number of rows available, and it is accompanied by the aria-rowindex property to identify the row indices of the rows that are present in the DOM.

The aria-rowcount is specified on the element with the table, grid, or treegrid role. Its value is an integer equal to the total number of rows available, including header rows. If the total number of rows is unknown, a value of -1 may be specified. Using a value of -1 indicates that more rows are available to include in the DOM without specifying the size of the available supply.

Using -1 could be an approach we could use as well, especially when it comes down to async loading combined with virtualization

Copy link
Member Author

Choose a reason for hiding this comment

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

OK I think the argument for the total number is fairly strong. We can test and get feedback.

@devongovett devongovett added this pull request to the merge queue Feb 28, 2026
Merged via the queue into main with commit 745de5b Feb 28, 2026
29 checks passed
@devongovett devongovett deleted the refactor-tree-collection branch February 28, 2026 00:19
pioug pushed a commit to pioug/react-spectrum that referenced this pull request Feb 28, 2026
…aseline-tracker

* origin/main:
  feat(S2): S2 ListView (adobe#8878)
  refactor: Centralize expandedKeys logic in TreeCollection (adobe#9711)
  chore: Warn if user has interactive elements in their custom Picker value (adobe#9710)
  feat: S2 unavailable menu item (adobe#9657)
  fix: Ensure that opening a submenu via enter/space moves focus to first item in submenu (adobe#9691)
  fix: prevent docs crash by making template elements always append children into .content (adobe#9703)
  docs(RAC): Add TreeSection docs (adobe#9699)
  docs(S2): add Typography search view (adobe#9524)
  docs(S2): fix clipping in Picker custom value AvatarGroup example (adobe#9702)
  fix: patch additional methods so React doesnt break with template elements (adobe#9385)
  tentative fix (adobe#9635)
  docs(S2): fix icon import clipboard content to add underscore for icons starting with number (adobe#9698)
  feat(S2): add ActionBar support to TreeView (adobe#9695)
  fix: combobox interactoutside (adobe#9646)
  fix: skip native Date fast path when local timezone is overridden via setLocalTimeZone (adobe#9678)
  chore: update storybook to 9 (adobe#8634)
  docs: improve custom render value S2 Picker example (adobe#9682)

# Conflicts:
#	yarn.lock
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