@hh.ru/magritte-ui-scrollable-container 5.2.60 → 6.0.0
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.
- package/ObservableValue.d.ts +13 -0
- package/ObservableValue.js +24 -0
- package/ObservableValue.js.map +1 -0
- package/ScrollStateProvider.d.ts +35 -0
- package/ScrollStateProvider.js +22 -0
- package/ScrollStateProvider.js.map +1 -0
- package/ScrollableContainer.d.ts +19 -12
- package/ScrollableContainer.js +48 -104
- package/ScrollableContainer.js.map +1 -1
- package/ScrollableContainerButton.d.ts +4 -0
- package/ScrollableContainerButton.js +28 -0
- package/ScrollableContainerButton.js.map +1 -0
- package/ScrollableContainerDefaultLayout.d.ts +3 -0
- package/ScrollableContainerDefaultLayout.js +72 -0
- package/ScrollableContainerDefaultLayout.js.map +1 -0
- package/ScrollableContainerDotPages.d.ts +3 -0
- package/ScrollableContainerDotPages.js +67 -0
- package/ScrollableContainerDotPages.js.map +1 -0
- package/ScrollableContainerGallery.d.ts +4 -0
- package/ScrollableContainerGallery.js +76 -0
- package/ScrollableContainerGallery.js.map +1 -0
- package/ScrollableItem.d.ts +3 -2
- package/ScrollableItem.js +15 -10
- package/ScrollableItem.js.map +1 -1
- package/index.css +18 -44
- package/index.d.ts +5 -3
- package/index.js +16 -6
- package/index.js.map +1 -1
- package/index.mock.d.ts +5 -4
- package/index.mock.js +35 -11
- package/index.mock.js.map +1 -1
- package/package.json +5 -4
- package/scrollableContainer-CsVTi8_Q.js +5 -0
- package/scrollableContainer-CsVTi8_Q.js.map +1 -0
- package/types.d.ts +61 -83
- package/types.js.map +1 -1
- package/useScrollContainer.d.ts +20 -13
- package/useScrollContainer.js +190 -7
- package/useScrollContainer.js.map +1 -1
- package/useScrollableControls.d.ts +4 -0
- package/useScrollableControls.js +57 -0
- package/useScrollableControls.js.map +1 -0
- package/utils.d.ts +80 -0
- package/utils.js +277 -1
- package/utils.js.map +1 -1
- package/useScrollContainer-JyxpLsxm.js +0 -491
- package/useScrollContainer-JyxpLsxm.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,
|
|
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;;;;"}
|
package/useScrollContainer.d.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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: (
|
|
26
|
+
export declare const useScrollContainer: () => UseScrollContainer;
|
|
20
27
|
export {};
|
package/useScrollContainer.js
CHANGED
|
@@ -1,9 +1,192 @@
|
|
|
1
1
|
import './index.css';
|
|
2
|
-
import 'react';
|
|
3
|
-
import '
|
|
4
|
-
import '@hh.ru/magritte-
|
|
5
|
-
import '
|
|
6
|
-
import './types.js';
|
|
7
|
-
import './
|
|
8
|
-
|
|
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,57 @@
|
|
|
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, computeScrollSnapBehavior, ensureItemReadyForScroll } 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
|
+
const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);
|
|
23
|
+
const target = scrollSnapBehavior(scrollState, nextIndex);
|
|
24
|
+
animateScrollToPosition(scrollState, target);
|
|
25
|
+
}, [scrollState]);
|
|
26
|
+
const scrollToIndex = useCallback((index) => {
|
|
27
|
+
scrollState.wasContainerInteracted = true;
|
|
28
|
+
if (!ensureItemReadyForScroll(scrollState, index)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const scrollSnapBehavior = computeScrollSnapBehavior(scrollState);
|
|
32
|
+
const target = scrollSnapBehavior(scrollState, index);
|
|
33
|
+
animateScrollToPosition(scrollState, target);
|
|
34
|
+
}, [scrollState]);
|
|
35
|
+
const scrollToFirstItem = useCallback(() => scrollToIndex(0), [scrollToIndex]);
|
|
36
|
+
const scrollToLastItem = useCallback(() => scrollToIndex(scrollState.items.get().length - 1), [scrollState, scrollToIndex]);
|
|
37
|
+
const scrollToItem = useCallback((ref) => {
|
|
38
|
+
const index = scrollState.items.get().findIndex((item) => item.current && item.current === ref.current);
|
|
39
|
+
index !== -1 && scrollToIndex(index);
|
|
40
|
+
}, [scrollState, scrollToIndex]);
|
|
41
|
+
return {
|
|
42
|
+
scrollLeft: () => scrollSide(),
|
|
43
|
+
scrollRight: () => scrollSide(true),
|
|
44
|
+
scrollToFirstItem,
|
|
45
|
+
scrollToLastItem,
|
|
46
|
+
scrollToIndex,
|
|
47
|
+
scrollToItem,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const useScrollableControls = () => {
|
|
51
|
+
const scrollState = useScrollState();
|
|
52
|
+
const controls = useScrollableControlsWithState(scrollState);
|
|
53
|
+
return controls;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { useScrollableControls, useScrollableControlsWithState };
|
|
57
|
+
//# 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 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 {};
|