Перейти к содержимому

Навигация

Почти любое приложение — это набор экранов. Переходы между ними бывают плоскими и вложенными, с возвратом на предыдущий экран. Есть и платформенные различия в анимации и стилях элементов навигации.

VKUI предоставляет компоненты для организации экранов, абстрагируя платформенные различия. На этой странице рассмотрим, как с их помощью:

  • построить иерархию экранов внутри одного сценария;
  • разделить приложение на независимые сценарии (например, по разделам или фичам).

В завершение соберём адаптивный пример со всеми навигационными компонентами.

Экран — отдельное состояние интерфейса, отображающееся в один момент времени.

Сценарий — последовательность экранов, объединённых одной задачей пользователя. Например, экран «Настройки», откуда можно перейти на экраны «Уведомления», «Конфиденциальность и безопасность» и другие связанные экраны.

Раздел — крупная часть приложения со своими сценариями. Например, «Профиль» или «Сообщения».

Для безопасных боковых отступов и корректной анимации навигационных компонентов оберните приложение в:

  • SplitLayout с header в виде заглушки PanelHeader — она компенсирует боковые отступы для видимых PanelHeader (на платформе vkcom заглушка не нужна).
  • SplitCol со свойствами stretchedOnMobile и autoSpaced:
    • stretchedOnMobile растягивает колонку на всю ширину на мобильных;
    • autoSpaced включает автоматические боковые отступы на широких экранах — это нужно, в частности, при использовании Group.

Обёртка одна на всё приложение, поэтому разместите её ближе к корню — в точке входа приложения.

src/App.tsx
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>

Ниже — пример описания начального экрана приложения в отдельном файле.

src/panels/home.tsx
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>

Представим простой пример в отдельном файле.

src/router.tsx
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 — универсальный вариант. Принимает любое количество View с уникальным id, а в свойство activeView передаётся id нужного сценария.

Root
  └─ View N
    └─ Panel N
      └─ PanelHeader
      └─ <content>
src/scenarios.tsx
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>
  );
};
src/router.tsx
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 — для мобильных приложений, где по дизайну нужна классическая нижняя панель с основными разделами. Пример — 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>
src/scenarios.tsx
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>
  );
};
src/router.tsx
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 в роли бокового меню.