SpatialNavigationRoot and Focusable Items in TV Apps
The SpatialNavigationRoot
component is the foundation of our spatial navigation system in TV apps. It establishes the root of our navigation tree and manages focus within its child components.
Examining SpatialNavigationRoot
Let’s examine the SpatialNavigationRoot
implementation in app/(drawer)/index.tsx
:
import { SpatialNavigationRoot } from 'react-tv-space-navigation';
import { useIsFocused } from '@react-navigation/native';
import { useMenuContext } from '../../components/MenuContext';
export default function IndexScreen() {
const { isOpen: isMenuOpen } = useMenuContext();
const isFocused = useIsFocused();
const isActive = isFocused && !isMenuOpen;
const onDirectionHandledWithoutMovement = useCallback(
(movement: Direction) => {
console.log("Direction " + movement);
if (movement === 'left' && focusedIndex === 0) {
navigation.dispatch(DrawerActions.openDrawer());
toggleMenu(true);
}
},
[toggleMenu, focusedIndex, navigation],
);
return (
<SpatialNavigationRoot
isActive={isActive}
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}>
<View style={styles.container}>
{/* Screen content */}
</View>
</SpatialNavigationRoot>
);
}
Key points about SpatialNavigationRoot
:
-
isActive
prop:isActive={isActive}
determines when this navigation root should be active.isActive
is calculated based on whether the screen is focused (isFocused
) and the menu is closed (!isMenuOpen
).- This ensures that spatial navigation only works when the screen is visible and ready for interaction.
-
onDirectionHandledWithoutMovement
prop:- This prop takes a callback function that handles directional input when focus can’t move in the specified direction.
- In our example, it’s used to open the drawer when the user tries to navigate left from the leftmost item.
- This is crucial for creating a seamless navigation experience, especially at the edges of your interface.
-
Wrapping the entire screen:
SpatialNavigationRoot
wraps the entire screen content.- This creates a self-contained navigation environment for the screen.
Making Individual Items Focusable
Within the SpatialNavigationRoot
, we need to make individual items focusable. This is typically done using the SpatialNavigationFocusableView
component.
Let’s look at how this is implemented in our content grid:
const renderScrollableRow = useCallback((title: string, ref: React.RefObject<FlatList>) => {
const renderItem = useCallback(({ item, index }: { item: CardData; index: number }) => (
<SpatialNavigationFocusableView
onSelect={() => {
router.push({
pathname: '/details',
params: {
title: item.title,
description: item.description,
headerImage: item.headerImage
}
});
}}
onFocus={() => setFocusedIndex(index)}
>
{({ isFocused }) => (
<View style={[styles.highlightThumbnail, isFocused && styles.highlightThumbnailFocused]}>
<Image source={item.headerImage} style={styles.headerImage} />
<View style={styles.thumbnailTextContainer}>
<Text style={styles.thumbnailText}>{item.title}</Text>
</View>
</View>
)}
</SpatialNavigationFocusableView>
), [router, styles]);
return (
<View style={styles.highlightsContainer}>
<Text style={styles.highlightsTitle}>{title}</Text>
<SpatialNavigationNode>
<DefaultFocus>
<SpatialNavigationVirtualizedList
data={moviesData}
orientation="horizontal"
renderItem={renderItem}
itemSize={scaledPixels(425)}
numberOfRenderedItems={6}
numberOfItemsVisibleOnScreen={4}
onEndReachedThresholdItemsNumber={3}
/>
</DefaultFocus>
</SpatialNavigationNode>
</View>
);
}, [styles]);
Key points about making items focusable:
-
SpatialNavigationFocusableView
:- This component wraps each item in our list, making it focusable.
- It provides props for handling selection and focus events.
-
onSelect
prop:onSelect
defines what happens when the item is selected (typically with the ‘OK’ button on a remote).- In this case, it navigates to a details screen with the item’s data.
-
onFocus
prop:onFocus
is called when the item gains focus.- Here, it updates the
focusedIndex
state, which can be used for other UI effects or logic.
-
Render prop pattern:
- The component uses a render prop that provides an
isFocused
boolean. - This allows us to apply different styles or render different content based on the focus state.
- The component uses a render prop that provides an
-
SpatialNavigationVirtualizedList
:- This is a virtualized list component that works with spatial navigation.
- It’s optimized for performance when rendering large lists of focusable items.
-
DefaultFocus
:- The
DefaultFocus
component specifies which item should receive focus by default when navigating to this list.
- The
Best Practices for Implementing Focusable Items
-
Clear Visual Feedback: Always provide clear visual indication of which item is focused. This could be a border, a change in size, or a change in color.
-
Consistent Behavior: Ensure that all focusable items behave consistently. For example, all items should respond to the ‘OK’ button in a similar manner.
-
Performance Considerations: For large lists of focusable items, use virtualized lists to maintain smooth performance.
-
Grouping: Use
SpatialNavigationNode
to group related focusable items. This can help create more intuitive navigation patterns. -
Default Focus: Always specify a sensible default focus for each screen or section. This ensures that users can start navigating immediately upon entering a new area of your app.
-
Edge Handling: Implement proper handling for navigation at the edges of your focusable areas. This might involve wrapping focus, stopping at edges, or transitioning to different areas of your UI.
Conclusion
Understanding how to implement SpatialNavigationRoot
and create focusable items is crucial for developing effective TV interfaces. These components form the foundation of your app’s navigation system, allowing users to easily move through your UI using a remote control.
In the next section, we’ll explore more the focus management techniques and implement if for some custom components.