Skip to content

Commit

Permalink
Merge pull request #58 from bamlab/feat/adding-indexRange
Browse files Browse the repository at this point in the history
Feat: add indexRange on SpatialNavigationNode
  • Loading branch information
pierpo authored Feb 7, 2024
2 parents ecce7f7 + 44183f4 commit 5180c50
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ The `SpatialNavigationNode` component receives the following props:
| `onSelect` | `function` | `undefined` | Callback function to be called when the node is selected. |
| `orientation` | `'vertical' \| 'horizontal` | `'vertical'` | Determines the orientation of the node. |
| `isFocusable` | `boolean` | `false` | Determines if the node is focusable or not. If it's `true`, the `children` prop must be a function that returns a React element and accepts a parameter with a `isFocused` property. If it's `false` or not provided, `children` can be any valid React node. |

| `alignInGrid` | `boolean` | `false` | Determines whether child lists should behave like a grid.

| `indexRange` | `number[]` | `undefined` | Determines the indexes when using long nodes in a grid. If a grid row has one `indexRange`, you should specify each element's `indexRange`. You can check for more details in [`GridWithLongNodesPage`](https://github.com/bamlab/react-tv-space-navigation/blob/31bfe1def4a7e18e9e41f26a520090d1b7a5b149/packages/example/src/pages/GridWithLongNodesPage.tsx) example or in [lrud documentation](https://github.com/bbc/lrud/blob/master/docs/usage.md#indexrange).

| `children` | `function` or `ReactNode` | `null` | Child elements of the component. It can be a function that returns a React element and accepts a parameter with a `isFocused` property when `isFocusable` is `true`. If `isFocusable` is `false` or not provided, it can be any valid React node. |

## Usage
Expand Down
3 changes: 3 additions & 0 deletions packages/example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ProgramInfo } from './src/modules/program/domain/programInfo';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { ProgramDetail } from './src/pages/ProgramDetail';
import { NonVirtualizedGridPage } from './src/pages/NonVirtualizedGridPage';
import { GridWithLongNodesPage } from './src/pages/GridWithLongNodesPage';

const Stack = createNativeStackNavigator<RootStackParamList>();

Expand All @@ -23,6 +24,7 @@ export type RootTabParamList = {
Home: undefined;
ProgramGridPage: undefined;
NonVirtualizedGridPage: undefined;
GridWithLongNodesPage: undefined;
};

export type RootStackParamList = {
Expand All @@ -48,6 +50,7 @@ const TabNavigator = () => {
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="ProgramGridPage" component={ProgramGridPage} />
<Tab.Screen name="NonVirtualizedGridPage" component={NonVirtualizedGridPage} />
<Tab.Screen name="GridWithLongNodesPage" component={GridWithLongNodesPage} />
</Tab.Navigator>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/example/src/design-system/theme/sizes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const sizes = {
program: {
landscape: { width: scaledPixels(250), height: scaledPixels(200) },
portrait: { width: scaledPixels(200), height: scaledPixels(250) },
long: { width: scaledPixels(416), height: scaledPixels(250) },
},
menu: {
open: scaledPixels(400),
Expand Down
50 changes: 50 additions & 0 deletions packages/example/src/modules/program/view/LongProgram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 LongProgramProps = {
isFocused?: boolean;
programInfo: ProgramInfo;
};

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

const scaleAnimation = useFocusAnimation(isFocused);

const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps();

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

LongProgram.displayName = 'LongProgram';

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

const LongProgramImage = styled(Image)({
height: '100%',
width: '100%',
});
14 changes: 12 additions & 2 deletions packages/example/src/modules/program/view/ProgramNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ import { SpatialNavigationNode } from 'react-tv-space-navigation';

import { ProgramInfo } from '../domain/programInfo';
import { Program } from './Program';
import { LongProgram } from './LongProgram';

type Props = {
programInfo: ProgramInfo;
onSelect?: () => void;
indexRange?: [number, number];
};

export const ProgramNode = ({ programInfo, onSelect }: Props) => {
export const ProgramNode = ({ programInfo, onSelect, indexRange }: Props) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect}>
<SpatialNavigationNode isFocusable onSelect={onSelect} indexRange={indexRange}>
{({ isFocused }) => <Program isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
);
};

export const LongProgramNode = ({ programInfo, onSelect, indexRange }: Props) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect} alignInGrid indexRange={indexRange}>
{({ isFocused }) => <LongProgram isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
);
};
85 changes: 85 additions & 0 deletions packages/example/src/pages/GridWithLongNodesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
DefaultFocus,
SpatialNavigationNode,
SpatialNavigationScrollView,
} from 'react-tv-space-navigation';
import { Page } from '../components/Page';
import '../components/configureRemoteControl';
import { programInfos } from '../modules/program/infra/programInfos';
import styled from '@emotion/native';
import { scaledPixels } from '../design-system/helpers/scaledPixels';
import { LongProgramNode, ProgramNode } from '../modules/program/view/ProgramNode';
import { theme } from '../design-system/theme/theme';

const HEADER_SIZE = scaledPixels(400);

export const GridWithLongNodesPage = () => {
return (
<Page>
<CenteringView>
<GridContainer>
<SpatialNavigationScrollView offsetFromStart={HEADER_SIZE + 20}>
<SpatialNavigationNode alignInGrid>
<DefaultFocus>
<>
<FirstRow />
<SecondRow />
</>
</DefaultFocus>
</SpatialNavigationNode>
</SpatialNavigationScrollView>
</GridContainer>
</CenteringView>
</Page>
);
};

const FirstRow = () => {
return (
<SpatialNavigationNode orientation="horizontal">
<ListContainer>
<LongProgramNode programInfo={programInfos[0]} indexRange={[0, 1]} />
<ProgramNode programInfo={programInfos[1]} indexRange={[2, 2]} />
<ProgramNode programInfo={programInfos[2]} indexRange={[3, 3]} />
<LongProgramNode programInfo={programInfos[3]} indexRange={[4, 5]} />
<ProgramNode programInfo={programInfos[4]} indexRange={[6, 6]} />
</ListContainer>
</SpatialNavigationNode>
);
};

const SecondRow = () => {
const programs = programInfos.slice(6, 13);
return (
<SpatialNavigationNode orientation="horizontal">
<ListContainer>
{/* <LongProgramNode programInfo={programInfos[0]} indexRange={[0, 1]} /> */}
{programs.map((program) => {
return <ProgramNode programInfo={program} key={program.id} />;
})}
</ListContainer>
</SpatialNavigationNode>
);
};

const ListContainer = styled.View(({ theme }) => ({
flexDirection: 'row',
flexWrap: 'wrap',
gap: theme.spacings.$4,
padding: theme.spacings.$4,
}));

const GridContainer = styled.View({
backgroundColor: theme.colors.background.mainHover,
margin: 'auto',
height: '95%',
width: '88%',
borderRadius: scaledPixels(20),
padding: scaledPixels(30),
});

const CenteringView = styled.View({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
});
4 changes: 4 additions & 0 deletions packages/lib/src/spatial-navigation/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSpatialNavigator } from '../context/SpatialNavigatorContext';
import { useBeforeMountEffect } from '../hooks/useBeforeMountEffect';
import { useUniqueId } from '../hooks/useUniqueId';
import { NodeOrientation } from '../types/orientation';
import { NodeIndexRange } from '@bam.tech/lrud';

type FocusableProps = {
isFocusable: true;
Expand All @@ -24,6 +25,7 @@ type DefaultProps = {
/** Use this for grid alignment.
* @see LRUD docs */
alignInGrid?: boolean;
indexRange?: NodeIndexRange;
};
type Props = DefaultProps & (FocusableProps | NonFocusableProps);

Expand Down Expand Up @@ -71,6 +73,7 @@ export const SpatialNavigationNode = ({
orientation = 'vertical',
isFocusable = false,
alignInGrid = false,
indexRange,
children,
}: Props) => {
const spatialNavigator = useSpatialNavigator();
Expand Down Expand Up @@ -118,6 +121,7 @@ export const SpatialNavigationNode = ({
onSelect: () => currentOnSelect.current?.(),
orientation,
isIndexAlign: alignInGrid,
indexRange,
});

return () => spatialNavigator.unregisterNode(id);
Expand Down

0 comments on commit 5180c50

Please sign in to comment.