diff --git a/docs/api.md b/docs/api.md index a5272d4d..edceaa42 100644 --- a/docs/api.md +++ b/docs/api.md @@ -241,8 +241,7 @@ It also ensures that the scroll event is propagated properly to parent ScrollVie | `data` | `Array` | The array of data items to render. ⚠️ You should memoize this array for maximum performance. A costly memo depends on it. | | `renderItem` | `(args: { item: T }) => JSX.Element` | A function that returns the JSX element to render for each item in the data array. The function receives an object with the item as a parameter. | | `itemSize` | `number \| ((item: T) => number)` | In case you specify a number it will behave like this : ff vertical, the height of an item; otherwise, the width. You can also specify a function which needs to return for each item of `data` its size in pixel in order for the list to handle various item sizes. ⚠️ You should memoize this function for maximal performances. An important memo depends on it. | -| `numberOfRenderedItems` | `number` | The number of items to be rendered (virtualization size). ⚠️ It must be at least equal to `numberOfItemsVisibleOnScreen +2` or when using jump-on-scroll : `(2 * numberOfItemsVisibleOnScreen) + 1` to ensure correct rendering. | -| `numberOfItemsVisibleOnScreen` | `number` | The number of items visible on the screen. This helps determine how to slice the data and when to stop the scroll at the end of the list. | +| `additionalItemsRendered` | `number` | Optional : The number of items to be rendered (virtualization size) additionally to the elements visible on screen. Base value is 4 for `stick-to-start` and `stick-to-end` scrolls, and twice the number of elements visible for `jump-on-scroll`. | | `onEndReached` | `() => void` | An optional callback function that is called when the user reaches the end of the list. Helps with pagination. | | `onEndReachedThresholdItemsNumber` | `number` | The number of items left to display before triggering the `onEndReached` callback. Defaults to 3. | | `style` | `ViewStyle` | Custom style to be applied to the VirtualizedList container. | @@ -319,10 +318,9 @@ VirtualizedGrids only support vertical orientation (vertically scrollable), but | `renderItem` | `(args: { item: T }) => JSX.Element` | A function that returns the JSX element to render for each item in the data array. The function receives an object with the item as a parameter. | | `numberOfColumns` | `Number` | The number of columns in the grid or the number of items per row. | | `itemHeight` | `Number` | The height of each item in the grid. | -| `numberOfRenderedRows` | `Number` | How many rows are rendered (virtualization size). | +| `additionalRenderedRows` | `Number` | Optional : The number of rows to be rendered (virtualization size) additionally to the rows visible on screen. Base value is 4 for `stick-to-start` and `stick-to-end` scrolls, and twice the number of elements visible for `jump-on-scroll`. | | `header` | `JSX.Element` | Optional header component you can provide to display at the top of a virtualized grid. If provided, you also need to provide its size (so that the grid knows how to scroll) | | `headerSize` | `Number` | The Size in pixels of the (optionnally) provided header. | -| `numberOfRowsVisibleOnScreen` | `Number` | How many rows are visible on the screen (helps with knowing how to slice the data and stop the scroll at the end of the list). | | `onEndReached` | `() => void` | An optional callback function that is called when the user reaches the end of the list. Helps with pagination. | | `onEndReachedThresholdRowsNumber` | `Number` | Number of rows left to display before triggering the onEndReached event. | | `style` | `Object` | Used to modify the style of the grid. | @@ -354,8 +352,7 @@ const renderItem = ({ item }) => { renderItem={renderItem} numberOfColumns={3} itemHeight={100} - numberOfRenderedRows={7} - numberOfRowsVisibleOnScreen={3} + additionalItemsRendered={5} onEndReachedThresholdRowsNumber={2} rowContainerStyle={{gap: 15}} /> diff --git a/packages/example/src/components/VirtualizedSpatialGrid.tsx b/packages/example/src/components/VirtualizedSpatialGrid.tsx index 2dd89175..7c1ceca6 100644 --- a/packages/example/src/components/VirtualizedSpatialGrid.tsx +++ b/packages/example/src/components/VirtualizedSpatialGrid.tsx @@ -10,8 +10,6 @@ import { Header } from '../modules/header/view/Header'; import { BottomArrow, TopArrow } from '../design-system/components/Arrows'; import { ProgramInfo } from '../modules/program/domain/programInfo'; -const NUMBER_OF_ROWS_VISIBLE_ON_SCREEN = 2; -const NUMBER_OF_RENDERED_ROWS = NUMBER_OF_ROWS_VISIBLE_ON_SCREEN + 5; const NUMBER_OF_COLUMNS = 7; const INFINITE_SCROLL_ROW_THRESHOLD = 2; @@ -44,8 +42,6 @@ export const VirtualizedSpatialGrid = ({ containerStyle }: { containerStyle?: Vi renderItem={renderItem} itemHeight={theme.sizes.program.portrait.height * 1.1} numberOfColumns={NUMBER_OF_COLUMNS} - numberOfRenderedRows={NUMBER_OF_RENDERED_ROWS} - numberOfRowsVisibleOnScreen={NUMBER_OF_ROWS_VISIBLE_ON_SCREEN} onEndReachedThresholdRowsNumber={INFINITE_SCROLL_ROW_THRESHOLD} rowContainerStyle={styles.rowStyle} ascendingArrow={} diff --git a/packages/example/src/modules/program/view/ProgramList.tsx b/packages/example/src/modules/program/view/ProgramList.tsx index 3386d35a..d99e23b9 100644 --- a/packages/example/src/modules/program/view/ProgramList.tsx +++ b/packages/example/src/modules/program/view/ProgramList.tsx @@ -21,7 +21,6 @@ import { useKey } from '../../../hooks/useKey'; import React from 'react'; const NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN = 7; -const WINDOW_SIZE = NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN + 8; const ROW_PADDING = scaledPixels(70); const GAP_BETWEEN_ELEMENTS = scaledPixels(30); @@ -101,8 +100,6 @@ export const ProgramList = React.forwardRef( data={programInfos} renderItem={renderItem} itemSize={itemSize} - numberOfRenderedItems={WINDOW_SIZE} - numberOfItemsVisibleOnScreen={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN} onEndReachedThresholdItemsNumber={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN} // @ts-expect-error TODO change the type from ReactElement to ReactNode in the core descendingArrow={isActive ? : null} diff --git a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx index 5bd1d60b..b167d8c0 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx @@ -38,10 +38,8 @@ describe('SpatialNavigationVirtualizedGrid', () => { @@ -56,7 +54,7 @@ describe('SpatialNavigationVirtualizedGrid', () => { expect(screen).toMatchSnapshot(); const listElement = component.getByTestId(gridTestId); - expect(listElement).toHaveStyle({ height: 700 }); + expect(listElement).toHaveStyle({ height: 1000 }); expect(screen.getByText('button 1')).toBeTruthy(); expectButtonToHaveFocus(component, 'button 1'); @@ -74,7 +72,13 @@ describe('SpatialNavigationVirtualizedGrid', () => { expect(screen.getByText('button 13')).toBeTruthy(); expect(screen.getByText('button 14')).toBeTruthy(); expect(screen.getByText('button 15')).toBeTruthy(); - expect(screen.queryByText('button 16')).toBeFalsy(); + expect(screen.getByText('button 16')).toBeTruthy(); + expect(screen.getByText('button 17')).toBeTruthy(); + expect(screen.getByText('button 18')).toBeTruthy(); + expect(screen.getByText('button 19')).toBeTruthy(); + expect(screen.getByText('button 20')).toBeTruthy(); + expect(screen.getByText('button 21')).toBeTruthy(); + expect(screen.queryByText('button 22')).toBeFalsy(); }); it('handles correctly RIGHT & DOWN and RENDERS new elements accordingly while deleting elements that are too far from scroll when on stick to start scroll', () => { @@ -84,7 +88,7 @@ describe('SpatialNavigationVirtualizedGrid', () => { const listElement = component.getByTestId(gridTestId); expectListToHaveScroll(listElement, 0); - expect(listElement).toHaveStyle({ height: 700 }); + expect(listElement).toHaveStyle({ height: 1000 }); testRemoteControlManager.handleRight(); @@ -104,70 +108,69 @@ describe('SpatialNavigationVirtualizedGrid', () => { expect(screen.getByText('button 13')).toBeTruthy(); expect(screen.getByText('button 14')).toBeTruthy(); expect(screen.getByText('button 15')).toBeTruthy(); - expect(screen.queryByText('button 16')).toBeFalsy(); + expect(screen.getByText('button 16')).toBeTruthy(); + expect(screen.getByText('button 17')).toBeTruthy(); + expect(screen.getByText('button 18')).toBeTruthy(); + expect(screen.getByText('button 19')).toBeTruthy(); + expect(screen.getByText('button 20')).toBeTruthy(); + expect(screen.getByText('button 21')).toBeTruthy(); + expect(screen.queryByText('button 22')).toBeFalsy(); testRemoteControlManager.handleDown(); expectListToHaveScroll(listElement, -100); - - expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 2')).toBeTruthy(); - expect(screen.getByText('button 3')).toBeTruthy(); - expect(screen.getByText('button 4')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); expectButtonToHaveFocus(component, 'button 5'); - expect(screen.getByText('button 6')).toBeTruthy(); - expect(screen.getByText('button 7')).toBeTruthy(); - expect(screen.getByText('button 8')).toBeTruthy(); - expect(screen.getByText('button 9')).toBeTruthy(); - expect(screen.getByText('button 10')).toBeTruthy(); - expect(screen.getByText('button 11')).toBeTruthy(); - expect(screen.getByText('button 12')).toBeTruthy(); - expect(screen.getByText('button 13')).toBeTruthy(); - expect(screen.getByText('button 14')).toBeTruthy(); - expect(screen.getByText('button 15')).toBeTruthy(); - expect(screen.queryByText('button 16')).toBeFalsy(); testRemoteControlManager.handleDown(); expectListToHaveScroll(listElement, -200); + expectButtonToHaveFocus(component, 'button 8'); + + testRemoteControlManager.handleDown(); + expectListToHaveScroll(listElement, -300); + expectButtonToHaveFocus(component, 'button 11'); expect(screen.queryByText('button 1')).toBeFalsy(); expect(screen.queryByText('button 2')).toBeFalsy(); expect(screen.queryByText('button 3')).toBeFalsy(); expect(screen.getByText('button 4')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.getByText('button 6')).toBeTruthy(); + + testRemoteControlManager.handleDown(); + expectListToHaveScroll(listElement, -400); + expectButtonToHaveFocus(component, 'button 14'); + + expect(screen.queryByText('button 4')).toBeFalsy(); + expect(screen.queryByText('button 5')).toBeFalsy(); + expect(screen.queryByText('button 6')).toBeFalsy(); expect(screen.getByText('button 7')).toBeTruthy(); - expect(screen.getByText('button 8')).toBeTruthy(); - expectButtonToHaveFocus(component, 'button 8'); - expect(screen.getByText('button 9')).toBeTruthy(); - expect(screen.getByText('button 10')).toBeTruthy(); - expect(screen.getByText('button 11')).toBeTruthy(); - expect(screen.getByText('button 12')).toBeTruthy(); - expect(screen.getByText('button 13')).toBeTruthy(); - expect(screen.getByText('button 14')).toBeTruthy(); - expect(screen.getByText('button 15')).toBeTruthy(); - expect(screen.getByText('button 16')).toBeTruthy(); - expect(screen.getByText('button 17')).toBeTruthy(); - expect(screen.getByText('button 18')).toBeTruthy(); - expect(screen.queryByText('button 19')).toBeFalsy(); - expect(screen).toMatchSnapshot(); + testRemoteControlManager.handleDown(); + expectListToHaveScroll(listElement, -500); + expectButtonToHaveFocus(component, 'button 17'); + + expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.queryByText('button 8')).toBeFalsy(); + expect(screen.queryByText('button 9')).toBeFalsy(); + expect(screen.getByText('button 10')).toBeTruthy(); testRemoteControlManager.handleDown(); - expectListToHaveScroll(listElement, -300); - expectButtonToHaveFocus(component, 'button 11'); + expectListToHaveScroll(listElement, -600); + expectButtonToHaveFocus(component, 'button 20'); + + expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.queryByText('button 8')).toBeFalsy(); + expect(screen.queryByText('button 9')).toBeFalsy(); + expect(screen.getByText('button 10')).toBeTruthy(); testRemoteControlManager.handleDown(); - expectListToHaveScroll(listElement, -400); - expectButtonToHaveFocus(component, 'button 14'); + expectListToHaveScroll(listElement, -700); + expectButtonToHaveFocus(component, 'button 23'); testRemoteControlManager.handleDown(); - expectListToHaveScroll(listElement, -400); - expectButtonToHaveFocus(component, 'button 17'); + expectListToHaveScroll(listElement, -700); + expectButtonToHaveFocus(component, 'button 26'); testRemoteControlManager.handleDown(); - expectListToHaveScroll(listElement, -400); - expectButtonToHaveFocus(component, 'button 19'); + expectListToHaveScroll(listElement, -700); + expectButtonToHaveFocus(component, 'button 28'); }); it('handles correctly RIGHT & DOWN and RENDERS new elements accordingly while deleting elements that are too far from scroll when on stick to end scroll', () => { @@ -178,8 +181,6 @@ describe('SpatialNavigationVirtualizedGrid', () => { renderItem={renderItem} data={createDataArray(19)} itemHeight={100} - numberOfRenderedRows={5} - numberOfRowsVisibleOnScreen={3} numberOfColumns={3} testID="test-grid" scrollBehavior="stick-to-end" @@ -231,8 +232,6 @@ describe('SpatialNavigationVirtualizedGrid', () => { renderItem={renderItem} data={createDataArray(19)} itemHeight={100} - numberOfRenderedRows={7} - numberOfRowsVisibleOnScreen={3} numberOfColumns={3} testID="test-grid" scrollBehavior="jump-on-scroll" diff --git a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx.snap b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx.snap index f37a4985..ffbe2415 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx.snap +++ b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SpatialNavigationVirtualizedGrid handles correctly RIGHT & DOWN and RENDERS new elements accordingly while deleting elements that are too far from scroll when on stick to start scroll 1`] = ` +exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] = ` - - - - - button 4 - - - - - - - button 5 - - - - - - - button 6 - - - - - - - - button 7 - - - - - - button 8 - - - - - - - button 9 - - - - - - - - - - - button 10 + button 1 - button 11 + button 2 - button 12 + button 3 @@ -362,7 +144,7 @@ exports[`SpatialNavigationVirtualizedGrid handles correctly RIGHT & DOWN and REN "position": "absolute", "transform": [ { - "translateY": 400, + "translateY": 100, }, ], } @@ -380,8 +162,8 @@ exports[`SpatialNavigationVirtualizedGrid handles correctly RIGHT & DOWN and REN > - button 13 + button 4 - button 14 + button 5 - button 15 + button 6 @@ -471,7 +253,7 @@ exports[`SpatialNavigationVirtualizedGrid handles correctly RIGHT & DOWN and REN "position": "absolute", "transform": [ { - "translateY": 500, + "translateY": 200, }, ], } @@ -489,64 +271,8 @@ exports[`SpatialNavigationVirtualizedGrid handles correctly RIGHT & DOWN and REN > - - button 16 - - - - - - - button 17 - - - - - - button 18 - - - - - - - - -`; - -exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] = ` - - - - - - - - - button 1 + button 7 - button 2 + button 8 - button 3 + button 9 @@ -722,7 +362,7 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] "position": "absolute", "transform": [ { - "translateY": 100, + "translateY": 300, }, ], } @@ -740,8 +380,8 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] > - button 4 + button 10 - button 5 + button 11 - button 6 + button 12 @@ -831,7 +471,7 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] "position": "absolute", "transform": [ { - "translateY": 200, + "translateY": 400, }, ], } @@ -849,8 +489,8 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] > - button 7 + button 13 - button 8 + button 14 - button 9 + button 15 @@ -940,7 +580,7 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] "position": "absolute", "transform": [ { - "translateY": 300, + "translateY": 500, }, ], } @@ -958,8 +598,8 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] > - button 10 + button 16 - button 11 + button 17 - button 12 + button 18 @@ -1049,7 +689,7 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] "position": "absolute", "transform": [ { - "translateY": 400, + "translateY": 600, }, ], } @@ -1067,8 +707,8 @@ exports[`SpatialNavigationVirtualizedGrid renders the correct number of item 1`] > - button 13 + button 19 - button 14 + button 20 - button 15 + button 21 diff --git a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.tsx b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.tsx index dda577dd..a815bbbf 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid.tsx @@ -27,10 +27,8 @@ type SpatialNavigationVirtualizedGridProps = Pick< itemHeight: number; header?: JSX.Element; headerSize?: number; - /** How many rows are RENDERED (virtualization size) */ - numberOfRenderedRows: number; - /** How many rows are visible on screen (helps with knowing how to slice our data and to stop the scroll at the end of the list) */ - numberOfRowsVisibleOnScreen: number; + /** How many rows are RENDERED ADDITIONALLY to those already visible (impacts virtualization size) */ + additionalRenderedRows?: number; /** Number of rows left to display before triggering onEndReached */ onEndReachedThresholdRowsNumber?: number; /** Number of columns in the grid OR number of items per rows */ @@ -192,8 +190,7 @@ export const SpatialNavigationVirtualizedGrid = typedMemo( itemHeight, header, headerSize, - numberOfRenderedRows, - numberOfRowsVisibleOnScreen, + additionalRenderedRows, onEndReachedThresholdRowsNumber, nbMaxOfItems, rowContainerStyle, @@ -252,8 +249,7 @@ export const SpatialNavigationVirtualizedGrid = typedMemo( { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} /> , @@ -78,8 +76,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} ref={currentListRef} /> @@ -123,7 +119,13 @@ describe('SpatialNavigationVirtualizedList', () => { expect(button5).toBeTruthy(); const button6 = screen.queryByText('button 6'); - expect(button6).toBeFalsy(); + expect(button6).toBeTruthy(); + + const button7 = screen.queryByText('button 7'); + expect(button7).toBeTruthy(); + + const button8 = screen.queryByText('button 8'); + expect(button8).toBeFalsy(); }); it('handles correctly RIGHT and RENDERS new elements accordingly while deleting elements that are too far from scroll', async () => { @@ -142,17 +144,17 @@ describe('SpatialNavigationVirtualizedList', () => { expectListToHaveScroll(listElement, -100); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 3'); expectListToHaveScroll(listElement, -200); - expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 1')).toBeTruthy(); expect(screen.getByText('button 2')).toBeTruthy(); - expect(screen.getByText('button 6')).toBeTruthy(); - expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); expect(screen).toMatchSnapshot(); @@ -160,10 +162,11 @@ describe('SpatialNavigationVirtualizedList', () => { expectButtonToHaveFocus(component, 'button 4'); expectListToHaveScroll(listElement, -300); - expect(screen.queryByText('button 2')).toBeFalsy(); + expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 2')).toBeTruthy(); expect(screen.getByText('button 3')).toBeTruthy(); - expect(screen.getByText('button 7')).toBeTruthy(); - expect(screen.queryByText('button 8')).toBeFalsy(); + expect(screen.getByText('button 8')).toBeTruthy(); + expect(screen.queryByText('button 9')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 5'); @@ -200,8 +203,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-start" /> @@ -221,26 +222,28 @@ describe('SpatialNavigationVirtualizedList', () => { expectListToHaveScroll(listElement, -100); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 3'); expectListToHaveScroll(listElement, -200); - expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 1')).toBeTruthy(); expect(screen.getByText('button 2')).toBeTruthy(); - expect(screen.getByText('button 6')).toBeTruthy(); - expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 4'); expectListToHaveScroll(listElement, -300); - expect(screen.queryByText('button 2')).toBeFalsy(); + expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 2')).toBeTruthy(); expect(screen.getByText('button 3')).toBeTruthy(); expect(screen.getByText('button 7')).toBeTruthy(); - expect(screen.queryByText('button 8')).toBeFalsy(); + expect(screen.getByText('button 8')).toBeTruthy(); + expect(screen.queryByText('button 9')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 5'); @@ -276,8 +279,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data.slice(0, 3)} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-start" /> @@ -333,8 +334,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-end" /> @@ -354,33 +353,33 @@ describe('SpatialNavigationVirtualizedList', () => { expectListToHaveScroll(listElement, 0); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 3'); expectListToHaveScroll(listElement, 0); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 4'); expectListToHaveScroll(listElement, -100); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 5'); expectListToHaveScroll(listElement, -200); - expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 1')).toBeTruthy(); expect(screen.getByText('button 2')).toBeTruthy(); - expect(screen.getByText('button 6')).toBeTruthy(); - expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 6'); @@ -412,8 +411,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={7} - numberOfItemsVisibleOnScreen={3} scrollBehavior="jump-on-scroll" /> @@ -495,8 +492,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={dataWithVariableSizes} itemSize={itemSize} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} /> , @@ -514,26 +509,25 @@ describe('SpatialNavigationVirtualizedList', () => { expectListToHaveScroll(listElement, -100); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 3'); expectListToHaveScroll(listElement, -300); - expect(screen.queryByText('button 1')).toBeFalsy(); - expect(screen.getByText('button 2')).toBeTruthy(); - expect(screen.getByText('button 6')).toBeTruthy(); - expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.getByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 4'); expectListToHaveScroll(listElement, -400); - expect(screen.queryByText('button 2')).toBeFalsy(); - expect(screen.getByText('button 3')).toBeTruthy(); - expect(screen.getByText('button 7')).toBeTruthy(); - expect(screen.queryByText('button 8')).toBeFalsy(); + expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 8')).toBeTruthy(); + expect(screen.queryByText('button 9')).toBeFalsy(); }); it('handles correctly different item sizes on stick to end scroll', async () => { @@ -545,8 +539,6 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={dataWithVariableSizes} itemSize={itemSize} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-end" /> @@ -565,33 +557,33 @@ describe('SpatialNavigationVirtualizedList', () => { expectListToHaveScroll(listElement, 0); expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 3'); expectListToHaveScroll(listElement, -100); - expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); - testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 4'); expectListToHaveScroll(listElement, -300); - expect(screen.getByText('button 1')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 6')).toBeFalsy(); - testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 5'); expectListToHaveScroll(listElement, -400); + expect(screen.getByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 7')).toBeTruthy(); + expect(screen.queryByText('button 8')).toBeFalsy(); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 6'); + expectListToHaveScroll(listElement, -600); + expect(screen.queryByText('button 1')).toBeFalsy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.getByText('button 5')).toBeTruthy(); - expect(screen.queryByText('button 7')).toBeFalsy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 8')).toBeTruthy(); + expect(screen.queryByText('button 9')).toBeFalsy(); }); it('jumps to first element on go to first button press', async () => { diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx.snap b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx.snap index 4462e6d2..f24e65d4 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx.snap +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx.snap @@ -28,6 +28,46 @@ exports[`SpatialNavigationVirtualizedList handles correctly RIGHT and RENDERS ne testID="test-list" > + + + + button 1 + + + + + + + button 7 + + + @@ -461,6 +541,86 @@ exports[`SpatialNavigationVirtualizedList renders the correct number of item 1`] + + + + button 6 + + + + + + + button 7 + + + diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx index 1ba076ed..dd9a4dfb 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx @@ -9,6 +9,8 @@ import { NodeOrientation } from '../../types/orientation'; import { typedMemo } from '../../helpers/TypedMemo'; import { getSizeInPxFromOneItemToAnother } from './helpers/getSizeInPxFromOneItemToAnother'; import { computeAllScrollOffsets } from './helpers/createScrollOffsetArray'; +import { getNumberOfItemsVisibleOnScreen } from './helpers/getNumberOfItemsVisibleOnScreen'; +import { getAdditionalNumberOfItemsRendered } from './helpers/getAdditionalNumberOfItemsRendered'; export type ScrollBehavior = 'stick-to-start' | 'stick-to-end' | 'jump-on-scroll'; export interface VirtualizedListProps { @@ -17,10 +19,8 @@ export interface VirtualizedListProps { /** If vertical the height of an item, otherwise the width */ itemSize: number | ((item: T) => number); currentlyFocusedItemIndex: number; - /** How many items are RENDERED (virtualization size) */ - numberOfRenderedItems: number; - /** How many items are visible on screen (helps with knowing how to slice our data and to stop the scroll at the end of the list) */ - numberOfItemsVisibleOnScreen: number; + /** How many items are RENDERED ADDITIONALLY to those already visible (impacts virtualization size). Defaults to 4 for stick-to-start & stick-to-end scrolls, and twice the number of elements visible + 1 for jump-on-scroll. */ + additionalItemsRendered?: number; onEndReached?: () => void; /** Number of items left to display before triggering onEndReached */ onEndReachedThresholdItemsNumber?: number; @@ -127,8 +127,7 @@ export const VirtualizedList = typedMemo( renderItem, itemSize, currentlyFocusedItemIndex, - numberOfRenderedItems, - numberOfItemsVisibleOnScreen, + additionalItemsRendered, onEndReached, onEndReachedThresholdItemsNumber = 3, style, @@ -140,10 +139,20 @@ export const VirtualizedList = typedMemo( scrollBehavior = 'stick-to-start', testID, }: VirtualizedListProps) => { + const numberOfItemsVisibleOnScreen = getNumberOfItemsVisibleOnScreen({ + data, + listSizeInPx, + itemSize, + }); + + const numberOfItemsToRender = additionalItemsRendered + ? additionalItemsRendered + numberOfItemsVisibleOnScreen + : getAdditionalNumberOfItemsRendered(scrollBehavior, numberOfItemsVisibleOnScreen); + const range = getRange({ data, currentlyFocusedItemIndex, - numberOfRenderedItems, + numberOfRenderedItems: numberOfItemsToRender, numberOfItemsVisibleOnScreen, scrollBehavior, }); @@ -201,8 +210,8 @@ export const VirtualizedList = typedMemo( * But with recycling, the first element won't be unmounted : it is moved to the end and its props are updated. * See https://medium.com/@moshe_31114/building-our-recycle-list-solution-in-react-17a21a9605a0 */ const recycledKeyExtractor = useCallback( - (index: number) => `recycled_item_${index % numberOfRenderedItems}`, - [numberOfRenderedItems], + (index: number) => `recycled_item_${index % numberOfItemsToRender}`, + [numberOfItemsToRender], ); const directionStyle = useMemo( diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedListWithSize.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedListWithSize.tsx index b9371004..eaf87579 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedListWithSize.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedListWithSize.tsx @@ -32,7 +32,7 @@ export const VirtualizedListWithSize = typedMemo( }} testID={props.testID ? props.testID + '-size-giver' : undefined} > - {listSizeInPx ? : null} + ); }, diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getAdditionalNumberOfItemsRendered.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getAdditionalNumberOfItemsRendered.ts new file mode 100644 index 00000000..dcbd1f89 --- /dev/null +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getAdditionalNumberOfItemsRendered.ts @@ -0,0 +1,14 @@ +import { ScrollBehavior } from '../VirtualizedList'; + +export const getAdditionalNumberOfItemsRendered = ( + scrollBehavior: ScrollBehavior, + numberOfElementsVisibleOnScreen: number, +) => { + switch (scrollBehavior) { + case 'stick-to-start': + case 'stick-to-end': + return 4 + numberOfElementsVisibleOnScreen; + case 'jump-on-scroll': + return 2 * numberOfElementsVisibleOnScreen + 1; + } +}; diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getNumberOfItemsVisibleOnScreen.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getNumberOfItemsVisibleOnScreen.ts new file mode 100644 index 00000000..2f9fc5c0 --- /dev/null +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getNumberOfItemsVisibleOnScreen.ts @@ -0,0 +1,56 @@ +const getMinSizeOfItems = ({ + data, + itemSize, +}: { + data: T[]; + itemSize: number | ((item: T) => number); +}) => { + if (typeof itemSize === 'number') { + return itemSize; + } + + if (data.length === 0) { + return 0; + } + + const firstElementSize = itemSize(data[0]); + + const minSize = data.reduce((smallestSize, item) => { + const currentSize = itemSize(item); + if (currentSize < smallestSize) return currentSize; + return smallestSize; + }, firstElementSize); + + if (minSize === 0) { + console.warn('The size of the smallest item in the list is 0. The list will appear empty.'); + } + + return minSize; +}; + +export const getNumberOfItemsVisibleOnScreen = ({ + data, + listSizeInPx, + itemSize, +}: { + data: T[]; + listSizeInPx: number; + itemSize: number | ((item: T) => number); +}) => { + if (data.length === 0) { + return 0; + } + + const itemSizeToComputeRanges = getMinSizeOfItems({ data, itemSize }); + + if (!itemSizeToComputeRanges) { + return 0; + } + + if (itemSizeToComputeRanges === 0) { + console.warn('The size of the smallest item in the list is 0. The list will appear empty.'); + return 0; + } + + return Math.floor(listSizeInPx / itemSizeToComputeRanges); +}; diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.test.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.test.ts index f29b0c17..5070859b 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.test.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.test.ts @@ -58,18 +58,4 @@ describe('getRange for custom virtualized list', () => { expect(expectedResult).toEqual(result); expect(console.error).toHaveBeenCalledTimes(1); }); - - it('should throw an error if the numberOfRenderedItems is inferior to the numberOfItemsVisibleOnScreen + 2', () => { - const input = new Array(30).fill(1); - - expect(() => - getRange({ - data: input, - currentlyFocusedItemIndex: 5, - numberOfRenderedItems: 6, - numberOfItemsVisibleOnScreen: 8, - scrollBehavior: 'stick-to-start', - }), - ).toThrowError(); - }); }); diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts index bf1d9f49..6636957c 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts @@ -32,24 +32,6 @@ const getRangeWithoutFloatHandling = ({ }) => { const numberOfItemsNotVisible = numberOfRenderedItems - numberOfItemsVisibleOnScreen; - /** - * NumberOfItemsNotVisible should be > 2 in order to be sure to have an element mounted on the left in order to go left - */ - if (numberOfItemsNotVisible < 2) { - throw new Error( - 'You have set a numberOfRenderedItems inferior to the numberOfItemsVisibleOnScreen + 2 in your SpatialNavigationVirtualizedList. You must change it.', - ); - } - - if ( - scrollBehavior === 'jump-on-scroll' && - numberOfRenderedItems < 2 * numberOfItemsVisibleOnScreen + 1 - ) { - console.error( - 'You have set a numberOfRenderedItems inferior to 2 * numberOfItemsVisibleOnScreen + 1 in your SpatialNavigationVirtualizedList with the jump-on-scroll scroll behavior. You must change it.', - ); - } - const lastDataIndex = data.length - 1; const { rawStartIndex, rawEndIndex } = getRawStartAndEndIndexes({