From e06970eb22dbf652f0f93e634541ea0400151345 Mon Sep 17 00:00:00 2001 From: Silvan <100023512+slvvn@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:24:34 +0000 Subject: [PATCH 1/4] feat: add support for `stick-to-center` in VirtualizedList --- docs/api.md | 2 +- .../SpatialNavigationVirtualizedList.test.tsx | 137 ++++++++++++++++++ .../virtualizedList/VirtualizedList.tsx | 2 +- .../helpers/computeTranslation.ts | 49 +++++++ .../virtualizedList/helpers/getRange.ts | 5 + .../getSizeInPxFromOneItemToAnother.ts | 9 +- 6 files changed, 200 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index 43cfbc85..45d55ad4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -248,7 +248,7 @@ It also ensures that the scroll event is propagated properly to parent ScrollVie | `orientation` | `'horizontal' \| 'vertical'` | The orientation of the list. Defaults to `'horizontal'`. | | `nbMaxOfItems` | `number` | The total number of expected items for infinite scroll. This helps with aligning items and is used for pagination. If not provided, it defaults to the length of the data array. | | `scrollDuration` | `number` | The duration of a scrolling animation inside the VirtualizedList. Defaults to 200ms. | -| `scrollBehavior` | `'stick-to-start' \| 'stick-to-end' \| 'jump-on-scroll'` | Determines the scrolling behavior. Defaults to `'stick-to-start'`. `'stick-to-start'` and `'stick-to-end'` fix the focused item at the beginning or the end of the visible items on screen. `jump-on-scroll` jumps from `numberOfItemsVisibleOnScreen` items when needed. Warning `jump-on-scroll` is not compatible with dynamic item size. | +| `scrollBehavior` | `'stick-to-start' \| 'stick-to-center' \| 'stick-to-end' \| 'jump-on-scroll'` | Determines the scrolling behavior. Defaults to `'stick-to-start'`. `'stick-to-start'` and `'stick-to-end'` fix the focused item at the beginning or the end of the visible items on screen. `'stick-to-end'` fixes the item at the center of the screen when possible, otherwise sticking to the sides of the list instead. `jump-on-scroll` jumps from `numberOfItemsVisibleOnScreen` items when needed. Warning `jump-on-scroll` is not compatible with dynamic item size. | | `ascendingArrow` | `ReactElement` | For web TVs cursor handling. Optional component to display as the arrow to scroll on the ascending order. | | `ascendingArrowContainerStyle` | `ViewStyle` | For web TVs cursor handling. Style of the view which wraps the ascending arrow. Hover this view will trigger the scroll. | | `descendingArrow` | `ReactElement` | For web TVs cursor handling. Optional component to display as the arrow to scroll on the descending order. | diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx index 22c51d8d..9fe16304 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx @@ -325,6 +325,143 @@ describe('SpatialNavigationVirtualizedList', () => { }); }); + + describe('stick-to-center', () => { + it('handles correctly stick-to-center lists', async () => { + const component = render( + + + + + , + ); + act(() => jest.runAllTimers()); + + setComponentLayoutSize(listTestId, component, { width: 300, height: 300 }); + + const listElement = await component.findByTestId(listTestId); + expectListToHaveScroll(listElement, 0); + // The size of the list should be the sum of the item sizes (virtualized or not) + expect(listElement).toHaveStyle({ width: 1000 }); + + // x x x X x x x x x x + //|x[x x x]x|x x x x x + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 2'); + expectListToHaveScroll(listElement, 0); + + expect(screen.getByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 5')).toBeTruthy(); + expect(screen.queryByText('button 6')).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, -200); + + expect(screen.queryByText('button 1')).toBeFalsy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 6')).toBeTruthy(); + expect(screen.queryByText('button 7')).toBeFalsy(); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 5'); + expectListToHaveScroll(listElement, -300); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 6'); + expectListToHaveScroll(listElement, -400); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 7'); + expectListToHaveScroll(listElement, -500); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 8'); + expectListToHaveScroll(listElement, -600); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 9'); + expectListToHaveScroll(listElement, -700); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 10'); + expectListToHaveScroll(listElement, -700); + }); + + it('handles correctly stick-to-center lists with elements < visible on screen', async () => { + const component = render( + + + + + , + ); + act(() => jest.runAllTimers()); + + setComponentLayoutSize(listTestId, component, { width: 300, height: 300 }); + + const listElement = await component.findByTestId(listTestId); + expectListToHaveScroll(listElement, 0); + // The size of the list should be the sum of the item sizes (virtualized or not) + expect(listElement).toHaveStyle({ width: 300 }); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 2'); + expectListToHaveScroll(listElement, 0); + + expect(screen.queryByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 3')).toBeTruthy(); + + testRemoteControlManager.handleRight(); + expectButtonToHaveFocus(component, 'button 3'); + expectListToHaveScroll(listElement, 0); + + expect(screen.queryByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 3')).toBeTruthy(); + + testRemoteControlManager.handleRight(); + expectListToHaveScroll(listElement, 0); + + expect(screen.queryByText('button 1')).toBeTruthy(); + expect(screen.getByText('button 2')).toBeTruthy(); + expect(screen.getByText('button 3')).toBeTruthy(); + + // We just reached the max of the list + testRemoteControlManager.handleRight(); + testRemoteControlManager.handleRight(); + testRemoteControlManager.handleRight(); + testRemoteControlManager.handleRight(); + expectListToHaveScroll(listElement, 0); + }); + }); + it('handles correctly RIGHT and RENDERS new elements accordingly while deleting elements that are too far from scroll when on stick to end scroll', async () => { const component = render( diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx index 398bbefd..305a062d 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx @@ -12,7 +12,7 @@ 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 type ScrollBehavior = 'stick-to-start' | 'stick-to-center' | 'stick-to-end' | 'jump-on-scroll'; export interface VirtualizedListProps { data: T[]; renderItem: (args: { item: T; index: number }) => JSX.Element; diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts index 4dfeb1f3..73d37559 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts @@ -19,6 +19,48 @@ const computeStickToStartTranslation = ({ return -scrollOffset; }; +const computeStickToCenterTranslation = ({ + currentlyFocusedItemIndex, + itemSizeInPx, + data, + listSizeInPx, +}: { + currentlyFocusedItemIndex: number; + itemSizeInPx: number | ((item: T) => number); + data: T[]; + listSizeInPx: number; +}) => { + const currentlyFocusedItemSize = + typeof itemSizeInPx === 'function' + ? itemSizeInPx(data[currentlyFocusedItemIndex]) + : itemSizeInPx; + + const sizeOfListFromStartToCurrentlyFocusedItem = getSizeInPxFromOneItemToAnother( + data, + itemSizeInPx, + 0, + currentlyFocusedItemIndex, + ); + const sizeOfListFromEndToCurrentlyFocusedItem = getSizeInPxFromOneItemToAnother( + data, + itemSizeInPx, + data.length - 1, + currentlyFocusedItemIndex, + ); + + if (sizeOfListFromStartToCurrentlyFocusedItem < listSizeInPx / 2) { + return 0; + } + + if (sizeOfListFromEndToCurrentlyFocusedItem < listSizeInPx / 2) { + return -sizeOfListFromStartToCurrentlyFocusedItem + listSizeInPx - sizeOfListFromEndToCurrentlyFocusedItem - currentlyFocusedItemSize; + } + + const scrollOffset = + sizeOfListFromStartToCurrentlyFocusedItem - (listSizeInPx / 2) + (currentlyFocusedItemSize / 2); + return -scrollOffset; +}; + const computeStickToEndTranslation = ({ currentlyFocusedItemIndex, itemSizeInPx, @@ -102,6 +144,13 @@ export const computeTranslation = ({ data, maxPossibleLeftAlignedIndex, }); + case 'stick-to-center': + return computeStickToCenterTranslation({ + currentlyFocusedItemIndex, + itemSizeInPx, + data, + listSizeInPx, + }); case 'stick-to-end': return computeStickToEndTranslation({ currentlyFocusedItemIndex, 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 6636957c..ef559ebc 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts @@ -90,6 +90,11 @@ const getRawStartAndEndIndexes = ({ 1 + halfNumberOfItemsNotVisible, }; + case 'stick-to-center': + return { + rawStartIndex: currentlyFocusedItemIndex - (halfNumberOfItemsNotVisible + 1), + rawEndIndex: currentlyFocusedItemIndex + (halfNumberOfItemsNotVisible + 1), + }; case 'stick-to-end': return { rawStartIndex: diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getSizeInPxFromOneItemToAnother.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getSizeInPxFromOneItemToAnother.ts index d4b39631..9895015e 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getSizeInPxFromOneItemToAnother.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getSizeInPxFromOneItemToAnother.ts @@ -2,6 +2,8 @@ * This function is used to compute the size in pixels of a range of items in a list. * If you want the size taken by items from index 0 to 5, you can call this function with * start = 0 and end = 5. The size is computed by summing the size of each item in the range. + * Similarly, if you want to calculate from the other direct, you can call this function + * with start = 5 and end = 0. * @param data The list of items * @param itemSizeInPx The size of an item in pixels. It can be a number or a function that takes an item and returns a number. * @param start The start index of the range @@ -14,8 +16,11 @@ export const getSizeInPxFromOneItemToAnother = ( start: number, end: number, ): number => { + const startIndex = start < end ? start : end; + const endIndex = end > start ? end : start; + if (typeof itemSizeInPx === 'function') { - return data.slice(start, end).reduce((acc, item) => acc + itemSizeInPx(item), 0); + return data.slice(startIndex, endIndex).reduce((acc, item) => acc + itemSizeInPx(item), 0); } - return data.slice(start, end).length * itemSizeInPx; + return data.slice(startIndex, endIndex).length * itemSizeInPx; }; From 805bc2f1f6a0c6d3f3ce832f1cd077001bfecd53 Mon Sep 17 00:00:00 2001 From: Silvan <100023512+slvvn@users.noreply.github.com> Date: Mon, 30 Sep 2024 07:40:17 +0000 Subject: [PATCH 2/4] fix: remove comment --- .../virtualizedList/SpatialNavigationVirtualizedList.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx index 9fe16304..44731053 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx @@ -325,7 +325,6 @@ describe('SpatialNavigationVirtualizedList', () => { }); }); - describe('stick-to-center', () => { it('handles correctly stick-to-center lists', async () => { const component = render( @@ -352,9 +351,6 @@ describe('SpatialNavigationVirtualizedList', () => { // The size of the list should be the sum of the item sizes (virtualized or not) expect(listElement).toHaveStyle({ width: 1000 }); - // x x x X x x x x x x - //|x[x x x]x|x x x x x - testRemoteControlManager.handleRight(); expectButtonToHaveFocus(component, 'button 2'); expectListToHaveScroll(listElement, 0); From 4b22ea5e74d24af2b87aec3dd07a46a13ddbe46c Mon Sep 17 00:00:00 2001 From: Pierre Poupin Date: Tue, 15 Oct 2024 17:19:42 +0200 Subject: [PATCH 3/4] fix: add wider range for stick to center boundaries --- .../SpatialNavigationVirtualizedList.test.tsx | 9 ++--- .../virtualizedList/VirtualizedList.tsx | 6 +++- .../virtualizedList/helpers/getRange.test.ts | 35 +++++++++++++++++++ .../virtualizedList/helpers/getRange.ts | 5 +-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx index 44731053..fa24f9cc 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedList.test.tsx @@ -335,15 +335,17 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-center" + additionalItemsRendered={0} /> , ); act(() => jest.runAllTimers()); + // Given this layout size, this item size, and the additional items rendered parameter: + // - number of visible items on screen = 3 + // - total amount of items rendered = 5 setComponentLayoutSize(listTestId, component, { width: 300, height: 300 }); const listElement = await component.findByTestId(listTestId); @@ -410,9 +412,8 @@ describe('SpatialNavigationVirtualizedList', () => { renderItem={renderItem} data={data.slice(0, 3)} itemSize={100} - numberOfRenderedItems={5} - numberOfItemsVisibleOnScreen={3} scrollBehavior="stick-to-center" + additionalItemsRendered={0} /> , diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx index 305a062d..3cdf09a8 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/VirtualizedList.tsx @@ -12,7 +12,11 @@ import { computeAllScrollOffsets } from './helpers/createScrollOffsetArray'; import { getNumberOfItemsVisibleOnScreen } from './helpers/getNumberOfItemsVisibleOnScreen'; import { getAdditionalNumberOfItemsRendered } from './helpers/getAdditionalNumberOfItemsRendered'; -export type ScrollBehavior = 'stick-to-start' | 'stick-to-center' | 'stick-to-end' | 'jump-on-scroll'; +export type ScrollBehavior = + | 'stick-to-start' + | 'stick-to-center' + | 'stick-to-end' + | 'jump-on-scroll'; export interface VirtualizedListProps { data: T[]; renderItem: (args: { item: T; index: number }) => JSX.Element; 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 5070859b..08560071 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,4 +58,39 @@ describe('getRange for custom virtualized list', () => { expect(expectedResult).toEqual(result); expect(console.error).toHaveBeenCalledTimes(1); }); + + describe('stick-to-center', () => { + it.each` + description | arraySize | focusIndex | result + ${'empty array'} | ${0} | ${0} | ${{ start: 0, end: 0 }} + ${'empty array and out of bounds focus'} | ${0} | ${3} | ${{ start: 0, end: 0 }} + ${'one element focused'} | ${1} | ${0} | ${{ start: 0, end: 0 }} + ${'one element and out of bounds focus'} | ${1} | ${12} | ${{ start: 0, end: 0 }} + ${'small array with focus in the middle'} | ${5} | ${3} | ${{ start: 0, end: 4 }} + ${'bigger number of rendered items than data with range out of bound'} | ${5} | ${12} | ${{ start: 0, end: 4 }} + ${'focus at the beginning of big array focus 0'} | ${30} | ${0} | ${{ start: 0, end: 7 }} + ${'focus at the beginning of big array focus 1'} | ${30} | ${1} | ${{ start: 0, end: 7 }} + ${'focus at the beginning of big array focus 2'} | ${30} | ${2} | ${{ start: 0, end: 7 }} + ${'focus at the beginning of big array focus 3'} | ${30} | ${3} | ${{ start: 0, end: 7 }} + ${'focus at the beginning of big array focus 4, starts scrolling'} | ${30} | ${4} | ${{ start: 1, end: 8 }} + ${'focus at the beginning of big array focus 5, scrolls'} | ${30} | ${5} | ${{ start: 2, end: 9 }} + ${'focus in the middle of big array, must scroll'} | ${30} | ${12} | ${{ start: 9, end: 16 }} + ${'focus at the end of big array'} | ${30} | ${29} | ${{ start: 22, end: 29 }} + ${'big array and focus out of bounds'} | ${30} | ${31} | ${{ start: 22, end: 29 }} + `( + 'should return proper range for array size $arraySize at index $focusIndex (case description: "$description")', + ({ arraySize, focusIndex, result }) => { + const input = new Array(arraySize).fill(1); + const expectedResult = getRange({ + data: input, + currentlyFocusedItemIndex: focusIndex, + numberOfRenderedItems: 8, + numberOfItemsVisibleOnScreen: 4, + scrollBehavior: 'stick-to-center', + }); + + expect(expectedResult).toEqual(result); + }, + ); + }); }); 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 ef559ebc..28c61413 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/getRange.ts @@ -79,6 +79,7 @@ const getRawStartAndEndIndexes = ({ scrollBehavior: ScrollBehavior; }) => { const halfNumberOfItemsNotVisible = numberOfItemsNotVisible / 2; + const stickToCenterBoundaries = (numberOfItemsVisibleOnScreen + numberOfItemsNotVisible - 1) / 2; switch (scrollBehavior) { case 'stick-to-start': @@ -92,8 +93,8 @@ const getRawStartAndEndIndexes = ({ }; case 'stick-to-center': return { - rawStartIndex: currentlyFocusedItemIndex - (halfNumberOfItemsNotVisible + 1), - rawEndIndex: currentlyFocusedItemIndex + (halfNumberOfItemsNotVisible + 1), + rawStartIndex: currentlyFocusedItemIndex - stickToCenterBoundaries, + rawEndIndex: currentlyFocusedItemIndex + stickToCenterBoundaries, }; case 'stick-to-end': return { From 6ad08a717cd3232efa7e57e52f65833b75707e1f Mon Sep 17 00:00:00 2001 From: Pierre Poupin Date: Tue, 15 Oct 2024 19:31:41 +0200 Subject: [PATCH 4/4] wip --- .../helpers/computeTranslation.test.ts | 132 ++++++++++++++++++ .../helpers/computeTranslation.ts | 36 ++++- .../helpers/createScrollOffsetArray.test.ts | 40 ++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 packages/lib/src/spatial-navigation/components/virtualizedList/helpers/createScrollOffsetArray.test.ts diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.test.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.test.ts index 73320629..dbacf154 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.test.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.test.ts @@ -290,6 +290,138 @@ describe('computeTranslation for virtualized list with stick-to-end scroll behav }); }); +describe('computeTranslation for virtualized list with stick-to-center scroll behavior', () => { + const scrollBehavior = 'stick-to-center'; + + it('should not scroll if currentlyFocusedItemIndex < numberOfItemsVisibleOnScreen', () => { + const expectedResult = computeTranslation({ + itemSizeInPx, + currentlyFocusedItemIndex: 3, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-0); + }); + + it('should not scroll if currentlyFocusedItemIndex < numberOfItemsVisibleOnScreen with dynamic sizes', () => { + const expectedResult = computeTranslation({ + itemSizeInPx: itemSizeInPxFunction, + currentlyFocusedItemIndex: 3, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-0); + }); + + it('should end-align focused item', () => { + const expectedResult = computeTranslation({ + itemSizeInPx, + currentlyFocusedItemIndex: 6, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-30); + }); + + it('should end-align focused item with dynamic sizes', () => { + const expectedResult = computeTranslation({ + itemSizeInPx: itemSizeInPxFunction, + currentlyFocusedItemIndex: 6, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-60); + }); + + it('should end-align last element if focused', () => { + const expectedResult = computeTranslation({ + itemSizeInPx, + currentlyFocusedItemIndex: 10, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-70); + }); + + it('should end-align last element if focused with dynamic sizes', () => { + const expectedResult = computeTranslation({ + itemSizeInPx: itemSizeInPxFunction, + currentlyFocusedItemIndex: 10, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 11, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-120); + }); + + it('should start-align first element if numberOfItems <= numberOfVisibleItemsOnScreen', () => { + const expectedResult = computeTranslation({ + itemSizeInPx, + currentlyFocusedItemIndex: 1, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 3, + scrollBehavior: scrollBehavior, + data, + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 7, + maxPossibleRightAlignedIndex: 3, + }); + + expect(expectedResult).toEqual(-0); + }); + + it('should start-align first element if numberOfItems <= numberOfVisibleItemsOnScreen with dynamic sizes', () => { + const expectedResult = computeTranslation({ + itemSizeInPx: itemSizeInPxFunction, + currentlyFocusedItemIndex: 1, + numberOfItemsVisibleOnScreen: 4, + nbMaxOfItems: 3, + scrollBehavior: scrollBehavior, + data: data.slice(0, 2), + listSizeInPx: 40, + maxPossibleLeftAlignedIndex: 0, + maxPossibleRightAlignedIndex: 2, + }); + + expect(expectedResult).toEqual(-0); + }); +}); + describe('computeTranslation for virtualized list with jumping scroll behavior', () => { const scrollBehavior = 'jump-on-scroll'; diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts index 73d37559..3b677b6f 100644 --- a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts @@ -24,11 +24,15 @@ const computeStickToCenterTranslation = ({ itemSizeInPx, data, listSizeInPx, + maxPossibleRightAlignedIndex, + maxPossibleLeftAlignedIndex, }: { currentlyFocusedItemIndex: number; itemSizeInPx: number | ((item: T) => number); data: T[]; listSizeInPx: number; + maxPossibleRightAlignedIndex: number; + maxPossibleLeftAlignedIndex: number; }) => { const currentlyFocusedItemSize = typeof itemSizeInPx === 'function' @@ -51,13 +55,35 @@ const computeStickToCenterTranslation = ({ if (sizeOfListFromStartToCurrentlyFocusedItem < listSizeInPx / 2) { return 0; } - - if (sizeOfListFromEndToCurrentlyFocusedItem < listSizeInPx / 2) { - return -sizeOfListFromStartToCurrentlyFocusedItem + listSizeInPx - sizeOfListFromEndToCurrentlyFocusedItem - currentlyFocusedItemSize; + + if (currentlyFocusedItemIndex > maxPossibleLeftAlignedIndex) { + console.log('currentlyFocusedItemIndex', currentlyFocusedItemIndex); } + // if (sizeOfListFromEndToCurrentlyFocusedItem < listSizeInPx / 2) { + // console.log('-----------'); + // console.log('currentlyFocusedItemIndex', currentlyFocusedItemIndex); + // const result = + // sizeOfListFromStartToCurrentlyFocusedItem - + // listSizeInPx + + // sizeOfListFromEndToCurrentlyFocusedItem + + // currentlyFocusedItemSize; + // console.log('list END inferior to half list size in px', result); + // console.log( + // 'sizeOfListFromStartToCurrentlyFocusedItem', + // sizeOfListFromStartToCurrentlyFocusedItem, + // ); + // console.log('listSizeInPx', listSizeInPx); + // console.log('sizeOfListFromEndToCurrentlyFocusedItem', sizeOfListFromEndToCurrentlyFocusedItem); + // console.log('currentlyFocusedItemSize', currentlyFocusedItemSize); + // console.log('-----------'); + + // return -result; + // } + const scrollOffset = - sizeOfListFromStartToCurrentlyFocusedItem - (listSizeInPx / 2) + (currentlyFocusedItemSize / 2); + sizeOfListFromStartToCurrentlyFocusedItem - listSizeInPx / 2 + currentlyFocusedItemSize / 2; + return -scrollOffset; }; @@ -150,6 +176,8 @@ export const computeTranslation = ({ itemSizeInPx, data, listSizeInPx, + maxPossibleLeftAlignedIndex, + maxPossibleRightAlignedIndex, }); case 'stick-to-end': return computeStickToEndTranslation({ diff --git a/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/createScrollOffsetArray.test.ts b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/createScrollOffsetArray.test.ts new file mode 100644 index 00000000..ef7f5a73 --- /dev/null +++ b/packages/lib/src/spatial-navigation/components/virtualizedList/helpers/createScrollOffsetArray.test.ts @@ -0,0 +1,40 @@ +// Import the function to test + +import { computeAllScrollOffsets } from './createScrollOffsetArray'; + +// Mock data and inputs +const itemSize = (item) => item.size; +// doesn't matter here +const numberOfItemsVisibleOnScreen = 0; +const data = [ + { name: 'data 0', size: 1 }, + { name: 'data 1', size: 1 }, + { name: 'data 2', size: 2 }, + { name: 'data 3', size: 1 }, + { name: 'data 4', size: 1 }, + { name: 'data 5', size: 2 }, + { name: 'data 6', size: 1 }, + { name: 'data 7', size: 1 }, + { name: 'data 8', size: 2 }, + { name: 'data 9', size: 1 }, + { name: 'data 10', size: 1 }, +]; +const nbMaxOfItems = data.length; +const scrollBehavior = 'stick-to-center'; +const listSizeInPx = 5; + +// Expected output +const expectedOutput = [0, 0, -0.5, -2, -3, -4.5, -6, -7, -8.5, -9, -9]; + +test('computeAllScrollOffsets returns an array of zeros with the same length as data', () => { + const result = computeAllScrollOffsets({ + itemSize, + nbMaxOfItems, + numberOfItemsVisibleOnScreen, + scrollBehavior, + data, + listSizeInPx, + }); + + expect(result).toEqual(expectedOutput); +});