Skip to content

Commit 64bccdb

Browse files
Revert "perf(TreeView): replace O(n) TreeWalker with O(depth) sibling traversal" (#7659)
1 parent adadd38 commit 64bccdb

3 files changed

Lines changed: 14 additions & 113 deletions

File tree

.changeset/treeview-sibling-traversal.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/react/src/TreeView/TreeView.test.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,6 @@ describe('Markup', () => {
103103
expect(subtree).toBeNull()
104104
})
105105

106-
it('does not render collapsed subtree children in the DOM', () => {
107-
const {queryByRole} = renderWithTheme(
108-
<TreeView aria-label="Test tree">
109-
<TreeView.Item id="parent">
110-
Parent
111-
<TreeView.SubTree>
112-
<TreeView.Item id="child">Child</TreeView.Item>
113-
</TreeView.SubTree>
114-
</TreeView.Item>
115-
</TreeView>,
116-
)
117-
118-
expect(queryByRole('treeitem', {name: 'Child'})).toBeNull()
119-
})
120-
121106
it('uses aria-current', () => {
122107
const {getByRole} = renderWithTheme(
123108
<TreeView aria-label="Test tree">

packages/react/src/TreeView/useRovingTabIndex.ts

Lines changed: 14 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -142,109 +142,30 @@ export function getElementState(element: HTMLElement): 'open' | 'closed' | 'end'
142142
}
143143
}
144144

145-
/**
146-
* Find the next or previous visible treeitem using direct DOM traversal.
147-
*
148-
* PERFORMANCE: This is O(tree depth) instead of O(n) because it walks
149-
* siblings and parent/child edges directly, rather than creating a TreeWalker
150-
* that scans from the root to find the current element on every keystroke.
151-
*
152-
* NOTE: This relies on TreeView.SubTree unmounting its children when collapsed
153-
* (returning null when !isExpanded). Because collapsed subtree children are
154-
* never in the DOM, we can safely skip them by only entering children of nodes
155-
* with aria-expanded="true". If SubTree ever changes to keep collapsed children
156-
* mounted (e.g. via CSS display:none), this logic would need to add filtering
157-
* for items inside collapsed parents.
158-
*/
159145
export function getVisibleElement(element: HTMLElement, direction: 'next' | 'previous'): HTMLElement | undefined {
160-
if (direction === 'next') {
161-
return getNextVisibleElement(element)
162-
}
163-
return getPreviousVisibleElement(element)
164-
}
165-
166-
function getNextVisibleElement(element: HTMLElement): HTMLElement | undefined {
167-
// If the current item is expanded, the next visible item is its first child
168-
if (element.getAttribute('aria-expanded') === 'true') {
169-
const firstChild = getFirstChildElement(element)
170-
if (firstChild) return firstChild
171-
}
172-
173-
// Otherwise, walk up the tree looking for a next sibling
174-
let current: HTMLElement | undefined = element
175-
while (current) {
176-
const next = getNextSiblingTreeItem(current)
177-
if (next) return next
178-
179-
// No next sibling at this level, try the parent's next sibling
180-
current = getParentElement(current)
181-
}
182-
183-
return undefined
184-
}
146+
const root = element.closest('[role=tree]')
185147

186-
function getPreviousVisibleElement(element: HTMLElement): HTMLElement | undefined {
187-
const prev = getPreviousSiblingTreeItem(element)
148+
if (!root) return
188149

189-
if (prev) {
190-
// Navigate to the deepest last visible descendant of the previous sibling
191-
return getDeepestLastDescendant(prev)
192-
}
150+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, node => {
151+
if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP
152+
return node.getAttribute('role') === 'treeitem' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
153+
})
193154

194-
// No previous sibling, the parent is the previous visible element
195-
return getParentElement(element)
196-
}
155+
let current = walker.firstChild()
197156

198-
/**
199-
* Walk into expanded subtrees to find the deepest last visible descendant.
200-
* For example, if the last sibling is an expanded directory whose last child
201-
* is also an expanded directory, we drill all the way down.
202-
*/
203-
function getDeepestLastDescendant(element: HTMLElement): HTMLElement {
204-
let current = element
205-
while (current.getAttribute('aria-expanded') === 'true') {
206-
const lastChild = getLastChildTreeItem(current)
207-
if (!lastChild) break
208-
current = lastChild
157+
while (current !== element) {
158+
current = walker.nextNode()
209159
}
210-
return current
211-
}
212160

213-
function getNextSiblingTreeItem(element: HTMLElement): HTMLElement | undefined {
214-
let sibling = element.nextElementSibling
215-
while (sibling) {
216-
if (sibling instanceof HTMLElement && sibling.getAttribute('role') === 'treeitem') {
217-
return sibling
218-
}
219-
sibling = sibling.nextElementSibling
220-
}
221-
return undefined
222-
}
161+
let next = direction === 'next' ? walker.nextNode() : walker.previousNode()
223162

224-
function getPreviousSiblingTreeItem(element: HTMLElement): HTMLElement | undefined {
225-
let sibling = element.previousElementSibling
226-
while (sibling) {
227-
if (sibling instanceof HTMLElement && sibling.getAttribute('role') === 'treeitem') {
228-
return sibling
229-
}
230-
sibling = sibling.previousElementSibling
163+
// If next element is nested inside a collapsed subtree, continue iterating
164+
while (next instanceof HTMLElement && next.parentElement?.closest('[role=treeitem][aria-expanded=false]')) {
165+
next = direction === 'next' ? walker.nextNode() : walker.previousNode()
231166
}
232-
return undefined
233-
}
234167

235-
function getLastChildTreeItem(element: HTMLElement): HTMLElement | undefined {
236-
// Find the [role=group] child (the subtree container), then get its last treeitem
237-
for (let i = element.children.length - 1; i >= 0; i--) {
238-
const child = element.children[i]
239-
if (child instanceof HTMLElement && child.getAttribute('role') === 'group') {
240-
let lastChild = child.lastElementChild
241-
while (lastChild && !(lastChild instanceof HTMLElement && lastChild.getAttribute('role') === 'treeitem')) {
242-
lastChild = lastChild.previousElementSibling
243-
}
244-
return lastChild instanceof HTMLElement ? lastChild : undefined
245-
}
246-
}
247-
return undefined
168+
return next instanceof HTMLElement ? next : undefined
248169
}
249170

250171
export function getFirstChildElement(element: HTMLElement): HTMLElement | undefined {

0 commit comments

Comments
 (0)