@@ -6,13 +6,14 @@ import {
66}  from  '@primer/octicons-react' 
77import  { useSSRSafeId }  from  '@react-aria/ssr' 
88import  React  from  'react' 
9- import  styled  from  'styled-components' 
9+ import  styled ,   { keyframes }  from  'styled-components' 
1010import  Box  from  '../Box' 
11+ import  { get }  from  '../constants' 
1112import  { useControllableState }  from  '../hooks/useControllableState' 
1213import  useSafeTimeout  from  '../hooks/useSafeTimeout' 
1314import  Spinner  from  '../Spinner' 
1415import  StyledOcticon  from  '../StyledOcticon' 
15- import  sx ,  { SxProp }  from  '../sx' 
16+ import  sx ,  { SxProp ,   merge }  from  '../sx' 
1617import  Text  from  '../Text' 
1718import  { Theme }  from  '../ThemeProvider' 
1819import  createSlots  from  '../utils/create-slots' 
@@ -112,12 +113,15 @@ export type TreeViewItemProps = {
112113  expanded ?: boolean 
113114  onExpandedChange ?: ( expanded : boolean )  =>  void 
114115  onSelect ?: ( event : React . MouseEvent < HTMLElement >  |  React . KeyboardEvent < HTMLElement > )  =>  void 
115- } 
116+ }   &   SxProp 
116117
117118const  { Slots,  Slot}  =  createSlots ( [ 'LeadingVisual' ,  'TrailingVisual' ] ) 
118119
119120const  Item  =  React . forwardRef < HTMLElement ,  TreeViewItemProps > ( 
120-   ( { current : isCurrentItem  =  false ,  defaultExpanded =  false ,  expanded,  onExpandedChange,  onSelect,  children} ,  ref )  =>  { 
121+   ( 
122+     { current : isCurrentItem  =  false ,  defaultExpanded =  false ,  expanded,  onExpandedChange,  onSelect,  children,  sx =  { } } , 
123+     ref 
124+   )  =>  { 
121125    const  itemId  =  useSSRSafeId ( ) 
122126    const  labelId  =  useSSRSafeId ( ) 
123127    const  leadingVisualId  =  useSSRSafeId ( ) 
@@ -219,54 +223,57 @@ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
219223                toggle ( event ) 
220224              } 
221225            } } 
222-             sx = { { 
223-               '--toggle-width' : '1rem' ,  // 16px 
224-               position : 'relative' , 
225-               display : 'grid' , 
226-               gridTemplateColumns : `calc(${ level  -  1 }  , 
227-               gridTemplateAreas : `"spacer toggle content"` , 
228-               width : '100%' , 
229-               height : '2rem' ,  // 32px 
230-               fontSize : 1 , 
231-               color : 'fg.default' , 
232-               borderRadius : 2 , 
233-               cursor : 'pointer' , 
234-               '&:hover' : { 
235-                 backgroundColor : 'actionListItem.default.hoverBg' , 
236-                 '@media (forced-colors: active)' : { 
237-                   outline : '2px solid transparent' , 
238-                   outlineOffset : - 2 
239-                 } 
240-               } , 
241-               '@media (pointer: coarse)' : { 
242-                 '--toggle-width' : '1.5rem' ,  // 24px 
243-                 height : '2.75rem'  // 44px 
244-               } , 
245-               // WARNING: styled-components v5.2 introduced a bug that changed 
246-               // how it expands `&` in CSS selectors. The following selectors 
247-               // are unnecessarily specific to work around that styled-components bug. 
248-               // Reference issue: https://github.com/styled-components/styled-components/issues/3265 
249-               [ `#${ itemId }  ] : { 
250-                 boxShadow : ( theme : Theme )  =>  `inset 0 0 0 2px ${ theme . colors . accent . emphasis }  , 
251-                 '@media (forced-colors: active)' : { 
252-                   outline : '2px solid SelectedItem' , 
253-                   outlineOffset : - 2 
226+             sx = { merge . all ( [ 
227+               { 
228+                 '--toggle-width' : '1rem' ,  // 16px 
229+                 position : 'relative' , 
230+                 display : 'grid' , 
231+                 gridTemplateColumns : `calc(${ level  -  1 }  , 
232+                 gridTemplateAreas : `"spacer toggle content"` , 
233+                 width : '100%' , 
234+                 minHeight : '2rem' ,  // 32px 
235+                 fontSize : 1 , 
236+                 color : 'fg.default' , 
237+                 borderRadius : 2 , 
238+                 cursor : 'pointer' , 
239+                 '&:hover' : { 
240+                   backgroundColor : 'actionListItem.default.hoverBg' , 
241+                   '@media (forced-colors: active)' : { 
242+                     outline : '2px solid transparent' , 
243+                     outlineOffset : - 2 
244+                   } 
245+                 } , 
246+                 '@media (pointer: coarse)' : { 
247+                   '--toggle-width' : '1.5rem' ,  // 24px 
248+                   minHeight : '2.75rem'  // 44px 
249+                 } , 
250+                 // WARNING: styled-components v5.2 introduced a bug that changed 
251+                 // how it expands `&` in CSS selectors. The following selectors 
252+                 // are unnecessarily specific to work around that styled-components bug. 
253+                 // Reference issue: https://github.com/styled-components/styled-components/issues/3265 
254+                 [ `#${ itemId }  ] : { 
255+                   boxShadow : ( theme : Theme )  =>  `inset 0 0 0 2px ${ theme . colors . accent . emphasis }  , 
256+                   '@media (forced-colors: active)' : { 
257+                     outline : '2px solid SelectedItem' , 
258+                     outlineOffset : - 2 
259+                   } 
260+                 } , 
261+                 '[role=treeitem][aria-current=true] > &:is(div)' : { 
262+                   bg : 'actionListItem.default.selectedBg' , 
263+                   '&::after' : { 
264+                     position : 'absolute' , 
265+                     top : 'calc(50% - 12px)' , 
266+                     left : - 2 , 
267+                     width : '4px' , 
268+                     height : '24px' , 
269+                     content : '""' , 
270+                     bg : 'accent.fg' , 
271+                     borderRadius : 2 
272+                   } 
254273                } 
255274              } , 
256-               '[role=treeitem][aria-current=true] > &:is(div)' : { 
257-                 bg : 'actionListItem.default.selectedBg' , 
258-                 '&::after' : { 
259-                   position : 'absolute' , 
260-                   top : 'calc(50% - 12px)' , 
261-                   left : - 2 , 
262-                   width : '4px' , 
263-                   height : '24px' , 
264-                   content : '""' , 
265-                   bg : 'accent.fg' , 
266-                   borderRadius : 2 
267-                 } 
268-               } 
269-             } } 
275+               sx  as  SxProp 
276+             ] ) } 
270277          > 
271278            < Box  sx = { { gridArea : 'spacer' ,  display : 'flex' } } > 
272279              < LevelIndicatorLines  level = { level }  /> 
@@ -401,9 +408,13 @@ export type SubTreeState = 'initial' | 'loading' | 'done' | 'error'
401408export  type  TreeViewSubTreeProps  =  { 
402409  children ?: React . ReactNode 
403410  state ?: SubTreeState 
411+   /** 
412+    * Display a skeleton loading state with the specified count of items 
413+    */ 
414+   count ?: number 
404415} 
405416
406- const  SubTree : React . FC < TreeViewSubTreeProps >  =  ( { state,  children} )  =>  { 
417+ const  SubTree : React . FC < TreeViewSubTreeProps >  =  ( { count ,   state,  children} )  =>  { 
407418  const  { announceUpdate}  =  React . useContext ( RootContext ) 
408419  const  { itemId,  isExpanded}  =  React . useContext ( ItemContext ) 
409420  const  [ isLoadingItemVisible ,  setIsLoadingItemVisible ]  =  React . useState ( false ) 
@@ -469,14 +480,112 @@ const SubTree: React.FC<TreeViewSubTreeProps> = ({state, children}) => {
469480        margin : 0 
470481      } } 
471482    > 
472-       { isLoadingItemVisible  ? < LoadingItem  ref = { loadingItemRef }  />  : children } 
483+       { isLoadingItemVisible  ? < LoadingItem  ref = { loadingItemRef }  count = { count }   />  : children } 
473484    </ Box > 
474485  ) 
475486} 
476487
477488SubTree . displayName  =  'TreeView.SubTree' 
478489
479- const  LoadingItem  =  React . forwardRef < HTMLElement > ( ( props ,  ref )  =>  { 
490+ const  shimmer  =  keyframes ` 
491+   from { mask-position: 200%; } 
492+   to { mask-position: 0%; } 
493+ ` 
494+ 
495+ const  SkeletonItem  =  styled . span ` 
496+   display: flex; 
497+   align-items: center; 
498+   column-gap: 0.5rem; 
499+   height: 2rem; 
500+ 
501+   @media (pointer: coarse) { 
502+     height: 2.75rem; 
503+   } 
504+ 
505+   @media (prefers-reduced-motion: no-preference) { 
506+     mask-image: linear-gradient(75deg, #000 30%, rgba(0, 0, 0, 0.65) 80%); 
507+     mask-size: 200%; 
508+     animation: ${ shimmer }  
509+     animation-duration: 1s; 
510+     animation-iteration-count: infinite; 
511+   } 
512+ 
513+   &::before { 
514+     content: ''; 
515+     display: block; 
516+     width: 1rem; 
517+     height: 1rem; 
518+     background-color: ${ get ( 'colors.neutral.subtle' ) }  
519+     border-radius: 3px; 
520+     @media (forced-colors: active) { 
521+       outline: 1px solid transparent; 
522+       outline-offset: -1px; 
523+     } 
524+   } 
525+ 
526+   &::after { 
527+     content: ''; 
528+     display: block; 
529+     width: var(--tree-item-loading-width, 67%); 
530+     height: 1rem; 
531+     background-color: ${ get ( 'colors.neutral.subtle' ) }  
532+     border-radius: 3px; 
533+     @media (forced-colors: active) { 
534+       outline: 1px solid transparent; 
535+       outline-offset: -1px; 
536+     } 
537+   } 
538+ 
539+   &:nth-of-type(5n + 1) { 
540+     --tree-item-loading-width: 67%; 
541+   } 
542+ 
543+   &:nth-of-type(5n + 2) { 
544+     --tree-item-loading-width: 47%; 
545+   } 
546+ 
547+   &:nth-of-type(5n + 3) { 
548+     --tree-item-loading-width: 73%; 
549+   } 
550+ 
551+   &:nth-of-type(5n + 4) { 
552+     --tree-item-loading-width: 64%; 
553+   } 
554+ 
555+   &:nth-of-type(5n + 5) { 
556+     --tree-item-loading-width: 50%; 
557+   } 
558+ ` 
559+ 
560+ type  LoadingItemProps  =  { 
561+   count ?: number 
562+ } 
563+ 
564+ const  LoadingItem  =  React . forwardRef < HTMLElement ,  LoadingItemProps > ( ( props ,  ref )  =>  { 
565+   const  { count}  =  props 
566+ 
567+   if  ( count )  { 
568+     return  ( 
569+       < Item 
570+         ref = { ref } 
571+         sx = { { 
572+           '&:hover' : { 
573+             backgroundColor : 'transparent' , 
574+             cursor : 'default' , 
575+             '@media (forced-colors: active)' : { 
576+               outline : 'none' 
577+             } 
578+           } 
579+         } } 
580+       > 
581+         { Array . from ( { length : count } ) . map ( ( _ ,  i )  =>  { 
582+           return  < SkeletonItem  aria-hidden = { true }  key = { i }  /> 
583+         } ) } 
584+         < VisuallyHidden > Loading { count }  items</ VisuallyHidden > 
585+       </ Item > 
586+     ) 
587+   } 
588+ 
480589  return  ( 
481590    < Item  ref = { ref } > 
482591      < LeadingVisual > 
0 commit comments