diff --git a/docs/api.md b/docs/api.md
index 3a6f5bce..b35a7fb1 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -55,6 +55,12 @@ The `SpatialNavigationNode` component receives the following props:
| `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:
+
+| Name | Type | Description |
+| ----------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| `focus` | `function` | Give the focus to the selected node. |
+
## Usage
```jsx
diff --git a/packages/example/src/modules/program/view/ProgramNode.tsx b/packages/example/src/modules/program/view/ProgramNode.tsx
index e97608fc..2d53a048 100644
--- a/packages/example/src/modules/program/view/ProgramNode.tsx
+++ b/packages/example/src/modules/program/view/ProgramNode.tsx
@@ -3,6 +3,8 @@ import { SpatialNavigationNode } from 'react-tv-space-navigation';
import { ProgramInfo } from '../domain/programInfo';
import { Program } from './Program';
import { LongProgram } from './LongProgram';
+import { forwardRef } from 'react';
+import { SpatialNavigationNodeRef } from '../../../../../lib/src/spatial-navigation/types/SpatialNavigationNodeRef';
type Props = {
programInfo: ProgramInfo;
@@ -10,18 +12,30 @@ type Props = {
indexRange?: [number, number];
};
-export const ProgramNode = ({ programInfo, onSelect, indexRange }: Props) => {
- return (
-
- {({ isFocused }) => }
-
- );
-};
+export const ProgramNode = forwardRef(
+ ({ programInfo, onSelect, indexRange }: Props, ref) => {
+ return (
+
+ {({ isFocused }) => }
+
+ );
+ },
+);
+ProgramNode.displayName = 'ProgramNode';
-export const LongProgramNode = ({ programInfo, onSelect, indexRange }: Props) => {
- return (
-
- {({ isFocused }) => }
-
- );
-};
+export const LongProgramNode = forwardRef(
+ ({ programInfo, onSelect, indexRange }: Props, ref) => {
+ return (
+
+ {({ isFocused }) => }
+
+ );
+ },
+);
+LongProgramNode.displayName = 'LongProgramNode';
diff --git a/packages/example/src/pages/GridWithLongNodesPage.tsx b/packages/example/src/pages/GridWithLongNodesPage.tsx
index 9135dcf2..d46e6a46 100644
--- a/packages/example/src/pages/GridWithLongNodesPage.tsx
+++ b/packages/example/src/pages/GridWithLongNodesPage.tsx
@@ -10,10 +10,16 @@ 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';
+import { MutableRefObject, forwardRef, useRef } from 'react';
+import { Button } from '../design-system/components/Button';
+import { SpatialNavigationNodeRef } from '../../../lib/src/spatial-navigation/types/SpatialNavigationNodeRef';
const HEADER_SIZE = scaledPixels(400);
export const GridWithLongNodesPage = () => {
+ const firstItemRef = useRef(null);
+ const lastItemRef = useRef(null);
+
return (
@@ -22,8 +28,9 @@ export const GridWithLongNodesPage = () => {
<>
-
-
+
+
+
>
@@ -34,11 +41,11 @@ export const GridWithLongNodesPage = () => {
);
};
-const FirstRow = () => {
+const FirstRow = forwardRef((_, ref) => {
return (
-
+
@@ -46,20 +53,44 @@ const FirstRow = () => {
);
-};
+});
+FirstRow.displayName = 'FirstRow';
-const SecondRow = () => {
+const SecondRow = forwardRef((_, ref) => {
const programs = programInfos.slice(6, 13);
return (
- {/* */}
- {programs.map((program) => {
- return ;
+ {programs.map((program, index) => {
+ return (
+
+ );
})}
);
+});
+SecondRow.displayName = 'SecondRow';
+
+const ButtonRow = ({
+ firstItemRef,
+ lastItemRef,
+}: {
+ firstItemRef: MutableRefObject;
+ lastItemRef: MutableRefObject;
+}) => {
+ return (
+
+
+
+
+ );
};
const ListContainer = styled.View(({ theme }) => ({
diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts
index 33e79151..99b0c196 100644
--- a/packages/lib/src/index.ts
+++ b/packages/lib/src/index.ts
@@ -9,6 +9,7 @@ export { SpatialNavigationVirtualizedList } from './spatial-navigation/component
export { SpatialNavigationVirtualizedGrid } from './spatial-navigation/components/virtualizedGrid/SpatialNavigationVirtualizedGrid';
export { useSpatialNavigatorFocusableAccessibilityProps } from './spatial-navigation/hooks/useSpatialNavigatorFocusableAccessibilityProps';
export { useLockSpatialNavigation } from './spatial-navigation/context/LockSpatialNavigationContext';
+export { SpatialNavigationNodeRef } from './spatial-navigation/types/SpatialNavigationNodeRef';
export const SpatialNavigation = {
configureRemoteControl,
diff --git a/packages/lib/src/spatial-navigation/components/Node.tsx b/packages/lib/src/spatial-navigation/components/Node.tsx
index ab34c1aa..c4224154 100644
--- a/packages/lib/src/spatial-navigation/components/Node.tsx
+++ b/packages/lib/src/spatial-navigation/components/Node.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { View } from 'react-native';
import { useSpatialNavigatorDefaultFocus } from '../context/DefaultFocusContext';
import { ParentIdContext, useParentId } from '../context/ParentIdContext';
@@ -8,6 +8,7 @@ import { useBeforeMountEffect } from '../hooks/useBeforeMountEffect';
import { useUniqueId } from '../hooks/useUniqueId';
import { NodeOrientation } from '../types/orientation';
import { NodeIndexRange } from '@bam.tech/lrud';
+import { SpatialNavigationNodeRef } from '../types/SpatialNavigationNodeRef';
type FocusableProps = {
isFocusable: true;
@@ -66,81 +67,95 @@ const useBindRefToChild = () => {
return { bindRefToChild, childRef };
};
-export const SpatialNavigationNode = ({
- onFocus,
- onBlur,
- onSelect,
- orientation = 'vertical',
- isFocusable = false,
- alignInGrid = false,
- indexRange,
- children,
-}: Props) => {
- 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_` });
-
- const { childRef, bindRefToChild } = useBindRefToChild();
-
- const scrollToNodeIfNeeded = useScrollToNodeIfNeeded({ childRef });
-
- /*
- * We don't re-register in LRUD on each render, because LRUD does not allow updating the nodes.
- * Therefore, the SpatialNavigator Node callbacks are registered at 1st render but can change (ie. if props change) afterwards.
- * Since we want the functions to always be up to date, we use a reference to them.
- */
-
- const currentOnSelect = useRef<() => void>();
- currentOnSelect.current = onSelect;
-
- const currentOnFocus = useRef<() => void>();
- currentOnFocus.current = () => {
- onFocus?.();
- scrollToNodeIfNeeded();
- };
-
- const currentOnBlur = useRef<() => void>();
- currentOnBlur.current = onBlur;
-
- const shouldHaveDefaultFocus = useSpatialNavigatorDefaultFocus();
-
- useBeforeMountEffect(() => {
- spatialNavigator.registerNode(id, {
- parent: parentId,
- isFocusable,
- onBlur: () => {
- currentOnBlur.current?.();
- setIsFocused(false);
- },
- onFocus: () => {
- currentOnFocus.current?.();
- setIsFocused(true);
- },
- onSelect: () => currentOnSelect.current?.(),
- orientation,
- isIndexAlign: alignInGrid,
+export const SpatialNavigationNode = forwardRef(
+ (
+ {
+ onFocus,
+ onBlur,
+ onSelect,
+ orientation = 'vertical',
+ isFocusable = false,
+ alignInGrid = false,
indexRange,
- onActive: () => setIsActive(true),
- onInactive: () => setIsActive(false),
- });
-
- return () => spatialNavigator.unregisterNode(id);
- }, [parentId]);
-
- useEffect(() => {
- if (shouldHaveDefaultFocus && isFocusable && !spatialNavigator.hasOneNodeFocused()) {
- spatialNavigator.grabFocus(id);
- }
- }, [id, isFocusable, shouldHaveDefaultFocus, spatialNavigator]);
-
- return (
-
- {typeof children === 'function'
- ? bindRefToChild(children({ isFocused, isActive }))
- : children}
-
- );
-};
+ children,
+ }: Props,
+ ref,
+ ) => {
+ 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_` });
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ focus: () => spatialNavigator.grabFocus(id),
+ }),
+ [spatialNavigator, id],
+ );
+
+ const { childRef, bindRefToChild } = useBindRefToChild();
+
+ const scrollToNodeIfNeeded = useScrollToNodeIfNeeded({ childRef });
+
+ /*
+ * We don't re-register in LRUD on each render, because LRUD does not allow updating the nodes.
+ * Therefore, the SpatialNavigator Node callbacks are registered at 1st render but can change (ie. if props change) afterwards.
+ * Since we want the functions to always be up to date, we use a reference to them.
+ */
+
+ const currentOnSelect = useRef<() => void>();
+ currentOnSelect.current = onSelect;
+
+ const currentOnFocus = useRef<() => void>();
+ currentOnFocus.current = () => {
+ onFocus?.();
+ scrollToNodeIfNeeded();
+ };
+
+ const currentOnBlur = useRef<() => void>();
+ currentOnBlur.current = onBlur;
+
+ const shouldHaveDefaultFocus = useSpatialNavigatorDefaultFocus();
+
+ useBeforeMountEffect(() => {
+ spatialNavigator.registerNode(id, {
+ parent: parentId,
+ isFocusable,
+ onBlur: () => {
+ currentOnBlur.current?.();
+ setIsFocused(false);
+ },
+ onFocus: () => {
+ currentOnFocus.current?.();
+ setIsFocused(true);
+ },
+ onSelect: () => currentOnSelect.current?.(),
+ orientation,
+ isIndexAlign: alignInGrid,
+ indexRange,
+ onActive: () => setIsActive(true),
+ onInactive: () => setIsActive(false),
+ });
+
+ return () => spatialNavigator.unregisterNode(id);
+ }, [parentId]);
+
+ useEffect(() => {
+ if (shouldHaveDefaultFocus && isFocusable && !spatialNavigator.hasOneNodeFocused()) {
+ spatialNavigator.grabFocus(id);
+ }
+ }, [id, isFocusable, shouldHaveDefaultFocus, spatialNavigator]);
+
+ return (
+
+ {typeof children === 'function'
+ ? bindRefToChild(children({ isFocused, isActive }))
+ : children}
+
+ );
+ },
+);
+SpatialNavigationNode.displayName = 'SpatialNavigationNode';
diff --git a/packages/lib/src/spatial-navigation/types/SpatialNavigationNodeRef.ts b/packages/lib/src/spatial-navigation/types/SpatialNavigationNodeRef.ts
new file mode 100644
index 00000000..01c6db57
--- /dev/null
+++ b/packages/lib/src/spatial-navigation/types/SpatialNavigationNodeRef.ts
@@ -0,0 +1,3 @@
+export type SpatialNavigationNodeRef = {
+ focus: () => void;
+};