Skip to content

Commit

Permalink
Merge pull request #75 from bamlab/feat/add-SpatialNavigationFocusabl…
Browse files Browse the repository at this point in the history
…eView

feat: add spatial navigation focusable view
  • Loading branch information
remilry committed Feb 21, 2024
2 parents 1a9afeb + cf059e9 commit e4646db
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 23 deletions.
37 changes: 29 additions & 8 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,8 @@ 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). |
| `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. |

The `SpatialNavigationNode` component ref expose the following methods:
Expand Down Expand Up @@ -147,6 +141,33 @@ const FocusableNode = () => (
</SpatialNavigationScrollView>;
```

# SpatialNavigationFocusableView

The `SpatialNavigationFocusableView`component is a simple wrapper component that contains a `SpatialNavigationNode`.
This component allows you to not forward the ref of the closest inner view.

## Props

The `SpatialNavigationFocusableView` component receives the following props :

[SpatialNavigationNode props](#props-1) except `isFocusable` that is already set to true.

| Name | Type | Default | Description |
| ----------- | ---------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| `style` | `ViewStyle` | `null` | Style for the View. This can be any valid React Native style

## Usage

```jsx
<SpatialNavigationFocusableView
onFocus={() => console.log('Node gained focus')}
onSelect={() => console.log('Node was selected')}
orientation="horizontal"
>
{({ isFocused }) => <Text style={{ color: isFocused ? 'red' : 'black' }}>Hello World!</Text>}
</SpatialNavigationFocusableView>
```

# SpatialNavigationView

The `SpatialNavigationView` component is a simple wrapper component that contains a `SpatialNavigationNode`.
Expand Down
6 changes: 3 additions & 3 deletions packages/example/src/components/Menu/MenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from '@emotion/native';
import { forwardRef } from 'react';
import { Animated, View } from 'react-native';
import {
SpatialNavigationNode,
SpatialNavigationFocusableView,
useSpatialNavigatorFocusableAccessibilityProps,
} from 'react-tv-space-navigation';
import { Typography } from '../../design-system/components/Typography';
Expand Down Expand Up @@ -40,11 +40,11 @@ ButtonContent.displayName = 'ButtonContent';

export const MenuButton = ({ label, isMenuOpen, onSelect }: ButtonProps) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect}>
<SpatialNavigationFocusableView onSelect={onSelect}>
{({ isFocused }) => (
<ButtonContent label={label} isFocused={isFocused} isMenuOpen={isMenuOpen} />
)}
</SpatialNavigationNode>
</SpatialNavigationFocusableView>
);
};

Expand Down
6 changes: 3 additions & 3 deletions packages/example/src/design-system/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef } from 'react';
import { Animated, View } from 'react-native';
import {
SpatialNavigationNode,
SpatialNavigationFocusableView,
useSpatialNavigatorFocusableAccessibilityProps,
} from 'react-tv-space-navigation';
import { Typography } from './Typography';
Expand Down Expand Up @@ -29,9 +29,9 @@ ButtonContent.displayName = 'ButtonContent';

export const Button = ({ label, onSelect }: ButtonProps) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect}>
<SpatialNavigationFocusableView onSelect={onSelect}>
{({ isFocused }) => <ButtonContent label={label} isFocused={isFocused} />}
</SpatialNavigationNode>
</SpatialNavigationFocusableView>
);
};

Expand Down
11 changes: 5 additions & 6 deletions packages/example/src/modules/program/view/ProgramNode.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SpatialNavigationNode } from 'react-tv-space-navigation';
import { SpatialNavigationFocusableView } from 'react-tv-space-navigation';

import { ProgramInfo } from '../domain/programInfo';
import { Program } from './Program';
Expand All @@ -15,9 +15,9 @@ type Props = {
export const ProgramNode = forwardRef<SpatialNavigationNodeRef, Props>(
({ programInfo, onSelect, indexRange }: Props, ref) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect} indexRange={indexRange} ref={ref}>
<SpatialNavigationFocusableView onSelect={onSelect} indexRange={indexRange} ref={ref}>
{({ isFocused }) => <Program isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
</SpatialNavigationFocusableView>
);
},
);
Expand All @@ -26,15 +26,14 @@ ProgramNode.displayName = 'ProgramNode';
export const LongProgramNode = forwardRef<SpatialNavigationNodeRef, Props>(
({ programInfo, onSelect, indexRange }: Props, ref) => {
return (
<SpatialNavigationNode
isFocusable
<SpatialNavigationFocusableView
onSelect={onSelect}
alignInGrid
indexRange={indexRange}
ref={ref}
>
{({ isFocused }) => <LongProgram isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
</SpatialNavigationFocusableView>
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SpatialNavigationNode } from 'react-tv-space-navigation';
import { SpatialNavigationFocusableView } from 'react-tv-space-navigation';

import { ProgramInfo } from '../domain/programInfo';
import { ProgramLandscape } from './ProgramLandscape';
Expand All @@ -10,8 +10,8 @@ type Props = {

export const ProgramNodeLandscape = ({ programInfo, onSelect }: Props) => {
return (
<SpatialNavigationNode isFocusable onSelect={onSelect}>
<SpatialNavigationFocusableView onSelect={onSelect}>
{({ isFocused }) => <ProgramLandscape isFocused={isFocused} programInfo={programInfo} />}
</SpatialNavigationNode>
</SpatialNavigationFocusableView>
);
};
1 change: 1 addition & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { SpatialNavigationVirtualizedGrid } from './spatial-navigation/component
export { useSpatialNavigatorFocusableAccessibilityProps } from './spatial-navigation/hooks/useSpatialNavigatorFocusableAccessibilityProps';
export { useLockSpatialNavigation } from './spatial-navigation/context/LockSpatialNavigationContext';
export { SpatialNavigationNodeRef } from './spatial-navigation/types/SpatialNavigationNodeRef';
export { SpatialNavigationFocusableView } from './spatial-navigation/components/FocusableView';

export const SpatialNavigation = {
configureRemoteControl,
Expand Down
26 changes: 26 additions & 0 deletions packages/lib/src/spatial-navigation/components/FocusableView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SpatialNavigationNode, SpatialNavigationNodeDefaultProps } from './Node';
import { View, ViewStyle } from 'react-native';
import { forwardRef } from 'react';
import { SpatialNavigationNodeRef } from '../types/SpatialNavigationNodeRef';

type Props = SpatialNavigationNodeDefaultProps & {
style?: ViewStyle;
children:
| React.ReactElement
| ((props: { isFocused: boolean; isActive: boolean }) => React.ReactElement);
};

export const SpatialNavigationFocusableView = forwardRef<SpatialNavigationNodeRef, Props>(
({ children, style, ...props }: Props, ref) => {
return (
<SpatialNavigationNode isFocusable {...props} ref={ref}>
{({ isFocused, isActive }) => (
<View style={style}>
{typeof children === 'function' ? children({ isFocused, isActive }) : children}
</View>
)}
</SpatialNavigationNode>
);
},
);
SpatialNavigationFocusableView.displayName = 'SpatialNavigationFocusableView';
2 changes: 2 additions & 0 deletions packages/lib/src/spatial-navigation/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type DefaultProps = {
};
type Props = DefaultProps & (FocusableProps | NonFocusableProps);

export type SpatialNavigationNodeDefaultProps = DefaultProps;

const useScrollToNodeIfNeeded = ({
childRef,
}: {
Expand Down

0 comments on commit e4646db

Please sign in to comment.