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