Skip to content

Commit

Permalink
feat: handling different item sizes in virtualized list
Browse files Browse the repository at this point in the history
  • Loading branch information
JulienIzz committed Feb 7, 2024
1 parent 18397bf commit ad40792
Show file tree
Hide file tree
Showing 17 changed files with 1,970 additions and 1,415 deletions.
48 changes: 24 additions & 24 deletions docs/api.md

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions packages/example/src/modules/program/view/ProgramLandscape.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled from '@emotion/native';
import React from 'react';
import { Animated, Image, View } from 'react-native';
import { ProgramInfo } from '../domain/programInfo';
import { useFocusAnimation } from '../../../design-system/helpers/useFocusAnimation';
import { useSpatialNavigatorFocusableAccessibilityProps } from 'react-tv-space-navigation';

type ProgramProps = {
isFocused?: boolean;
programInfo: ProgramInfo;
};

export const ProgramLandscape = React.forwardRef<View, ProgramProps>(
({ isFocused = false, programInfo }, ref) => {
const imageSource = programInfo.image;

const scaleAnimation = useFocusAnimation(isFocused);

const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

return (
<ProgramContainer
style={scaleAnimation} // Apply the animated scale transform
ref={ref}
isFocused={isFocused}
{...accessibilityProps}
>
<ProgramImage source={imageSource} />
</ProgramContainer>
);
},
);

ProgramLandscape.displayName = 'ProgramSquare';

const ProgramContainer = styled(Animated.View)<{ isFocused: boolean }>(({ isFocused, theme }) => ({
height: theme.sizes.program.portrait.height,
width: theme.sizes.program.landscape.width * 2,
overflow: 'hidden',
borderRadius: 20,
borderColor: isFocused ? theme.colors.primary.light : 'transparent',
borderWidth: 3,
}));

const ProgramImage = styled(Image)({
height: '100%',
width: '100%',
});
2 changes: 1 addition & 1 deletion packages/example/src/modules/program/view/ProgramList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const ProgramList = ({
);
const theme = useTheme();

const programInfos = useMemo(() => getPrograms(), []);
const programInfos = useMemo(() => getPrograms(1000), []);

return (
<Container style={containerStyle}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import styled from '@emotion/native';
import { useTheme } from '@emotion/react';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useCallback, useMemo } from 'react';
import { SpatialNavigationVirtualizedList } from 'react-tv-space-navigation';
import { RootStackParamList } from '../../../../App';
import { ProgramNode } from './ProgramNode';
import { scaledPixels } from '../../../design-system/helpers/scaledPixels';
import { VariableSizeProgramInfo } from '../domain/variableSizeProgramInfo';
import { ProgramNodeLandscape } from './ProgramNodeLandscape';
import { getVariableSizePrograms } from '../infra/variableSizeProgramInfo';

const NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN = 7;
const WINDOW_SIZE = NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN + 8;
const ROW_PADDING = scaledPixels(70);

export const ProgramList = ({
orientation,
containerStyle,
}: {
orientation?: 'vertical' | 'horizontal';
containerStyle?: object;
}) => {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();

const renderItem = useCallback(
({ item }: { item: VariableSizeProgramInfo }) =>
item.type === 'portrait' ? (
<ProgramNode
programInfo={item}
onSelect={() => navigation.push('ProgramDetail', { programInfo: item })}
/>
) : (
<ProgramNodeLandscape
programInfo={item}
onSelect={() => navigation.push('ProgramDetail', { programInfo: item })}
/>
),
[navigation],
);
const theme = useTheme();

const programInfos = useMemo(() => getVariableSizePrograms(1000), []);

return (
<Container style={containerStyle}>
<SpatialNavigationVirtualizedList
orientation={orientation}
data={programInfos}
renderItem={renderItem}
itemSize={(item) =>
item.type === 'portrait'
? theme.sizes.program.portrait.width + 30
: theme.sizes.program.landscape.width * 2 + 45
}
numberOfRenderedItems={WINDOW_SIZE}
numberOfItemsVisibleOnScreen={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
onEndReachedThresholdItemsNumber={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
scrollBehavior="stick-to-end"
/>
</Container>
);
};

export const ProgramsRowVariableSize = ({ containerStyle }: { containerStyle?: object }) => {
const theme = useTheme();
return (
<ProgramList
containerStyle={{
...containerStyle,
height: theme.sizes.program.portrait.height + ROW_PADDING,
}}
/>
);
};

const Container = styled.View(({ theme }) => ({
backgroundColor: theme.colors.background.mainHover,
padding: theme.spacings.$8,
borderRadius: scaledPixels(20),
overflow: 'hidden',
}));
13 changes: 13 additions & 0 deletions packages/example/src/modules/program/view/ProgramListWithTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Box } from '../../../design-system/components/Box';
import { Spacer } from '../../../design-system/components/Spacer';
import { Typography } from '../../../design-system/components/Typography';
import { ProgramsRow } from './ProgramList';
import { ProgramsRowVariableSize } from './ProgramListVariableSize';

type Props = {
title: string;
Expand All @@ -18,3 +19,15 @@ export const ProgramListWithTitle = ({ title }: Props) => {
</Box>
);
};

export const ProgramListWithTitleAndVariableSizes = ({ title }: Props) => {
return (
<Box direction="vertical">
<Typography variant="body" fontWeight="strong">
{title}
</Typography>
<Spacer direction="vertical" gap="$2" />
<ProgramsRowVariableSize />
</Box>
);
};
17 changes: 17 additions & 0 deletions packages/example/src/modules/program/view/ProgramNodeLandscape.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SpatialNavigationNode } from 'react-tv-space-navigation';

import { ProgramInfo } from '../domain/programInfo';
import { ProgramLandscape } from './ProgramLandscape';

type Props = {
programInfo: ProgramInfo;
onSelect?: () => void;
};

export const ProgramNodeLandscape = ({ programInfo, onSelect }: Props) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect}>
{({ isFocused }) => <ProgramLandscape isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
);
};
7 changes: 6 additions & 1 deletion packages/example/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import '../components/configureRemoteControl';
import { Box } from '../design-system/components/Box';
import { Spacer } from '../design-system/components/Spacer';
import { Typography } from '../design-system/components/Typography';
import { ProgramListWithTitle } from '../modules/program/view/ProgramListWithTitle';
import {
ProgramListWithTitle,
ProgramListWithTitleAndVariableSizes,
} from '../modules/program/view/ProgramListWithTitle';

export const Home = () => {
return (
Expand All @@ -23,6 +26,8 @@ export const Home = () => {
<ProgramListWithTitle title="Watch again" />
<Spacer gap="$6" />
<ProgramListWithTitle title="You may also like..." />
<Spacer gap="$6" />
<ProgramListWithTitleAndVariableSizes title="Our selection"></ProgramListWithTitleAndVariableSizes>
</Box>
</SpatialNavigationScrollView>
</DefaultFocus>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RenderResult, act, render, screen } from '@testing-library/react-native';
import { RenderResult, act, fireEvent, render, screen } from '@testing-library/react-native';
import { ItemWithIndex } from '../virtualizedList/VirtualizedList';
import { PropsTestButton, TestButton } from '../tests/TestButton';
import { SpatialNavigationRoot } from '../Root';
Expand Down Expand Up @@ -30,6 +30,16 @@ describe('SpatialNavigationVirtualizedGrid', () => {
return data;
}

const gridTestId = 'test-grid';

const fireLayoutEvent = (component: RenderResult, width: number, height: number) => {
const listElementSizeGiver = component.getByTestId(gridTestId + '-size-giver');

fireEvent(listElementSizeGiver, 'layout', {
nativeEvent: { layout: { width: width, height: height } },
});
};

const renderGrid = () =>
render(
<SpatialNavigationRoot>
Expand All @@ -49,9 +59,13 @@ describe('SpatialNavigationVirtualizedGrid', () => {

it('renders the correct number of item', () => {
const component = renderGrid();
fireLayoutEvent(component, 300, 300);

expect(screen).toMatchSnapshot();

const listElement = component.getByTestId(gridTestId);
expect(listElement).toHaveStyle({ height: 700 });

expect(screen.getByText('button 1')).toBeTruthy();
expectButtonToHaveFocus(component, 'button 1');
expect(screen.getByText('button 2')).toBeTruthy();
Expand All @@ -73,10 +87,12 @@ describe('SpatialNavigationVirtualizedGrid', () => {

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', () => {
const component = renderGrid();
fireLayoutEvent(component, 300, 300);
act(() => jest.runAllTimers());

const listElement = component.getByTestId('test-grid');
const listElement = component.getByTestId(gridTestId);
expect(listElement).toHaveStyle({ transform: [{ translateY: 0 }] });
expect(listElement).toHaveStyle({ height: 700 });

testRemoteControlManager.handleRight();

Expand Down Expand Up @@ -179,10 +195,12 @@ describe('SpatialNavigationVirtualizedGrid', () => {
</DefaultFocus>
</SpatialNavigationRoot>,
);
fireLayoutEvent(component, 300, 300);
act(() => jest.runAllTimers());

const listElement = component.getByTestId('test-grid');
const listElement = component.getByTestId(gridTestId);
expect(listElement).toHaveStyle({ transform: [{ translateY: 0 }] });
expect(listElement).toHaveStyle({ height: 700 });

testRemoteControlManager.handleRight();
expectButtonToHaveFocus(component, 'button 2');
Expand Down Expand Up @@ -230,10 +248,12 @@ describe('SpatialNavigationVirtualizedGrid', () => {
</DefaultFocus>
</SpatialNavigationRoot>,
);
fireLayoutEvent(component, 300, 300);
act(() => jest.runAllTimers());

const listElement = component.getByTestId('test-grid');
const listElement = component.getByTestId(gridTestId);
expect(listElement).toHaveStyle({ transform: [{ translateY: 0 }] });
expect(listElement).toHaveStyle({ height: 700 });

testRemoteControlManager.handleRight();
expectButtonToHaveFocus(component, 'button 2');
Expand Down
Loading

0 comments on commit ad40792

Please sign in to comment.