@hh.ru/magritte-ui-scrollable-container 5.3.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/ObservableValue.d.ts +13 -0
  2. package/ObservableValue.js +24 -0
  3. package/ObservableValue.js.map +1 -0
  4. package/ScrollStateProvider.d.ts +35 -0
  5. package/ScrollStateProvider.js +22 -0
  6. package/ScrollStateProvider.js.map +1 -0
  7. package/ScrollableContainer.d.ts +19 -12
  8. package/ScrollableContainer.js +48 -104
  9. package/ScrollableContainer.js.map +1 -1
  10. package/ScrollableContainerButton.d.ts +4 -0
  11. package/ScrollableContainerButton.js +28 -0
  12. package/ScrollableContainerButton.js.map +1 -0
  13. package/ScrollableContainerDefaultLayout.d.ts +3 -0
  14. package/ScrollableContainerDefaultLayout.js +72 -0
  15. package/ScrollableContainerDefaultLayout.js.map +1 -0
  16. package/ScrollableContainerDotPages.d.ts +3 -0
  17. package/ScrollableContainerDotPages.js +67 -0
  18. package/ScrollableContainerDotPages.js.map +1 -0
  19. package/ScrollableContainerGallery.d.ts +4 -0
  20. package/ScrollableContainerGallery.js +76 -0
  21. package/ScrollableContainerGallery.js.map +1 -0
  22. package/ScrollableItem.d.ts +3 -2
  23. package/ScrollableItem.js +15 -10
  24. package/ScrollableItem.js.map +1 -1
  25. package/index.css +18 -44
  26. package/index.d.ts +5 -3
  27. package/index.js +16 -6
  28. package/index.js.map +1 -1
  29. package/index.mock.d.ts +5 -4
  30. package/index.mock.js +35 -11
  31. package/index.mock.js.map +1 -1
  32. package/package.json +4 -3
  33. package/scrollableContainer-DSb3wQO4.js +5 -0
  34. package/scrollableContainer-DSb3wQO4.js.map +1 -0
  35. package/types.d.ts +61 -83
  36. package/types.js.map +1 -1
  37. package/useScrollContainer.d.ts +20 -13
  38. package/useScrollContainer.js +190 -7
  39. package/useScrollContainer.js.map +1 -1
  40. package/useScrollableControls.d.ts +4 -0
  41. package/useScrollableControls.js +60 -0
  42. package/useScrollableControls.js.map +1 -0
  43. package/utils.d.ts +80 -0
  44. package/utils.js +277 -1
  45. package/utils.js.map +1 -1
  46. package/useScrollContainer-DvtB6-c_.js +0 -491
  47. package/useScrollContainer-DvtB6-c_.js.map +0 -1
package/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../src/types.ts"],"sourcesContent":["import { HTMLAttributes, RefObject, Dispatch, SetStateAction } from 'react';\n\nimport { DotPagesProps } from '@hh.ru/magritte-ui-dot-pages';\n\nexport const OFFSET_ATTR = Symbol('scroll-item-offset');\n\nexport interface ItemWithOffset {\n [OFFSET_ATTR]: { left: number; right: number };\n}\n\n/**\n * Переменные полностью описывающие состояние скролла, хранятся в одном объекте, чтобы не плодить\n * кучу ref'ов и передавать всё состояние в функции одним аргументом, а не длинным списком переменных\n */\nexport interface ScrollInternalState {\n /** реф на внешнюю обертку, на нее добавляются классы для фейдв по краям и показа/скрытия кнопок скролла */\n wrapperRef: RefObject<HTMLDivElement>;\n /** контейнер с помощью изменения положения которого обеспечивается скролл */\n positionContainerRef: RefObject<HTMLDivElement>;\n /** контейнер в котором лежит еонтейнер позиционирования, определяет видимую область */\n containerRef: RefObject<HTMLDivElement>;\n /** ширина контейнера позиционирования */\n positionContainerWidth: number;\n /** сдвиг контейнера позиционирования относительно родителя */\n positionContainerTranslation: number;\n /** ширина контейнера определяющего видимую область */\n containerWidth: number;\n /** список элементов которые скроллятся */\n items: RefObject<HTMLElement & ItemWithOffset>[];\n /** размер коррекции отступов */\n correction: { leftCorrection: number; rightCorrection: number };\n /** флаг режима без кнопок */\n disableButtons: boolean;\n /**\n * флаг сигнализирующий о том, что происходит анимация подскролла к элементу\n * когда флаг установлен отключаются обработчики wheel и touch cобытий\n * */\n scrollAnimationInProgress: boolean;\n scrollToItemInProgress: RefObject<unknown> | null;\n /**\n * если при маунте дочерних компонентов был вызван scrollToItem, то элемент к которому нужно\n * подскроллить сохраняется здесь мы не можем сразу после маунта скроллить, т.к. неизвестна ширина\n * контейнера позиционирования, кроме того в этом случае подскролл должен происходить без анимации\n */\n scrollToOnMount: RefObject<HTMLElement> | null;\n /**\n * флаг, сигнализирующий о том, что в контейнере зарегистрированы элементы, для которых не произведен расчет позиции\n * это может произойти из-за того, что расчет позиции дебаунсится\n * при наличии этого флага во всех функциях оcуществляющих скролл нужно предварительно синхронно рассчитать позиции\n * всех зарегистрированных элементов\n */\n recalcRequired: boolean;\n /** нужно ли центрировать элемент при событии touchend/inertial end animate */\n needCorrectionOnScrollEnd: boolean;\n /** флаг режима снаппинга */\n snapToNearestItem: boolean;\n /** текущий dot */\n activeDot: number;\n /** экшн для обновления dot */\n setActiveDot: Dispatch<SetStateAction<number>>;\n setCountItems: Dispatch<SetStateAction<number>>;\n /** элемент, на котором был пользователь до скролла */\n previousActiveIndexItem: number;\n /** направление скролла */\n directionRight: boolean;\n /** Колбэк при прокрутке контейнера */\n onScroll?: () => void;\n /* Было ли взаимодействие с компонентом - скролл или прокрутка к элементу */\n wasInteracted: boolean;\n /* Флаг мобильного разрешения */\n isMobile: boolean;\n}\n\nexport interface ScrollableControls {\n /** двигаем ScrollableContainer налево*/\n scrollLeft: () => void;\n /** двигаем ScrollableContainer направо*/\n scrollRight: () => void;\n /** двигаем ScrollableContainer к первому элементу*/\n scrollToFirstItem: () => void;\n /** двигаем ScrollableContainer к последнему элементу*/\n scrollToLastItem: () => void;\n /** функция, возвращающая true если ScrollableContainer до конца прокручен вправо */\n isScrollRightDisabled: () => boolean;\n /** функция, возвращающая true если ScrollableContainer до конца прокручен влево */\n isScrollLeftDisabled: () => boolean;\n /** скролл к элементу по индексу, элементы сортируются по расположению справа на лево */\n scrollToIndex: (index: number) => void;\n /**\n * скролл к элементу по ref, должен передаваться ref на элемент который был\n * зарегистрирован с помощью registerItem API\n **/\n scrollToItem: (ref: RefObject<HTMLElement>) => void;\n}\n\n/**\n * Пропы для настройки коррекции отступов в режиме без кнопок, когда надо чтобы край скролла соответствовал\n * краю родительского контейнера, у которого могут быть отступы. Задаются в виде положительного числа.\n * более специфичные пропсы переопределяют менее специфичные. При выборе значения коррекции для разрешения выбирается\n * ближайшее заданное значение для разрешений меньшего или равного размера. Т.е. если задано correction и correctionM,\n * то для разрешений S и XS будет использовано correction, а для остальных correctionM. Если задано\n * еще и correctionLeftM, то для разрешений начиная с M и больше для левого края будет использовано\n * correctionLeftM, а для правого correctionM.\n */\nexport interface SpacingCorrectionProps {\n /** Величина коррекции отступа, положительное число или 0 */\n correction?: number;\n /** Величина коррекции отступа для разрешения XS и больше, положительное число или 0 */\n correctionXS?: number;\n /** Величина коррекции отступа для разрешения S и больше, положительное число или 0 */\n correctionS?: number;\n /** Величина коррекции отступа для разрешения M и больше, положительное число или 0 */\n correctionM?: number;\n /** Величина коррекции отступа для разрешения L и больше, положительное число или 0 */\n correctionL?: number;\n /** Величина коррекции отступа для разрешения XL и больше, положительное число или 0 */\n correctionXL?: number;\n /** Величина коррекции отступа для разрешения XXL и больше, положительное число или 0 */\n correctionXXL?: number;\n /** Величина коррекции отступа слева для разрешения XS и больше, положительное число или 0 */\n correctionLeftXS?: number;\n /** Величина коррекции левого отступа для разрешения S и больше, положительное число или 0 */\n correctionLeftS?: number;\n /** Величина коррекции левого отступа для разрешения M и больше, положительное число или 0 */\n correctionLeftM?: number;\n /** Величина коррекции левого отступа для разрешения L и больше, положительное число или 0 */\n correctionLeftL?: number;\n /** Величина коррекции левого отступа для разрешения XL и больше, положительное число или 0 */\n correctionLeftXL?: number;\n /** Величина коррекции левого отступа для разрешения XXL и больше, положительное число или 0 */\n correctionLeftXXL?: number;\n /** Величина коррекции отступа справа для разрешения XS и больше, положительное число или 0 */\n correctionRightXS?: number;\n /** Величина коррекции отступа справа для разрешения S и больше, положительное число или 0 */\n correctionRightS?: number;\n /** Величина коррекции отступа справа для разрешения M и больше, положительное число или 0 */\n correctionRightM?: number;\n /** Величина коррекции отступа справа для разрешения L и больше, положительное число или 0 */\n correctionRightL?: number;\n /** Величина коррекции отступа справа для разрешения XL и больше, положительное число или 0 */\n correctionRightXL?: number;\n /** Величина коррекции отступа справа для разрешения XXL и больше, положительное число или 0 */\n correctionRightXXL?: number;\n}\n\nexport interface ScrollableContainerBaseProps {\n /** Нужно ли показывать кнопки навигации (предпологает использование `correction`)*/\n disableButtons?: boolean;\n /** флаг режима снаппинга */\n snapToNearestItem?: boolean;\n /** Нужно ли показывать dot-pages*/\n showDotPages?: boolean;\n /** Колбэк при изменении active dot в компоненте dot-pages*/\n onChangeDot?: (dot: number) => void;\n /** Свойства dot-pages*/\n dotPagesProps?: Omit<DotPagesProps, 'active' | 'count' | 'data-qa'>;\n /** Колбэк при прокрутке контейнера */\n onScroll?: () => void;\n}\n\nexport type ScrollableContainerProps = Omit<\n HTMLAttributes<HTMLDivElement>,\n keyof SpacingCorrectionProps | 'disableButtons' | 'onScroll'\n> &\n ScrollableContainerBaseProps &\n SpacingCorrectionProps;\n"],"names":[],"mappings":"MAIa,WAAW,GAAG,MAAM,CAAC,oBAAoB;;;;"}
1
+ {"version":3,"file":"types.js","sources":["../src/types.ts"],"sourcesContent":["import type { HTMLAttributes, RefObject, MouseEventHandler, ReactNode } from 'react';\n\nimport { TokenBaseStaticSpace } from '@hh.ru/magritte-design-tokens/types';\nimport type { DotPagesProps } from '@hh.ru/magritte-ui-dot-pages';\n\nexport const OFFSET_ATTR = Symbol('scroll-item-offset');\nexport interface ItemWithOffset {\n [OFFSET_ATTR]: { left: number; right: number };\n}\n\nexport interface ControlWithClick {\n onClick?: MouseEventHandler;\n 'data-qa'?: string;\n}\n\nexport interface RegisterAndRemoveItem {\n /** колбэк для регистрации элемента галерии */\n registerItem: (ref: RefObject<HTMLElement>) => void;\n /** колбэк для удаления элемента галерии */\n removeItem: (ref: RefObject<HTMLElement>) => void;\n}\n\nexport interface ScrollDisableState {\n /** заблокирован ли скролл влево */\n isScrollRightDisabled: boolean;\n /** заблокирован ли скролл вправо */\n isScrollLeftDisabled: boolean;\n}\n\nexport interface ScrollableControls {\n /** двигаем ScrollableContainer налево */\n scrollLeft: () => void;\n /** двигаем ScrollableContainer направо */\n scrollRight: () => void;\n /** двигаем ScrollableContainer к первому элементу */\n scrollToFirstItem: () => void;\n /** двигаем ScrollableContainer к последнему элементу */\n scrollToLastItem: () => void;\n /** скролл к элементу по индексу, элементы сортируются по расположению справа на лево */\n scrollToIndex: (index: number) => void;\n /**\n * скролл к элементу по ref, должен передаваться ref на элемент который был\n * зарегистрирован с помощью registerItem API\n **/\n scrollToItem: (ref: RefObject<HTMLElement>) => void;\n}\n\n/**\n * Пропы для настройки коррекции отступов в режиме без кнопок, когда надо чтобы край скролла соответствовал\n * краю родительского контейнера, у которого могут быть отступы. Задаются в виде положительного числа.\n * более специфичные пропсы переопределяют менее специфичные. При выборе значения коррекции для разрешения выбирается\n * ближайшее заданное значение для разрешений меньшего или равного размера. Т.е. если задано correction и correctionM,\n * то для разрешений S и XS будет использовано correction, а для остальных correctionM. Если задано\n * еще и correctionLeftM, то для разрешений начиная с M и больше для левого края будет использовано\n * correctionLeftM, а для правого correctionM.\n */\nexport interface SpacingCorrectionProps {\n /** Величина коррекции отступа, положительное число или 0 */\n correction?: number;\n /** Величина коррекции отступа для разрешения XS и больше, положительное число или 0 */\n correctionXS?: number;\n /** Величина коррекции отступа для разрешения S и больше, положительное число или 0 */\n correctionS?: number;\n /** Величина коррекции отступа для разрешения M и больше, положительное число или 0 */\n correctionM?: number;\n /** Величина коррекции отступа для разрешения L и больше, положительное число или 0 */\n correctionL?: number;\n /** Величина коррекции отступа для разрешения XL и больше, положительное число или 0 */\n correctionXL?: number;\n /** Величина коррекции отступа для разрешения XXL и больше, положительное число или 0 */\n correctionXXL?: number;\n /** Величина коррекции отступа слева для разрешения XS и больше, положительное число или 0 */\n correctionLeftXS?: number;\n /** Величина коррекции левого отступа для разрешения S и больше, положительное число или 0 */\n correctionLeftS?: number;\n /** Величина коррекции левого отступа для разрешения M и больше, положительное число или 0 */\n correctionLeftM?: number;\n /** Величина коррекции левого отступа для разрешения L и больше, положительное число или 0 */\n correctionLeftL?: number;\n /** Величина коррекции левого отступа для разрешения XL и больше, положительное число или 0 */\n correctionLeftXL?: number;\n /** Величина коррекции левого отступа для разрешения XXL и больше, положительное число или 0 */\n correctionLeftXXL?: number;\n /** Величина коррекции отступа справа для разрешения XS и больше, положительное число или 0 */\n correctionRightXS?: number;\n /** Величина коррекции отступа справа для разрешения S и больше, положительное число или 0 */\n correctionRightS?: number;\n /** Величина коррекции отступа справа для разрешения M и больше, положительное число или 0 */\n correctionRightM?: number;\n /** Величина коррекции отступа справа для разрешения L и больше, положительное число или 0 */\n correctionRightL?: number;\n /** Величина коррекции отступа справа для разрешения XL и больше, положительное число или 0 */\n correctionRightXL?: number;\n /** Величина коррекции отступа справа для разрешения XXL и больше, положительное число или 0 */\n correctionRightXXL?: number;\n}\n\nexport type ScrollSnapAlign = 'start' | 'end' | 'center' | 'none';\n\nexport type ScrollableContainerProps = SpacingCorrectionProps & {\n /** Контент ScrollableContainer */\n children: ReactNode;\n /**\n * Определяет, как ближайший элемент будет размещён во вьюпорте при автоматическом скролле\n *\n * - `center` - ближайший элемент центрируется во вьюпорте\n * - `start` - ближайший элемент выравнивается по левой границе вьюпорта\n * - `end` - ближайший элемент выравнивается по правой границе вьюпорта\n */\n scrollSnapAlign?: ScrollSnapAlign;\n} & HTMLAttributes<HTMLDivElement>;\n\nexport interface ScrollableItemProps extends HTMLAttributes<HTMLDivElement> {\n /** Элемент галереи */\n children: ReactNode;\n}\n\nexport interface ScrollableContainerGalleryProps {\n /** Колбэк при прокрутке контейнера */\n onScroll?: (scrollInfo: ScrollDisableState) => void;\n /** Колбэк при инициализации контейнера */\n onReady?: (scrollInfo: ScrollDisableState) => void;\n /** Колбэк при изменении размеров контейнера */\n onChangeSize?: (scrollInfo: ScrollDisableState) => void;\n /** Элементы галереи, которые отображаются внутри контейнера и прокручиваются по горизонтали */\n children: ReactNode;\n}\n\nexport interface ScrollableContainerDotPagesProps {\n /** Колбэк при изменении active dot в компоненте dot-pages*/\n onChangeDot?: (dot: number) => void;\n /** Свойства dot-pages*/\n dotPagesProps?: Omit<DotPagesProps, 'active' | 'count'>;\n /** Верхний отступ для DotPages */\n offsetTop?: TokenBaseStaticSpace;\n}\n\nexport interface ScrollableContainerButtonProps {\n /** Кнопка для скрола налево/направо */\n children: ReactNode;\n}\n\nexport interface ScrollableContainerDefaultLayoutProps {\n /** Контент ScrollableContainerGallery */\n children: ReactNode;\n}\n"],"names":[],"mappings":"MAKa,WAAW,GAAG,MAAM,CAAC,oBAAoB;;;;"}
@@ -1,20 +1,27 @@
1
- import { RefObject } from 'react';
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * Переменные полностью описывающие состояние скролла, хранятся в одном объекте, чтобы не плодить
4
+ * кучу ref'ов и передавать всё состояние в функции одним аргументом, а не длинным списком переменных
5
+ */
6
+ export interface ScrollInternalState {
7
+ /** контейнер, в котором лежит positionContainer (определяет видимую область) */
8
+ containerRef: RefObject<HTMLDivElement>;
9
+ /** контейнер, с помощью изменения положения которого обеспечивается скролл */
10
+ positionContainerRef: RefObject<HTMLDivElement>;
11
+ /** размер коррекции отступов */
12
+ correction: {
13
+ leftCorrection: number;
14
+ rightCorrection: number;
15
+ };
16
+ wasInteracted: boolean;
17
+ }
2
18
  interface UseScrollContainer {
3
19
  registerItem: (ref: RefObject<HTMLElement>) => void;
4
20
  removeItem: (ref: RefObject<HTMLElement>) => void;
5
- scrollToItem: (ref: RefObject<HTMLElement>) => void;
6
- scrollToFirstItem: VoidFunction;
7
- scrollToLastItem: VoidFunction;
8
- scrollSide: (right?: boolean) => void;
9
21
  initScrollHandlers: () => VoidFunction;
10
- wrapperRef: RefObject<HTMLDivElement>;
11
- positionContainerRef: RefObject<HTMLDivElement>;
12
22
  containerRef: RefObject<HTMLDivElement>;
13
- countItems: number;
14
- activeDot: number;
15
- isScrollRightDisabled: () => boolean;
16
- isScrollLeftDisabled: () => boolean;
17
- scrollToIndex: (index: number) => void;
23
+ positionContainerRef: RefObject<HTMLDivElement>;
24
+ isTouch: boolean;
18
25
  }
19
- export declare const useScrollContainer: (leftCorrection: number, rightCorrection: number, disableButtons: boolean, isMobile: boolean, snapToNearestItem?: boolean, onScroll?: () => void) => UseScrollContainer;
26
+ export declare const useScrollContainer: () => UseScrollContainer;
20
27
  export {};
@@ -1,9 +1,192 @@
1
1
  import './index.css';
2
- import 'react';
3
- import 'classnames';
4
- import '@hh.ru/magritte-common-func-utils';
5
- import '@hh.ru/magritte-internal-custom-scroll';
6
- import './types.js';
7
- import './utils.js';
8
- export { u as useScrollContainer } from './useScrollContainer-DvtB6-c_.js';
2
+ import { useState, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
3
+ import { debounce, requestAnimation } from '@hh.ru/magritte-common-func-utils';
4
+ import { clamp, initScrollHandlers } from '@hh.ru/magritte-internal-custom-scroll';
5
+ import { useScrollState } from './ScrollStateProvider.js';
6
+ import { OFFSET_ATTR } from './types.js';
7
+ import { useScrollableControls } from './useScrollableControls.js';
8
+ import { useInitOnce, findIndexNearestToSnapPoint, computeScrollSnapBehavior, animateScrollToPosition } from './utils.js';
9
+ import 'motion/react';
10
+ import './ObservableValue.js';
11
+ import '@hh.ru/magritte-ui-breakpoint';
12
+
13
+ const EPSILON = 0.5;
14
+ const MIN_DELTA_FOR_TOUCH = 5;
15
+ const MAX_OFFSET = { [OFFSET_ATTR]: { left: Number.MAX_SAFE_INTEGER } };
16
+ const useScrollContainer = () => {
17
+ const scrollState = useScrollState();
18
+ const { scrollToIndex } = useScrollableControls();
19
+ const [isTouch, setIsTouch] = useState(false);
20
+ const leftCorrection = scrollState.leftCorrection.get();
21
+ const rightCorrection = scrollState.rightCorrection.get();
22
+ const scrollStateRef = useRef({
23
+ containerRef: useRef(null),
24
+ positionContainerRef: useRef(null),
25
+ correction: { leftCorrection, rightCorrection },
26
+ wasInteracted: false,
27
+ });
28
+ scrollStateRef.current.correction = { leftCorrection, rightCorrection };
29
+ const resizeOberverRef = useRef(null);
30
+ // recalc positions + report widths into store
31
+ const recalculateItemPositions = useCallback(() => {
32
+ const state = scrollStateRef.current;
33
+ const { containerRef, correction, positionContainerRef } = state;
34
+ const { leftCorrection, rightCorrection } = correction;
35
+ const items = scrollState.items.get();
36
+ if (!containerRef.current || !positionContainerRef.current) {
37
+ return;
38
+ }
39
+ const offsetLeft = positionContainerRef.current?.getBoundingClientRect().left ?? 0;
40
+ items.forEach((itemRef) => {
41
+ if (!itemRef.current) {
42
+ return;
43
+ }
44
+ const rect = itemRef.current.getBoundingClientRect();
45
+ itemRef.current[OFFSET_ATTR] = { left: rect.left - offsetLeft, right: rect.right - offsetLeft };
46
+ });
47
+ items.sort((a, b) => (a.current || MAX_OFFSET)[OFFSET_ATTR].left - (b.current || MAX_OFFSET)[OFFSET_ATTR].left);
48
+ const viewportWidth = containerRef.current.offsetWidth - leftCorrection - rightCorrection;
49
+ const contentWidth = items.reduce((pos, itemRef) => Math.max(pos, itemRef.current?.[OFFSET_ATTR]?.right || 0), 0);
50
+ scrollState.viewportWidth.set(viewportWidth);
51
+ scrollState.contentWidth.set(contentWidth);
52
+ scrollState.items.set(items);
53
+ if (scrollState.indexForDeferScroll !== -1) {
54
+ scrollToIndex(scrollState.indexForDeferScroll);
55
+ scrollState.indexForDeferScroll = -1;
56
+ }
57
+ }, [scrollState, scrollToIndex]);
58
+ // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
59
+ const recalculateItemPositionsDebounced = useCallback(debounce(recalculateItemPositions, 10), [
60
+ recalculateItemPositions,
61
+ ]);
62
+ // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
63
+ const scrollToItemDebounced = useInitOnce(() => debounce(() => {
64
+ if (scrollState.scrollSnapAlign === 'none') {
65
+ return;
66
+ }
67
+ const targetItemIndex = findIndexNearestToSnapPoint(scrollState);
68
+ const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);
69
+ const target = scrollSnapBehavior(scrollState, targetItemIndex);
70
+ animateScrollToPosition(scrollState, target);
71
+ }, 100));
72
+ useEffect(() => {
73
+ const state = scrollStateRef.current;
74
+ const observer = new ResizeObserver(requestAnimation(() => {
75
+ recalculateItemPositions();
76
+ }));
77
+ scrollState.items.get().forEach((item) => item.current && observer.observe(item.current));
78
+ state.containerRef.current && observer.observe(state.containerRef.current);
79
+ resizeOberverRef.current = observer;
80
+ return () => observer.disconnect();
81
+ // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
82
+ }, []);
83
+ // Слушатель нужен для пересчета размеров в случае, когда родительский контейнер открывается с анимацией,
84
+ // на которую не срабатывает ResizeObserver - например, Modal с transition: scale
85
+ useLayoutEffect(() => {
86
+ const handler = (event) => {
87
+ const { containerRef } = scrollStateRef.current;
88
+ if (!scrollState.wasContainerInteracted &&
89
+ event.target instanceof Element &&
90
+ containerRef.current &&
91
+ event.target.contains(containerRef.current)) {
92
+ recalculateItemPositions();
93
+ }
94
+ };
95
+ document.addEventListener('transitionend', handler);
96
+ return () => document.removeEventListener('transitionend', handler);
97
+ }, [recalculateItemPositions, scrollState]);
98
+ // Слушатель нужен, чтобы не вызывать скролл в родительском контейнере (например в bs)
99
+ useEffect(() => {
100
+ const wrapperRef = scrollStateRef.current.containerRef.current;
101
+ if (!wrapperRef) {
102
+ return void 0;
103
+ }
104
+ const handleTouchStartHandler = (event) => {
105
+ event.stopPropagation();
106
+ };
107
+ wrapperRef.addEventListener('touchstart', handleTouchStartHandler);
108
+ return () => {
109
+ wrapperRef.removeEventListener('touchstart', handleTouchStartHandler);
110
+ };
111
+ }, []);
112
+ // события тача
113
+ const didContainerMove = useRef(false);
114
+ const onTouchStart = useCallback(() => {
115
+ setIsTouch(true);
116
+ didContainerMove.current = false;
117
+ scrollState.scrollPosition.stop();
118
+ }, [scrollState]);
119
+ const onTouchMove = useCallback((event) => {
120
+ scrollState.wasContainerInteracted = true;
121
+ if (!didContainerMove.current) {
122
+ // надо отличить тач от свайпа
123
+ didContainerMove.current = Math.abs(event.delta) > MIN_DELTA_FOR_TOUCH;
124
+ }
125
+ scrollState.scrollPosition.set(clamp(scrollState.scrollPosition.get() + event.delta, scrollState.viewportWidth.get() - scrollState.contentWidth.get(), 0));
126
+ }, [scrollState]);
127
+ const onTouchEnd = useCallback(() => {
128
+ if (didContainerMove.current) {
129
+ scrollToItemDebounced();
130
+ }
131
+ }, [scrollToItemDebounced]);
132
+ // события мыши
133
+ const didContainerHovered = useRef(false);
134
+ const onMouseEnter = useCallback(() => {
135
+ didContainerHovered.current = true;
136
+ }, []);
137
+ const onWheelMove = useCallback((event) => {
138
+ setIsTouch(false);
139
+ scrollState.wasContainerInteracted = true;
140
+ if (!didContainerHovered.current) {
141
+ return;
142
+ }
143
+ const currentPosition = scrollState.scrollPosition.get();
144
+ const nextPosition = clamp(currentPosition + event.delta, scrollState.viewportWidth.get() - scrollState.contentWidth.get(), 0);
145
+ if (Math.abs(nextPosition - currentPosition) > EPSILON) {
146
+ scrollState.scrollPosition.set(nextPosition);
147
+ scrollToItemDebounced();
148
+ event.preventDefault();
149
+ }
150
+ }, [scrollToItemDebounced, scrollState]);
151
+ const onMouseLeave = useCallback(() => {
152
+ didContainerHovered.current = false;
153
+ }, []);
154
+ const _initScrollHandlers = useCallback(() => initScrollHandlers({
155
+ axis: 'horizontal',
156
+ wrapperRef: scrollStateRef.current.containerRef,
157
+ onTouchStart,
158
+ onTouchMove,
159
+ onTouchEnd,
160
+ onMouseEnter,
161
+ onWheelMove,
162
+ onMouseLeave,
163
+ }), [onTouchMove, onTouchEnd, onWheelMove, onTouchStart, onMouseEnter, onMouseLeave]);
164
+ const registerItem = useCallback((ref) => {
165
+ if (!ref.current) {
166
+ return;
167
+ }
168
+ scrollState.items.get().push(ref);
169
+ scrollState.items.set(scrollState.items.get());
170
+ resizeOberverRef.current?.observe(ref.current);
171
+ recalculateItemPositions();
172
+ }, [recalculateItemPositions, scrollState]);
173
+ const removeItem = useCallback((ref) => {
174
+ if (!ref.current) {
175
+ return;
176
+ }
177
+ scrollState.items.set(scrollState.items.get().filter((stored) => stored.current !== ref.current));
178
+ resizeOberverRef.current?.unobserve(ref.current);
179
+ recalculateItemPositionsDebounced();
180
+ }, [recalculateItemPositionsDebounced, scrollState]);
181
+ return {
182
+ registerItem,
183
+ removeItem,
184
+ initScrollHandlers: _initScrollHandlers,
185
+ containerRef: scrollStateRef.current.containerRef,
186
+ positionContainerRef: scrollStateRef.current.positionContainerRef,
187
+ isTouch,
188
+ };
189
+ };
190
+
191
+ export { useScrollContainer };
9
192
  //# sourceMappingURL=useScrollContainer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollContainer.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
1
+ {"version":3,"file":"useScrollContainer.js","sources":["../src/useScrollContainer.ts"],"sourcesContent":["import { useRef, useEffect, type RefObject, useCallback, useLayoutEffect, useState } from 'react';\n\nimport { requestAnimation, debounce } from '@hh.ru/magritte-common-func-utils';\nimport { clamp, type ScrollGestureEvent, initScrollHandlers } from '@hh.ru/magritte-internal-custom-scroll';\nimport { useScrollState } from '@hh.ru/magritte-ui-scrollable-container/ScrollStateProvider';\nimport { type ItemWithOffset, OFFSET_ATTR } from '@hh.ru/magritte-ui-scrollable-container/types';\nimport { useScrollableControls } from '@hh.ru/magritte-ui-scrollable-container/useScrollableControls';\nimport {\n useInitOnce,\n findIndexNearestToSnapPoint,\n animateScrollToPosition,\n computeScrollSnapBehavior,\n} from '@hh.ru/magritte-ui-scrollable-container/utils';\n\nconst EPSILON = 0.5;\nconst MIN_DELTA_FOR_TOUCH = 5;\nconst MAX_OFFSET = { [OFFSET_ATTR]: { left: Number.MAX_SAFE_INTEGER } };\n\n/**\n * Переменные полностью описывающие состояние скролла, хранятся в одном объекте, чтобы не плодить\n * кучу ref'ов и передавать всё состояние в функции одним аргументом, а не длинным списком переменных\n */\nexport interface ScrollInternalState {\n /** контейнер, в котором лежит positionContainer (определяет видимую область) */\n containerRef: RefObject<HTMLDivElement>;\n /** контейнер, с помощью изменения положения которого обеспечивается скролл */\n positionContainerRef: RefObject<HTMLDivElement>;\n /** размер коррекции отступов */\n correction: { leftCorrection: number; rightCorrection: number };\n /* Было ли взаимодействие с компонентом - скролл или прокрутка к элементу */\n wasInteracted: boolean;\n}\n\ninterface UseScrollContainer {\n registerItem: (ref: RefObject<HTMLElement>) => void;\n removeItem: (ref: RefObject<HTMLElement>) => void;\n initScrollHandlers: () => VoidFunction;\n containerRef: RefObject<HTMLDivElement>;\n positionContainerRef: RefObject<HTMLDivElement>;\n isTouch: boolean;\n}\n\nexport const useScrollContainer = (): UseScrollContainer => {\n const scrollState = useScrollState();\n const { scrollToIndex } = useScrollableControls();\n const [isTouch, setIsTouch] = useState(false);\n const leftCorrection = scrollState.leftCorrection.get();\n const rightCorrection = scrollState.rightCorrection.get();\n const scrollStateRef = useRef<ScrollInternalState>({\n containerRef: useRef<HTMLDivElement>(null),\n positionContainerRef: useRef<HTMLDivElement>(null),\n correction: { leftCorrection, rightCorrection },\n wasInteracted: false,\n });\n scrollStateRef.current.correction = { leftCorrection, rightCorrection };\n const resizeOberverRef = useRef<ResizeObserver | null>(null);\n\n // recalc positions + report widths into store\n const recalculateItemPositions = useCallback(() => {\n const state = scrollStateRef.current;\n const { containerRef, correction, positionContainerRef } = state;\n const { leftCorrection, rightCorrection } = correction;\n const items = scrollState.items.get();\n if (!containerRef.current || !positionContainerRef.current) {\n return;\n }\n const offsetLeft = positionContainerRef.current?.getBoundingClientRect().left ?? 0;\n items.forEach((itemRef) => {\n if (!itemRef.current) {\n return;\n }\n const rect = itemRef.current.getBoundingClientRect();\n itemRef.current[OFFSET_ATTR] = { left: rect.left - offsetLeft, right: rect.right - offsetLeft };\n });\n items.sort((a, b) => (a.current || MAX_OFFSET)[OFFSET_ATTR].left - (b.current || MAX_OFFSET)[OFFSET_ATTR].left);\n const viewportWidth = containerRef.current.offsetWidth - leftCorrection - rightCorrection;\n const contentWidth = items.reduce(\n (pos, itemRef) => Math.max(pos, itemRef.current?.[OFFSET_ATTR]?.right || 0),\n 0\n );\n scrollState.viewportWidth.set(viewportWidth);\n scrollState.contentWidth.set(contentWidth);\n scrollState.items.set(items);\n if (scrollState.indexForDeferScroll !== -1) {\n scrollToIndex(scrollState.indexForDeferScroll);\n scrollState.indexForDeferScroll = -1;\n }\n }, [scrollState, scrollToIndex]);\n\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n const recalculateItemPositionsDebounced = useCallback(debounce(recalculateItemPositions, 10), [\n recalculateItemPositions,\n ]);\n\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n const scrollToItemDebounced = useInitOnce(() =>\n debounce(() => {\n if (scrollState.scrollSnapAlign === 'none') {\n return;\n }\n const targetItemIndex = findIndexNearestToSnapPoint(scrollState);\n const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);\n const target = scrollSnapBehavior(scrollState, targetItemIndex);\n animateScrollToPosition(scrollState, target);\n }, 100)\n );\n\n useEffect(() => {\n const state = scrollStateRef.current;\n const observer = new ResizeObserver(\n requestAnimation(() => {\n recalculateItemPositions();\n })\n );\n\n scrollState.items.get().forEach((item) => item.current && observer.observe(item.current));\n state.containerRef.current && observer.observe(state.containerRef.current);\n resizeOberverRef.current = observer;\n return () => observer.disconnect();\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n }, []);\n\n // Слушатель нужен для пересчета размеров в случае, когда родительский контейнер открывается с анимацией,\n // на которую не срабатывает ResizeObserver - например, Modal с transition: scale\n useLayoutEffect(() => {\n const handler = (event: TransitionEvent) => {\n const { containerRef } = scrollStateRef.current;\n if (\n !scrollState.wasContainerInteracted &&\n event.target instanceof Element &&\n containerRef.current &&\n event.target.contains(containerRef.current)\n ) {\n recalculateItemPositions();\n }\n };\n\n document.addEventListener('transitionend', handler);\n return () => document.removeEventListener('transitionend', handler);\n }, [recalculateItemPositions, scrollState]);\n\n // Слушатель нужен, чтобы не вызывать скролл в родительском контейнере (например в bs)\n useEffect(() => {\n const wrapperRef = scrollStateRef.current.containerRef.current;\n if (!wrapperRef) {\n return void 0;\n }\n const handleTouchStartHandler = (event: TouchEvent) => {\n event.stopPropagation();\n };\n wrapperRef.addEventListener('touchstart', handleTouchStartHandler);\n return () => {\n wrapperRef.removeEventListener('touchstart', handleTouchStartHandler);\n };\n }, []);\n\n // события тача\n const didContainerMove = useRef(false);\n const onTouchStart = useCallback(() => {\n setIsTouch(true);\n didContainerMove.current = false;\n scrollState.scrollPosition.stop();\n }, [scrollState]);\n\n const onTouchMove = useCallback(\n (event: ScrollGestureEvent) => {\n scrollState.wasContainerInteracted = true;\n if (!didContainerMove.current) {\n // надо отличить тач от свайпа\n didContainerMove.current = Math.abs(event.delta) > MIN_DELTA_FOR_TOUCH;\n }\n scrollState.scrollPosition.set(\n clamp(\n scrollState.scrollPosition.get() + event.delta,\n scrollState.viewportWidth.get() - scrollState.contentWidth.get(),\n 0\n )\n );\n },\n [scrollState]\n );\n\n const onTouchEnd = useCallback(() => {\n if (didContainerMove.current) {\n scrollToItemDebounced();\n }\n }, [scrollToItemDebounced]);\n\n // события мыши\n const didContainerHovered = useRef(false);\n const onMouseEnter = useCallback(() => {\n didContainerHovered.current = true;\n }, []);\n\n const onWheelMove = useCallback(\n (event: ScrollGestureEvent) => {\n setIsTouch(false);\n scrollState.wasContainerInteracted = true;\n if (!didContainerHovered.current) {\n return;\n }\n const currentPosition = scrollState.scrollPosition.get();\n const nextPosition = clamp(\n currentPosition + event.delta,\n scrollState.viewportWidth.get() - scrollState.contentWidth.get(),\n 0\n );\n if (Math.abs(nextPosition - currentPosition) > EPSILON) {\n scrollState.scrollPosition.set(nextPosition);\n scrollToItemDebounced();\n event.preventDefault();\n }\n },\n [scrollToItemDebounced, scrollState]\n );\n\n const onMouseLeave = useCallback(() => {\n didContainerHovered.current = false;\n }, []);\n\n const _initScrollHandlers = useCallback(\n () =>\n initScrollHandlers({\n axis: 'horizontal',\n wrapperRef: scrollStateRef.current.containerRef,\n onTouchStart,\n onTouchMove,\n onTouchEnd,\n onMouseEnter,\n onWheelMove,\n onMouseLeave,\n }),\n [onTouchMove, onTouchEnd, onWheelMove, onTouchStart, onMouseEnter, onMouseLeave]\n );\n\n const registerItem = useCallback(\n (ref: RefObject<HTMLElement>) => {\n if (!ref.current) {\n return;\n }\n scrollState.items.get().push(ref as unknown as RefObject<HTMLElement & ItemWithOffset>);\n scrollState.items.set(scrollState.items.get());\n resizeOberverRef.current?.observe(ref.current);\n recalculateItemPositions();\n },\n [recalculateItemPositions, scrollState]\n );\n\n const removeItem = useCallback(\n (ref: RefObject<HTMLElement>) => {\n if (!ref.current) {\n return;\n }\n scrollState.items.set(scrollState.items.get().filter((stored) => stored.current !== ref.current));\n resizeOberverRef.current?.unobserve(ref.current);\n recalculateItemPositionsDebounced();\n },\n [recalculateItemPositionsDebounced, scrollState]\n );\n\n return {\n registerItem,\n removeItem,\n initScrollHandlers: _initScrollHandlers,\n containerRef: scrollStateRef.current.containerRef,\n positionContainerRef: scrollStateRef.current.positionContainerRef,\n isTouch,\n };\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type ScrollState } from '@hh.ru/magritte-ui-scrollable-container/ScrollStateProvider';
2
+ import type { ScrollableControls } from '@hh.ru/magritte-ui-scrollable-container/types';
3
+ export declare const useScrollableControlsWithState: (scrollState: ScrollState) => ScrollableControls;
4
+ export declare const useScrollableControls: () => ScrollableControls;
@@ -0,0 +1,60 @@
1
+ import './index.css';
2
+ import { useCallback } from 'react';
3
+ import { clamp } from '@hh.ru/magritte-internal-custom-scroll';
4
+ import { useScrollState } from './ScrollStateProvider.js';
5
+ import { animateScrollToPosition, findIndexNearestToSnapPoint, ensureItemReadyForScroll, computeScrollSnapBehavior } from './utils.js';
6
+ import 'motion/react';
7
+ import './ObservableValue.js';
8
+ import '@hh.ru/magritte-ui-breakpoint';
9
+ import './types.js';
10
+
11
+ const useScrollableControlsWithState = (scrollState) => {
12
+ const scrollSide = useCallback((toRight = false) => {
13
+ scrollState.wasContainerInteracted = true;
14
+ if (scrollState.scrollSnapAlign === 'none') {
15
+ const delta = toRight ? -scrollState.viewportWidth.get() : scrollState.viewportWidth.get();
16
+ const position = scrollState.scrollPosition.get() + delta;
17
+ animateScrollToPosition(scrollState, position);
18
+ return;
19
+ }
20
+ const index = findIndexNearestToSnapPoint(scrollState);
21
+ const nextIndex = clamp(toRight ? index + 1 : index - 1, 0, scrollState.items.get().length - 1);
22
+ if (!ensureItemReadyForScroll(scrollState, nextIndex)) {
23
+ return;
24
+ }
25
+ const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);
26
+ const target = scrollSnapBehavior(scrollState, nextIndex);
27
+ animateScrollToPosition(scrollState, target);
28
+ }, [scrollState]);
29
+ const scrollToIndex = useCallback((index) => {
30
+ scrollState.wasContainerInteracted = true;
31
+ if (!ensureItemReadyForScroll(scrollState, index)) {
32
+ return;
33
+ }
34
+ const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);
35
+ const target = scrollSnapBehavior(scrollState, index);
36
+ animateScrollToPosition(scrollState, target);
37
+ }, [scrollState]);
38
+ const scrollToFirstItem = useCallback(() => scrollToIndex(0), [scrollToIndex]);
39
+ const scrollToLastItem = useCallback(() => scrollToIndex(scrollState.items.get().length - 1), [scrollState, scrollToIndex]);
40
+ const scrollToItem = useCallback((ref) => {
41
+ const index = scrollState.items.get().findIndex((item) => item.current && item.current === ref.current);
42
+ index !== -1 && scrollToIndex(index);
43
+ }, [scrollState, scrollToIndex]);
44
+ return {
45
+ scrollLeft: () => scrollSide(),
46
+ scrollRight: () => scrollSide(true),
47
+ scrollToFirstItem,
48
+ scrollToLastItem,
49
+ scrollToIndex,
50
+ scrollToItem,
51
+ };
52
+ };
53
+ const useScrollableControls = () => {
54
+ const scrollState = useScrollState();
55
+ const controls = useScrollableControlsWithState(scrollState);
56
+ return controls;
57
+ };
58
+
59
+ export { useScrollableControls, useScrollableControlsWithState };
60
+ //# sourceMappingURL=useScrollableControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollableControls.js","sources":["../src/useScrollableControls.tsx"],"sourcesContent":["import { useCallback, type RefObject } from 'react';\n\nimport { clamp } from '@hh.ru/magritte-internal-custom-scroll';\nimport { useScrollState, type ScrollState } from '@hh.ru/magritte-ui-scrollable-container/ScrollStateProvider';\nimport type { ScrollableControls } from '@hh.ru/magritte-ui-scrollable-container/types';\nimport {\n findIndexNearestToSnapPoint,\n animateScrollToPosition,\n ensureItemReadyForScroll,\n computeScrollSnapBehavior,\n} from '@hh.ru/magritte-ui-scrollable-container/utils';\n\nexport const useScrollableControlsWithState: (scrollState: ScrollState) => ScrollableControls = (scrollState) => {\n const scrollSide = useCallback(\n (toRight: boolean = false) => {\n scrollState.wasContainerInteracted = true;\n if (scrollState.scrollSnapAlign === 'none') {\n const delta = toRight ? -scrollState.viewportWidth.get() : scrollState.viewportWidth.get();\n const position = scrollState.scrollPosition.get() + delta;\n animateScrollToPosition(scrollState, position);\n return;\n }\n const index = findIndexNearestToSnapPoint(scrollState);\n const nextIndex = clamp(toRight ? index + 1 : index - 1, 0, scrollState.items.get().length - 1);\n if (!ensureItemReadyForScroll(scrollState, nextIndex)) {\n return;\n }\n const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);\n const target = scrollSnapBehavior(scrollState, nextIndex);\n animateScrollToPosition(scrollState, target);\n },\n [scrollState]\n );\n\n const scrollToIndex = useCallback(\n (index: number) => {\n scrollState.wasContainerInteracted = true;\n if (!ensureItemReadyForScroll(scrollState, index)) {\n return;\n }\n const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);\n const target = scrollSnapBehavior(scrollState, index);\n animateScrollToPosition(scrollState, target);\n },\n [scrollState]\n );\n\n const scrollToFirstItem = useCallback(() => scrollToIndex(0), [scrollToIndex]);\n\n const scrollToLastItem = useCallback(\n () => scrollToIndex(scrollState.items.get().length - 1),\n [scrollState, scrollToIndex]\n );\n\n const scrollToItem = useCallback(\n (ref: RefObject<HTMLElement>) => {\n const index = scrollState.items.get().findIndex((item) => item.current && item.current === ref.current);\n index !== -1 && scrollToIndex(index);\n },\n [scrollState, scrollToIndex]\n );\n\n return {\n scrollLeft: () => scrollSide(),\n scrollRight: () => scrollSide(true),\n scrollToFirstItem,\n scrollToLastItem,\n scrollToIndex,\n scrollToItem,\n };\n};\n\nexport const useScrollableControls: () => ScrollableControls = () => {\n const scrollState = useScrollState();\n const controls = useScrollableControlsWithState(scrollState);\n return controls;\n};\n"],"names":[],"mappings":"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"}
package/utils.d.ts CHANGED
@@ -1 +1,81 @@
1
+ import { Breakpoint } from '@hh.ru/magritte-ui-breakpoint';
2
+ import { type ScrollState } from '@hh.ru/magritte-ui-scrollable-container/ScrollStateProvider';
3
+ import { type SpacingCorrectionProps, type ScrollDisableState } from '@hh.ru/magritte-ui-scrollable-container/types';
4
+ export declare const BREAKPOINTS_LIST: Breakpoint[];
5
+ export declare const NO_BUTTONS_START_FROM_BREAKPOINT = Breakpoint.S;
1
6
  export declare function debounce<T extends unknown[]>(callback: (...args: T) => void, waitMs: number): typeof callback;
7
+ interface ActualCorrection {
8
+ leftCorrection: number;
9
+ rightCorrection: number;
10
+ }
11
+ /**
12
+ * По переданным параметрам коррекции возвращает значения отступов для текущего брейкпоинта
13
+ * @param props Параметры коррекции
14
+ * @param breakpoint Текуший брейкпоинт
15
+ * @returns Объект содержащий значения отступов
16
+ */
17
+ export declare const getActualCorrection: (props: SpacingCorrectionProps, breakpoint: Breakpoint) => ActualCorrection;
18
+ export declare const useInitOnce: <T>(func: () => T) => T;
19
+ /**
20
+ * На основе стейта расчитывает флаги скрола
21
+ * @param scrollState стейт скролла
22
+ * @returns cостояние блокировки скролла
23
+ */
24
+ export declare const getDisableState: (scrollState: ScrollState) => ScrollDisableState;
25
+ /**
26
+ * Анимирует scrollPosition с учетом границ контенера
27
+ * @param scrollState стейт скролла
28
+ * @param position новая позиция
29
+ * @returns void
30
+ */
31
+ export declare const animateScrollToPosition: (scrollState: ScrollState, position: number | null) => void;
32
+ /**
33
+ * scrollTo* может быть вызвана раньше чем появяться метрики элементов
34
+ * например, scrollToIndex функция может вызвана в useEffect в tabs
35
+ * запоминаем индекс и вызовем скролл, когда метрики будут посчитаны
36
+ */
37
+ export declare const ensureItemReadyForScroll: (scrollState: ScrollState, index: number) => boolean;
38
+ /**
39
+ * На основе позиции, находит ближайший элемент к центру viewport
40
+ * @param scrollState стейт скролла
41
+ * @returns индекс найденого элемента
42
+ */
43
+ export declare const findIndexNearestToSnapPoint: (state: ScrollState) => number;
44
+ interface ScrollBehavior {
45
+ (scrollState: ScrollState, index: number): number | null;
46
+ }
47
+ /**
48
+ * Определят позицию, при которой элемент будет находится на левой границе viewport
49
+ * @param scrollState стейт скролла
50
+ * @param index индекс элемента
51
+ * @returns новая позиция
52
+ */
53
+ export declare const computeStartPositionForIndex: ScrollBehavior;
54
+ /**
55
+ * Определят позицию, при которой элемент будет находится на правой границе viewport
56
+ * @param scrollState стейт скролла
57
+ * @param index индекс элемента
58
+ * @returns новая позиция
59
+ */
60
+ export declare const computeEndPositionForIndex: ScrollBehavior;
61
+ /**
62
+ * Определят позицию, при которой элемент будет в центре viewport
63
+ * @param scrollState стейт скролла
64
+ * @param index индекс элемента
65
+ * @returns новая позиция
66
+ */
67
+ export declare const computeCenterPositionForIndex: ScrollBehavior;
68
+ /**
69
+ * Определят позицию, при которой элемент будет находится во viewport (с учетом боковых кнопок в fade)
70
+ * @param scrollState стейт скролла
71
+ * @param index индекс элемента
72
+ * @returns новая позиция
73
+ */
74
+ export declare const computePositionForIndex: ScrollBehavior;
75
+ /**
76
+ * Определят поведение "подскрола" ближайшего элемента
77
+ * @param scrollState стейт скролла
78
+ * @returns StrategyPosition
79
+ */
80
+ export declare const computeScrollSnapBehavior: (scrollState: ScrollState) => ScrollBehavior;
81
+ export {};