10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
- import { act , fireEvent , mockClickDefault , pointerMap , render , within } from '@react-spectrum/test-utils-internal' ;
13
+ import { act , fireEvent , mockClickDefault , pointerMap , render , setupIntersectionObserverMock , within } from '@react-spectrum/test-utils-internal' ;
14
14
import {
15
15
Button ,
16
16
Checkbox ,
17
+ Collection ,
17
18
Dialog ,
18
19
DialogTrigger ,
19
20
DropIndicator ,
@@ -32,6 +33,7 @@ import {
32
33
} from '../' ;
33
34
import { getFocusableTreeWalker } from '@react-aria/focus' ;
34
35
import React from 'react' ;
36
+ import { UNSTABLE_GridListLoadingSentinel } from '../src/GridList' ;
35
37
import { User } from '@react-aria/test-utils' ;
36
38
import userEvent from '@testing-library/user-event' ;
37
39
@@ -827,9 +829,9 @@ describe('GridList', () => {
827
829
let { getAllByRole} = renderGridList ( { selectionMode : 'single' , onSelectionChange} ) ;
828
830
let items = getAllByRole ( 'row' ) ;
829
831
830
- await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
832
+ await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
831
833
expect ( onSelectionChange ) . toBeCalledTimes ( 1 ) ;
832
-
834
+
833
835
await user . pointer ( { target : items [ 0 ] , keys : '[/MouseLeft]' } ) ;
834
836
expect ( onSelectionChange ) . toBeCalledTimes ( 1 ) ;
835
837
} ) ;
@@ -839,9 +841,9 @@ describe('GridList', () => {
839
841
let { getAllByRole} = renderGridList ( { selectionMode : 'single' , onSelectionChange, shouldSelectOnPressUp : false } ) ;
840
842
let items = getAllByRole ( 'row' ) ;
841
843
842
- await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
844
+ await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
843
845
expect ( onSelectionChange ) . toBeCalledTimes ( 1 ) ;
844
-
846
+
845
847
await user . pointer ( { target : items [ 0 ] , keys : '[/MouseLeft]' } ) ;
846
848
expect ( onSelectionChange ) . toBeCalledTimes ( 1 ) ;
847
849
} ) ;
@@ -851,11 +853,215 @@ describe('GridList', () => {
851
853
let { getAllByRole} = renderGridList ( { selectionMode : 'single' , onSelectionChange, shouldSelectOnPressUp : true } ) ;
852
854
let items = getAllByRole ( 'row' ) ;
853
855
854
- await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
856
+ await user . pointer ( { target : items [ 0 ] , keys : '[MouseLeft>]' } ) ;
855
857
expect ( onSelectionChange ) . toBeCalledTimes ( 0 ) ;
856
-
858
+
857
859
await user . pointer ( { target : items [ 0 ] , keys : '[/MouseLeft]' } ) ;
858
860
expect ( onSelectionChange ) . toBeCalledTimes ( 1 ) ;
859
861
} ) ;
860
862
} ) ;
863
+
864
+ describe ( 'async loading' , ( ) => {
865
+ let items = [
866
+ { name : 'Foo' } ,
867
+ { name : 'Bar' } ,
868
+ { name : 'Baz' }
869
+ ] ;
870
+ let renderEmptyState = ( ) => {
871
+ return (
872
+ < div > empty state</ div >
873
+ ) ;
874
+ } ;
875
+ let AsyncGridList = ( props ) => {
876
+ let { items, isLoading, onLoadMore, ...listBoxProps } = props ;
877
+ return (
878
+ < GridList
879
+ { ...listBoxProps }
880
+ aria-label = "async gridlist"
881
+ renderEmptyState = { ( ) => renderEmptyState ( ) } >
882
+ < Collection items = { items } >
883
+ { ( item ) => (
884
+ < GridListItem id = { item . name } > { item . name } </ GridListItem >
885
+ ) }
886
+ </ Collection >
887
+ < UNSTABLE_GridListLoadingSentinel isLoading = { isLoading } onLoadMore = { onLoadMore } >
888
+ Loading...
889
+ </ UNSTABLE_GridListLoadingSentinel >
890
+ </ GridList >
891
+ ) ;
892
+ } ;
893
+
894
+ let onLoadMore = jest . fn ( ) ;
895
+ let observe = jest . fn ( ) ;
896
+ afterEach ( ( ) => {
897
+ jest . clearAllMocks ( ) ;
898
+ } ) ;
899
+
900
+ it ( 'should render the loading element when loading' , async ( ) => {
901
+ let tree = render ( < AsyncGridList isLoading items = { items } /> ) ;
902
+
903
+ let gridListTester = testUtilUser . createTester ( 'GridList' , { root : tree . getByRole ( 'grid' ) } ) ;
904
+ let rows = gridListTester . rows ;
905
+ expect ( rows ) . toHaveLength ( 4 ) ;
906
+ let loaderRow = rows [ 3 ] ;
907
+ expect ( loaderRow ) . toHaveTextContent ( 'Loading...' ) ;
908
+
909
+ let sentinel = tree . getByTestId ( 'loadMoreSentinel' ) ;
910
+ expect ( sentinel . parentElement ) . toHaveAttribute ( 'inert' ) ;
911
+ } ) ;
912
+
913
+ it ( 'should render the sentinel but not the loading indicator when not loading' , async ( ) => {
914
+ let tree = render ( < AsyncGridList items = { items } /> ) ;
915
+
916
+ let gridListTester = testUtilUser . createTester ( 'GridList' , { root : tree . getByRole ( 'grid' ) } ) ;
917
+ let rows = gridListTester . rows ;
918
+ expect ( rows ) . toHaveLength ( 3 ) ;
919
+ expect ( tree . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
920
+ expect ( tree . getByTestId ( 'loadMoreSentinel' ) ) . toBeInTheDocument ( ) ;
921
+ } ) ;
922
+
923
+ it ( 'should properly render the renderEmptyState if gridlist is empty, even when loading' , async ( ) => {
924
+ let tree = render ( < AsyncGridList items = { [ ] } /> ) ;
925
+
926
+ let gridListTester = testUtilUser . createTester ( 'GridList' , { root : tree . getByRole ( 'grid' ) } ) ;
927
+ let rows = gridListTester . rows ;
928
+ expect ( rows ) . toHaveLength ( 1 ) ;
929
+ expect ( rows [ 0 ] ) . toHaveTextContent ( 'empty state' ) ;
930
+ expect ( tree . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
931
+ expect ( tree . getByTestId ( 'loadMoreSentinel' ) ) . toBeInTheDocument ( ) ;
932
+
933
+ tree . rerender ( < AsyncGridList items = { [ ] } isLoading /> ) ;
934
+ rows = gridListTester . rows ;
935
+ expect ( rows ) . toHaveLength ( 1 ) ;
936
+ expect ( rows [ 0 ] ) . toHaveTextContent ( 'empty state' ) ;
937
+ expect ( tree . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
938
+ expect ( tree . getByTestId ( 'loadMoreSentinel' ) ) . toBeInTheDocument ( ) ;
939
+ } ) ;
940
+
941
+ it ( 'should only fire loadMore when not loading and intersection is detected' , async ( ) => {
942
+ let observer = setupIntersectionObserverMock ( {
943
+ observe
944
+ } ) ;
945
+
946
+ let tree = render ( < AsyncGridList items = { items } onLoadMore = { onLoadMore } isLoading /> ) ;
947
+ let sentinel = tree . getByTestId ( 'loadMoreSentinel' ) ;
948
+ expect ( observe ) . toHaveBeenCalledTimes ( 2 ) ;
949
+ expect ( observe ) . toHaveBeenLastCalledWith ( sentinel ) ;
950
+ expect ( onLoadMore ) . toHaveBeenCalledTimes ( 0 ) ;
951
+
952
+ act ( ( ) => { observer . instance . triggerCallback ( [ { isIntersecting : true } ] ) ; } ) ;
953
+ expect ( onLoadMore ) . toHaveBeenCalledTimes ( 0 ) ;
954
+
955
+ tree . rerender ( < AsyncGridList items = { items } onLoadMore = { onLoadMore } /> ) ;
956
+ expect ( observe ) . toHaveBeenCalledTimes ( 3 ) ;
957
+ expect ( observe ) . toHaveBeenLastCalledWith ( sentinel ) ;
958
+ expect ( onLoadMore ) . toHaveBeenCalledTimes ( 0 ) ;
959
+
960
+ act ( ( ) => { observer . instance . triggerCallback ( [ { isIntersecting : true } ] ) ; } ) ;
961
+ expect ( onLoadMore ) . toHaveBeenCalledTimes ( 1 ) ;
962
+ } ) ;
963
+
964
+ describe ( 'virtualized' , ( ) => {
965
+ let items = [ ] ;
966
+ for ( let i = 0 ; i < 50 ; i ++ ) {
967
+ items . push ( { name : 'Foo' + i } ) ;
968
+ }
969
+ let clientWidth , clientHeight ;
970
+
971
+ beforeAll ( ( ) => {
972
+ clientWidth = jest . spyOn ( window . HTMLElement . prototype , 'clientWidth' , 'get' ) . mockImplementation ( ( ) => 100 ) ;
973
+ clientHeight = jest . spyOn ( window . HTMLElement . prototype , 'clientHeight' , 'get' ) . mockImplementation ( ( ) => 100 ) ;
974
+ } ) ;
975
+
976
+ afterAll ( function ( ) {
977
+ clientWidth . mockReset ( ) ;
978
+ clientHeight . mockReset ( ) ;
979
+ } ) ;
980
+
981
+ let VirtualizedAsyncGridList = ( props ) => {
982
+ let { items, isLoading, onLoadMore, ...listBoxProps } = props ;
983
+ return (
984
+ < Virtualizer
985
+ layout = { ListLayout }
986
+ layoutOptions = { {
987
+ rowHeight : 25 ,
988
+ loaderHeight : 30
989
+ } } >
990
+ < GridList
991
+ { ...listBoxProps }
992
+ aria-label = "async virtualized gridlist"
993
+ renderEmptyState = { ( ) => renderEmptyState ( ) } >
994
+ < Collection items = { items } >
995
+ { ( item ) => (
996
+ < GridListItem id = { item . name } > { item . name } </ GridListItem >
997
+ ) }
998
+ </ Collection >
999
+ < UNSTABLE_GridListLoadingSentinel isLoading = { isLoading } onLoadMore = { onLoadMore } >
1000
+ Loading...
1001
+ </ UNSTABLE_GridListLoadingSentinel >
1002
+ </ GridList >
1003
+ </ Virtualizer >
1004
+ ) ;
1005
+ } ;
1006
+
1007
+ it ( 'should always render the sentinel even when virtualized' , async ( ) => {
1008
+ let tree = render ( < VirtualizedAsyncGridList isLoading items = { items } /> ) ;
1009
+
1010
+ let gridListTester = testUtilUser . createTester ( 'GridList' , { root : tree . getByRole ( 'grid' ) } ) ;
1011
+ let rows = gridListTester . rows ;
1012
+ expect ( rows ) . toHaveLength ( 8 ) ;
1013
+ let loaderRow = rows [ 7 ] ;
1014
+ expect ( loaderRow ) . toHaveTextContent ( 'Loading...' ) ;
1015
+ expect ( loaderRow ) . toHaveAttribute ( 'aria-rowindex' , '51' ) ;
1016
+ let loaderParentStyles = loaderRow . parentElement . style ;
1017
+
1018
+ // 50 items * 25px = 1250
1019
+ expect ( loaderParentStyles . top ) . toBe ( '1250px' ) ;
1020
+ expect ( loaderParentStyles . height ) . toBe ( '30px' ) ;
1021
+
1022
+ let sentinel = within ( loaderRow . parentElement ) . getByTestId ( 'loadMoreSentinel' ) ;
1023
+ expect ( sentinel . parentElement ) . toHaveAttribute ( 'inert' ) ;
1024
+ } ) ;
1025
+
1026
+ it ( 'should not reserve room for the loader if isLoading is false or if gridlist is empty' , async ( ) => {
1027
+ let tree = render ( < VirtualizedAsyncGridList items = { items } /> ) ;
1028
+
1029
+ let gridListTester = testUtilUser . createTester ( 'GridList' , { root : tree . getByRole ( 'grid' ) } ) ;
1030
+ let rows = gridListTester . rows ;
1031
+ expect ( rows ) . toHaveLength ( 7 ) ;
1032
+ expect ( within ( gridListTester . gridlist ) . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
1033
+
1034
+ let sentinel = within ( gridListTester . gridlist ) . getByTestId ( 'loadMoreSentinel' ) ;
1035
+ let sentinelParentStyles = sentinel . parentElement . parentElement . style ;
1036
+ expect ( sentinelParentStyles . top ) . toBe ( '1250px' ) ;
1037
+ expect ( sentinelParentStyles . height ) . toBe ( '0px' ) ;
1038
+ expect ( sentinel . parentElement ) . toHaveAttribute ( 'inert' ) ;
1039
+
1040
+ tree . rerender ( < VirtualizedAsyncGridList items = { [ ] } /> ) ;
1041
+ rows = gridListTester . rows ;
1042
+ expect ( rows ) . toHaveLength ( 1 ) ;
1043
+ let emptyStateRow = rows [ 0 ] ;
1044
+ expect ( emptyStateRow ) . toHaveTextContent ( 'empty state' ) ;
1045
+ expect ( within ( gridListTester . gridlist ) . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
1046
+
1047
+ sentinel = within ( gridListTester . gridlist ) . getByTestId ( 'loadMoreSentinel' ) ;
1048
+ sentinelParentStyles = sentinel . parentElement . parentElement . style ;
1049
+ expect ( sentinelParentStyles . top ) . toBe ( '0px' ) ;
1050
+ expect ( sentinelParentStyles . height ) . toBe ( '0px' ) ;
1051
+
1052
+ // Same as above, setting isLoading when gridlist is empty shouldnt change the layout
1053
+ tree . rerender ( < VirtualizedAsyncGridList items = { [ ] } isLoading /> ) ;
1054
+ rows = gridListTester . rows ;
1055
+ expect ( rows ) . toHaveLength ( 1 ) ;
1056
+ emptyStateRow = rows [ 0 ] ;
1057
+ expect ( emptyStateRow ) . toHaveTextContent ( 'empty state' ) ;
1058
+ expect ( within ( gridListTester . gridlist ) . queryByText ( 'Loading...' ) ) . toBeFalsy ( ) ;
1059
+
1060
+ sentinel = within ( gridListTester . gridlist ) . getByTestId ( 'loadMoreSentinel' ) ;
1061
+ sentinelParentStyles = sentinel . parentElement . parentElement . style ;
1062
+ expect ( sentinelParentStyles . top ) . toBe ( '0px' ) ;
1063
+ expect ( sentinelParentStyles . height ) . toBe ( '0px' ) ;
1064
+ } ) ;
1065
+ } ) ;
1066
+ } ) ;
861
1067
} ) ;
0 commit comments