@hh.ru/magritte-ui-bottom-sheet 4.1.22
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/BottomSheet.d.ts +3 -0
- package/BottomSheet.js +549 -0
- package/BottomSheet.js.map +1 -0
- package/BottomSheetContentOverlay.d.ts +2 -0
- package/BottomSheetContentOverlay.js +12 -0
- package/BottomSheetContentOverlay.js.map +1 -0
- package/BottomSheetContext.d.ts +5 -0
- package/BottomSheetContext.js +7 -0
- package/BottomSheetContext.js.map +1 -0
- package/BottomSheetFooter.d.ts +2 -0
- package/BottomSheetFooter.js +8 -0
- package/BottomSheetFooter.js.map +1 -0
- package/ClickInterceptor.d.ts +5 -0
- package/ClickInterceptor.js +61 -0
- package/ClickInterceptor.js.map +1 -0
- package/bottom-sheet-32fa90fb.js +5 -0
- package/bottom-sheet-32fa90fb.js.map +1 -0
- package/index.css +239 -0
- package/index.d.ts +5 -0
- package/index.js +22 -0
- package/index.js.map +1 -0
- package/index.mock.d.ts +6 -0
- package/index.mock.js +14 -0
- package/index.mock.js.map +1 -0
- package/package.json +45 -0
- package/types.d.ts +51 -0
- package/types.js +3 -0
- package/types.js.map +1 -0
package/BottomSheet.d.ts
ADDED
package/BottomSheet.js
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import './index.css';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { forwardRef, useRef, useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Transition } from 'react-transition-group';
|
|
6
|
+
import classnames from 'classnames';
|
|
7
|
+
import { disableOverscroll, disableScroll } from '@hh.ru/magritte-common-modal-helper';
|
|
8
|
+
import { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';
|
|
9
|
+
import { useSwipe } from '@hh.ru/magritte-common-use-swipe';
|
|
10
|
+
import { InternalLayerName } from '@hh.ru/magritte-internal-layer-name';
|
|
11
|
+
import { BottomSheetContext } from './BottomSheetContext.js';
|
|
12
|
+
import { ClickInterceptor } from './ClickInterceptor.js';
|
|
13
|
+
import { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';
|
|
14
|
+
import { Divider } from '@hh.ru/magritte-ui-divider';
|
|
15
|
+
import { Layer } from '@hh.ru/magritte-ui-layer';
|
|
16
|
+
import { NavigationBarContext } from '@hh.ru/magritte-ui-navigation-bar';
|
|
17
|
+
import { s as styles } from './bottom-sheet-32fa90fb.js';
|
|
18
|
+
|
|
19
|
+
const CSS_VAR_CONTENT_OVERLAY_TOP = '--content-overlay-top';
|
|
20
|
+
const CSS_VAR_CONTENT_OVERLAY_HEIGHT = '--content-overlay-height';
|
|
21
|
+
const CSS_VAR_INITIAL_VIEWPORT_HEIGHT = '--initial-viewport-height';
|
|
22
|
+
const CSS_VAR_ENTER_ANIMATION_DURATION = '--enter-animation-duration';
|
|
23
|
+
const CSS_VAR_EXIT_ANIMATION_DURATION = '--exit-animation-duration';
|
|
24
|
+
const CSS_VAR_HEIGHT_ANIMATION_DURATION = '--height-transition-duration';
|
|
25
|
+
const CSS_VAR_OVERLAY_OPACITY = '--overlay-opacity';
|
|
26
|
+
const CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET = '--virtual-keyboard-top-offset';
|
|
27
|
+
const CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET = '--virtual-keyboard-bottom-offset';
|
|
28
|
+
const INITIAL_VIEWPORT_HEIGHT = (typeof window !== 'undefined' ? window.visualViewport?.height : null) ?? 0;
|
|
29
|
+
const NAVIGATION_BAR_SIZE_OVERRIDE = { size: 'standard' };
|
|
30
|
+
const SWIPE_THRESHOLD_REF = { current: { max: Math.round(INITIAL_VIEWPORT_HEIGHT * 0.8) } };
|
|
31
|
+
const forceRepaint = (node) => node.scrollTop;
|
|
32
|
+
const isSafari = typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
33
|
+
const toNumber = (value) => {
|
|
34
|
+
const result = parseInt(value, 10);
|
|
35
|
+
return Number.isInteger(result) ? result : 0;
|
|
36
|
+
};
|
|
37
|
+
const changeSwipeProgressState = (state, scrollContainerRef, isSwipeInProgress, allowScrollWhileFocused) => {
|
|
38
|
+
if (!state.current) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
state.current.isSwipeInProgress = isSwipeInProgress;
|
|
42
|
+
if (!scrollContainerRef.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
scrollContainerRef.current.classList.toggle(styles.scrollContainerNoScroll, isSwipeInProgress || (!allowScrollWhileFocused && state.current.hasFocus));
|
|
46
|
+
};
|
|
47
|
+
const BottomSheet = forwardRef(({ allowScrollWhileFocused, children, footer, header, height = 'content', interceptClickHandlers = true, keyboardOverlaysContent = true, onAppear, onBeforeExit, onAfterExit, onClose, showDivider = 'always-if-has-scroll', showOverlay = true, visible = false, withContentPaddings = true, }, ref) => {
|
|
48
|
+
const contentRef = useRef(null);
|
|
49
|
+
const contentOverlayRef = useRef(null);
|
|
50
|
+
const cssVariablesContainerRef = useRef(null);
|
|
51
|
+
const fixedGrabberRef = useRef(null);
|
|
52
|
+
const footerRef = useRef(null);
|
|
53
|
+
const headerRef = useRef(null);
|
|
54
|
+
const heightTransitionElementRef = useRef(null);
|
|
55
|
+
const scrollContainerRef = useRef(null);
|
|
56
|
+
const stickyGrabberRef = useRef(null);
|
|
57
|
+
const scrollContainerRefMulti = useMultipleRefs(scrollContainerRef, ref);
|
|
58
|
+
const { isMobile } = useBreakpoint();
|
|
59
|
+
const currentVisible = isMobile && visible;
|
|
60
|
+
const layoutMetricsRef = useRef({
|
|
61
|
+
documentHeight: 0,
|
|
62
|
+
fillHeight: 0,
|
|
63
|
+
grabberSpacing: 0,
|
|
64
|
+
viewportShift: 0,
|
|
65
|
+
});
|
|
66
|
+
const prevPropsRef = useRef({ children, height, visible });
|
|
67
|
+
const stateRef = useRef({
|
|
68
|
+
grabber: 'sticky',
|
|
69
|
+
hasFocus: false,
|
|
70
|
+
isSwipeEnabled: false,
|
|
71
|
+
isSwipeInProgress: false,
|
|
72
|
+
swipeOffset: 0,
|
|
73
|
+
resizeRAFHandle: null,
|
|
74
|
+
virtualKeyboardHeight: 0,
|
|
75
|
+
});
|
|
76
|
+
const [animationTimeout, setAnimationTimeout] = useState(null);
|
|
77
|
+
const [heightDiff, setHeightDiff] = useState(null);
|
|
78
|
+
const [isDividerVisible, setDividerVisible] = useState(false);
|
|
79
|
+
const [onCloseContractCheck, setOnCloseContractCheck] = useState(false);
|
|
80
|
+
const setCSSVariable = (name, value) => cssVariablesContainerRef.current !== null &&
|
|
81
|
+
cssVariablesContainerRef.current.style.setProperty(name, value);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!currentVisible) {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
layoutMetricsRef.current.documentHeight = document.documentElement.clientHeight;
|
|
87
|
+
if (!showOverlay) {
|
|
88
|
+
const enableOverscroll = disableOverscroll();
|
|
89
|
+
return enableOverscroll;
|
|
90
|
+
}
|
|
91
|
+
const enableScroll = disableScroll();
|
|
92
|
+
return enableScroll;
|
|
93
|
+
}, [showOverlay, currentVisible]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const animationTimeoutElement = document.createElement('div');
|
|
96
|
+
animationTimeoutElement.classList.add(styles.animationTimeout);
|
|
97
|
+
document.body.appendChild(animationTimeoutElement);
|
|
98
|
+
const style = window.getComputedStyle(animationTimeoutElement);
|
|
99
|
+
const enter = toNumber(style.getPropertyValue(CSS_VAR_ENTER_ANIMATION_DURATION));
|
|
100
|
+
const exit = toNumber(style.getPropertyValue(CSS_VAR_EXIT_ANIMATION_DURATION));
|
|
101
|
+
const height = toNumber(style.getPropertyValue(CSS_VAR_HEIGHT_ANIMATION_DURATION));
|
|
102
|
+
document.body.removeChild(animationTimeoutElement);
|
|
103
|
+
setAnimationTimeout({ appear: { enter, exit }, height: { enter: height, exit } });
|
|
104
|
+
}, [setAnimationTimeout]);
|
|
105
|
+
// Пересчитывает флаги отображения граббера и дивайдера в зависимости от скролла
|
|
106
|
+
const recalcScrollFlags = useCallback(() => {
|
|
107
|
+
// расчет состояния граббера в зависимости от того, проскроллен ли контент до верхней границы вьюпорта
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
if (fixedGrabberRef.current !== null && stickyGrabberRef.current !== null) {
|
|
110
|
+
const stickyGrabberDOMRect = stickyGrabberRef.current.getBoundingClientRect();
|
|
111
|
+
const grabberTop = Math.round(stickyGrabberDOMRect.top);
|
|
112
|
+
if (grabberTop >= layoutMetricsRef.current.grabberSpacing * 2 || heightDiff !== null) {
|
|
113
|
+
if (stateRef.current.grabber !== 'sticky') {
|
|
114
|
+
stateRef.current.grabber = 'sticky';
|
|
115
|
+
stickyGrabberRef.current.classList.remove(styles.grabberInvisible);
|
|
116
|
+
fixedGrabberRef.current.classList.add(styles.grabberInvisible);
|
|
117
|
+
fixedGrabberRef.current.classList.remove(styles.grabberFixed, styles.grabberFakeSticky);
|
|
118
|
+
fixedGrabberRef.current.style.top = ``;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (grabberTop > layoutMetricsRef.current.grabberSpacing) {
|
|
122
|
+
if (stateRef.current.grabber !== 'fake-sticky') {
|
|
123
|
+
stateRef.current.grabber = 'fake-sticky';
|
|
124
|
+
stickyGrabberRef.current.classList.add(styles.grabberInvisible);
|
|
125
|
+
fixedGrabberRef.current.classList.add(styles.grabberFakeSticky);
|
|
126
|
+
fixedGrabberRef.current.classList.remove(styles.grabberSticky, styles.grabberInvisible);
|
|
127
|
+
}
|
|
128
|
+
fixedGrabberRef.current.style.top = `${grabberTop + layoutMetricsRef.current.viewportShift}px`;
|
|
129
|
+
}
|
|
130
|
+
else if (stateRef.current.grabber !== 'fixed') {
|
|
131
|
+
stateRef.current.grabber = 'fixed';
|
|
132
|
+
stickyGrabberRef.current.classList.add(styles.grabberInvisible);
|
|
133
|
+
fixedGrabberRef.current.classList.add(styles.grabberFixed);
|
|
134
|
+
fixedGrabberRef.current.classList.remove(styles.grabberFakeSticky, styles.grabberInvisible);
|
|
135
|
+
fixedGrabberRef.current.style.top = ``;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// расчет того, нужно ли показывать дивайдер, в зависимости от того, проскроллен ли контент до конца
|
|
140
|
+
if (contentRef.current !== null && scrollContainerRef.current !== null) {
|
|
141
|
+
if (footer && (showDivider === 'always-if-has-scroll' || showDivider === 'if-not-scrolled-to-end')) {
|
|
142
|
+
const hasScroll = contentRef.current.clientHeight > scrollContainerRef.current.clientHeight + 1;
|
|
143
|
+
const isScrolledToContentEnd = Math.round(scrollContainerRef.current.scrollTop + scrollContainerRef.current.clientHeight) ===
|
|
144
|
+
scrollContainerRef.current.scrollHeight;
|
|
145
|
+
setDividerVisible((showDivider === 'always-if-has-scroll' && hasScroll) ||
|
|
146
|
+
(showDivider === 'if-not-scrolled-to-end' && hasScroll && !isScrolledToContentEnd));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, [footer, heightDiff, setDividerVisible, showDivider]);
|
|
150
|
+
const resetScrollFlags = useCallback(() => {
|
|
151
|
+
if (fixedGrabberRef.current !== null && stickyGrabberRef.current !== null) {
|
|
152
|
+
stateRef.current.grabber = 'sticky';
|
|
153
|
+
fixedGrabberRef.current.classList.add(styles.grabberInvisible);
|
|
154
|
+
fixedGrabberRef.current.classList.remove(styles.grabberTransitionAnimation);
|
|
155
|
+
stickyGrabberRef.current.classList.remove(styles.grabberInvisible);
|
|
156
|
+
}
|
|
157
|
+
setDividerVisible(false);
|
|
158
|
+
}, [setDividerVisible]);
|
|
159
|
+
const resetHeightDiff = useCallback(() => setHeightDiff(null), [setHeightDiff]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const meta = document.querySelector('meta[name="viewport"]') ?? document.createElement('meta');
|
|
162
|
+
const attributesStr = meta.getAttribute('content');
|
|
163
|
+
const attributes = (attributesStr !== null
|
|
164
|
+
? Object.fromEntries(attributesStr.split(',').map((keyValuePairStr) => keyValuePairStr.split('=')))
|
|
165
|
+
: {});
|
|
166
|
+
attributes['interactive-widget'] = keyboardOverlaysContent ? 'resizes-visual' : 'resizes-content';
|
|
167
|
+
const attributesStrUpdated = Object.entries(attributes)
|
|
168
|
+
.map((keyValuePair) => keyValuePair.join('='))
|
|
169
|
+
.join(',');
|
|
170
|
+
meta.setAttribute('name', 'viewport');
|
|
171
|
+
meta.setAttribute('content', attributesStrUpdated);
|
|
172
|
+
}, [keyboardOverlaysContent]);
|
|
173
|
+
const recalcKeyboardOffsets = useCallback(() => {
|
|
174
|
+
if (!headerRef.current || !scrollContainerRef.current || !visualViewport) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (stateRef.current.hasFocus) {
|
|
178
|
+
// терминология: https://developer.chrome.com/blog/viewport-resize-behavior/
|
|
179
|
+
//
|
|
180
|
+
// делим браузеры на три группы в зависимости от поведения при открытии виртуальной клавиатуры:
|
|
181
|
+
// 1. Safari — ресайзит Visual Viewport, не меняет Layout Viewport.
|
|
182
|
+
// В нем не нужно ничего корректировать при keyboardOverlaysContent=true,
|
|
183
|
+
// а при keyboardOverlaysContent=false нужно сдвинуть НИЖНИЙ край контейнера ВВЕРХ,
|
|
184
|
+
// чтобы он совпал с границей Visual Viewport
|
|
185
|
+
// 2. Chrome < 108 & Chromium-based — ресайзит и Visual Viewport, и Layout Viewport
|
|
186
|
+
// В нем не нужно ничего корректировать при keyboardOverlaysContent=false,
|
|
187
|
+
// а при keyboardOverlaysContent=true нужно сдвинуть НИЖНИЙ край контейнера ВНИЗ,
|
|
188
|
+
// чтобы футер уехал под клавиатуру
|
|
189
|
+
// 3. Chrome >= 108 — поддерживает Virtual Keyboard API и meta-тег interactive-widget
|
|
190
|
+
// Используем поведение `resizes-visual` (как в Safari) в случае keyboardOverlaysContent=true
|
|
191
|
+
// и `resizes-content` (как в Chrome < 108 & Chromium-based) в случае keyboardOverlaysContent=false
|
|
192
|
+
// Таким образом в нем ничего не нужно корректировать.
|
|
193
|
+
const scrollContainerDOMREct = scrollContainerRef.current.getBoundingClientRect();
|
|
194
|
+
// любой браузер может сдвинуть Visual Viewport вверх, если фокусируемый инпут находится близко к нижней границе
|
|
195
|
+
// из-за этого может возникнуть проблема, что ВЕРХНИЙ край контента уехал за границу Visual Viewport
|
|
196
|
+
// сдвигаем ВЕРХНИЙ край контейнера ВНИЗ, чтобы он совпал с границей Visual Viewport
|
|
197
|
+
const visualViewportShift = Math.round(layoutMetricsRef.current.grabberSpacing - scrollContainerDOMREct.top);
|
|
198
|
+
// запоминаем сдвиг для коррекции позиции граббера
|
|
199
|
+
layoutMetricsRef.current.viewportShift = visualViewportShift;
|
|
200
|
+
// клавиатура ПОВЕРХ контента
|
|
201
|
+
// этот кейс нужно корректировать только в Chrome < 108 & Chromium-based
|
|
202
|
+
if (keyboardOverlaysContent) {
|
|
203
|
+
// браузеры из этой группы меняют Layout Viewport
|
|
204
|
+
const layoutViewportDiff = Math.round(layoutMetricsRef.current.documentHeight - document.documentElement.clientHeight);
|
|
205
|
+
if (layoutViewportDiff > 0) {
|
|
206
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
207
|
+
// сдвигаем НИЖНИЙ край контейнера ВНИЗ, чтобы футер уехал под клавиатуру
|
|
208
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${-layoutViewportDiff}px`);
|
|
209
|
+
// при этом может возникнуть проблема, что клавиатура перекрыла хедер
|
|
210
|
+
// проверяем это и компенсируем величину перекрытия при необходимости
|
|
211
|
+
const headerDOMRect = headerRef.current.getBoundingClientRect();
|
|
212
|
+
const headerOutOfViewportHeight = Math.round(headerDOMRect.bottom + layoutViewportDiff - layoutMetricsRef.current.documentHeight);
|
|
213
|
+
if (headerOutOfViewportHeight > 0) {
|
|
214
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${headerOutOfViewportHeight - layoutViewportDiff}px`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// клавиатура ПОД контентом
|
|
220
|
+
// этот кейс нужно корректировать только в Safari
|
|
221
|
+
// Safari ресайзит Visual Viewport
|
|
222
|
+
const visualViewportDiff = Math.round(scrollContainerDOMREct.bottom - visualViewport.height);
|
|
223
|
+
if (visualViewportDiff > 0) {
|
|
224
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
225
|
+
// сдвигаем НИЖНИЙ край контейнера ВВЕРХ, чтобы он совпал с границей Visual Viewport
|
|
226
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${visualViewportDiff}px`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
232
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
233
|
+
}
|
|
234
|
+
}, [keyboardOverlaysContent]);
|
|
235
|
+
const handleFocus = useCallback((event) => {
|
|
236
|
+
if (!scrollContainerRef.current) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const focusedElement = event.target;
|
|
240
|
+
const initialViewportHeight = visualViewport?.height;
|
|
241
|
+
const resizeRAFStart = performance.now();
|
|
242
|
+
if (!(focusedElement instanceof HTMLInputElement) || stateRef.current.resizeRAFHandle !== null) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const handleResize = () => {
|
|
246
|
+
if (stateRef.current.resizeRAFHandle !== null) {
|
|
247
|
+
cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
248
|
+
stateRef.current.resizeRAFHandle = null;
|
|
249
|
+
}
|
|
250
|
+
recalcKeyboardOffsets();
|
|
251
|
+
};
|
|
252
|
+
const waitForResize = () => {
|
|
253
|
+
if (performance.now() - resizeRAFStart > 1000 || visualViewport?.height !== initialViewportHeight) {
|
|
254
|
+
visualViewport?.removeEventListener('resize', handleResize);
|
|
255
|
+
stateRef.current.resizeRAFHandle = null;
|
|
256
|
+
recalcKeyboardOffsets();
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
stateRef.current.resizeRAFHandle = requestAnimationFrame(waitForResize);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const handleBlur = () => {
|
|
263
|
+
if (!scrollContainerRef.current) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
focusedElement.removeEventListener('blur', handleBlur);
|
|
267
|
+
stateRef.current.hasFocus = false;
|
|
268
|
+
scrollContainerRef.current.classList.remove(styles.scrollContainerNoScroll, styles.virtualKeyboardAnimation);
|
|
269
|
+
if (isSafari) {
|
|
270
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
271
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
272
|
+
}
|
|
273
|
+
if (stateRef.current.resizeRAFHandle !== null) {
|
|
274
|
+
cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
275
|
+
stateRef.current.resizeRAFHandle = null;
|
|
276
|
+
}
|
|
277
|
+
visualViewport?.removeEventListener('resize', handleResize);
|
|
278
|
+
};
|
|
279
|
+
focusedElement.addEventListener('blur', handleBlur);
|
|
280
|
+
stateRef.current.hasFocus = true;
|
|
281
|
+
if (!allowScrollWhileFocused) {
|
|
282
|
+
scrollContainerRef.current.classList.add(styles.scrollContainerNoScroll);
|
|
283
|
+
}
|
|
284
|
+
scrollContainerRef.current.classList.add(styles.virtualKeyboardAnimation);
|
|
285
|
+
stateRef.current.resizeRAFHandle = requestAnimationFrame(waitForResize);
|
|
286
|
+
visualViewport?.addEventListener('resize', handleResize);
|
|
287
|
+
}, [allowScrollWhileFocused, recalcKeyboardOffsets]);
|
|
288
|
+
// Запускает анимацию translateY для показа/скрытия боттомшита или изменения высоты контента
|
|
289
|
+
const runTranslateYAnimation = useCallback(() => {
|
|
290
|
+
if (!contentRef.current ||
|
|
291
|
+
!contentOverlayRef.current ||
|
|
292
|
+
!footerRef.current ||
|
|
293
|
+
!heightTransitionElementRef.current ||
|
|
294
|
+
!scrollContainerRef.current ||
|
|
295
|
+
!visualViewport) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (heightDiff !== null) {
|
|
299
|
+
if (heightDiff > 0) {
|
|
300
|
+
contentRef.current.style.transform = `translateY(${heightDiff}px)`;
|
|
301
|
+
contentOverlayRef.current.style.transform = `translateY(${heightDiff}px)`;
|
|
302
|
+
footerRef.current.style.transform = `translateY(${-heightDiff}px)`;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
heightTransitionElementRef.current.style.flexBasis = `${-heightDiff}px`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const contentDOMREct = contentRef.current.getBoundingClientRect();
|
|
310
|
+
const translateY = scrollContainerRef.current.clientHeight + stateRef.current.swipeOffset - contentDOMREct.top;
|
|
311
|
+
contentRef.current.style.transform = `translateY(${translateY}px)`;
|
|
312
|
+
contentOverlayRef.current.style.transform = `translateY(${translateY}px)`;
|
|
313
|
+
setCSSVariable(CSS_VAR_OVERLAY_OPACITY, `0`);
|
|
314
|
+
}
|
|
315
|
+
forceRepaint(scrollContainerRef.current);
|
|
316
|
+
}, [heightDiff]);
|
|
317
|
+
const resetTranslateY = useCallback(() => {
|
|
318
|
+
if (!contentRef.current ||
|
|
319
|
+
!contentOverlayRef.current ||
|
|
320
|
+
!footerRef.current ||
|
|
321
|
+
!heightTransitionElementRef.current ||
|
|
322
|
+
!scrollContainerRef.current) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (heightDiff !== null) {
|
|
326
|
+
contentRef.current.style.transform = ``;
|
|
327
|
+
contentOverlayRef.current.style.transform = ``;
|
|
328
|
+
footerRef.current.style.transform = ``;
|
|
329
|
+
heightTransitionElementRef.current.style.flexBasis = ``;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
contentRef.current.style.transform = `translateY(${stateRef.current.swipeOffset}px)`;
|
|
333
|
+
contentOverlayRef.current.style.transform = `translateY(${stateRef.current.swipeOffset}px)`;
|
|
334
|
+
setCSSVariable(CSS_VAR_OVERLAY_OPACITY, `1`);
|
|
335
|
+
}
|
|
336
|
+
forceRepaint(scrollContainerRef.current);
|
|
337
|
+
}, [heightDiff]);
|
|
338
|
+
const handleAppearAnimationEnd = useCallback(() => {
|
|
339
|
+
let scrollTop = scrollContainerRef.current?.scrollTop ?? 0;
|
|
340
|
+
let scrollLocked = false;
|
|
341
|
+
// Обработчик скролла для блокировки оверскролла в браузерах без поддержки css правила overscroll-behavior
|
|
342
|
+
const handleScroll = (event) => {
|
|
343
|
+
const currentTarget = event.currentTarget;
|
|
344
|
+
const forward = currentTarget.scrollTop > scrollTop;
|
|
345
|
+
const maxScroll = currentTarget.scrollHeight - currentTarget.offsetHeight;
|
|
346
|
+
if (!scrollLocked && forward && currentTarget.scrollTop > maxScroll) {
|
|
347
|
+
currentTarget.style.overflow = 'hidden';
|
|
348
|
+
currentTarget.scrollTop = maxScroll;
|
|
349
|
+
scrollLocked = true;
|
|
350
|
+
scrollTop = maxScroll;
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
currentTarget.style.overflow = 'auto';
|
|
353
|
+
scrollLocked = false;
|
|
354
|
+
}, 1);
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
scrollTop = currentTarget.scrollTop;
|
|
359
|
+
};
|
|
360
|
+
// удаления хендлеров нет т.к. установлен флаг unmountOnExit, т.е. элемент удаляется из DOM
|
|
361
|
+
// после завершения анимации
|
|
362
|
+
scrollContainerRef.current?.addEventListener('touchmove', handleScroll, { passive: false });
|
|
363
|
+
scrollContainerRef.current?.addEventListener('scroll', handleScroll, { passive: false });
|
|
364
|
+
recalcScrollFlags();
|
|
365
|
+
onAppear?.();
|
|
366
|
+
}, [onAppear, recalcScrollFlags]);
|
|
367
|
+
const handleExitAnimationStart = useCallback(() => {
|
|
368
|
+
resetTranslateY();
|
|
369
|
+
onBeforeExit?.();
|
|
370
|
+
}, [resetTranslateY, onBeforeExit]);
|
|
371
|
+
const handleExitAnimationEnd = useCallback(() => {
|
|
372
|
+
stateRef.current.grabber = 'sticky';
|
|
373
|
+
stateRef.current.hasFocus = false;
|
|
374
|
+
stateRef.current.isSwipeEnabled = false;
|
|
375
|
+
changeSwipeProgressState(stateRef, scrollContainerRef, false, !!allowScrollWhileFocused);
|
|
376
|
+
stateRef.current.swipeOffset = 0;
|
|
377
|
+
stateRef.current.resizeRAFHandle !== null && cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
378
|
+
stateRef.current.resizeRAFHandle = null;
|
|
379
|
+
layoutMetricsRef.current.fillHeight = 0;
|
|
380
|
+
layoutMetricsRef.current.grabberSpacing = 0;
|
|
381
|
+
setCSSVariable(CSS_VAR_OVERLAY_OPACITY, `1`);
|
|
382
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
383
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
384
|
+
if (fixedGrabberRef.current !== null) {
|
|
385
|
+
fixedGrabberRef.current.classList.add(styles.grabberInvisible);
|
|
386
|
+
fixedGrabberRef.current.classList.remove(styles.grabberFixed, styles.grabberFakeSticky);
|
|
387
|
+
fixedGrabberRef.current.style.transform = ``;
|
|
388
|
+
}
|
|
389
|
+
if (scrollContainerRef.current !== null) {
|
|
390
|
+
scrollContainerRef.current.classList.remove(styles.scrollContainerNoScroll, styles.virtualKeyboardAnimation);
|
|
391
|
+
}
|
|
392
|
+
resetScrollFlags();
|
|
393
|
+
setOnCloseContractCheck(false);
|
|
394
|
+
onAfterExit?.();
|
|
395
|
+
}, [resetScrollFlags, setOnCloseContractCheck, onAfterExit, allowScrollWhileFocused]);
|
|
396
|
+
const handleClose = useCallback(() => {
|
|
397
|
+
onClose();
|
|
398
|
+
setOnCloseContractCheck(true);
|
|
399
|
+
}, [onClose, setOnCloseContractCheck]);
|
|
400
|
+
const handleSwipeStart = useCallback(() => {
|
|
401
|
+
if (!scrollContainerRef.current) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (Object.is(Math.round(scrollContainerRef.current.scrollTop), 0)) {
|
|
405
|
+
stateRef.current.isSwipeEnabled = true;
|
|
406
|
+
stateRef.current.swipeOffset = 0;
|
|
407
|
+
}
|
|
408
|
+
}, []);
|
|
409
|
+
const handleSwipeMove = useCallback((event) => {
|
|
410
|
+
if (!contentRef.current ||
|
|
411
|
+
!contentOverlayRef.current ||
|
|
412
|
+
!stateRef.current.isSwipeEnabled ||
|
|
413
|
+
!scrollContainerRef.current) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const selection = document.getSelection();
|
|
417
|
+
if (selection && !selection.isCollapsed) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (event.distanceY > 0) {
|
|
421
|
+
contentRef.current.style.transform = `translateY(${event.distanceY}px)`;
|
|
422
|
+
contentOverlayRef.current.style.transform = `translateY(${event.distanceY}px)`;
|
|
423
|
+
if (!stateRef.current.isSwipeInProgress) {
|
|
424
|
+
changeSwipeProgressState(stateRef, scrollContainerRef, true, !!allowScrollWhileFocused);
|
|
425
|
+
}
|
|
426
|
+
recalcScrollFlags();
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
contentRef.current.style.transform = ``;
|
|
430
|
+
contentOverlayRef.current.style.transform = ``;
|
|
431
|
+
stateRef.current.isSwipeEnabled = false;
|
|
432
|
+
changeSwipeProgressState(stateRef, scrollContainerRef, false, !!allowScrollWhileFocused);
|
|
433
|
+
}
|
|
434
|
+
stateRef.current.swipeOffset = event.distanceY;
|
|
435
|
+
}, [recalcScrollFlags, allowScrollWhileFocused]);
|
|
436
|
+
const handleSwipeCancel = useCallback(() => {
|
|
437
|
+
if (!contentRef.current || !contentOverlayRef.current || !scrollContainerRef.current) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (stateRef.current.isSwipeEnabled && stateRef.current.isSwipeInProgress) {
|
|
441
|
+
contentRef.current.style.transform = ``;
|
|
442
|
+
contentOverlayRef.current.style.transform = ``;
|
|
443
|
+
}
|
|
444
|
+
stateRef.current.isSwipeEnabled = false;
|
|
445
|
+
changeSwipeProgressState(stateRef, scrollContainerRef, false, !!allowScrollWhileFocused);
|
|
446
|
+
stateRef.current.swipeOffset = 0;
|
|
447
|
+
recalcScrollFlags();
|
|
448
|
+
}, [recalcScrollFlags, allowScrollWhileFocused]);
|
|
449
|
+
const handleSwipeEnd = useCallback((event) => {
|
|
450
|
+
if (stateRef.current.isSwipeEnabled && stateRef.current.isSwipeInProgress) {
|
|
451
|
+
handleClose();
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
handleSwipeCancel(event);
|
|
455
|
+
}
|
|
456
|
+
}, [handleClose, handleSwipeCancel]);
|
|
457
|
+
const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useSwipe({
|
|
458
|
+
thresholdYRef: SWIPE_THRESHOLD_REF,
|
|
459
|
+
onSwipeStart: handleSwipeStart,
|
|
460
|
+
onSwipeMove: handleSwipeMove,
|
|
461
|
+
onSwipeEnd: handleSwipeEnd,
|
|
462
|
+
onSwipeCancel: handleSwipeCancel,
|
|
463
|
+
});
|
|
464
|
+
const swipeHandlers = useMemo(() => {
|
|
465
|
+
const withStopPropagation = (callback) => (event) => {
|
|
466
|
+
event.stopPropagation();
|
|
467
|
+
callback?.(event);
|
|
468
|
+
};
|
|
469
|
+
return {
|
|
470
|
+
onTouchStart: withStopPropagation(onTouchStart),
|
|
471
|
+
onTouchMove: withStopPropagation(onTouchMove),
|
|
472
|
+
onTouchEnd,
|
|
473
|
+
onTouchCancel,
|
|
474
|
+
};
|
|
475
|
+
}, [onTouchStart, onTouchMove, onTouchEnd, onTouchCancel]);
|
|
476
|
+
useLayoutEffect(() => {
|
|
477
|
+
if (!contentRef.current ||
|
|
478
|
+
!footerRef.current ||
|
|
479
|
+
!headerRef.current ||
|
|
480
|
+
!scrollContainerRef.current ||
|
|
481
|
+
!visualViewport) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (visible) {
|
|
485
|
+
stateRef.current.grabber = contentRef.current.offsetTop > 0 ? 'sticky' : 'sticky-inside';
|
|
486
|
+
if (visible !== prevPropsRef.current.visible) {
|
|
487
|
+
layoutMetricsRef.current.fillHeight = contentRef.current.offsetTop;
|
|
488
|
+
layoutMetricsRef.current.grabberSpacing = scrollContainerRef.current.offsetTop;
|
|
489
|
+
setCSSVariable(CSS_VAR_CONTENT_OVERLAY_TOP, `${contentRef.current.offsetTop + headerRef.current.clientHeight}px`);
|
|
490
|
+
setCSSVariable(CSS_VAR_CONTENT_OVERLAY_HEIGHT, `${footerRef.current.offsetTop - headerRef.current.clientHeight}px`);
|
|
491
|
+
setCSSVariable(CSS_VAR_INITIAL_VIEWPORT_HEIGHT, `${INITIAL_VIEWPORT_HEIGHT}px`);
|
|
492
|
+
}
|
|
493
|
+
else if (children !== prevPropsRef.current.children || height !== prevPropsRef.current.height) {
|
|
494
|
+
const scrollTop = scrollContainerRef.current.scrollTop;
|
|
495
|
+
scrollContainerRef.current.scrollTo({ top: 0 });
|
|
496
|
+
const _heightDiff = layoutMetricsRef.current.fillHeight - contentRef.current.offsetTop;
|
|
497
|
+
layoutMetricsRef.current.fillHeight = contentRef.current.offsetTop;
|
|
498
|
+
if (_heightDiff !== 0 && !stateRef.current.hasFocus) {
|
|
499
|
+
setHeightDiff(_heightDiff);
|
|
500
|
+
}
|
|
501
|
+
scrollContainerRef.current.scrollTo({ top: scrollTop });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
prevPropsRef.current = { ...prevPropsRef.current, children, height, visible };
|
|
505
|
+
}, [children, height, setHeightDiff, visible]);
|
|
506
|
+
if (onCloseContractCheck && visible) {
|
|
507
|
+
console.error('onClose did not set visible=false', Date.now());
|
|
508
|
+
}
|
|
509
|
+
const contextValue = useMemo(() => ({ contentOverlayRef }), [contentOverlayRef]);
|
|
510
|
+
if (!animationTimeout) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const renderFunc = (appearTransition, heightTransition) => (jsx(Layer, { layer: InternalLayerName.BottomSheet, children: jsxs("div", { className: classnames(styles.overlay, {
|
|
514
|
+
[styles.overlayInvisible]: !showOverlay || (showOverlay && appearTransition !== 'exiting' && !currentVisible),
|
|
515
|
+
}), "data-qa": "bottom-sheet-overlay", children: [jsx("div", { className: classnames(styles.overlayBackground, {
|
|
516
|
+
[styles.appearAnimation]: appearTransition === 'entering',
|
|
517
|
+
[styles.disappearAnimation]: appearTransition === 'exiting',
|
|
518
|
+
}) }), jsx("div", { className: classnames(styles.grabber, styles.grabberTransitionAnimation, {
|
|
519
|
+
[styles.grabberInvisible]: appearTransition !== 'entered' || heightTransition !== 'exited',
|
|
520
|
+
[styles.grabberFixed]: stateRef.current.grabber === 'fixed' || stateRef.current.grabber === 'sticky-inside',
|
|
521
|
+
[styles.grabberFakeSticky]: stateRef.current.grabber === 'fake-sticky',
|
|
522
|
+
}), ref: fixedGrabberRef }), jsxs("div", { className: classnames(styles.scrollContainer, {
|
|
523
|
+
[styles.scrollContainerNoScroll]: !allowScrollWhileFocused && stateRef.current.hasFocus,
|
|
524
|
+
[styles.virtualKeyboardAnimation]: stateRef.current.hasFocus,
|
|
525
|
+
}), "data-qa": "bottom-sheet-container", onScroll: recalcScrollFlags, ref: scrollContainerRefMulti, children: [jsx("div", { className: classnames(styles.fill, {
|
|
526
|
+
[styles.fillFullScreen]: height === 'full-screen',
|
|
527
|
+
[styles.fillHalfScreen]: height === 'half-screen',
|
|
528
|
+
}), "data-qa": "bottom-sheet-fill", onClick: handleClose }), jsxs("div", { className: classnames(styles.content, {
|
|
529
|
+
[styles.appearAnimation]: appearTransition === 'entering',
|
|
530
|
+
[styles.closeBySwipeAnimation]: appearTransition === 'exiting' && stateRef.current.isSwipeInProgress,
|
|
531
|
+
[styles.contentFullScreen]: height === 'full-screen',
|
|
532
|
+
[styles.disappearAnimation]: appearTransition === 'exiting',
|
|
533
|
+
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
534
|
+
}), "data-qa": "bottom-sheet-content", ref: contentRef, ...swipeHandlers, children: [jsx("div", { className: classnames(styles.grabber, styles.grabberSticky, {
|
|
535
|
+
[styles.grabberStickyInside]: appearTransition === 'entering' && stateRef.current.grabber === 'sticky-inside',
|
|
536
|
+
}), ref: stickyGrabberRef }), jsxs("div", { className: styles.visualContainer, children: [jsx("div", { className: styles.header, onFocus: handleFocus, ref: headerRef, children: jsx(NavigationBarContext.Provider, { value: NAVIGATION_BAR_SIZE_OVERRIDE, children: header }) }), jsx("div", { className: classnames(styles.main, {
|
|
537
|
+
[styles.mainWithPaddings]: withContentPaddings,
|
|
538
|
+
[styles.mainWithoutHeader]: !header,
|
|
539
|
+
}), children: jsx(BottomSheetContext.Provider, { value: contextValue, children: children }) }), jsx("div", { className: classnames(styles.heightTransitionElement, {
|
|
540
|
+
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
541
|
+
}), ref: heightTransitionElementRef }), jsxs("div", { className: classnames(styles.footer, {
|
|
542
|
+
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
543
|
+
}), ref: footerRef, children: [isDividerVisible && jsx(Divider, {}), interceptClickHandlers ? jsx(ClickInterceptor, { children: footer }) : footer] })] })] })] }), jsx("div", { className: styles.contentOverlay, ref: contentOverlayRef })] }) }));
|
|
544
|
+
return createPortal(jsx("div", { className: styles.cssVariablesContainer, "data-qa": "bottom-sheet-css-variables", ref: cssVariablesContainerRef, children: jsx(Transition, { appear: true, in: currentVisible, mountOnEnter: true, onEnter: runTranslateYAnimation, onEntering: resetTranslateY, onEntered: handleAppearAnimationEnd, onExit: handleExitAnimationStart, onExiting: runTranslateYAnimation, onExited: handleExitAnimationEnd, timeout: animationTimeout.appear, unmountOnExit: true, nodeRef: contentRef, children: (appearTransition) => (jsx(Transition, { in: heightDiff !== null, onEnter: runTranslateYAnimation, onEntering: resetTranslateY, onEntered: resetHeightDiff, onExited: recalcScrollFlags, timeout: animationTimeout.height, nodeRef: contentRef, children: (heightTransition) => renderFunc(appearTransition, heightTransition) })) }) }), document.body);
|
|
545
|
+
});
|
|
546
|
+
BottomSheet.displayName = 'BottomSheet';
|
|
547
|
+
|
|
548
|
+
export { BottomSheet };
|
|
549
|
+
//# sourceMappingURL=BottomSheet.js.map
|