diff --git a/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.test.tsx b/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.test.tsx
new file mode 100644
index 0000000000..accd2be623
--- /dev/null
+++ b/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.test.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import { ConfigProvider } from '../ConfigProvider/ConfigProvider';
+import { Panel } from '../Panel/Panel';
+import { Root } from '../Root/Root';
+import { View } from '../View/View';
+import { useNavDirection } from './NavTransitionDirectionContext';
+
+function setup({ withAnimationsMode }: { withAnimationsMode: boolean }) {
+ const TestContent = () => {
+ const direction = useNavDirection();
+ return Direction: {direction || 'undefined'};
+ };
+
+ const TestComponent = ({
+ activeView,
+ activePanel = 'v2.1',
+ }: {
+ activeView: string;
+ activePanel?: string;
+ }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ return { TestComponent };
+}
+
+describe('useNavTransition', () => {
+ beforeAll(() => jest.useFakeTimers());
+ afterAll(() => jest.useRealTimers());
+
+ it.each([
+ ['properly detects transition direction without animations', false],
+ ['properly detects transition direction with animations', true],
+ ])('%s', (_name, withAnimationsMode) => {
+ const { TestComponent } = setup({ withAnimationsMode });
+ // transition between views
+ const component = render();
+ expect(screen.queryByText('Direction: undefined')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: backwards')).toBeTruthy();
+
+ component.rerender();
+ act(() => {
+ jest.runAllTimers();
+ });
+ expect(screen.queryByText('Direction: backwards')).toBeTruthy();
+
+ component.rerender();
+ act(() => {
+ jest.runAllTimers();
+ });
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+
+ component.rerender();
+ act(() => {
+ jest.runAllTimers();
+ });
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+
+ // transition between panels
+ component.rerender();
+ act(() => {
+ jest.runAllTimers();
+ });
+ expect(screen.queryByText('Direction: backwards')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: backwards')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+
+ component.rerender();
+ expect(screen.queryByText('Direction: backwards')).toBeTruthy();
+
+ // transition to another view
+ component.rerender();
+ expect(screen.queryByText('Direction: forwards')).toBeTruthy();
+ });
+});
diff --git a/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.tsx b/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.tsx
new file mode 100644
index 0000000000..68fd2129fc
--- /dev/null
+++ b/packages/vkui/src/components/NavTransitionDirectionContext/NavTransitionDirectionContext.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+
+type DirectionContextType = boolean | undefined;
+
+const TransitionDirectionContext = React.createContext(undefined);
+
+export const NavTransitionDirectionProvider = ({
+ children,
+ isBack: isBackProp,
+}: React.PropsWithChildren<{ isBack: DirectionContextType }>) => {
+ const parentIsBack = React.useContext(TransitionDirectionContext);
+ // if local isBack is undefined then transition happend on the parent side (probably Root)
+ const isBack = isBackProp !== undefined ? isBackProp : parentIsBack;
+
+ // 'direction' should always represent the direction state of the panel on mount
+ // save the on mount value of the panel to the state
+ // to make sure we don't trigger new re-render for the panel
+ // due to change in the prop passed to provider
+ const [isBackOnMount] = React.useState(isBack);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export type TransitionDirection = undefined | 'forwards' | 'backwards';
+
+export const useNavDirection = (): TransitionDirection => {
+ const isBack = React.useContext(TransitionDirectionContext);
+ const transitionDirection = isBack === undefined ? undefined : isBack ? 'backwards' : 'forwards';
+
+ return transitionDirection;
+};
diff --git a/packages/vkui/src/components/Root/Readme.md b/packages/vkui/src/components/Root/Readme.md
index e1608903de..caaf1e700f 100644
--- a/packages/vkui/src/components/Root/Readme.md
+++ b/packages/vkui/src/components/Root/Readme.md
@@ -4,6 +4,9 @@
При смене значения свойства `activeView` происходит плавный переход от одной `View` к другой.
Как только он заканчивается, вызывается свойство-функция `onTransition`.
+Чтобы понять был это переход вперёд или назад можно воспользоваться хуком [`useNavDirection()`](#/View?id=usenavdirection_example).
+Этот хук работает даже если анимации выключены (``).
+
```jsx
const [activeView, setActiveView] = useState('view1');
diff --git a/packages/vkui/src/components/Root/Root.tsx b/packages/vkui/src/components/Root/Root.tsx
index 32bb5c6edb..5f41a6eaf2 100644
--- a/packages/vkui/src/components/Root/Root.tsx
+++ b/packages/vkui/src/components/Root/Root.tsx
@@ -10,6 +10,7 @@ import { warnOnce } from '../../lib/warnOnce';
import { ScrollContext } from '../AppRoot/ScrollContext';
import { useConfigProvider } from '../ConfigProvider/ConfigProviderContext';
import { NavTransitionProvider } from '../NavTransitionContext/NavTransitionContext';
+import { NavTransitionDirectionProvider } from '../NavTransitionDirectionContext/NavTransitionDirectionContext';
import { SplitColContext } from '../SplitCol/SplitColContext';
import styles from './Root.module.css';
@@ -145,16 +146,18 @@ export const Root = ({
transition && viewId === activeView && !isBack && styles['Root__view--show-forward'],
)}
>
-
-
- {view}
-
-
+
+
+
+ {view}
+
+
+
);
})}
diff --git a/packages/vkui/src/components/View/Readme.md b/packages/vkui/src/components/View/Readme.md
index a3d536d79a..5bcb70ac04 100644
--- a/packages/vkui/src/components/View/Readme.md
+++ b/packages/vkui/src/components/View/Readme.md
@@ -4,6 +4,8 @@
При смене значения свойства `activePanel` происходит плавный переход от одной панели к другой.
Как только он заканчивается, вызывается свойство-функция `onTransition`.
+Чтобы понять был это переход вперёд или назад можно воспользоваться хуком [`useNavDirection()`](#/View?id=usenavdirection_example). Этот хук работает даже если анимации выключены (``).
+
```jsx
const [activePanel, setActivePanel] = useState('panel1');
@@ -35,7 +37,9 @@ const [activePanel, setActivePanel] = useState('panel1');
;
```
-### [iOS Swipe Back](https://vkcom.github.io/VKUI/#/View?id=iosswipeback)
+
+
+## [iOS Swipe Back](https://vkcom.github.io/VKUI/#/View?id=iosswipeback)
В iOS есть возможность свайпнуть от левого края назад, чтобы перейти на предыдущую панель. Для того, чтобы
повторить такое поведение в VKUI, нужно:
@@ -195,3 +199,285 @@ const SettingsPanelContent = ({ name, onChangeName }) => {
;
```
+
+
+
+## [useNavDirection(): определение типа перехода (вперёд/назад), с которым была отрисована панель.](#/View?id=usenavdirection_example)
+
+Хук `useNavDirection()` возвращает одно из трёх значений:
+
+- `undefined` означает, что компонент был смонтирован без перехода (тип перехода может быть не определён при самом первом монтировании приложения, когда ещё не было переходов между [View](#/View) и [Panel](#/Panel));
+- `"forwards"` переход вперёд;
+- `"backwards"` переход назад.
+
+Xук возвращает правильное значение даже если анимация **выключена** через [ConfigProvider](#/ConfigProvider) (``).
+
+Значение известно ещё до завершения анимации и определяется один раз, при первом монтировании панели.
+
+Этот хук можно использовать для определения типа анимации перехода не только между `Panel` внутри одного `View`, но и между `View` внутри `Root`.
+
+
+
+Хук также работает в режиме [iOS Swipe Back](#/View?id=iosswipeback). Тип перехода известен как только пользователь начал движение.
+
+
+
+В примере ниже c помощью спиннера имитируется загрузка данных если панель отрисована с анимацией перехода вперед.
+Используется два `View` и по три `Panel` компонента в каждом, чтобы показать, что тип перехода известен как при переходе между `View`, так и при переходе между `Panel`.
+
+На третьем `View` пример со свайпом в iOS от левого края назад, где видно, что панель на которую идёт переход определяет его тип в самом начале свайпа.
+
+```jsx
+const Content = () => {
+ const direction = useNavDirection();
+
+ const [spinner, setSpinner] = useState(null);
+
+ React.useEffect(
+ function simulateDataLoadingWhenMovingForwards() {
+ let timerId = null;
+ const loadData = () => {
+ setSpinner();
+ timerId = setTimeout(() => setSpinner(null), 1000);
+ };
+
+ if (direction !== 'backwards') {
+ loadData();
+ }
+
+ return () => clearTimeout(timerId);
+ },
+ [direction],
+ );
+
+ return (
+