Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Feat/add stick to center #164

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,140 @@ describe('SpatialNavigationVirtualizedList', () => {
});
});

describe('stick-to-center', () => {
it('handles correctly stick-to-center lists', async () => {
const component = render(
<SpatialNavigationRoot>
<DefaultFocus>
<SpatialNavigationVirtualizedList
testID="test-list"
renderItem={renderItem}
data={data}
itemSize={100}
scrollBehavior="stick-to-center"
additionalItemsRendered={0}
/>
</DefaultFocus>
</SpatialNavigationRoot>,
);
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);
expectListToHaveScroll(listElement, 0);
// The size of the list should be the sum of the item sizes (virtualized or not)
expect(listElement).toHaveStyle({ width: 1000 });

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(
<SpatialNavigationRoot>
<DefaultFocus>
<SpatialNavigationVirtualizedList
testID="test-list"
renderItem={renderItem}
data={data.slice(0, 3)}
itemSize={100}
scrollBehavior="stick-to-center"
additionalItemsRendered={0}
/>
</DefaultFocus>
</SpatialNavigationRoot>,
);
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(
<SpatialNavigationRoot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-end' | 'jump-on-scroll';
export type ScrollBehavior =
| 'stick-to-start'
| 'stick-to-center'
| 'stick-to-end'
| 'jump-on-scroll';
export interface VirtualizedListProps<T> {
data: T[];
renderItem: (args: { item: T; index: number }) => JSX.Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,74 @@
return -scrollOffset;
};

const computeStickToCenterTranslation = <T>({
currentlyFocusedItemIndex,
itemSizeInPx,
data,
listSizeInPx,
maxPossibleRightAlignedIndex,

Check failure on line 27 in packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'maxPossibleRightAlignedIndex' is defined but never used
maxPossibleLeftAlignedIndex,
}: {
currentlyFocusedItemIndex: number;
itemSizeInPx: number | ((item: T) => number);
data: T[];
listSizeInPx: number;
maxPossibleRightAlignedIndex: number;
maxPossibleLeftAlignedIndex: number;
}) => {
const currentlyFocusedItemSize =
typeof itemSizeInPx === 'function'
? itemSizeInPx(data[currentlyFocusedItemIndex])
: itemSizeInPx;

const sizeOfListFromStartToCurrentlyFocusedItem = getSizeInPxFromOneItemToAnother(
data,
itemSizeInPx,
0,
currentlyFocusedItemIndex,
);
const sizeOfListFromEndToCurrentlyFocusedItem = getSizeInPxFromOneItemToAnother(

Check failure on line 48 in packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'sizeOfListFromEndToCurrentlyFocusedItem' is assigned a value but never used
data,
itemSizeInPx,
data.length - 1,
currentlyFocusedItemIndex,
);

if (sizeOfListFromStartToCurrentlyFocusedItem < listSizeInPx / 2) {
return 0;
}

if (currentlyFocusedItemIndex > maxPossibleLeftAlignedIndex) {
console.log('currentlyFocusedItemIndex', currentlyFocusedItemIndex);

Check failure on line 60 in packages/lib/src/spatial-navigation/components/virtualizedList/helpers/computeTranslation.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected console statement
}

// 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;

return -scrollOffset;
};

const computeStickToEndTranslation = <T>({
currentlyFocusedItemIndex,
itemSizeInPx,
Expand Down Expand Up @@ -102,6 +170,15 @@
data,
maxPossibleLeftAlignedIndex,
});
case 'stick-to-center':
return computeStickToCenterTranslation({
currentlyFocusedItemIndex,
itemSizeInPx,
data,
listSizeInPx,
maxPossibleLeftAlignedIndex,
maxPossibleRightAlignedIndex,
});
case 'stick-to-end':
return computeStickToEndTranslation({
currentlyFocusedItemIndex,
Expand Down
Loading
Loading