@@ -10,7 +10,6 @@ import styled, {keyframes} from 'styled-components'
1010import  { get }  from  '../constants' 
1111import  { ConfirmationDialog }  from  '../Dialog/ConfirmationDialog' 
1212import  { useControllableState }  from  '../hooks/useControllableState' 
13- import  useSafeTimeout  from  '../hooks/useSafeTimeout' 
1413import  { useId }  from  '../hooks/useId' 
1514import  Spinner  from  '../Spinner' 
1615import  sx ,  { SxProp }  from  '../sx' 
@@ -510,17 +509,10 @@ export type TreeViewSubTreeProps = {
510509const  SubTree : React . FC < TreeViewSubTreeProps >  =  ( { count,  state,  children} )  =>  { 
511510  const  { announceUpdate}  =  React . useContext ( RootContext ) 
512511  const  { itemId,  isExpanded,  isSubTreeEmpty,  setIsSubTreeEmpty}  =  React . useContext ( ItemContext ) 
513-   const  [ isLoadingItemVisible ,  setIsLoadingItemVisible ]  =  React . useState ( false ) 
514-   const  { safeSetTimeout}  =  useSafeTimeout ( ) 
515512  const  loadingItemRef  =  React . useRef < HTMLElement > ( null ) 
516513  const  ref  =  React . useRef < HTMLElement > ( null ) 
517-   const  [ isPending ,  setPending ]  =  React . useState ( state  ===  'loading' ) 
518- 
519-   React . useEffect ( ( )  =>  { 
520-     if  ( state  ===  'loading' )  { 
521-       setPending ( true ) 
522-     } 
523-   } ,  [ state ] ) 
514+   const  [ loadingFocused ,  setLoadingFocused ]  =  React . useState ( false ) 
515+   const  previousState  =  usePreviousValue ( state ) 
524516
525517  React . useEffect ( ( )  =>  { 
526518    // If `state` is undefined, we're working in a synchronous context and need 
@@ -536,61 +528,66 @@ const SubTree: React.FC<TreeViewSubTreeProps> = ({count, state, children}) => {
536528    } 
537529  } ,  [ state ,  isSubTreeEmpty ,  setIsSubTreeEmpty ,  children ] ) 
538530
539-   // If a consumer sets state="done" without having a previous state (like `loading`), 
540-   // then it would announce on the first render. Using isPending is to only 
541-   // announce being "loaded" when the state has changed from `loading` --> `done`. 
531+   // Handle transition from loading to done state 
542532  React . useEffect ( ( )  =>  { 
543-     if  ( isPending  &&  state  ===  'done' )  { 
544-       const  parentItem  =  document . getElementById ( itemId ) 
533+     if  ( previousState  ===  'loading'  &&  state  ===  'done' )  { 
534+       const  parentElement  =  document . getElementById ( itemId ) 
535+       if  ( ! parentElement )  return 
536+ 
537+       // Announce update to screen readers 
538+       const  parentName  =  getAccessibleName ( parentElement ) 
545539
546-       if  ( ! parentItem )  return 
540+       if  ( ref . current ?. childElementCount )  { 
541+         announceUpdate ( `${ parentName }   content loaded` ) 
542+       }  else  { 
543+         announceUpdate ( `${ parentName }   is empty` ) 
544+       } 
547545
548-       const  { current : node }  =  ref 
549-       const  parentName  =  getAccessibleName ( parentItem ) 
546+       // Move focus to the first child if the loading indicator 
547+       // was focused when the async items finished loading 
548+       if  ( loadingFocused )  { 
549+         const  firstChild  =  getFirstChildElement ( parentElement ) 
550550
551-       safeSetTimeout ( ( )  =>  { 
552-         if  ( node  &&  node . childElementCount  >  0 )  { 
553-           announceUpdate ( `${ parentName }   content loaded` ) 
551+         if  ( firstChild )  { 
552+           firstChild . focus ( ) 
554553        }  else  { 
555-           announceUpdate ( ` ${ parentName }  is empty` ) 
554+           parentElement . focus ( ) 
556555        } 
557-       } ) 
558556
559-       setPending ( false ) 
557+         setLoadingFocused ( false ) 
558+       } 
560559    } 
561-   } ,  [ state ,  itemId ,  announceUpdate ,  safeSetTimeout ,   isPending ] ) 
560+   } ,  [ loadingFocused ,   previousState ,   state ,  itemId ,  announceUpdate ,  ref ] ) 
562561
563-   // Manage  loading indicator state  
562+   // Track focus on the  loading indicator 
564563  React . useEffect ( ( )  =>  { 
565-     // If we're in the loading state, but not showing the loading indicator yet, 
566-     // show the loading indicator 
567-     if  ( state  ===  'loading'  &&  ! isLoadingItemVisible )  { 
568-       setIsLoadingItemVisible ( true ) 
564+     function  handleFocus ( )  { 
565+       setLoadingFocused ( true ) 
569566    } 
570567
571-     // If we're not in the loading state, but we're still showing a loading indicator, 
572-     // hide the loading indicator and move focus if necessary 
573-     if  ( state  !==  'loading'  &&  isLoadingItemVisible )  { 
574-       const  isLoadingItemFocused  =  document . activeElement  ===  loadingItemRef . current 
568+     function  handleBlur ( event : FocusEvent )  { 
569+       // Skip blur events that are caused by the element being removed from the DOM. 
570+       // This can happen when the loading indicator is focused when async items are 
571+       // done loading and the loading indicator is removed from the DOM. 
572+       // If `loadingFocused` is `true` when `state` is `"done"` then the loading indicator 
573+       // was focused when the async items finished loading and we need to move focus to the 
574+       // first child. 
575+       if  ( ! event . relatedTarget )  return 
575576
576-       setIsLoadingItemVisible ( false ) 
577+       setLoadingFocused ( false ) 
578+     } 
577579
578-       if  ( isLoadingItemFocused )  { 
579-         safeSetTimeout ( ( )  =>  { 
580-           const  parentElement  =  document . getElementById ( itemId ) 
581-           if  ( ! parentElement )  return 
580+     const  loadingElement  =  loadingItemRef . current 
581+     if  ( ! loadingElement )  return 
582582
583-           const  firstChild  =  getFirstChildElement ( parentElement ) 
583+     loadingElement . addEventListener ( 'focus' ,  handleFocus ) 
584+     loadingElement . addEventListener ( 'blur' ,  handleBlur ) 
584585
585-           if  ( firstChild )  { 
586-             firstChild . focus ( ) 
587-           }  else  { 
588-             parentElement . focus ( ) 
589-           } 
590-         } ) 
591-       } 
586+     return  ( )  =>  { 
587+       loadingElement . removeEventListener ( 'focus' ,  handleFocus ) 
588+       loadingElement . removeEventListener ( 'blur' ,  handleBlur ) 
592589    } 
593-   } ,  [ state ,   safeSetTimeout ,   isLoadingItemVisible ,   itemId ] ) 
590+   } ,  [ loadingItemRef ,   state ] ) 
594591
595592  if  ( ! isExpanded )  { 
596593    return  null 
@@ -607,13 +604,23 @@ const SubTree: React.FC<TreeViewSubTreeProps> = ({count, state, children}) => {
607604      // @ts -ignore Box doesn't have type support for `ref` used in combination with `as` 
608605      ref = { ref } 
609606    > 
610-       { isLoadingItemVisible  ? < LoadingItem  ref = { loadingItemRef }  count = { count }  />  : children } 
607+       { state   ===   'loading'  ? < LoadingItem  ref = { loadingItemRef }  count = { count }  />  : children } 
611608    </ ul > 
612609  ) 
613610} 
614611
615612SubTree . displayName  =  'TreeView.SubTree' 
616613
614+ function  usePreviousValue < T > ( value : T ) : T  { 
615+   const  ref  =  React . useRef ( value ) 
616+ 
617+   React . useEffect ( ( )  =>  { 
618+     ref . current  =  value 
619+   } ,  [ value ] ) 
620+ 
621+   return  ref . current 
622+ } 
623+ 
617624const  shimmer  =  keyframes ` 
618625  from { mask-position: 200%; } 
619626  to { mask-position: 0%; } 
0 commit comments