mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 23:12:13 +00:00
feat(useVerticalSpatialNav): added hook for enabling spatial nav on vertical navigation
This commit is contained in:
parent
41865276d5
commit
476f2f8551
3 changed files with 68 additions and 4 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
// Copyright (C) 2017-2023 Smart code 203358507
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar';
|
import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar';
|
||||||
import styles from './MainNavBars.less';
|
import styles from './MainNavBars.less';
|
||||||
|
import { useGamepad, useVerticalSpatialNavigation } from 'stremio/services';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'board', label: 'Board', icon: 'home', href: '#/' },
|
{ id: 'board', label: 'Board', icon: 'home', href: '#/' },
|
||||||
|
|
@ -21,7 +22,13 @@ type Props = {
|
||||||
children?: React.ReactNode,
|
children?: React.ReactNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GAMEPAD_HANDLER_ID = 'vertical-nav';
|
||||||
|
|
||||||
const MainNavBars = memo(({ className, route, query, children }: Props) => {
|
const MainNavBars = memo(({ className, route, query, children }: Props) => {
|
||||||
|
const navRef = React.useRef(null);
|
||||||
|
|
||||||
|
useVerticalSpatialNavigation(navRef, GAMEPAD_HANDLER_ID);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, styles['main-nav-bars-container'])}>
|
<div className={classnames(className, styles['main-nav-bars-container'])}>
|
||||||
<HorizontalNavBar
|
<HorizontalNavBar
|
||||||
|
|
@ -34,6 +41,7 @@ const MainNavBars = memo(({ className, route, query, children }: Props) => {
|
||||||
navMenu={true}
|
navMenu={true}
|
||||||
/>
|
/>
|
||||||
<VerticalNavBar
|
<VerticalNavBar
|
||||||
|
ref={navRef}
|
||||||
className={styles['vertical-nav-bar']}
|
className={styles['vertical-nav-bar']}
|
||||||
selected={route}
|
selected={route}
|
||||||
tabs={TABS}
|
tabs={TABS}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ const { useTranslation } = require('react-i18next');
|
||||||
const NavTabButton = require('./NavTabButton');
|
const NavTabButton = require('./NavTabButton');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
|
const VerticalNavBar = React.memo(React.forwardRef(({ className, selected, tabs }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<nav className={classnames(className, styles['vertical-nav-bar-container'])}>
|
<nav ref={ref} className={classnames(className, styles['vertical-nav-bar-container'])}>
|
||||||
{
|
{
|
||||||
Array.isArray(tabs) ?
|
Array.isArray(tabs) ?
|
||||||
tabs.map((tab, index) => (
|
tabs.map((tab, index) => (
|
||||||
|
|
@ -30,7 +30,7 @@ const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
});
|
}));
|
||||||
|
|
||||||
VerticalNavBar.displayName = 'VerticalNavBar';
|
VerticalNavBar.displayName = 'VerticalNavBar';
|
||||||
|
|
||||||
|
|
|
||||||
56
src/services/SpatialNavigation/useSpatialNavigation.tsx
Normal file
56
src/services/SpatialNavigation/useSpatialNavigation.tsx
Normal 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,
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue