Навигация
Почти любое приложение — это набор экранов. Переходы между ними бывают плоскими и вложенными, с возвратом на предыдущий экран. Есть и платформенные различия в анимации и стилях элементов навигации.
VKUI предоставляет компоненты для организации экранов, абстрагируя платформенные различия. На этой странице рассмотрим, как с их помощью:
- построить иерархию экранов внутри одного сценария;
- разделить приложение на независимые сценарии (например, по разделам или фичам).
В завершение соберём адаптивный пример со всеми навигационными компонентами.
Предисловие
Термины
Экран — отдельное состояние интерфейса, отображающееся в один момент времени.
Сценарий — последовательность экранов, объединённых одной задачей пользователя. Например, экран «Настройки», откуда можно перейти на экраны «Уведомления», «Конфиденциальность и безопасность» и другие связанные экраны.
Раздел — крупная часть приложения со своими сценариями. Например, «Профиль» или «Сообщения».
Предварительная настройка
Для безопасных боковых отступов и корректной анимации навигационных компонентов оберните приложение в:
SplitLayoutсheaderв виде заглушкиPanelHeader— она компенсирует боковые отступы для видимыхPanelHeader(на платформе vkcom заглушка не нужна).SplitColсо свойствамиstretchedOnMobileиautoSpaced:stretchedOnMobileрастягивает колонку на всю ширину на мобильных;autoSpacedвключает автоматические боковые отступы на широких экранах — это нужно, в частности, при использованииGroup.
Обёртка одна на всё приложение, поэтому разместите её ближе к корню — в точке входа приложения.
import { SplitLayout, SplitCol } from '@vkontakte/vkui';
export default function App() {
return (
<SplitLayout header={<PanelHeader delimiter="none" />}>
<SplitCol stretchedOnMobile autoSpaced>
{/* ... */}
</SplitCol>
</Panel>
);
};Экран
Описывается компонентом Panel. Чтобы пользователь понимал, где находится, добавьте заголовок через PanelHeader.
Panel
└─ PanelHeader
└─ <content>Ниже — пример описания начального экрана приложения в отдельном файле.
import { type PanelProps, Panel, PanelHeader } from '@vkontakte/vkui';
/**
* Как пример, наследуем весь `PanelProps`, но достаточно будет
* передавать только идентификатор (`Pick<PanelProps, 'id'>`),
* а остальные свойства определять тут при необходимости.
*/
export const Home = (props: PanelProps) => {
return (
<Panel {...props}>
<PanelHeader>Главная</PanelHeader>
Привет, Мир!
</Panel>
);
};Сценарий
За переключение экранов отвечает View. Он принимает любое количество Panel с уникальным id, а в свойство activePanel передаётся id нужного экрана.
View
└─ Panel N
└─ PanelHeader
└─ <content>Представим простой пример в отдельном файле.
import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
export const Router = () => {
const [activePanel, setActivePanel] = useState('panel-1');
return (
<View activePanel={activePanel}>
<Panel id="panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};Раздел
Для разделов есть два компонента: Root и Epic. Выбор зависит от требований к приложению и дизайну.
Root
Root — универсальный вариант. Принимает любое количество View с уникальным id, а в свойство activeView передаётся id нужного сценария.
Root
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>import { useState } from 'react';
import { View, Panel, PanelHeader, PanelHeaderBack, Button } from '@vkontakte/vkui';
export const FirstScenario = ({ onBack }) => {
const [activePanel, setActivePanel] = useState('first-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="first-panel-1">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="first-panel-2">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};
export const SecondScenario = ({ onBack }) => {
const [activePanel, setActivePanel] = useState('second-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="second-panel-1">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="second-panel-2">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};import { useState } from 'react';
import { Root } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
export const Router = () => {
const [activeView, setActiveView] = useState('main');
const onBack = () => setActiveView('main');
return (
<Root activeView={activeView}>
<View id="main" activePanel="main-panel">
<Panel id="main-panel">
<PanelHeader>Главный экран</PanelHeader>
<Button onClick={() => setActiveView('first')}>Перейти к сценарию 1</Button>
<Button onClick={() => setActiveView('second')}>Перейти к сценарию 2</Button>
</Panel>
</View>
<FirstScenario id="first" onBack={onBack} />
<SecondScenario id="second" onBack={onBack} />
</Root>
);
};Epic
Epic — для мобильных приложений, где по дизайну нужна классическая нижняя панель с основными разделами. Пример — m.vk.com ↗ или нативное приложение VK.
Отличия от Root:
- принимает
Tabbarчерез свойствоtabbar; - потомками могут быть не только
View, но иRoot— для более глубокой иерархии сценариев; - переключает разделы без анимаций.
Принимает любое количество View и/или Root с уникальным id, а в свойство activeStory передаётся id нужного сценария.
Epic
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>
└─ Root N
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
export const FirstScenario = () => {
const [activePanel, setActivePanel] = useState('first-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="first-panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="first-panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};
export const SecondScenario = () => {
const [activePanel, setActivePanel] = useState('second-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="second-panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="second-panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};import { useState } from 'react';
import { Epic } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
export const Router = () => {
const [activeStory, setActiveStory] = useState('main');
return (
<Epic
activeStory={activeStory}
tabbar={
<Tabbar>
<TabbarItem
label="Главный экран"
selected={activeStory === id}
onClick={() => setActiveStory('main')}
>
🏠
</TabbarItem>
<TabbarItem
label="Перейти к сценарию 1"
selected={activeStory === id}
onClick={() => setActiveStory('first')}
>
1️⃣
</TabbarItem>
<TabbarItem
label="Перейти к сценарию 2"
selected={activeStory === id}
onClick={() => setActiveStory('second')}
>
2️⃣
</TabbarItem>
</Tabbar>
}
>
<View id="main" activePanel="main-panel">
<Panel id="main-panel">
<PanelHeader>Главный экран</PanelHeader>
</Panel>
</View>
<FirstScenario id="first" />
<SecondScenario id="second" />
</Epic>
);
};Интерактивный пример
Создадим приложение вот с такой структурой:
app (Epic)
└─ profile (View)
└─ profile-panel (Panel)
└─ feed (View)
└─ feed-panel (Panel)
└─ messenger (View)
└─ messenger-panel (Panel)
└─ more (Root)
└─ main-scenario (View)
└─ main-menu-panel (Panel)
└─ notification-scenario (View)
└─ notification-main-panel (Panel)
└─ notification-messenger-panel (Panel)
└─ about-scenario (View)
└─ about-version-panel (Panel)Для навигации по основным разделам на мобильных используется Tabbar, на настольных — SplitCol с Cell в роли бокового меню.