12
12
13
13
import { action } from '@storybook/addon-actions' ;
14
14
import { Collection , DropIndicator , GridLayout , Header , ListBox , ListBoxItem , ListBoxProps , ListBoxSection , ListLayout , Separator , Text , useDragAndDrop , Virtualizer , WaterfallLayout } from 'react-aria-components' ;
15
+ import { Key , useAsyncList , useListData } from 'react-stately' ;
16
+ import { Layout , LayoutInfo , Rect , Size } from '@react-stately/virtualizer' ;
15
17
import { MyListBoxItem } from './utils' ;
16
- import React from 'react' ;
17
- import { Size } from '@react-stately/virtualizer' ;
18
+ import React , { useMemo } from 'react' ;
18
19
import styles from '../example/index.css' ;
19
20
import { UNSTABLE_ListBoxLoadingIndicator } from '../src/ListBox' ;
20
- import { useAsyncList , useListData } from 'react-stately' ;
21
21
22
22
export default {
23
23
title : 'React Aria Components'
@@ -510,7 +510,63 @@ AsyncListBox.story = {
510
510
}
511
511
} ;
512
512
513
- export const AsyncListBoxVirtualized = ( ) => {
513
+ class HorizontalLayout extends Layout {
514
+ protected rowWidth : number ;
515
+
516
+ constructor ( options ) {
517
+ super ( ) ;
518
+ this . rowWidth = options . rowWidth ?? 100 ;
519
+ }
520
+
521
+ // Determine which items are visible within the given rectangle.
522
+ getVisibleLayoutInfos ( rect : Rect ) : LayoutInfo [ ] {
523
+ let virtualizer = this . virtualizer ! ;
524
+ let keys = Array . from ( virtualizer . collection . getKeys ( ) ) ;
525
+ let startIndex = Math . max ( 0 , Math . floor ( rect . x / 100 ) ) ;
526
+ let endIndex = Math . min ( keys . length - 1 , Math . ceil ( rect . maxX / 100 ) ) ;
527
+ let layoutInfos = [ ] as LayoutInfo [ ] ;
528
+ for ( let i = startIndex ; i <= endIndex ; i ++ ) {
529
+ let layoutInfo = this . getLayoutInfo ( keys [ i ] ) ;
530
+ if ( layoutInfo ) {
531
+ layoutInfos . push ( layoutInfo ) ;
532
+ }
533
+ }
534
+
535
+ // Always add persisted keys (e.g. the focused item), even when out of view.
536
+ for ( let key of virtualizer . persistedKeys ) {
537
+ let item = virtualizer . collection . getItem ( key ) ;
538
+ let layoutInfo = this . getLayoutInfo ( key ) ;
539
+ if ( item ?. index && layoutInfo ) {
540
+ if ( item ?. index < startIndex ) {
541
+ layoutInfos . unshift ( layoutInfo ) ;
542
+ } else if ( item ?. index > endIndex ) {
543
+ layoutInfos . push ( layoutInfo ) ;
544
+ }
545
+ }
546
+ }
547
+
548
+ return layoutInfos ;
549
+ }
550
+
551
+ // Provide a LayoutInfo for a specific item.
552
+ getLayoutInfo ( key : Key ) : LayoutInfo | null {
553
+ let node = this . virtualizer ! . collection . getItem ( key ) ;
554
+ if ( ! node ) {
555
+ return null ;
556
+ }
557
+
558
+ let rect = new Rect ( node . index * this . rowWidth , 0 , this . rowWidth , 100 ) ;
559
+ return new LayoutInfo ( node . type , node . key , rect ) ;
560
+ }
561
+
562
+ // Provide the total size of all items.
563
+ getContentSize ( ) : Size {
564
+ let numItems = this . virtualizer ! . collection . size ;
565
+ return new Size ( numItems * this . rowWidth , 100 ) ;
566
+ }
567
+ }
568
+
569
+ export const AsyncListBoxVirtualized = ( args ) => {
514
570
let list = useAsyncList < Character > ( {
515
571
async load ( { signal, cursor, filterText} ) {
516
572
if ( cursor ) {
@@ -528,25 +584,56 @@ export const AsyncListBoxVirtualized = () => {
528
584
}
529
585
} ) ;
530
586
587
+ let layout = useMemo ( ( ) => {
588
+ return args . orientation === 'horizontal' ? new HorizontalLayout ( { rowWidth : 100 } ) : new ListLayout ( { rowHeight : 50 , padding : 4 } ) ;
589
+ } , [ args . orientation ] ) ;
531
590
return (
532
591
< Virtualizer
533
- // TODO: loadMore doesn't quite work if we dont set a rowHeight, this is because when
534
- // Will also need to test against a case where there are sections being loaded and/or estimated height
535
- // layout={new ListLayout({estimatedRowHeight: 50})}
536
- // layout={ListLayout}
537
- layout = { new ListLayout ( { rowHeight : 25 } ) } >
592
+ layout = { layout } >
538
593
< ListBox
539
- className = { styles . menu }
540
- style = { { height : 400 } }
594
+ { ...args }
595
+ style = { {
596
+ height : args . orientation === 'horizontal' ? 100 : 400 ,
597
+ width : args . orientation === 'horizontal' ? 400 : 100 ,
598
+ border : '1px solid gray' ,
599
+ background : 'lightgray' ,
600
+ overflow : 'auto' ,
601
+ padding : 'unset' ,
602
+ display : 'flex'
603
+ } }
541
604
aria-label = "async virtualized listbox"
542
605
isLoading = { list . isLoading }
543
606
onLoadMore = { list . loadMore }
544
607
renderEmptyState = { ( ) => list . isLoading ? 'Loading spinner' : 'No results found' } >
545
608
< Collection items = { list . items } >
546
- { item => < MyListBoxItem id = { item . name } > { item . name } </ MyListBoxItem > }
609
+ { ( item : Character ) => (
610
+ < MyListBoxItem
611
+ style = { {
612
+ backgroundColor : 'lightgrey' ,
613
+ border : '1px solid black' ,
614
+ boxSizing : 'border-box' ,
615
+ height : '100%' ,
616
+ width : '100%'
617
+ } }
618
+ id = { item . name } >
619
+ { item . name }
620
+ </ MyListBoxItem >
621
+ ) }
547
622
</ Collection >
548
623
{ list . isLoading && list . items . length > 0 && < MyListBoxLoaderIndicator /> }
549
624
</ ListBox >
550
625
</ Virtualizer >
551
626
) ;
552
627
} ;
628
+
629
+ AsyncListBoxVirtualized . story = {
630
+ args : {
631
+ orientation : 'horizontal'
632
+ } ,
633
+ argTypes : {
634
+ orientation : {
635
+ control : 'radio' ,
636
+ options : [ 'horizontal' , 'vertical' ]
637
+ }
638
+ }
639
+ } ;
0 commit comments