feat(useVerticalSpatialNav): added hook for enabling spatial nav on vertical navigation

This commit is contained in:
Botzy 2025-03-26 19:00:55 +02:00
parent 41865276d5
commit 476f2f8551
3 changed files with 68 additions and 4 deletions

View file

@ -1,9 +1,10 @@
// Copyright (C) 2017-2023 Smart code 203358507
import React, { memo } from 'react';
import React, { memo, useEffect } from 'react';
import classnames from 'classnames';
import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar';
import styles from './MainNavBars.less';
import { useGamepad, useVerticalSpatialNavigation } from 'stremio/services';
const TABS = [
{ id: 'board', label: 'Board', icon: 'home', href: '#/' },
@ -21,7 +22,13 @@ type Props = {
children?: React.ReactNode,
};
const GAMEPAD_HANDLER_ID = 'vertical-nav';
const MainNavBars = memo(({ className, route, query, children }: Props) => {
const navRef = React.useRef(null);
useVerticalSpatialNavigation(navRef, GAMEPAD_HANDLER_ID);
return (
<div className={classnames(className, styles['main-nav-bars-container'])}>
<HorizontalNavBar
@ -34,6 +41,7 @@ const MainNavBars = memo(({ className, route, query, children }: Props) => {
navMenu={true}
/>
<VerticalNavBar
ref={navRef}
className={styles['vertical-nav-bar']}
selected={route}
tabs={TABS}

View file

@ -7,10 +7,10 @@ const { useTranslation } = require('react-i18next');
const NavTabButton = require('./NavTabButton');
const styles = require('./styles');
const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
const VerticalNavBar = React.memo(React.forwardRef(({ className, selected, tabs }, ref) => {
const { t } = useTranslation();
return (
<nav className={classnames(className, styles['vertical-nav-bar-container'])}>
<nav ref={ref} className={classnames(className, styles['vertical-nav-bar-container'])}>
{
Array.isArray(tabs) ?
tabs.map((tab, index) => (
@ -30,7 +30,7 @@ const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
}
</nav>
);
});
}));
VerticalNavBar.displayName = 'VerticalNavBar';

View file

@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { useGamepad } from '../GamepadContext';
const useVerticalSpatialNavigation = (sectionRef: React.RefObject<HTMLDivElement>, gamepadHandlerId: string) => {
const gamepad = useGamepad();
useEffect(() => {
const focusableSelector = 'a';
const focusableElements = () =>
Array.from(sectionRef.current?.querySelectorAll(focusableSelector) || []);
const moveFocus = (direction: 'prev' | 'next') => {
const elements = focusableElements();
if (!elements.length) return;
const currentIndex = elements.findIndex((item) => item.classList.contains('selected'));
let nextIndex = currentIndex;
if (direction === 'next')
nextIndex = (elements.length + currentIndex + 1) % elements.length;
if (direction === 'prev')
nextIndex = (elements.length + currentIndex - 1) % elements.length;
elements[nextIndex]?.click();
};
const handleKeyDown = (event) => {
if (!event.nativeEvent?.spatialNavigationPrevented) {
switch (event.key) {
case 'Tab':
moveFocus('next');
break;
default:
break;
}
}
};
document.addEventListener('keydown', handleKeyDown);
gamepad?.on('buttonLT', gamepadHandlerId, () => moveFocus('prev'));
gamepad?.on('buttonRT', gamepadHandlerId, () => moveFocus('next'));
return () => {
document.removeEventListener('keydown', handleKeyDown);
gamepad?.off('buttonLT', gamepadHandlerId);
gamepad?.off('buttonRT', gamepadHandlerId);
};
}, [gamepad, sectionRef]);
return sectionRef;
};
export {
useVerticalSpatialNavigation,
};