@@ -81,11 +81,21 @@ function calculateState(
8181 // Count matches that are inside or at collapsed nodes
8282 flattenResult . data . forEach ( ( item ) => {
8383 if ( item . collapsed && item . path ) {
84- const prefix = item . path + '/' ;
84+ const itemPath = item . path ;
8585 const count = allMatchPaths . filter ( ( matchPath ) => {
86- // Match if path is exactly the item path (match at the node itself)
87- // or starts with prefix (match inside the node)
88- return matchPath === item . path || matchPath . startsWith ( prefix ) ;
86+ const sameStart = matchPath . startsWith ( itemPath ) ;
87+ if ( ! sameStart ) {
88+ return false ;
89+ }
90+ if ( itemPath . length === matchPath . length ) {
91+ // exact match
92+ return true ;
93+ }
94+ if ( matchPath [ itemPath . length ] === '/' ) {
95+ // match inside the node
96+ return true ;
97+ }
98+ return false ;
8999 } ) . length ;
90100 if ( count > 0 ) {
91101 item . hiddenMatches = count ;
@@ -234,12 +244,17 @@ export class StructuredYson extends React.PureComponent<Props, State> {
234244 } ;
235245
236246 findCollapsedParent = ( matchPath : string , collapsedState : CollapsedState ) : string | null => {
237- const pathParts = matchPath . split ( '/' ) ;
238- for ( let i = 1 ; i <= pathParts . length ; i ++ ) {
239- const checkPath = pathParts . slice ( 0 , i ) . join ( '/' ) ;
247+ let nextSlash = 0 ;
248+ while ( ( nextSlash = matchPath . indexOf ( '/' , nextSlash ) ) !== - 1 ) {
249+ const checkPath = matchPath . slice ( 0 , nextSlash ) ;
240250 if ( collapsedState [ checkPath ] ) {
241251 return checkPath ;
242252 }
253+ nextSlash ++ ;
254+ }
255+ // Check the full path as well
256+ if ( collapsedState [ matchPath ] ) {
257+ return matchPath ;
243258 }
244259 return null ;
245260 } ;
@@ -259,36 +274,76 @@ export class StructuredYson extends React.PureComponent<Props, State> {
259274
260275 const targetMatchPath = allMatchPaths [ nextTotalIndex ] ;
261276
262- const collapsedParent = this . findCollapsedParent ( targetMatchPath , collapsedState ) ;
277+ // Expand all collapsed parents at once
278+ let effectiveCollapsedState = collapsedState ;
279+ let hasCollapsedParents = false ;
263280
264- // If target is not hidden, it's visible - navigate to it
265- if ( collapsedParent = == null ) {
266- // Count how many visible matches are before our target
267- let visibleMatchCount = 0 ;
268- for ( let i = 0 ; i < nextTotalIndex ; i ++ ) {
269- if ( this . findCollapsedParent ( allMatchPaths [ i ] , collapsedState ) === null ) {
270- visibleMatchCount ++ ;
281+ let nextSlash = 0 ;
282+ while ( ( nextSlash = targetMatchPath . indexOf ( '/' , nextSlash ) ) ! == - 1 ) {
283+ const checkPath = targetMatchPath . slice ( 0 , nextSlash ) ;
284+ if ( collapsedState [ checkPath ] ) {
285+ if ( ! hasCollapsedParents ) {
286+ effectiveCollapsedState = { ... collapsedState } ;
287+ hasCollapsedParents = true ;
271288 }
289+ delete effectiveCollapsedState [ checkPath ] ;
272290 }
273-
274- // Navigate to the visible match
275- if ( visibleMatchCount < matchedRows . length ) {
276- this . setState ( { matchIndex : nextTotalIndex } ) ;
277- this . tableRef . current ?. scrollToIndex ( matchedRows [ visibleMatchCount ] ) ;
278- this . searchRef . current ?. focus ( ) ;
291+ nextSlash ++ ;
292+ }
293+ // Check the full path as well
294+ if ( collapsedState [ targetMatchPath ] ) {
295+ if ( ! hasCollapsedParents ) {
296+ effectiveCollapsedState = { ...collapsedState } ;
297+ hasCollapsedParents = true ;
279298 }
299+ delete effectiveCollapsedState [ targetMatchPath ] ;
300+ }
301+
302+ // If we expanded any parents, recalculate state and retry
303+ if ( hasCollapsedParents ) {
304+ this . updateState ( { collapsedState : effectiveCollapsedState } , ( ) => {
305+ // After state recalculation, find the target in the new allMatchPaths
306+ const newAllMatchPaths = this . state . allMatchPaths ;
307+ const newTargetIndex = newAllMatchPaths . indexOf ( targetMatchPath ) ;
308+ if ( newTargetIndex !== - 1 ) {
309+ // Navigate to the target using its new index
310+ const newMatchedRows = this . state . matchedRows ;
311+ let visibleMatchCount = 0 ;
312+ for ( let i = 0 ; i < newTargetIndex ; i ++ ) {
313+ if (
314+ this . findCollapsedParent (
315+ newAllMatchPaths [ i ] ,
316+ this . state . collapsedState ,
317+ ) === null
318+ ) {
319+ visibleMatchCount ++ ;
320+ }
321+ }
322+ if ( visibleMatchCount < newMatchedRows . length ) {
323+ this . setState ( { matchIndex : newTargetIndex } ) ;
324+ this . tableRef . current ?. scrollToIndex ( newMatchedRows [ visibleMatchCount ] ) ;
325+ this . searchRef . current ?. focus ( ) ;
326+ }
327+ }
328+ } ) ;
280329 return ;
281330 }
282331
283- // Target is hidden - expand the collapsed parent
284- const newCollapsedState = { ...collapsedState } ;
285- delete newCollapsedState [ collapsedParent ] ;
332+ // Target is visible - navigate to it
333+ // Count how many visible matches are before our target
334+ let visibleMatchCount = 0 ;
335+ for ( let i = 0 ; i < nextTotalIndex ; i ++ ) {
336+ if ( this . findCollapsedParent ( allMatchPaths [ i ] , effectiveCollapsedState ) === null ) {
337+ visibleMatchCount ++ ;
338+ }
339+ }
286340
287- // Recalculate state with new collapsed state
288- this . updateState ( { collapsedState : newCollapsedState , matchIndex : nextTotalIndex } , ( ) => {
289- // Retry navigation to the same target
290- this . onNextMatch ( null , 0 ) ;
291- } ) ;
341+ // Navigate to the visible match
342+ if ( visibleMatchCount < matchedRows . length ) {
343+ this . setState ( { matchIndex : nextTotalIndex } ) ;
344+ this . tableRef . current ?. scrollToIndex ( matchedRows [ visibleMatchCount ] ) ;
345+ this . searchRef . current ?. focus ( ) ;
346+ }
292347 } ;
293348
294349 onPrevMatch = ( ) => {
0 commit comments