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

Add isActive to SpatialNavigationNode #65

Merged
merged 4 commits into from
Feb 15, 2024
Merged
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
24 changes: 18 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +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. |

| `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. |
| `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` | `({ isFocused, isActive }: { isFocused: boolean, isActive: boolean }) => ReactNode` 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 All @@ -71,6 +68,21 @@ The `SpatialNavigationNode` component receives the following props:
</SpatialNavigationNode>
```

```jsx
<SpatialNavigationNode
orientation="horizontal"
isFocusable={false}
>
{({ isActive }) => (
// This node is active if one of its nodes is focused
<View style={{ backgroundColor: isActive ? 'grey' : 'black' }}>
<FocusableNodes/>
</View>)}
</SpatialNavigationNode>
```

Note : It is not recommend to use isActive on virtualized focusable nodes, as this can lead to unexpected behaviour.

## Important note

The SpatialNavigationNode will use the ref of your component to handle the scrolling.
Expand Down
1 change: 1 addition & 0 deletions packages/example/src/design-system/theme/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const colors = {
background: {
main: '#111111',
mainHover: '#1a1a1a',
mainActive: '#2a2a2a',
light: '#EEF0F5',
lightHover: '#D5D7DC',
contrastText: '#FFFFFF',
Expand Down
34 changes: 20 additions & 14 deletions packages/example/src/modules/program/view/ProgramList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { SpatialNavigationNode, SpatialNavigationVirtualizedList } from 'react-tv-space-navigation';
import { RootStackParamList } from '../../../../App';
import { ProgramInfo } from '../domain/programInfo';
import { getPrograms } from '../infra/programInfos';
Expand Down Expand Up @@ -37,17 +37,21 @@ export const ProgramList = ({
const programInfos = useMemo(() => getPrograms(), []);

return (
<Container style={containerStyle}>
<SpatialNavigationVirtualizedList
orientation={orientation}
data={programInfos}
renderItem={renderItem}
itemSize={theme.sizes.program.portrait.width + 30}
numberOfRenderedItems={WINDOW_SIZE}
numberOfItemsVisibleOnScreen={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
onEndReachedThresholdItemsNumber={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
/>
</Container>
<SpatialNavigationNode>
{({ isActive }) => (
<Container isActive={isActive} style={containerStyle}>
<SpatialNavigationVirtualizedList
orientation={orientation}
data={programInfos}
renderItem={renderItem}
itemSize={theme.sizes.program.portrait.width + 30}
numberOfRenderedItems={WINDOW_SIZE}
numberOfItemsVisibleOnScreen={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
onEndReachedThresholdItemsNumber={NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN}
/>
</Container>
)}
</SpatialNavigationNode>
);
};

Expand All @@ -63,8 +67,10 @@ export const ProgramsRow = ({ containerStyle }: { containerStyle?: object }) =>
);
};

const Container = styled.View(({ theme }) => ({
backgroundColor: theme.colors.background.mainHover,
const Container = styled.View<{ isActive: boolean }>(({ isActive, theme }) => ({
backgroundColor: isActive
? theme.colors.background.mainActive
: theme.colors.background.mainHover,
padding: theme.spacings.$8,
borderRadius: scaledPixels(20),
overflow: 'hidden',
Expand Down
11 changes: 8 additions & 3 deletions packages/lib/src/spatial-navigation/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { NodeIndexRange } from '@bam.tech/lrud';

type FocusableProps = {
isFocusable: true;
children: (props: { isFocused: boolean }) => React.ReactElement;
children: (props: { isFocused: boolean; isActive: boolean }) => React.ReactElement;
};
type NonFocusableProps = {
isFocusable?: false;
children: React.ReactElement;
children: React.ReactElement | ((props: { isActive: boolean }) => React.ReactElement);
};
type DefaultProps = {
onFocus?: () => void;
Expand Down Expand Up @@ -79,6 +79,7 @@ export const SpatialNavigationNode = ({
const spatialNavigator = useSpatialNavigator();
const parentId = useParentId();
const [isFocused, setIsFocused] = useState(false);
const [isActive, setIsActive] = useState(false);
// If parent changes, we have to re-register the Node + all children -> adding the parentId to the nodeId makes the children re-register.
const id = useUniqueId({ prefix: `${parentId}_node_` });

Expand Down Expand Up @@ -122,6 +123,8 @@ export const SpatialNavigationNode = ({
orientation,
isIndexAlign: alignInGrid,
indexRange,
onActive: () => setIsActive(true),
onInactive: () => setIsActive(false),
});

return () => spatialNavigator.unregisterNode(id);
Expand All @@ -135,7 +138,9 @@ export const SpatialNavigationNode = ({

return (
<ParentIdContext.Provider value={id}>
{typeof children === 'function' ? bindRefToChild(children({ isFocused })) : children}
{typeof children === 'function'
? bindRefToChild(children({ isFocused, isActive }))
: children}
</ParentIdContext.Provider>
);
};
Loading