From bb5ee0dcb7422a14c0191998da88b8e951e85c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Leroy?= Date: Tue, 6 Feb 2024 15:54:14 +0100 Subject: [PATCH 1/4] feat(core): add indexRange props to SpatialNavigationNode --- docs/api.md | 3 +++ packages/lib/src/spatial-navigation/components/Node.tsx | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/docs/api.md b/docs/api.md index 1c1b36d5..604e91a9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -51,6 +51,9 @@ 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. | + +| `indexRange` | `number[]` | `undefined` | Determines the indexes when using long nodes in a grid. cf : 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 diff --git a/packages/lib/src/spatial-navigation/components/Node.tsx b/packages/lib/src/spatial-navigation/components/Node.tsx index 36051aa1..32d4f3e7 100644 --- a/packages/lib/src/spatial-navigation/components/Node.tsx +++ b/packages/lib/src/spatial-navigation/components/Node.tsx @@ -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; @@ -24,6 +25,7 @@ type DefaultProps = { /** Use this for grid alignment. * @see LRUD docs */ alignInGrid?: boolean; + indexRange?: NodeIndexRange; }; type Props = DefaultProps & (FocusableProps | NonFocusableProps); @@ -71,6 +73,7 @@ export const SpatialNavigationNode = ({ orientation = 'vertical', isFocusable = false, alignInGrid = false, + indexRange, children, }: Props) => { const spatialNavigator = useSpatialNavigator(); @@ -118,6 +121,7 @@ export const SpatialNavigationNode = ({ onSelect: () => currentOnSelect.current?.(), orientation, isIndexAlign: alignInGrid, + indexRange, }); return () => spatialNavigator.unregisterNode(id); From 9287f5a7b8a2fc0ad3659f897c1615ba6dc3872a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Leroy?= Date: Tue, 6 Feb 2024 16:09:44 +0100 Subject: [PATCH 2/4] chore(example): adding example of grid with long nodes --- packages/example/App.tsx | 3 + .../example/src/design-system/theme/sizes.ts | 1 + .../src/modules/program/view/LongProgram.tsx | 50 +++++++++++ .../src/modules/program/view/ProgramNode.tsx | 14 ++- .../src/pages/GridWithLongNodesPage.tsx | 85 +++++++++++++++++++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 packages/example/src/modules/program/view/LongProgram.tsx create mode 100644 packages/example/src/pages/GridWithLongNodesPage.tsx diff --git a/packages/example/App.tsx b/packages/example/App.tsx index f8cb626f..b170d952 100644 --- a/packages/example/App.tsx +++ b/packages/example/App.tsx @@ -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(); @@ -23,6 +24,7 @@ export type RootTabParamList = { Home: undefined; ProgramGridPage: undefined; NonVirtualizedGridPage: undefined; + GridWithLongNodesPage: undefined; }; export type RootStackParamList = { @@ -48,6 +50,7 @@ const TabNavigator = () => { + ); }; diff --git a/packages/example/src/design-system/theme/sizes.ts b/packages/example/src/design-system/theme/sizes.ts index 27184ed4..36af9455 100644 --- a/packages/example/src/design-system/theme/sizes.ts +++ b/packages/example/src/design-system/theme/sizes.ts @@ -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), diff --git a/packages/example/src/modules/program/view/LongProgram.tsx b/packages/example/src/modules/program/view/LongProgram.tsx new file mode 100644 index 00000000..b9e1a866 --- /dev/null +++ b/packages/example/src/modules/program/view/LongProgram.tsx @@ -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( + ({ isFocused = false, programInfo }, ref) => { + const imageSource = programInfo.image; + + const scaleAnimation = useFocusAnimation(isFocused); + + const accessibilityProps = useSpatialNavigatorFocusableAccessibilityProps(); + + return ( + + + + ); + }, +); + +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%', +}); diff --git a/packages/example/src/modules/program/view/ProgramNode.tsx b/packages/example/src/modules/program/view/ProgramNode.tsx index cc84a27b..e97608fc 100644 --- a/packages/example/src/modules/program/view/ProgramNode.tsx +++ b/packages/example/src/modules/program/view/ProgramNode.tsx @@ -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 ( - + {({ isFocused }) => } ); }; + +export const LongProgramNode = ({ programInfo, onSelect, indexRange }: Props) => { + return ( + + {({ isFocused }) => } + + ); +}; diff --git a/packages/example/src/pages/GridWithLongNodesPage.tsx b/packages/example/src/pages/GridWithLongNodesPage.tsx new file mode 100644 index 00000000..9135dcf2 --- /dev/null +++ b/packages/example/src/pages/GridWithLongNodesPage.tsx @@ -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 ( + + + + + + + <> + + + + + + + + + + ); +}; + +const FirstRow = () => { + return ( + + + + + + + + + + ); +}; + +const SecondRow = () => { + const programs = programInfos.slice(6, 13); + return ( + + + {/* */} + {programs.map((program) => { + return ; + })} + + + ); +}; + +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', +}); From 7b38b1014f125fafcf8e7add4a22f3fde3f1e28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Leroy?= Date: Tue, 6 Feb 2024 16:39:44 +0100 Subject: [PATCH 3/4] doc: Adding alignInGrid doc --- docs/api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.md b/docs/api.md index 604e91a9..2ee63bf3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -52,6 +52,8 @@ The `SpatialNavigationNode` component receives the following props: | `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. cf : 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. | From 61a7cf5f46f26c8aed1bc8fb179f58ef398198ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Leroy?= Date: Tue, 6 Feb 2024 17:49:32 +0100 Subject: [PATCH 4/4] doc: Improve indexRange documentation --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 2ee63bf3..57e59135 100644 --- a/docs/api.md +++ b/docs/api.md @@ -54,7 +54,7 @@ The `SpatialNavigationNode` component receives the following props: | `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. cf : https://github.com/bbc/lrud/blob/master/docs/usage.md#indexrange +| `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. |