@@ -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 } * (var(--toggle-width) / 2)) var(--toggle-width) 1fr` ,
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 } :focus-visible > &:is(div)` ] : {
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 } * (var(--toggle-width) / 2)) var(--toggle-width) 1fr` ,
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 } :focus-visible > &:is(div)` ] : {
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