@hh.ru/magritte-ui-swipe 4.0.15

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/Swipe.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { FC } from 'react';
2
+ import { SwipeProps } from './types';
3
+ export declare const Swipe: FC<SwipeProps>;
package/Swipe.js ADDED
@@ -0,0 +1,516 @@
1
+ import './index.css';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { useRef, useState, useCallback, useEffect } from 'react';
4
+ import { useSpring, config, animated } from '@react-spring/web';
5
+ import classNames from 'classnames';
6
+ import { useDebounce } from '@hh.ru/magritte-common-func-utils';
7
+ import { useSwipe, MAIN_AXIS_THRESHOLD_PX } from '@hh.ru/magritte-common-use-swipe/useSwipe';
8
+ import { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';
9
+ import { Layer, LayerName } from '@hh.ru/magritte-ui-layer';
10
+
11
+ var styles = {"text":"magritte-text___gUsfc_4-0-15","text_max-lines":"magritte-text_max-lines___foe9-_4-0-15","textMaxLines":"magritte-text_max-lines___foe9-_4-0-15","text-dynamic":"magritte-text-dynamic___KzitV_4-0-15","textDynamic":"magritte-text-dynamic___KzitV_4-0-15","text-dynamic_stretched":"magritte-text-dynamic_stretched___hv3Th_4-0-15","textDynamicStretched":"magritte-text-dynamic_stretched___hv3Th_4-0-15","text_style-primary":"magritte-text_style-primary___vYlpl_4-0-15","textStylePrimary":"magritte-text_style-primary___vYlpl_4-0-15","text-dynamic-disabled":"magritte-text-dynamic-disabled___M1GFt_4-0-15","textDynamicDisabled":"magritte-text-dynamic-disabled___M1GFt_4-0-15","text_style-secondary":"magritte-text_style-secondary___qb-4O_4-0-15","textStyleSecondary":"magritte-text_style-secondary___qb-4O_4-0-15","text_style-tertiary":"magritte-text_style-tertiary___atZvg_4-0-15","textStyleTertiary":"magritte-text_style-tertiary___atZvg_4-0-15","text_style-accent":"magritte-text_style-accent___JDNe4_4-0-15","textStyleAccent":"magritte-text_style-accent___JDNe4_4-0-15","text_style-accent-secondary":"magritte-text_style-accent-secondary___JM6z5_4-0-15","textStyleAccentSecondary":"magritte-text_style-accent-secondary___JM6z5_4-0-15","text_style-positive":"magritte-text_style-positive___OjJIB_4-0-15","textStylePositive":"magritte-text_style-positive___OjJIB_4-0-15","text_style-positive-secondary":"magritte-text_style-positive-secondary___LkPe1_4-0-15","textStylePositiveSecondary":"magritte-text_style-positive-secondary___LkPe1_4-0-15","text_style-negative":"magritte-text_style-negative___wBLmp_4-0-15","textStyleNegative":"magritte-text_style-negative___wBLmp_4-0-15","text_style-negative-secondary":"magritte-text_style-negative-secondary___CO58y_4-0-15","textStyleNegativeSecondary":"magritte-text_style-negative-secondary___CO58y_4-0-15","text_style-warning":"magritte-text_style-warning___23C3Y_4-0-15","textStyleWarning":"magritte-text_style-warning___23C3Y_4-0-15","text_style-warning-secondary":"magritte-text_style-warning-secondary___ZJ3uf_4-0-15","textStyleWarningSecondary":"magritte-text_style-warning-secondary___ZJ3uf_4-0-15","text_style-special":"magritte-text_style-special___LaqKY_4-0-15","textStyleSpecial":"magritte-text_style-special___LaqKY_4-0-15","text_style-special-secondary":"magritte-text_style-special-secondary___-whB-_4-0-15","textStyleSpecialSecondary":"magritte-text_style-special-secondary___-whB-_4-0-15","text_style-contrast":"magritte-text_style-contrast___Bx24w_4-0-15","textStyleContrast":"magritte-text_style-contrast___Bx24w_4-0-15","text_style-contrast-secondary":"magritte-text_style-contrast-secondary___xMS2f_4-0-15","textStyleContrastSecondary":"magritte-text_style-contrast-secondary___xMS2f_4-0-15","text_typography-title-1-semibold":"magritte-text_typography-title-1-semibold___WDZXM_4-0-15","textTypographyTitle1Semibold":"magritte-text-typography-title-1-semibold___RE-5E_4-0-15","text_typography-title-2-semibold":"magritte-text_typography-title-2-semibold___4PvYn_4-0-15","textTypographyTitle2Semibold":"magritte-text-typography-title-2-semibold___r-gyn_4-0-15","text_typography-title-3-semibold":"magritte-text_typography-title-3-semibold___E7pmd_4-0-15","textTypographyTitle3Semibold":"magritte-text-typography-title-3-semibold___AxOn9_4-0-15","text_typography-title-4-semibold":"magritte-text_typography-title-4-semibold___u-8Kd_4-0-15","textTypographyTitle4Semibold":"magritte-text-typography-title-4-semibold___4FOf2_4-0-15","text_typography-title-5-semibold":"magritte-text_typography-title-5-semibold___PRTQc_4-0-15","textTypographyTitle5Semibold":"magritte-text-typography-title-5-semibold___ayTa0_4-0-15","text_typography-subtitle-1-semibold":"magritte-text_typography-subtitle-1-semibold___TNYKE_4-0-15","textTypographySubtitle1Semibold":"magritte-text_typography-subtitle-1-semibold___TNYKE_4-0-15","text_typography-subtitle-2-semibold":"magritte-text_typography-subtitle-2-semibold___8GJr9_4-0-15","textTypographySubtitle2Semibold":"magritte-text_typography-subtitle-2-semibold___8GJr9_4-0-15","text_typography-subtitle-3-semibold":"magritte-text_typography-subtitle-3-semibold___scHMl_4-0-15","textTypographySubtitle3Semibold":"magritte-text_typography-subtitle-3-semibold___scHMl_4-0-15","text_typography-subtitle-4-semibold":"magritte-text_typography-subtitle-4-semibold___Yr4Bt_4-0-15","textTypographySubtitle4Semibold":"magritte-text_typography-subtitle-4-semibold___Yr4Bt_4-0-15","text_typography-label-1-regular":"magritte-text_typography-label-1-regular___JMO56_4-0-15","textTypographyLabel1Regular":"magritte-text_typography-label-1-regular___JMO56_4-0-15","text_typography-label-2-regular":"magritte-text_typography-label-2-regular___7gS1P_4-0-15","textTypographyLabel2Regular":"magritte-text_typography-label-2-regular___7gS1P_4-0-15","text_typography-label-3-regular":"magritte-text_typography-label-3-regular___DogGv_4-0-15","textTypographyLabel3Regular":"magritte-text_typography-label-3-regular___DogGv_4-0-15","text_typography-label-4-regular":"magritte-text_typography-label-4-regular___TIPoS_4-0-15","textTypographyLabel4Regular":"magritte-text_typography-label-4-regular___TIPoS_4-0-15","text_typography-label-5-regular":"magritte-text_typography-label-5-regular___y55-P_4-0-15","textTypographyLabel5Regular":"magritte-text_typography-label-5-regular___y55-P_4-0-15","text_typography-paragraph-1-regular":"magritte-text_typography-paragraph-1-regular___ZvHBC_4-0-15","textTypographyParagraph1Regular":"magritte-text_typography-paragraph-1-regular___ZvHBC_4-0-15","text_typography-paragraph-2-regular":"magritte-text_typography-paragraph-2-regular___ndKyc_4-0-15","textTypographyParagraph2Regular":"magritte-text_typography-paragraph-2-regular___ndKyc_4-0-15","text_typography-paragraph-3-regular":"magritte-text_typography-paragraph-3-regular___g0oZF_4-0-15","textTypographyParagraph3Regular":"magritte-text_typography-paragraph-3-regular___g0oZF_4-0-15","text_typography-paragraph-4-regular":"magritte-text_typography-paragraph-4-regular___Iy3Zy_4-0-15","textTypographyParagraph4Regular":"magritte-text_typography-paragraph-4-regular___Iy3Zy_4-0-15","text_typography-custom-1-semibold":"magritte-text_typography-custom-1-semibold___Pv6Lp_4-0-15","textTypographyCustom1Semibold":"magritte-text_typography-custom-1-semibold___Pv6Lp_4-0-15","text_typography-custom-2-semibold":"magritte-text_typography-custom-2-semibold___UP1hj_4-0-15","textTypographyCustom2Semibold":"magritte-text_typography-custom-2-semibold___UP1hj_4-0-15","text_typography-custom-3-semibold":"magritte-text_typography-custom-3-semibold___6yO4m_4-0-15","textTypographyCustom3Semibold":"magritte-text_typography-custom-3-semibold___6yO4m_4-0-15","text_typography-custom-4-semibold":"magritte-text_typography-custom-4-semibold___NEahp_4-0-15","textTypographyCustom4Semibold":"magritte-text_typography-custom-4-semibold___NEahp_4-0-15","text_typography-custom-5-semibold":"magritte-text_typography-custom-5-semibold___-UJfV_4-0-15","textTypographyCustom5Semibold":"magritte-text_typography-custom-5-semibold___-UJfV_4-0-15","text_typography-custom-1-italic":"magritte-text_typography-custom-1-italic___-CpMs_4-0-15","textTypographyCustom1Italic":"magritte-text_typography-custom-1-italic___-CpMs_4-0-15","text_typography-custom-2-italic":"magritte-text_typography-custom-2-italic___SvOG4_4-0-15","textTypographyCustom2Italic":"magritte-text_typography-custom-2-italic___SvOG4_4-0-15","text_typography-custom-3-italic":"magritte-text_typography-custom-3-italic___-g0na_4-0-15","textTypographyCustom3Italic":"magritte-text_typography-custom-3-italic___-g0na_4-0-15","text_typography-custom-4-italic":"magritte-text_typography-custom-4-italic___gOGey_4-0-15","textTypographyCustom4Italic":"magritte-text_typography-custom-4-italic___gOGey_4-0-15","text_typography-custom-5-italic":"magritte-text_typography-custom-5-italic___VR9wy_4-0-15","textTypographyCustom5Italic":"magritte-text_typography-custom-5-italic___VR9wy_4-0-15","text_typography-custom-1-medium":"magritte-text_typography-custom-1-medium___hik0e_4-0-15","textTypographyCustom1Medium":"magritte-text_typography-custom-1-medium___hik0e_4-0-15","text_typography-custom-2-medium":"magritte-text_typography-custom-2-medium___-f6Sh_4-0-15","textTypographyCustom2Medium":"magritte-text_typography-custom-2-medium___-f6Sh_4-0-15","text_typography-custom-3-medium":"magritte-text_typography-custom-3-medium___pLvIb_4-0-15","textTypographyCustom3Medium":"magritte-text_typography-custom-3-medium___pLvIb_4-0-15","text_typography-custom-4-medium":"magritte-text_typography-custom-4-medium___zZteM_4-0-15","textTypographyCustom4Medium":"magritte-text_typography-custom-4-medium___zZteM_4-0-15","text_typography-custom-5-medium":"magritte-text_typography-custom-5-medium___gW1Y8_4-0-15","textTypographyCustom5Medium":"magritte-text_typography-custom-5-medium___gW1Y8_4-0-15","text-typography-title-1-semibold":"magritte-text-typography-title-1-semibold___RE-5E_4-0-15","text-typography-title-2-semibold":"magritte-text-typography-title-2-semibold___r-gyn_4-0-15","text-typography-title-3-semibold":"magritte-text-typography-title-3-semibold___AxOn9_4-0-15","text-typography-title-4-semibold":"magritte-text-typography-title-4-semibold___4FOf2_4-0-15","text-typography-title-5-semibold":"magritte-text-typography-title-5-semibold___ayTa0_4-0-15","root":"magritte-root___gbkM-_4-0-15","swipeble":"magritte-swipeble___EVyAI_4-0-15","childrenWrapper":"magritte-childrenWrapper___1tUbf_4-0-15","buttons":"magritte-buttons___PHNdL_4-0-15","buttons-left":"magritte-buttons-left___WPiN4_4-0-15","buttonsLeft":"magritte-buttons-left___WPiN4_4-0-15","buttons-right":"magritte-buttons-right___G3u6y_4-0-15","buttonsRight":"magritte-buttons-right___G3u6y_4-0-15","button":"magritte-button___jZvyY_4-0-15","button-hide":"magritte-button-hide___5AEdR_4-0-15","buttonHide":"magritte-button-hide___5AEdR_4-0-15","button-grow":"magritte-button-grow___IT4MK_4-0-15","buttonGrow":"magritte-button-grow___IT4MK_4-0-15","button-content":"magritte-button-content___hlzd-_4-0-15","buttonContent":"magritte-button-content___hlzd-_4-0-15","button-content-column":"magritte-button-content-column___48Tzo_4-0-15","buttonContentColumn":"magritte-button-content-column___48Tzo_4-0-15","button-label":"magritte-button-label___l03VO_4-0-15","buttonLabel":"magritte-button-label___l03VO_4-0-15","button_style-neutral":"magritte-button_style-neutral___1yyqh_4-0-15","buttonStyleNeutral":"magritte-button_style-neutral___1yyqh_4-0-15","button_style-accent":"magritte-button_style-accent___L9krS_4-0-15","buttonStyleAccent":"magritte-button_style-accent___L9krS_4-0-15","button_style-positive":"magritte-button_style-positive___2pgae_4-0-15","buttonStylePositive":"magritte-button_style-positive___2pgae_4-0-15","button_style-negative":"magritte-button_style-negative___UClE4_4-0-15","buttonStyleNegative":"magritte-button_style-negative___UClE4_4-0-15","button_style-special":"magritte-button_style-special___sGvAc_4-0-15","buttonStyleSpecial":"magritte-button_style-special___sGvAc_4-0-15","button_style-warning":"magritte-button_style-warning___5bEK0_4-0-15","buttonStyleWarning":"magritte-button_style-warning___5bEK0_4-0-15","icon-wrapper":"magritte-icon-wrapper___0xJZX_4-0-15","iconWrapper":"magritte-icon-wrapper___0xJZX_4-0-15"};
12
+
13
+ const MIN_BUTTON_WIDTH = 76;
14
+ const OPEN_THRESHOLD = 1 / 3;
15
+ const MIN_SWIPE_DISTANCE = MIN_BUTTON_WIDTH * OPEN_THRESHOLD;
16
+ var ButtonsState;
17
+ (function (ButtonsState) {
18
+ ButtonsState["Close"] = "close";
19
+ ButtonsState["LeftOpen"] = "leftOpen";
20
+ ButtonsState["RightOpen"] = "rightOpen";
21
+ })(ButtonsState || (ButtonsState = {}));
22
+ var LongState;
23
+ (function (LongState) {
24
+ LongState["Off"] = "off";
25
+ LongState["Left"] = "left";
26
+ LongState["Right"] = "right";
27
+ })(LongState || (LongState = {}));
28
+ const Swipe = ({ children, leftButtons, rightButtons, onStart, onFinish, useLongSwipe = true, dataQa, demo = false, twoSidedDemoMode = 'right-to-left', demoAppearTime = 200, demoIdleTime = 300, demoDisappearTime = 200, onFinishDemo, onStartDemo, onResetDemo, }) => {
29
+ const rootRef = useRef(null);
30
+ const swipeableRef = useRef(null);
31
+ const childrenWrapperRef = useRef(null);
32
+ const [swipeableWidth, setSwipeableWidth] = useState(0);
33
+ const leftButtonsRef = useRef(null);
34
+ const rightButtonsRef = useRef(null);
35
+ const sumRefs = useRef([]); // ссылки на кнопки, по которым считается реальный размер
36
+ const leftButtonsSumSizeRef = useRef(0); // реальный размер, требуемый левым кнопкам
37
+ const rightButtonsSumSizeRef = useRef(0); // реальный размер, требуемый правым кнопкам
38
+ const leftButtonsSizeRef = useRef(0); // установленный размер левых кнопок
39
+ const rigthButtonsSizeRef = useRef(0); // установленный размер правых кнопок
40
+ const prevDistance = useRef(0);
41
+ const swipeThresholdRef = useRef({ max: MIN_SWIPE_DISTANCE });
42
+ const swipeLockRef = useRef(false);
43
+ const { isMobile } = useBreakpoint();
44
+ const [leftBlockStyles, leftBlockApi] = useSpring(() => ({
45
+ width: 0,
46
+ }));
47
+ const [rightBlockStyles, rightBlockApi] = useSpring(() => ({
48
+ width: 0,
49
+ }));
50
+ const [longState, setLongState] = useState(LongState.Off);
51
+ const stateRef = useRef({
52
+ candidate: ButtonsState.Close,
53
+ now: ButtonsState.Close,
54
+ next: ButtonsState.Close, // следующее состояние, после завершения свайпа
55
+ });
56
+ const [demoInProgress, setDemoInProgress] = useState(false);
57
+ const setAnimateButtonsWidth = useCallback((flag) => {
58
+ if (leftButtonsRef.current) {
59
+ leftButtonsRef.current.style.transitionProperty = flag ? 'width' : 'none';
60
+ }
61
+ if (rightButtonsRef.current) {
62
+ rightButtonsRef.current.style.transitionProperty = flag ? 'width' : 'none';
63
+ }
64
+ }, []);
65
+ const setWidth = useDebounce(useCallback(() => {
66
+ if (swipeableRef.current && childrenWrapperRef.current && rootRef.current) {
67
+ const value = rootRef.current.getBoundingClientRect().width;
68
+ // устанавливаем размер области равной размеру контента,
69
+ // чтобы она не растягивалась, когда растягиваются кнопки
70
+ setSwipeableWidth(value);
71
+ swipeableRef.current.style.width = `${value}px`;
72
+ childrenWrapperRef.current.style.width = `${value}px`;
73
+ childrenWrapperRef.current.style.minWidth = `${value}px`;
74
+ }
75
+ }, [setSwipeableWidth]), 64);
76
+ useEffect(() => {
77
+ setWidth();
78
+ }, [setWidth]);
79
+ useEffect(() => {
80
+ if (window && isMobile) {
81
+ window.addEventListener('resize', setWidth);
82
+ }
83
+ return () => {
84
+ if (window && isMobile) {
85
+ window.removeEventListener('resize', setWidth);
86
+ }
87
+ };
88
+ }, [setWidth, isMobile]);
89
+ // обновляет порог срабатывания для useSwipe в зависимости от состояния
90
+ const updateSwipeThreshold = useCallback(() => {
91
+ const { now, candidate } = stateRef.current;
92
+ // если открыта одна из сторон, то мы можем свайпнуть к закрытию, или к активации длинного свайпа
93
+ if ([ButtonsState.LeftOpen, ButtonsState.RightOpen].includes(now)) {
94
+ swipeThresholdRef.current =
95
+ now === ButtonsState.LeftOpen
96
+ ? {
97
+ // свайп к закрытию
98
+ minDelta: -MIN_SWIPE_DISTANCE,
99
+ // свайп к активации длинного свайпа
100
+ maxDelta: useLongSwipe ? MIN_SWIPE_DISTANCE : Number.MAX_SAFE_INTEGER,
101
+ }
102
+ : {
103
+ // свайп к закрытию
104
+ maxDelta: MIN_SWIPE_DISTANCE,
105
+ // свайп к активации длинного свайпа
106
+ minDelta: useLongSwipe ? -MIN_SWIPE_DISTANCE : -Number.MAX_SAFE_INTEGER,
107
+ };
108
+ }
109
+ else if (candidate === ButtonsState.LeftOpen) {
110
+ // если кандидат на открытие слева, то мы можем свайпнуть к открытию слева
111
+ swipeThresholdRef.current = { maxDelta: MIN_SWIPE_DISTANCE };
112
+ }
113
+ else if (candidate === ButtonsState.RightOpen) {
114
+ // если кандидат на открытие справа, то мы можем свайпнуть к открытию справа, дельта с отрицательным знаком
115
+ // потому что свайпаем вправо
116
+ swipeThresholdRef.current = { minDelta: -MIN_SWIPE_DISTANCE };
117
+ }
118
+ }, [useLongSwipe]);
119
+ // анимирует размер кнопок для перехода в переданное состояние
120
+ const animateToState = useCallback((toState, velocity) => {
121
+ leftBlockApi.stop();
122
+ rightBlockApi.stop();
123
+ stateRef.current.now = toState;
124
+ const animationConfig = {
125
+ width: 0,
126
+ config: {
127
+ ...config.gentle,
128
+ clamp: true,
129
+ velocity: 0,
130
+ },
131
+ };
132
+ let api;
133
+ // если нужно закрыть кнопки, то сначала определяем с какой стороны кнопки открыты
134
+ // получаем апи для акнимации кнопок с нужной стороны
135
+ // вычисляем начальную скорость, т.к. анимация закрытия кнопок с разных сторон направлена в разные стороны
136
+ // то для правой стороны нужно поменять знак начальной скорости
137
+ if (toState === ButtonsState.Close) {
138
+ const fromLeft = leftBlockApi.current[0].get().width > 0;
139
+ api = fromLeft ? leftBlockApi : rightBlockApi;
140
+ const v = fromLeft ? velocity : -velocity;
141
+ // скорость делим на 1000 т.к. в событии приходит скорость в пикселях в секунду
142
+ // а нужна в пикселях в миллисекунду
143
+ animationConfig.config.velocity = v / 1000;
144
+ }
145
+ else {
146
+ // если нужно открыть кнопки, то получаем апи для анимации блока кнопок с нужной стороны
147
+ // вычисляем начальную скорость, т.к. анимация открытия кнопок с разных сторон направлена в разные стороны
148
+ // то для правой стороны нужно поменять знак начальной скорости
149
+ api = toState === ButtonsState.LeftOpen ? leftBlockApi : rightBlockApi;
150
+ const v = toState === ButtonsState.LeftOpen ? velocity : -velocity;
151
+ animationConfig.width = Math.min(swipeableWidth, (toState === ButtonsState.LeftOpen ? leftButtonsSumSizeRef : rightButtonsSumSizeRef).current);
152
+ animationConfig.config.velocity = v / 1000;
153
+ }
154
+ void api.start(animationConfig)[0].then(({ finished }) => {
155
+ if (finished) {
156
+ stateRef.current.candidate = ButtonsState.Close;
157
+ }
158
+ });
159
+ }, [leftBlockApi, rightBlockApi, swipeableWidth]);
160
+ const handleSwipeStart = useCallback(() => {
161
+ if (demoInProgress || swipeLockRef.current) {
162
+ return;
163
+ }
164
+ rightBlockApi.stop();
165
+ leftBlockApi.stop();
166
+ onStart?.();
167
+ onResetDemo?.();
168
+ updateSwipeThreshold();
169
+ if (longState !== LongState.Off) {
170
+ setLongState(LongState.Off);
171
+ }
172
+ prevDistance.current = 0;
173
+ setAnimateButtonsWidth(false);
174
+ }, [
175
+ demoInProgress,
176
+ rightBlockApi,
177
+ leftBlockApi,
178
+ onStart,
179
+ onResetDemo,
180
+ updateSwipeThreshold,
181
+ longState,
182
+ setAnimateButtonsWidth,
183
+ ]);
184
+ const handleChangeButtonsSizeByDistance = useCallback((distance) => {
185
+ // работаем с изменением дистанции, чтобы добавлять ее к существующей ширине
186
+ const distanceDelta = distance - prevDistance.current;
187
+ if (distanceDelta === 0) {
188
+ return;
189
+ }
190
+ prevDistance.current = distance;
191
+ // определяем сторону свайпа, проверяем есть ли с этой стороны кнопки, если есть меняем ширину блока кнопок
192
+ const isLeft = stateRef.current.now === ButtonsState.LeftOpen ||
193
+ stateRef.current.candidate === ButtonsState.LeftOpen ||
194
+ (stateRef.current.now !== ButtonsState.RightOpen &&
195
+ stateRef.current.candidate === ButtonsState.Close &&
196
+ distanceDelta > 0);
197
+ const hasButtons = isLeft ? !!leftButtonsRef.current : !!rightButtonsRef.current;
198
+ const api = isLeft ? leftBlockApi : rightBlockApi;
199
+ const currentWidth = api.current[0].get().width;
200
+ if (!hasButtons) {
201
+ return;
202
+ }
203
+ const width = Math.max(currentWidth + (isLeft ? distanceDelta : -distanceDelta), 0);
204
+ api.set({ width });
205
+ // если ширина блока кнопок > 0 фиксируем сторону-кандидат
206
+ // чтобы не было возможности двигать влево-вправо за один свайп
207
+ if (width > 0) {
208
+ stateRef.current.candidate = isLeft ? ButtonsState.LeftOpen : ButtonsState.RightOpen;
209
+ updateSwipeThreshold();
210
+ }
211
+ // определяем значение justifyContent у контейнера в зависимости от того, с какой стороны сейчас раскрыты кнопки
212
+ // если раскрыты слева, то контент должен уходить за пределы области в право, ставим flex-start
213
+ // если раскрыты справа, то контент должен уходить за пределы области в лево, ставим flex-end
214
+ // визуально ничего не меняется, потому что контент в контейнере растянут на всю ширину
215
+ if (swipeableRef.current) {
216
+ swipeableRef.current.style.justifyContent =
217
+ stateRef.current.candidate === ButtonsState.RightOpen ? 'flex-end' : 'flex-start';
218
+ }
219
+ // определяем выполнены ли условия для перевода компонента в состояние длинного свайпа
220
+ // длинный свайп может быть отключен вообще или может быть недоступен если это сдвиг на проекцию
221
+ const sumSize = isLeft ? leftButtonsSumSizeRef.current : rightButtonsSumSizeRef.current;
222
+ if (useLongSwipe && width > sumSize + MIN_SWIPE_DISTANCE) {
223
+ setLongState(isLeft ? LongState.Left : LongState.Right);
224
+ stateRef.current.next = ButtonsState.Close;
225
+ }
226
+ else {
227
+ setLongState(LongState.Off);
228
+ }
229
+ }, [leftBlockApi, rightBlockApi, useLongSwipe, updateSwipeThreshold]);
230
+ const handleSwipeMove = useCallback((event) => {
231
+ if (demoInProgress || swipeLockRef.current) {
232
+ return;
233
+ }
234
+ // сдвиг в след за пальцем, можем делать длинный свайп
235
+ handleChangeButtonsSizeByDistance(event.distanceX);
236
+ }, [demoInProgress, handleChangeButtonsSizeByDistance]);
237
+ const runSwipeAnimation = useCallback(() => {
238
+ if (!swipeableRef.current) {
239
+ return;
240
+ }
241
+ if (stateRef.current.next === ButtonsState.Close) {
242
+ // свайп завершен, кнопки должны закрыться, потому что не выполнен минимальный размер раскрытия
243
+ // сбрасываем ширину в 0
244
+ if (leftButtonsRef.current) {
245
+ leftButtonsRef.current.style.width = '0';
246
+ }
247
+ leftButtonsSizeRef.current = 0;
248
+ if (rightButtonsRef.current) {
249
+ rightButtonsRef.current.style.width = '0';
250
+ }
251
+ rigthButtonsSizeRef.current = 0;
252
+ }
253
+ else if (stateRef.current.next === ButtonsState.LeftOpen) {
254
+ // выполнено условие раскрытия слева
255
+ // раскрываем кнопки на ширину их контента, но не больше ширины контейнера
256
+ const maxLeftSize = Math.min(swipeableWidth, leftButtonsSumSizeRef.current);
257
+ if (leftButtonsRef.current) {
258
+ leftButtonsRef.current.style.width = `${maxLeftSize}px`;
259
+ }
260
+ leftButtonsSizeRef.current = maxLeftSize;
261
+ rigthButtonsSizeRef.current = 0;
262
+ }
263
+ else if (stateRef.current.next === ButtonsState.RightOpen) {
264
+ // выполнено условие раскрытия справа
265
+ // раскрываем кнопки на ширину их контента, но не больше ширины контейнера
266
+ const maxRightSize = Math.min(swipeableWidth, rightButtonsSumSizeRef.current);
267
+ if (rightButtonsRef.current) {
268
+ rightButtonsRef.current.style.width = `${maxRightSize}px`;
269
+ }
270
+ rigthButtonsSizeRef.current = maxRightSize;
271
+ leftButtonsSizeRef.current = 0;
272
+ }
273
+ stateRef.current.now = stateRef.current.next;
274
+ }, [swipeableWidth]);
275
+ const handleSwipeFinish = useCallback((event) => {
276
+ if (demoInProgress) {
277
+ return;
278
+ }
279
+ onFinish?.();
280
+ // если это длинный свайп, то трггерим клик на соответствующую кнопку, и закрываем блок кнопок
281
+ // если не установлен флаг keepStateAfterLong
282
+ if (longState !== LongState.Off) {
283
+ const buttons = longState === LongState.Left ? leftButtons : rightButtons;
284
+ const button = longState === LongState.Left ? buttons?.[0] : buttons?.[buttons.length - 1];
285
+ button?.onClick();
286
+ onResetDemo?.();
287
+ swipeLockRef.current = !!button?.keepStateAfterLong;
288
+ if (!button?.keepStateAfterLong) {
289
+ animateToState(ButtonsState.Close, event.velocityX);
290
+ }
291
+ return;
292
+ }
293
+ // если это не длинный свайп, то анимируем к соответствующему состоянию
294
+ // если открыт левый или правый блок, то анимируем в закрытое состояние
295
+ // если закрыты оба блока, то анимируем к открытию стороны-кадидата
296
+ const { now, candidate } = stateRef.current;
297
+ animateToState([ButtonsState.LeftOpen, ButtonsState.RightOpen].includes(now) ? ButtonsState.Close : candidate, event.velocityX);
298
+ }, [demoInProgress, longState, leftButtons, rightButtons, onFinish, onResetDemo, animateToState]);
299
+ const handleSwipeCancel = useCallback((event) => {
300
+ animateToState(stateRef.current.now, event.velocityX);
301
+ }, [animateToState]);
302
+ const swipeHandlers = useSwipe({
303
+ thresholdXRef: swipeThresholdRef,
304
+ onSwipeStart: handleSwipeStart,
305
+ onSwipeMove: handleSwipeMove,
306
+ onSwipeEnd: handleSwipeFinish,
307
+ onSwipeCancel: handleSwipeCancel,
308
+ });
309
+ const preventHorizontalScroll = useCallback((e) => {
310
+ if (Math.abs(prevDistance.current) > MAIN_AXIS_THRESHOLD_PX) {
311
+ e.preventDefault();
312
+ }
313
+ }, []);
314
+ const swipebleRefCurrent = swipeableRef.current;
315
+ // выключаем вертикальный скролл, если у нас был минимальный горизонтальный скролл, чтобы не дергалось
316
+ useEffect(() => {
317
+ if (swipebleRefCurrent) {
318
+ swipebleRefCurrent.addEventListener('touchmove', preventHorizontalScroll, { passive: false });
319
+ }
320
+ return () => {
321
+ if (swipebleRefCurrent) {
322
+ swipebleRefCurrent.removeEventListener('touchmove', preventHorizontalScroll);
323
+ }
324
+ };
325
+ }, [preventHorizontalScroll, swipebleRefCurrent]);
326
+ const getButtonsFromSide = useCallback((side = stateRef.current.now) => {
327
+ const buttons = side === ButtonsState.RightOpen ? rightButtonsRef.current : leftButtonsRef.current;
328
+ const oppositeButtons = buttons === rightButtonsRef.current ? leftButtonsRef.current : rightButtonsRef.current;
329
+ return { buttons, oppositeButtons };
330
+ }, []);
331
+ const unmountDemoEndHandler = useCallback((event) => {
332
+ if (event.propertyName !== 'width') {
333
+ return;
334
+ }
335
+ setDemoInProgress(false);
336
+ const isTwoSidedSwipe = !!rightButtonsRef.current && !!leftButtonsRef.current;
337
+ const isLeft = isTwoSidedSwipe
338
+ ? twoSidedDemoMode === 'right-to-left' || twoSidedDemoMode === 'left'
339
+ : !!leftButtonsRef.current;
340
+ const buttons = isLeft ? leftButtonsRef.current : rightButtonsRef.current;
341
+ buttons?.removeEventListener('transitionend', unmountDemoEndHandler);
342
+ rootRef.current?.style.removeProperty('--transition-duration');
343
+ rootRef.current?.style.removeProperty('--transition-delay');
344
+ onResetDemo?.();
345
+ onFinishDemo?.();
346
+ }, [onFinishDemo, onResetDemo, twoSidedDemoMode]);
347
+ const demoSideAppearHandler = useCallback((event) => {
348
+ if (event.propertyName !== 'width') {
349
+ return;
350
+ }
351
+ const currentSide = stateRef.current.now;
352
+ const { buttons, oppositeButtons } = getButtonsFromSide(currentSide);
353
+ const appearedIsFirstOfSides = !!rightButtonsRef.current &&
354
+ !!leftButtonsRef.current &&
355
+ ((currentSide === ButtonsState.LeftOpen && twoSidedDemoMode === 'left-to-right') ||
356
+ (currentSide === ButtonsState.RightOpen && twoSidedDemoMode === 'right-to-left'));
357
+ buttons?.removeEventListener('transitionend', demoSideAppearHandler);
358
+ if (rootRef.current) {
359
+ rootRef.current.style.setProperty('--transition-duration', `${appearedIsFirstOfSides ? demoAppearTime : demoDisappearTime}ms`);
360
+ rootRef.current.style.setProperty('--transition-delay', `${demoIdleTime}ms`);
361
+ }
362
+ stateRef.current.next = ButtonsState.Close;
363
+ stateRef.current.candidate = stateRef.current.next;
364
+ runSwipeAnimation();
365
+ if (appearedIsFirstOfSides) {
366
+ stateRef.current.next =
367
+ currentSide === ButtonsState.LeftOpen ? ButtonsState.RightOpen : ButtonsState.LeftOpen;
368
+ stateRef.current.candidate = stateRef.current.next;
369
+ runSwipeAnimation();
370
+ const twiceSideTransitionHandler = () => {
371
+ oppositeButtons?.removeEventListener('transitionstart', twiceSideTransitionHandler);
372
+ if (oppositeButtons && childrenWrapperRef.current) {
373
+ const isRight = currentSide === ButtonsState.RightOpen;
374
+ childrenWrapperRef.current.style.transform = `translateX(${isRight ? leftButtonsSumSizeRef.current : -rightButtonsSumSizeRef.current}px)`;
375
+ }
376
+ };
377
+ oppositeButtons?.addEventListener('transitionstart', twiceSideTransitionHandler);
378
+ oppositeButtons?.addEventListener('transitionend', demoSideAppearHandler);
379
+ }
380
+ else {
381
+ const startDestroyDemoHandler = () => {
382
+ buttons?.removeEventListener('transitionstart', startDestroyDemoHandler);
383
+ childrenWrapperRef.current?.style.removeProperty('transform');
384
+ };
385
+ buttons?.addEventListener('transitionstart', startDestroyDemoHandler);
386
+ buttons?.addEventListener('transitionend', unmountDemoEndHandler);
387
+ }
388
+ }, [
389
+ getButtonsFromSide,
390
+ twoSidedDemoMode,
391
+ runSwipeAnimation,
392
+ demoAppearTime,
393
+ demoDisappearTime,
394
+ demoIdleTime,
395
+ unmountDemoEndHandler,
396
+ ]);
397
+ const startDemo = useCallback((side) => {
398
+ onResetDemo?.();
399
+ onStartDemo?.();
400
+ setDemoInProgress(true);
401
+ stateRef.current.now = ButtonsState.Close;
402
+ stateRef.current.next = side;
403
+ stateRef.current.candidate = side;
404
+ if (swipeableRef.current) {
405
+ swipeableRef.current.style.justifyContent = side === ButtonsState.RightOpen ? 'flex-end' : 'flex-start';
406
+ }
407
+ if (rootRef.current) {
408
+ rootRef.current.style.setProperty('--transition-duration', `${demoAppearTime}ms`);
409
+ }
410
+ setLongState(LongState.Off);
411
+ setAnimateButtonsWidth(true);
412
+ runSwipeAnimation();
413
+ const { buttons } = getButtonsFromSide(side);
414
+ buttons?.addEventListener('transitionend', demoSideAppearHandler);
415
+ }, [
416
+ demoAppearTime,
417
+ demoSideAppearHandler,
418
+ getButtonsFromSide,
419
+ runSwipeAnimation,
420
+ onResetDemo,
421
+ onStartDemo,
422
+ setAnimateButtonsWidth,
423
+ ]);
424
+ useEffect(() => {
425
+ if (demo &&
426
+ !demoInProgress &&
427
+ stateRef.current.now === ButtonsState.Close &&
428
+ swipeableRef.current?.style.width) {
429
+ const isTwoSidedSwipe = Boolean(rightButtonsRef.current && leftButtonsRef.current);
430
+ if (isTwoSidedSwipe) {
431
+ startDemo(twoSidedDemoMode === 'right' || twoSidedDemoMode === 'right-to-left'
432
+ ? ButtonsState.RightOpen
433
+ : ButtonsState.LeftOpen);
434
+ }
435
+ else {
436
+ startDemo(rightButtonsRef.current ? ButtonsState.RightOpen : ButtonsState.LeftOpen);
437
+ }
438
+ }
439
+ if (!isMobile && demoInProgress) {
440
+ setDemoInProgress(false);
441
+ stateRef.current = {
442
+ candidate: ButtonsState.Close,
443
+ now: ButtonsState.Close,
444
+ next: ButtonsState.Close,
445
+ };
446
+ }
447
+ }, [demo, twoSidedDemoMode, startDemo, demoInProgress, isMobile, onResetDemo]);
448
+ // подсчитываем реальный необходимый размер для кнопок
449
+ // и назначаем проперти исходя из положения кнопки (последняя/не последняя)
450
+ const handleButtonContentRef = useCallback((left, isGrow) => (ref) => {
451
+ // нужно чтобы посчитать размеры для каждой кнопки один раз (не придумал, как сделать лучше)
452
+ if (sumRefs.current.includes(ref) || !ref) {
453
+ return;
454
+ }
455
+ const width = ref?.offsetWidth ?? 0;
456
+ if (!isGrow) {
457
+ ref.style.maxWidth = `${width}px`;
458
+ }
459
+ sumRefs.current.push(ref);
460
+ if (left) {
461
+ leftButtonsSumSizeRef.current += width;
462
+ }
463
+ else {
464
+ rightButtonsSumSizeRef.current += width;
465
+ }
466
+ }, []);
467
+ const renderButtons = (buttons, ref, left) => {
468
+ const isTwoSidedDemoInProgress = leftButtons && rightButtons && demoInProgress;
469
+ return (jsx(Layer, { layer: LayerName.Content, children: jsx(animated.div, { className: classNames(styles.buttons, {
470
+ [styles.buttonsLeft]: left && twoSidedDemoMode === 'right-to-left' && isTwoSidedDemoInProgress,
471
+ [styles.buttonsRight]: !left && twoSidedDemoMode === 'left-to-right' && isTwoSidedDemoInProgress,
472
+ }),
473
+ // "длинная" кнопка растягивается максимум на ширину контейнера (buttons)
474
+ // чтобы не было видно пустоты между ней и контейнером
475
+ // нужно устанавливать выравнивание в зависимости от стороны
476
+ style: {
477
+ justifyContent: left ? 'flex-start' : 'flex-end',
478
+ ...(left ? leftBlockStyles : rightBlockStyles),
479
+ }, ref: ref, "data-qa": `swipe-buttons-${left ? 'left' : 'right'}`, children: buttons.map((item, index) => {
480
+ if (!item) {
481
+ return null;
482
+ }
483
+ const isGrow = left ? index === 0 : index === buttons.length - 1;
484
+ const isHide = (left && index !== 0 && longState === LongState.Left) ||
485
+ (!left && index !== buttons.length - 1 && longState === LongState.Right);
486
+ const Icon = item.icon;
487
+ const iconProps = item.showLabel
488
+ ? { 'aria-hidden': true, focusable: false }
489
+ : { 'aria-label': item.label };
490
+ return (jsx("button", { className: classNames(styles.button, {
491
+ [styles.buttonGrow]: isGrow,
492
+ [styles.buttonHide]: isHide,
493
+ [styles[`button_style-${item.style}`]]: item.style,
494
+ }),
495
+ // это нужно чтобы анимировался длинный свайп.
496
+ // Здесь должно быть значение максимально близкое к тому,
497
+ // какой максимум может занимать одна кнопка, если кнопок больше 1
498
+ style: isGrow ? { maxWidth: `${swipeableWidth}px` } : {}, onClick: () => {
499
+ if (demoInProgress) {
500
+ return;
501
+ }
502
+ if (item.closeOnAction) {
503
+ animateToState(ButtonsState.Close, 0);
504
+ }
505
+ item.onClick();
506
+ }, "data-qa": `swipe-button-${left ? 'left' : 'right'}-${index}`, children: jsx("div", { className: styles.buttonContent, ref: handleButtonContentRef(left, isGrow), "data-qa": `swipe-button-content-${left ? 'left' : 'right'}-${index}`, children: jsxs("div", { className: styles.buttonContentColumn, children: [Icon && (jsx("div", { className: styles.iconWrapper, children: jsx(Icon, { ...iconProps }) })), item.showLabel && jsx("span", { className: styles.buttonLabel, children: item.label })] }) }) }, index));
507
+ }) }) }));
508
+ };
509
+ if (!isMobile) {
510
+ return null;
511
+ }
512
+ return (jsx("div", { className: styles.root, ref: rootRef, "data-qa": dataQa ?? 'swipe-root', children: jsxs("div", { className: classNames(styles.swipeble), ref: swipeableRef, ...swipeHandlers, "data-qa": "swipe-swipeable", children: [!!leftButtons?.length && renderButtons(leftButtons, leftButtonsRef, true), jsx("div", { ref: childrenWrapperRef, className: styles.childrenWrapper, children: children }), !!rightButtons?.length && renderButtons(rightButtons, rightButtonsRef, false)] }) }));
513
+ };
514
+
515
+ export { Swipe };
516
+ //# sourceMappingURL=Swipe.js.map