@@ -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- */
159145export 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
250171export function getFirstChildElement ( element : HTMLElement ) : HTMLElement | undefined {
0 commit comments