@@ -6,13 +6,14 @@ import {
6
6
} from '@primer/octicons-react'
7
7
import { useSSRSafeId } from '@react-aria/ssr'
8
8
import React from 'react'
9
- import styled from 'styled-components'
9
+ import styled , { keyframes } from 'styled-components'
10
10
import Box from '../Box'
11
+ import { get } from '../constants'
11
12
import { useControllableState } from '../hooks/useControllableState'
12
13
import useSafeTimeout from '../hooks/useSafeTimeout'
13
14
import Spinner from '../Spinner'
14
15
import StyledOcticon from '../StyledOcticon'
15
- import sx , { SxProp } from '../sx'
16
+ import sx , { SxProp , merge } from '../sx'
16
17
import Text from '../Text'
17
18
import { Theme } from '../ThemeProvider'
18
19
import createSlots from '../utils/create-slots'
@@ -112,12 +113,15 @@ export type TreeViewItemProps = {
112
113
expanded ?: boolean
113
114
onExpandedChange ?: ( expanded : boolean ) => void
114
115
onSelect ?: ( event : React . MouseEvent < HTMLElement > | React . KeyboardEvent < HTMLElement > ) => void
115
- }
116
+ } & SxProp
116
117
117
118
const { Slots, Slot} = createSlots ( [ 'LeadingVisual' , 'TrailingVisual' ] )
118
119
119
120
const 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
+ ) => {
121
125
const itemId = useSSRSafeId ( )
122
126
const labelId = useSSRSafeId ( )
123
127
const leadingVisualId = useSSRSafeId ( )
@@ -219,54 +223,57 @@ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
219
223
toggle ( event )
220
224
}
221
225
} }
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
+ }
254
273
}
255
274
} ,
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
+ ] ) }
270
277
>
271
278
< Box sx = { { gridArea : 'spacer' , display : 'flex' } } >
272
279
< LevelIndicatorLines level = { level } />
@@ -401,9 +408,13 @@ export type SubTreeState = 'initial' | 'loading' | 'done' | 'error'
401
408
export type TreeViewSubTreeProps = {
402
409
children ?: React . ReactNode
403
410
state ?: SubTreeState
411
+ /**
412
+ * Display a skeleton loading state with the specified count of items
413
+ */
414
+ count ?: number
404
415
}
405
416
406
- const SubTree : React . FC < TreeViewSubTreeProps > = ( { state, children} ) => {
417
+ const SubTree : React . FC < TreeViewSubTreeProps > = ( { count , state, children} ) => {
407
418
const { announceUpdate} = React . useContext ( RootContext )
408
419
const { itemId, isExpanded} = React . useContext ( ItemContext )
409
420
const [ isLoadingItemVisible , setIsLoadingItemVisible ] = React . useState ( false )
@@ -469,14 +480,112 @@ const SubTree: React.FC<TreeViewSubTreeProps> = ({state, children}) => {
469
480
margin : 0
470
481
} }
471
482
>
472
- { isLoadingItemVisible ? < LoadingItem ref = { loadingItemRef } /> : children }
483
+ { isLoadingItemVisible ? < LoadingItem ref = { loadingItemRef } count = { count } /> : children }
473
484
</ Box >
474
485
)
475
486
}
476
487
477
488
SubTree . displayName = 'TreeView.SubTree'
478
489
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
+
480
589
return (
481
590
< Item ref = { ref } >
482
591
< LeadingVisual >
0 commit comments