Пространственная навигация (Spatial navigation) — это возможность перемещаться между фокусируемыми элементами в зависимости от их положения в документе. Пространственная навигация часто называется «направленной навигацией», которая обеспечивает четырехнаправленную навигацию: сверху, слева, снизу, справа. Пользователи обычно знакомы с двусторонней навигацией с использованием «клавиш табуляции» для направления вперед, так и «клавиши Shift+Tab» для обратного направления.
Есть спецификация для браузеров, которая пока в статусе "draft".
Мы рекомендуем использовать эту библиотеку для Canvas Apps на наших девайсах: TV, SberBox и др, а библиотеку считать устаревшей.
Для мобильных устройств или других устройств с сенсорным экраном это не надо.
Документации с аннотацией типов также доступна на сайте - https://plasma.sberdevices.ru/spatial/
- Установка
- Минимальная настройка приложения для работы с
@salutejs/spatial
- Углубление в
@salutejs/spatial
- Хуки
- Полезные методы SpatialNavigation
- Оптимизация и ускорение работы
- Запуск тестов
- Pitfalls
npm install --save @salutejs/spatial
Нужно выполнить три обязательных шага.
import { useSpatnavInitialization } from '@salutejs/spatial';
import { Page } from './pages/Page';
const App = () => {
useSpatnavInitialization();
return <Page />;
};
Для навигации @salutejs/spatial
использует секции. Секцию можно добавить с помощью хука useSection
.
import { useSection } from '@salutejs/spatial';
const Page = ({ children }) => {
const [sectionProps] = useSection('sectionName');
return <div {...sectionProps} />;
};
export default Page;
Для того, чтобы браузер имел возможность фокусироваться на DOM элемент, этот элемент должен иметь атрибут tabindex="-1".
Далее для работы внутренних функций @salutejs/spatial
необходимо добавить DOM элементу CSS класс "sn-section-item".
<>
<div className="sn-section-item" tabIndex={-1}>
навигация работает
</div>
<div className="sn-section-item any-class my-class" tabIndex={-1}>
навигация работает
</div>
</>
Если убрать класс sn-section-item
, то элемент исключается из навигации.
<>
<div className="sn-section-item" tabIndex={-1}>
навигация работает
</div>
<div className="any-class my-class" tabIndex={-1}>
навигация НЕ работает
</div>
</>
Добавим элементы в секцию.
import { useEffect, useRef } from 'react';
import { useSection } from '@salutejs/spatial';
const Page = ({ children }) => {
// создание секции
const [sectionProps] = useSection('sectionName');
// установка фокуса на элемент
const ref = useRef(null);
useEffect(() => {
const focusable = ref.current;
if (focusable) {
focusable.focus();
}
}, []);
return (
<div {...sectionProps}>
<div ref={ref} className="sn-section-item" tabIndex={-1}>
навигация работает (после выполнения useEffect, фокус будет установлен на этот элемент)
</div>
<div className="sn-section-item" tabIndex={-1}>
навигация работает
</div>
<div>навигация НЕ работает</div>
</div>
);
};
export default Page;
Готово! Навигация настроена. Но надо подчеркнуть, что после инициализации и добавления секций фокус автоматически не устанавливается ни на один элемент. Это надо делать вручную или с помощью хука useDefaultSectionFocus
.
Здесь были рассмотрены только необходимые действия. Для более гибкой настройки секций и навигации читайте далее.
Можно инициализировать @salutejs/spatial
без использования хука useSpatnavInitialization
, если такое требуется.
import { spatnavInstance } from '@salutejs/spatial';
// вызывать только на клиенте
spatnavInstance.init();
Аналогично можно и отменить инициализацию. Например, при переходе на страницу, где пространственная навигация не нужна.
spatnavInstance.unInit();
Секция — это элементы навигации, объединённые в группу. У секции есть корневой элемент.
Включение секции в навигацию происходит с помощью хука useSection
. У корневого элемента секции должны быть установлены аттрибуты id="имя секции, переданное в useSection"
и className="sn-section-root"
, которые возвращает хук useSection
.
Элементы секции должны быть потомками корневого элемента и иметь аттрибут className="sn-section-item"
.
import { useSection } from '@salutejs/spatial';
const Page = ({ children }) => {
const [section1] = useSection('section1');
const [section2] = useSection('section2');
return (
<>
<div {...section1}>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1
</div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1
</div>
<div>
<div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1
</div>
</div>
</div>
</div>
<div {...section2}>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section2
</div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section2
</div>
</div>
</>
);
};
Также хук useSection
возвращает функцию кастомизации. С её помощью можно гибко настроить правила навигации внутри и между секциями. А также включить или выключить секцию целиком.
import { useSection } from '@salutejs/spatial';
const Page = ({ children }) => {
const [section1, customize1] = useSection('section1');
const [section2, customize2] = useSection('section2');
useEffect(() => {
// выключаем навигацию в секции section1 целиком
customize1({
disabled: true,
});
customize2({
simpleSectionOptions: { type: 'row' },
// установка элемента по умолчанию для секции
getDefaultElement: (section2Root) => section2Root.firstElementChild,
enterTo: 'default-element',
});
}, [[customize1, customize2]]);
// установка секции по умолчанию и установка фокуса на элемент из этой секции, выбранный по правилам определённым в её конфиге
// https://plasma.sberdevices.ru/spatial/functions/useDefaultSectionFocus.html
useDefaultSectionFocus('section2');
return (
<>
<div {...section1}>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1, навигация выключена
</div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1, навигация выключена
</div>
<div>
<div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section1, навигация выключена
</div>
</div>
</div>
</div>
<div {...section2}>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section2. После выполнения всех хуков, фокус будет установлен на этот элемент
</div>
<div className="sn-section-item" tabIndex={-1}>
принадлежит секции section2
</div>
</div>
</>
);
};
О всех параметрах секции можно почитать в документации к типу Config
. Параметры передаются в функцию customize
.
useSpatnavInitialization
- инициализация навигации;useSection
- создание секции;useSelfSection
- создание секции, состоящей только из одного элемента;useDefaultSectionFocus
.
У инстанса SpatialNavigation есть ряд методов для упрощения некоторых сценариев:
- Включение и выключение навигации для данной секции или полностью
- Удаление или добавление секции в навигацию
- Изменение глобального конфига или конфига секции
- Фокусирование указанной секции
Например, фокусирование на определенной секции на монтирование компонента.
import { useSection, spatnavInstance } from '@salutejs/spatial';
const Page = ({ children }) => {
const [sectionProps] = useSection('suggest');
useEffect(() => {
spatnavInstance.focus('suggest');
}, []);
<Suggest {...sectionProps}>
...
<Suggest/>
};
Полный список методов и их сигнатуры смотрите в коде SpatialNavigation или на советующей странице документации
Для ускорения расчётов в @salutejs/spatial
используются Intersection и Mutation observer. Первый следит за тем какой элемент находится во вьюпорте браузера. @salutejs/spatial
в первую очередь будет пытаться найти подходящий для навигации элемент именно среди видимых элементов.
Mutation observer нужен для того, чтобы при обновлении DOM дерева, новые элементы навигации были обработаны Intersection observer'ом.
@salutejs/spatial
делает довольно много расчётов, чтобы понять какой элемент больше подходит для навигации.
Но если в вашей вёрстке есть списки, в которых элементы всегда расположенны в ряд или столбик, то лучше включить режим простых секций. Этот режим переопределяет поведение навигации при выборе следующего или предыдущего элемента.
Вместо расчётов @salutejs/spatial
просто возьмёт нужный элемент из DOM с помощью nextSiblingElement
или previousSiblingElement
.
Для того, чтобы этот режим работал необходимо передать соответствующую опцию в конфиг секции. И, обратите внимание, что все элементы секции должны быть на одном уровне в DOM.
В обоих примерах ниже режим простых секций будет работать.
import { useSection } from '@salutejs/spatial';
const Page = ({ children }) => {
const [section1, customize1] = useSection('section1');
const [section2, customize2] = useSection('section2');
useEffect(() => {
customize1({
simpleSectionOptions: { type: 'column' },
});
customize2({
simpleSectionOptions: { type: 'row' },
});
}, [customize1, customize2]);
return (
<>
<div {...section1}>
<div>
<div>
<div className="sn-section-item" tabIndex={-1} />
<div className="sn-section-item" tabIndex={-1} />
</div>
</div>
</div>
<div {...section2}>
<div className="sn-section-item" tabIndex={-1} />
<div className="sn-section-item" tabIndex={-1} />
</div>
</>
);
};
Для запуска тесов нужно собрать пакет spatial, запустить test-app
и cypress
.
npm run build
npm run test-app:start
npm run cypress:open
В открывшемся окне Cypress выбрать E2E Testing
, тестировать можно как в Chrome так и в Electron.
@salutejs/spatial не компилируется в CommonJS, поэтому если вы используете Next.js выполните следующее:
В next.config.js
вам необходимо добавить свойство transpilePackages
.
const config = {
transpilePackages: ['@salutejs/spatial'],
};
Документация по transpilePackages.
Воспользуйтесь пакетом next-transpile-modules.