@hh.ru/magritte-ui-bottom-sheet 4.1.40 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BottomSheet.js +399 -354
- package/BottomSheet.js.map +1 -1
- package/BottomSheetFooter.js +1 -1
- package/bottom-sheet-D8XuBr1R.js +5 -0
- package/bottom-sheet-D8XuBr1R.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-Uf681RNm.js +0 -5
- package/bottom-sheet-Uf681RNm.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, CustomScrollContextProvider } from '@hh.ru/magritte-internal-custom-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,134 @@ 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-D8XuBr1R.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 scrollContextProviderRef = useRef(null);
|
|
80
|
+
const LayoutMetrics = useRef((() => {
|
|
81
|
+
let cachedBottomSheetHeight = null;
|
|
82
|
+
let cachedContentHeight = null;
|
|
83
|
+
let cachedInitialOffset = null;
|
|
84
|
+
let cachedRemainingAvailableHeight = null;
|
|
85
|
+
let cachedScrollContainerHeight = null;
|
|
86
|
+
return {
|
|
87
|
+
get bottomSheetHeight() {
|
|
88
|
+
if (cachedBottomSheetHeight === null && visualContainerRef.current !== null) {
|
|
89
|
+
cachedBottomSheetHeight = visualContainerRef.current.clientHeight;
|
|
90
|
+
}
|
|
91
|
+
return cachedBottomSheetHeight ?? 0;
|
|
92
|
+
},
|
|
93
|
+
get contentHeight() {
|
|
94
|
+
if (cachedContentHeight === null && contentRef.current !== null) {
|
|
95
|
+
cachedContentHeight = contentRef.current.clientHeight;
|
|
96
|
+
}
|
|
97
|
+
return cachedContentHeight ?? 0;
|
|
98
|
+
},
|
|
99
|
+
get initialOffset() {
|
|
100
|
+
if (cachedInitialOffset === null &&
|
|
101
|
+
visualContainerRef.current !== null &&
|
|
102
|
+
visualViewport !== null) {
|
|
103
|
+
cachedInitialOffset =
|
|
104
|
+
height === 'half-screen'
|
|
105
|
+
? Math.max(Math.round(visualContainerRef.current.clientHeight - visualViewport.height / 2), 0)
|
|
106
|
+
: 0;
|
|
107
|
+
}
|
|
108
|
+
return cachedInitialOffset ?? 0;
|
|
109
|
+
},
|
|
110
|
+
get remainingAvailableHeight() {
|
|
111
|
+
if (cachedRemainingAvailableHeight === null && grabberRef.current !== null) {
|
|
112
|
+
cachedRemainingAvailableHeight = grabberRef.current.offsetTop;
|
|
113
|
+
}
|
|
114
|
+
return cachedRemainingAvailableHeight ?? 0;
|
|
115
|
+
},
|
|
116
|
+
get scrollContainerHeight() {
|
|
117
|
+
if (cachedScrollContainerHeight === null && scrollContainerRef.current !== null) {
|
|
118
|
+
cachedScrollContainerHeight = scrollContainerRef.current.clientHeight;
|
|
119
|
+
}
|
|
120
|
+
return cachedScrollContainerHeight ?? 0;
|
|
121
|
+
},
|
|
122
|
+
invalidateCache() {
|
|
123
|
+
cachedBottomSheetHeight = null;
|
|
124
|
+
cachedContentHeight = null;
|
|
125
|
+
cachedInitialOffset = null;
|
|
126
|
+
cachedRemainingAvailableHeight = null;
|
|
127
|
+
cachedScrollContainerHeight = null;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
})());
|
|
81
131
|
const setCSSVariable = (name, value) => cssVariablesContainerRef.current !== null && cssVariablesContainerRef.current.style.setProperty(name, value);
|
|
82
132
|
useEffect(() => {
|
|
83
133
|
if (!currentVisible) {
|
|
84
134
|
return void 0;
|
|
85
135
|
}
|
|
86
|
-
|
|
136
|
+
DOCUMENT_HEIGHT.current = document.documentElement.clientHeight;
|
|
137
|
+
if (visualViewport !== null) {
|
|
138
|
+
SWIPE_THRESHOLD.current = { max: Math.round(visualViewport.height * 0.8) };
|
|
139
|
+
}
|
|
87
140
|
if (!showOverlay) {
|
|
88
141
|
const enableOverscroll = disableOverscroll();
|
|
89
142
|
return enableOverscroll;
|
|
90
143
|
}
|
|
91
144
|
const enableScroll = disableScroll();
|
|
92
145
|
return enableScroll;
|
|
93
|
-
}, [
|
|
146
|
+
}, [currentVisible, showOverlay]);
|
|
94
147
|
useEffect(() => {
|
|
95
148
|
const animationTimeoutElement = document.createElement('div');
|
|
96
149
|
animationTimeoutElement.classList.add(styles.animationTimeout);
|
|
@@ -102,63 +155,60 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
102
155
|
document.body.removeChild(animationTimeoutElement);
|
|
103
156
|
setAnimationTimeout({ appear: { enter, exit }, height: { enter: height, exit } });
|
|
104
157
|
}, [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
|
-
}
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!currentVisible || !contentRef.current) {
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
// при изменении высоты контента анимируем ее
|
|
163
|
+
// самому боттомшиту задаем фиксированную высоту и пересчитываем ее самостоятельно,
|
|
164
|
+
// чтобы не было мерцания, когда новый контент отрисовался до срабатывания колбека ResizeObserver
|
|
165
|
+
let prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
166
|
+
if (visualContainerRef.current !== null) {
|
|
167
|
+
visualContainerRef.current.style.height = `${LayoutMetrics.current.bottomSheetHeight}px`;
|
|
168
|
+
}
|
|
169
|
+
const observer = new ResizeObserver(() => {
|
|
170
|
+
if (stateRef.current.heightAnimationRunning) {
|
|
171
|
+
return;
|
|
137
172
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
LayoutMetrics.current.invalidateCache();
|
|
174
|
+
// в общем случае видимую высоту контента можно посчитать как min(scrollContainer.height, contentHeight)
|
|
175
|
+
const contentHeightDiff = LayoutMetrics.current.contentHeight - prevContentHeight;
|
|
176
|
+
if (contentHeightDiff > 0) {
|
|
177
|
+
// но если высота контента увеличилась, мы не знаем новую высоту scrollContainer.height
|
|
178
|
+
// т.к. фиксированная высота боттомшита не дает scrollContainer увеличиться
|
|
179
|
+
// но сам боттомшит может увеличиться не больше чем на расстояние между ним и верхним краем экрана
|
|
180
|
+
const heightDiff = Math.round(Math.min(contentHeightDiff, LayoutMetrics.current.remainingAvailableHeight));
|
|
181
|
+
// триггерим анимацию даже при нулевом heightDiff, чтобы сбросить позицию скролла
|
|
182
|
+
setVisibleHeightDiff(heightDiff);
|
|
183
|
+
stateRef.current.heightAnimationRunning = true;
|
|
184
|
+
}
|
|
185
|
+
else if (contentHeightDiff < 0) {
|
|
186
|
+
// если высота контента уменьшилась, новая высота scrollContainer будет такая же или меньше
|
|
187
|
+
// поэтому можем посчитать новую видимую высоту контента и сравнить со старой
|
|
188
|
+
const prevVisibleHeight = Math.min(LayoutMetrics.current.scrollContainerHeight, prevContentHeight);
|
|
189
|
+
const newVisibleHeight = Math.min(LayoutMetrics.current.scrollContainerHeight, LayoutMetrics.current.contentHeight);
|
|
190
|
+
const heightDiff = Math.round(newVisibleHeight - prevVisibleHeight);
|
|
191
|
+
// триггерим анимацию даже при нулевом heightDiff, чтобы сбросить позицию скролла
|
|
192
|
+
setVisibleHeightDiff(heightDiff);
|
|
193
|
+
stateRef.current.heightAnimationRunning = true;
|
|
147
194
|
}
|
|
195
|
+
prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
196
|
+
});
|
|
197
|
+
observer.observe(contentRef.current);
|
|
198
|
+
return () => observer.disconnect();
|
|
199
|
+
}, [currentVisible, setVisibleHeightDiff]);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!currentVisible || isSafari()) {
|
|
202
|
+
return;
|
|
148
203
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
204
|
+
// используем Virtual Keyboard API через meta-тег вместо navigator.virtualKeyboard,
|
|
205
|
+
// потому что второй способ работает только на страницах, открытых через HTTPS, что мешает тестированию
|
|
206
|
+
let meta = document.querySelector('meta[name="viewport"]');
|
|
207
|
+
if (!meta) {
|
|
208
|
+
meta = document.createElement('meta');
|
|
209
|
+
meta.setAttribute('name', 'viewport');
|
|
210
|
+
document.head.appendChild(meta);
|
|
156
211
|
}
|
|
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
212
|
const attributesStr = meta.getAttribute('content');
|
|
163
213
|
const attributes = (attributesStr !== null
|
|
164
214
|
? Object.fromEntries(attributesStr.split(',').map((keyValuePairStr) => keyValuePairStr.split('=')))
|
|
@@ -167,11 +217,10 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
167
217
|
const attributesStrUpdated = Object.entries(attributes)
|
|
168
218
|
.map((keyValuePair) => keyValuePair.join('='))
|
|
169
219
|
.join(',');
|
|
170
|
-
meta.setAttribute('name', 'viewport');
|
|
171
220
|
meta.setAttribute('content', attributesStrUpdated);
|
|
172
|
-
}, [keyboardOverlaysContent]);
|
|
221
|
+
}, [currentVisible, isSafari, keyboardOverlaysContent]);
|
|
173
222
|
const recalcKeyboardOffsets = useCallback(() => {
|
|
174
|
-
if (!headerRef.current || !
|
|
223
|
+
if (!headerRef.current || !overlayRef.current || !visualViewport) {
|
|
175
224
|
return;
|
|
176
225
|
}
|
|
177
226
|
if (stateRef.current.hasFocus) {
|
|
@@ -182,26 +231,24 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
182
231
|
// В нем не нужно ничего корректировать при keyboardOverlaysContent=true,
|
|
183
232
|
// а при keyboardOverlaysContent=false нужно сдвинуть НИЖНИЙ край контейнера ВВЕРХ,
|
|
184
233
|
// чтобы он совпал с границей Visual Viewport
|
|
185
|
-
// 2. Chrome < 108 & Chromium-based — ресайзит и Visual Viewport, и Layout Viewport
|
|
234
|
+
// 2. Chrome < 108 & Chromium-based — ресайзит и Visual Viewport, и Layout Viewport.
|
|
186
235
|
// В нем не нужно ничего корректировать при keyboardOverlaysContent=false,
|
|
187
236
|
// а при keyboardOverlaysContent=true нужно сдвинуть НИЖНИЙ край контейнера ВНИЗ,
|
|
188
237
|
// чтобы футер уехал под клавиатуру
|
|
189
|
-
// 3. Chrome >= 108 — поддерживает Virtual Keyboard API и meta-тег interactive-widget
|
|
238
|
+
// 3. Chrome >= 108 — поддерживает Virtual Keyboard API и meta-тег interactive-widget.
|
|
190
239
|
// Используем поведение `resizes-visual` (как в Safari) в случае keyboardOverlaysContent=true
|
|
191
240
|
// и `resizes-content` (как в Chrome < 108 & Chromium-based) в случае keyboardOverlaysContent=false
|
|
192
|
-
// Таким образом в нем ничего не нужно
|
|
193
|
-
const
|
|
241
|
+
// Таким образом в нем ничего не нужно корректировать
|
|
242
|
+
const overlayDOMRect = overlayRef.current.getBoundingClientRect();
|
|
194
243
|
// любой браузер может сдвинуть Visual Viewport вверх, если фокусируемый инпут находится близко к нижней границе
|
|
195
244
|
// из-за этого может возникнуть проблема, что ВЕРХНИЙ край контента уехал за границу Visual Viewport
|
|
196
245
|
// сдвигаем ВЕРХНИЙ край контейнера ВНИЗ, чтобы он совпал с границей Visual Viewport
|
|
197
|
-
const visualViewportShift = Math.round(
|
|
198
|
-
//
|
|
199
|
-
layoutMetricsRef.current.viewportShift = visualViewportShift;
|
|
200
|
-
// клавиатура ПОВЕРХ контента
|
|
246
|
+
const visualViewportShift = Math.round(-overlayDOMRect.top);
|
|
247
|
+
// keyboardOverlaysContent=true, клавиатура ПОВЕРХ контента
|
|
201
248
|
// этот кейс нужно корректировать только в Chrome < 108 & Chromium-based
|
|
202
249
|
if (keyboardOverlaysContent) {
|
|
203
250
|
// браузеры из этой группы меняют Layout Viewport
|
|
204
|
-
const layoutViewportDiff = Math.round(
|
|
251
|
+
const layoutViewportDiff = Math.round(DOCUMENT_HEIGHT.current - document.documentElement.clientHeight);
|
|
205
252
|
if (layoutViewportDiff > 0) {
|
|
206
253
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
207
254
|
// сдвигаем НИЖНИЙ край контейнера ВНИЗ, чтобы футер уехал под клавиатуру
|
|
@@ -209,17 +256,16 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
209
256
|
// при этом может возникнуть проблема, что клавиатура перекрыла хедер
|
|
210
257
|
// проверяем это и компенсируем величину перекрытия при необходимости
|
|
211
258
|
const headerDOMRect = headerRef.current.getBoundingClientRect();
|
|
212
|
-
const headerOutOfViewportHeight = Math.round(headerDOMRect.bottom + layoutViewportDiff -
|
|
259
|
+
const headerOutOfViewportHeight = Math.round(headerDOMRect.bottom + layoutViewportDiff - DOCUMENT_HEIGHT.current);
|
|
213
260
|
if (headerOutOfViewportHeight > 0) {
|
|
214
261
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${headerOutOfViewportHeight - layoutViewportDiff}px`);
|
|
215
262
|
}
|
|
216
263
|
}
|
|
217
264
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const visualViewportDiff = Math.round(scrollContainerDOMREct.bottom - visualViewport.height);
|
|
265
|
+
// keyboardOverlaysContent=false, клавиатура ПОД контентом
|
|
266
|
+
// этот кейс нужно корректировать только в Safari
|
|
267
|
+
if (!keyboardOverlaysContent && isSafari()) {
|
|
268
|
+
const visualViewportDiff = Math.round(overlayDOMRect.bottom - visualViewport.height);
|
|
223
269
|
if (visualViewportDiff > 0) {
|
|
224
270
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
225
271
|
// сдвигаем НИЖНИЙ край контейнера ВВЕРХ, чтобы он совпал с границей Visual Viewport
|
|
@@ -231,11 +277,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
231
277
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
232
278
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
233
279
|
}
|
|
234
|
-
}, [keyboardOverlaysContent]);
|
|
280
|
+
}, [isSafari, keyboardOverlaysContent]);
|
|
235
281
|
const handleFocus = useCallback((event) => {
|
|
236
|
-
if (!scrollContainerRef.current) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
282
|
const focusedElement = event.target;
|
|
240
283
|
const initialViewportHeight = visualViewport?.height;
|
|
241
284
|
const resizeRAFStart = performance.now();
|
|
@@ -248,7 +291,12 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
248
291
|
stateRef.current.resizeRAFHandle = null;
|
|
249
292
|
}
|
|
250
293
|
recalcKeyboardOffsets();
|
|
294
|
+
if (!stateRef.current.hasFocus) {
|
|
295
|
+
visualViewport?.removeEventListener('resize', handleResize);
|
|
296
|
+
}
|
|
251
297
|
};
|
|
298
|
+
// если спамить фокус/блюр инпута, ивент visualViewport.resize может не долететь
|
|
299
|
+
// поэтому проверяем изменение высоты в рекурсивном RAF
|
|
252
300
|
const waitForResize = () => {
|
|
253
301
|
if (performance.now() - resizeRAFStart > 1000 || visualViewport?.height !== initialViewportHeight) {
|
|
254
302
|
visualViewport?.removeEventListener('resize', handleResize);
|
|
@@ -260,292 +308,289 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
260
308
|
}
|
|
261
309
|
};
|
|
262
310
|
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) {
|
|
311
|
+
if (isSafari()) {
|
|
270
312
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
271
313
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
272
314
|
}
|
|
315
|
+
stateRef.current.hasFocus = false;
|
|
273
316
|
if (stateRef.current.resizeRAFHandle !== null) {
|
|
274
317
|
cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
275
318
|
stateRef.current.resizeRAFHandle = null;
|
|
276
319
|
}
|
|
277
|
-
|
|
320
|
+
focusedElement.removeEventListener('blur', handleBlur);
|
|
278
321
|
};
|
|
279
|
-
focusedElement.addEventListener('blur', handleBlur);
|
|
280
322
|
stateRef.current.hasFocus = true;
|
|
281
|
-
if (!allowScrollWhileFocused) {
|
|
282
|
-
scrollContainerRef.current.classList.add(styles.scrollContainerNoScroll);
|
|
283
|
-
}
|
|
284
|
-
scrollContainerRef.current.classList.add(styles.virtualKeyboardAnimation);
|
|
285
323
|
stateRef.current.resizeRAFHandle = requestAnimationFrame(waitForResize);
|
|
286
324
|
visualViewport?.addEventListener('resize', handleResize);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
325
|
+
focusedElement.addEventListener('blur', handleBlur);
|
|
326
|
+
}, [isSafari, recalcKeyboardOffsets]);
|
|
327
|
+
// contentOverlay совпадает по границам с контентом боттомшита, но лежит вне боттомшита,
|
|
328
|
+
// чтобы чайлды contentOverlay не обрезались границами боттомшита
|
|
329
|
+
// например, снекбар, лежащий внутри contentOverlay, при смахивании может оказаться в любом месте экрана
|
|
330
|
+
// поэтому позицию contentOverlay нужно синхронизировать
|
|
331
|
+
const recalcContentOverlayPosition = useCallback(() => {
|
|
332
|
+
if (contentOverlayRef.current !== null &&
|
|
333
|
+
scrollContainerRef.current !== null &&
|
|
334
|
+
visualContainerRef.current !== null) {
|
|
335
|
+
contentOverlayRef.current.style.top = `${scrollContainerRef.current.offsetTop + visualContainerRef.current.offsetTop}px`;
|
|
336
|
+
contentOverlayRef.current.style.height = `${scrollContainerRef.current.clientHeight}px`;
|
|
297
337
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
338
|
+
}, []);
|
|
339
|
+
const recalcScrollFlags = useCallback(() => {
|
|
340
|
+
if (dividerRef.current !== null) {
|
|
341
|
+
const prevDividerVisible = stateRef.current.dividerVisible;
|
|
342
|
+
const isNotScrolledToEnd = LayoutMetrics.current.contentHeight + stateRef.current.scrollOffset >
|
|
343
|
+
LayoutMetrics.current.scrollContainerHeight;
|
|
344
|
+
stateRef.current.dividerVisible =
|
|
345
|
+
showDivider === 'always' || (showDivider === 'with-scroll' && isNotScrolledToEnd);
|
|
346
|
+
if (stateRef.current.dividerVisible !== prevDividerVisible) {
|
|
347
|
+
dividerRef.current.classList.toggle(styles.dividerVisible, stateRef.current.dividerVisible);
|
|
303
348
|
}
|
|
304
|
-
|
|
305
|
-
|
|
349
|
+
}
|
|
350
|
+
if (grabberRef.current !== null) {
|
|
351
|
+
const prevGrabberUnsafe = stateRef.current.grabberUnsafe;
|
|
352
|
+
stateRef.current.grabberUnsafe =
|
|
353
|
+
Math.round(Math.max(stateRef.current.scrollOffset, 0) + stateRef.current.swipeOffset) ===
|
|
354
|
+
LayoutMetrics.current.remainingAvailableHeight;
|
|
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
|
-
|
|
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
|
+
scrollContextProviderRef.current?.notify({ scrollTop: 0 });
|
|
388
|
+
}, [recalcContentOverlayPosition, recalcScrollFlags]);
|
|
367
389
|
const handleExitAnimationStart = useCallback(() => {
|
|
368
|
-
|
|
390
|
+
setTransformToVisible();
|
|
369
391
|
onBeforeExit?.();
|
|
370
|
-
}, [
|
|
392
|
+
}, [setTransformToVisible, onBeforeExit]);
|
|
371
393
|
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
394
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
383
395
|
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);
|
|
396
|
+
stateRef.current.resizeRAFHandle !== null && cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
397
|
+
stateRef.current = { ...INITIAL_STATE };
|
|
394
398
|
onAfterExit?.();
|
|
395
|
-
}, [
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return;
|
|
399
|
+
}, [onAfterExit]);
|
|
400
|
+
const handleHeightAnimationStart = useCallback(() => {
|
|
401
|
+
LayoutMetrics.current.invalidateCache();
|
|
402
|
+
stateRef.current.scrollOffset = LayoutMetrics.current.initialOffset;
|
|
403
|
+
stateRef.current.swipeOffset = 0;
|
|
404
|
+
if (contentRef.current !== null) {
|
|
405
|
+
contentRef.current.style.transform = translateY(0);
|
|
403
406
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
stateRef.current.swipeOffset = 0;
|
|
407
|
+
if (swipeContainerRef.current !== null) {
|
|
408
|
+
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.current.initialOffset);
|
|
407
409
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (!contentRef.current ||
|
|
411
|
-
!contentOverlayRef.current ||
|
|
412
|
-
!stateRef.current.isSwipeEnabled ||
|
|
413
|
-
!scrollContainerRef.current) {
|
|
414
|
-
return;
|
|
410
|
+
if (visibleHeightDiff !== null && visualContainerRef.current !== null) {
|
|
411
|
+
visualContainerRef.current.style.height = `${LayoutMetrics.current.bottomSheetHeight + visibleHeightDiff}px`;
|
|
415
412
|
}
|
|
416
|
-
|
|
417
|
-
|
|
413
|
+
scrollContextProviderRef.current?.notify({ scrollTop: 0 });
|
|
414
|
+
}, [visibleHeightDiff]);
|
|
415
|
+
const handleHeightAnimationEnd = useCallback(() => {
|
|
416
|
+
LayoutMetrics.current.invalidateCache();
|
|
417
|
+
stateRef.current.heightAnimationRunning = false;
|
|
418
|
+
setVisibleHeightDiff(null);
|
|
419
|
+
requestAnimationFrame(recalcContentOverlayPosition);
|
|
420
|
+
requestAnimationFrame(recalcScrollFlags);
|
|
421
|
+
}, [setVisibleHeightDiff, recalcContentOverlayPosition, recalcScrollFlags]);
|
|
422
|
+
const handleSwipeMove = useCallback((event) => {
|
|
423
|
+
if ((stateRef.current.touchAction !== null && stateRef.current.touchAction !== 'swipe') ||
|
|
424
|
+
hasSelectedText()) {
|
|
418
425
|
return;
|
|
419
426
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
// храним неокругленное значение для translateY, чтобы анимация была плавнее
|
|
428
|
+
let newSwipeOffset = stateRef.current.swipeOffset + event.deltaY;
|
|
429
|
+
if (Math.round(newSwipeOffset) <= 0) {
|
|
430
|
+
// боттомшит уперся в верхний край экрана, не даем свайпать дальше
|
|
431
|
+
newSwipeOffset = 0;
|
|
432
|
+
if (stateRef.current.touchAction === 'swipe') {
|
|
433
|
+
stateRef.current.touchAction = 'complete';
|
|
425
434
|
}
|
|
426
|
-
recalcScrollFlags();
|
|
427
435
|
}
|
|
428
436
|
else {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
stateRef.current.isSwipeEnabled = false;
|
|
432
|
-
changeSwipeProgressState(stateRef, scrollContainerRef, false, !!allowScrollWhileFocused);
|
|
437
|
+
// свайп в процессе
|
|
438
|
+
stateRef.current.touchAction = 'swipe';
|
|
433
439
|
}
|
|
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 = ``;
|
|
440
|
+
if (stateRef.current.swipeOffset !== newSwipeOffset) {
|
|
441
|
+
stateRef.current.swipeOffset = newSwipeOffset;
|
|
442
|
+
if (swipeContainerRef.current !== null) {
|
|
443
|
+
swipeContainerRef.current.style.transform = translateY(Math.max(stateRef.current.scrollOffset, 0) + newSwipeOffset);
|
|
444
|
+
}
|
|
445
|
+
recalcScrollFlags();
|
|
443
446
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
stateRef.current.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
447
|
+
}, [recalcScrollFlags]);
|
|
448
|
+
const handleSwipeCancel = useCallback(() => {
|
|
449
|
+
if (stateRef.current.touchAction === 'swipe') {
|
|
450
|
+
stateRef.current.swipeOffset = 0;
|
|
451
|
+
stateRef.current.touchAction = null;
|
|
452
|
+
if (swipeContainerRef.current !== null) {
|
|
453
|
+
swipeContainerRef.current.classList.add(styles.swipeCancelAnimation);
|
|
454
|
+
const swipeContainer = swipeContainerRef.current;
|
|
455
|
+
setTimeout(() => swipeContainer.classList.remove(styles.swipeCancelAnimation), 100);
|
|
456
|
+
swipeContainerRef.current.style.transform = translateY(Math.max(stateRef.current.scrollOffset, 0));
|
|
457
|
+
}
|
|
458
|
+
recalcScrollFlags();
|
|
452
459
|
}
|
|
453
|
-
|
|
454
|
-
|
|
460
|
+
}, [recalcScrollFlags]);
|
|
461
|
+
const handleSwipeEnd = useCallback(() => {
|
|
462
|
+
if (stateRef.current.touchAction === 'swipe') {
|
|
463
|
+
onCloseRef.current();
|
|
455
464
|
}
|
|
456
|
-
}, [
|
|
457
|
-
const
|
|
458
|
-
thresholdYRef:
|
|
459
|
-
onSwipeStart: handleSwipeStart,
|
|
465
|
+
}, []);
|
|
466
|
+
const swipeHandlers = useSwipeHandlers({
|
|
467
|
+
thresholdYRef: SWIPE_THRESHOLD,
|
|
460
468
|
onSwipeMove: handleSwipeMove,
|
|
461
469
|
onSwipeEnd: handleSwipeEnd,
|
|
462
470
|
onSwipeCancel: handleSwipeCancel,
|
|
463
471
|
});
|
|
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;
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (!currentVisible || visualContainerRef.current === null) {
|
|
474
|
+
return void 0;
|
|
483
475
|
}
|
|
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`);
|
|
476
|
+
const handleScroll = (event) => {
|
|
477
|
+
if ((!allowScrollWhileFocused && stateRef.current.hasFocus) ||
|
|
478
|
+
(stateRef.current.touchAction !== null && stateRef.current.touchAction !== 'scroll') ||
|
|
479
|
+
hasSelectedText()) {
|
|
480
|
+
return;
|
|
492
481
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (
|
|
499
|
-
|
|
482
|
+
if (LayoutMetrics.current.initialOffset !== 0 ||
|
|
483
|
+
LayoutMetrics.current.contentHeight > LayoutMetrics.current.scrollContainerHeight) {
|
|
484
|
+
// храним неокругленное значение для translateY, чтобы анимация была плавнее
|
|
485
|
+
let newScrollOffset = stateRef.current.scrollOffset + event.delta;
|
|
486
|
+
const roundedNewScrollOffset = Math.round(newScrollOffset);
|
|
487
|
+
if (roundedNewScrollOffset >= LayoutMetrics.current.initialOffset) {
|
|
488
|
+
// скролла нет (touchAction is null)
|
|
489
|
+
// либо контент проскроллен в начало, тогда не даем скроллить дальше
|
|
490
|
+
newScrollOffset = LayoutMetrics.current.initialOffset;
|
|
491
|
+
if (stateRef.current.touchAction === 'scroll') {
|
|
492
|
+
stateRef.current.touchAction = 'complete';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (LayoutMetrics.current.contentHeight + roundedNewScrollOffset <=
|
|
496
|
+
LayoutMetrics.current.scrollContainerHeight) {
|
|
497
|
+
// скролла нет (touchAction is null)
|
|
498
|
+
// либо контент проскроллен до конца, тогда не даем скроллить дальше
|
|
499
|
+
newScrollOffset = LayoutMetrics.current.scrollContainerHeight - LayoutMetrics.current.contentHeight;
|
|
500
|
+
if (stateRef.current.touchAction === 'scroll') {
|
|
501
|
+
stateRef.current.touchAction = 'complete';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
// скролл в процессе
|
|
506
|
+
stateRef.current.touchAction = 'scroll';
|
|
507
|
+
}
|
|
508
|
+
if (stateRef.current.scrollOffset !== newScrollOffset) {
|
|
509
|
+
const offsetWasPositive = stateRef.current.scrollOffset > 0;
|
|
510
|
+
stateRef.current.scrollOffset = newScrollOffset;
|
|
511
|
+
if (contentRef.current !== null && swipeContainerRef.current !== null) {
|
|
512
|
+
if (newScrollOffset > 0) {
|
|
513
|
+
if (!offsetWasPositive) {
|
|
514
|
+
contentRef.current.style.transform = translateY(0);
|
|
515
|
+
}
|
|
516
|
+
swipeContainerRef.current.style.transform = translateY(newScrollOffset);
|
|
517
|
+
if (footerRef.current !== null) {
|
|
518
|
+
footerRef.current.style.transform = translateY(-newScrollOffset);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
contentRef.current.style.transform = translateY(newScrollOffset);
|
|
523
|
+
if (offsetWasPositive) {
|
|
524
|
+
swipeContainerRef.current.style.transform = translateY(0);
|
|
525
|
+
if (footerRef.current !== null) {
|
|
526
|
+
footerRef.current.style.transform = translateY(0);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
recalcScrollFlags();
|
|
532
|
+
scrollContextProviderRef.current?.notify({
|
|
533
|
+
scrollTop: Math.max(-stateRef.current.scrollOffset, 0),
|
|
534
|
+
});
|
|
500
535
|
}
|
|
501
|
-
scrollContainerRef.current.scrollTo({ top: scrollTop });
|
|
502
536
|
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
537
|
+
};
|
|
538
|
+
const handleScrollEnd = () => {
|
|
539
|
+
if (stateRef.current.touchAction === 'scroll') {
|
|
540
|
+
stateRef.current.touchAction = null;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const handleTouchStart = (event) => {
|
|
544
|
+
if (stateRef.current.touchAction === 'complete') {
|
|
545
|
+
stateRef.current.touchAction = null;
|
|
546
|
+
}
|
|
547
|
+
swipeHandlers.onTouchStart(event);
|
|
548
|
+
};
|
|
549
|
+
const handleTouchMove = (event) => {
|
|
550
|
+
event.preventDefault();
|
|
551
|
+
event.stopPropagation();
|
|
552
|
+
swipeHandlers.onTouchMove(event);
|
|
553
|
+
};
|
|
554
|
+
const removeScrollHandlers = initTouchHandlers('vertical', visualContainerRef, handleScroll, handleScrollEnd);
|
|
555
|
+
const visualContainer = visualContainerRef.current;
|
|
556
|
+
visualContainer.addEventListener('touchstart', handleTouchStart);
|
|
557
|
+
visualContainer.addEventListener('touchmove', handleTouchMove);
|
|
558
|
+
visualContainer.addEventListener('touchend', swipeHandlers.onTouchEnd);
|
|
559
|
+
visualContainer.addEventListener('touchcancel', swipeHandlers.onTouchCancel);
|
|
560
|
+
return () => {
|
|
561
|
+
removeScrollHandlers();
|
|
562
|
+
visualContainer.removeEventListener('touchstart', handleTouchStart);
|
|
563
|
+
visualContainer.removeEventListener('touchmove', handleTouchMove);
|
|
564
|
+
visualContainer.removeEventListener('touchend', swipeHandlers.onTouchEnd);
|
|
565
|
+
visualContainer.removeEventListener('touchcancel', swipeHandlers.onTouchCancel);
|
|
566
|
+
};
|
|
567
|
+
}, [allowScrollWhileFocused, currentVisible, recalcScrollFlags, swipeHandlers]);
|
|
568
|
+
const { onTouchEnd, ...eventHandlers } = useNoBubbling();
|
|
511
569
|
if (!animationTimeout) {
|
|
512
570
|
return null;
|
|
513
571
|
}
|
|
514
|
-
const renderFunc = (appearTransition, heightTransition) => (jsx(Layer, { layer: InternalLayerName.BottomSheet, children: jsxs("div", { ...eventHandlers, className: classnames(styles.
|
|
515
|
-
|
|
516
|
-
|
|
572
|
+
const renderFunc = (appearTransition, heightTransition) => (jsx(Layer, { layer: InternalLayerName.BottomSheet, children: jsxs("div", { ...eventHandlers, className: styles.overlay, children: [jsx("div", { className: classnames(styles.overlayBackground, {
|
|
573
|
+
[styles.overlayBackgroundVisible]: showOverlay,
|
|
574
|
+
[styles.appearAnimation]: appearTransition === 'entering',
|
|
575
|
+
[styles.disappearAnimation]: appearTransition === 'exiting',
|
|
576
|
+
}), "data-qa": "bottom-sheet-overlay", onClick: appearTransition === 'entered' ? onCloseRef.current : undefined, ref: overlayRef }), jsxs("div", { className: classnames(styles.swipeContainer, {
|
|
517
577
|
[styles.appearAnimation]: appearTransition === 'entering',
|
|
578
|
+
[styles.closeBySwipeAnimation]: appearTransition === 'exiting' && stateRef.current.touchAction === 'swipe',
|
|
518
579
|
[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',
|
|
580
|
+
}), "data-qa": "bottom-sheet-container", ref: swipeContainerRef, children: [jsx("div", { className: classnames(styles.grabber, styles.grabberTransitionAnimation, {
|
|
581
|
+
[styles.grabberEnsureSafe]: stateRef.current.grabberUnsafe,
|
|
582
|
+
}), ref: grabberRef }), jsx("div", { className: classnames(styles.visualContainer, {
|
|
583
|
+
[styles.visualContainerFullScreen]: height === 'full-screen',
|
|
534
584
|
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
535
|
-
}), "data-qa": "bottom-sheet-content", ref:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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);
|
|
585
|
+
}), "data-qa": "bottom-sheet-content", ref: bottomSheetRef, children: jsxs(CustomScrollContextProvider, { ref: scrollContextProviderRef, 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, {
|
|
586
|
+
[styles.contentWithPaddings]: withContentPaddings,
|
|
587
|
+
[styles.contentWithoutHeader]: !header,
|
|
588
|
+
}), 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, {
|
|
589
|
+
[styles.dividerVisible]: stateRef.current.dividerVisible,
|
|
590
|
+
}), ref: dividerRef, children: jsx(Divider, {}) })), interceptClickHandlers ? jsx(ClickInterceptor, { children: footer }) : footer] })] }) }), jsx("div", { className: styles.contentOverlay, ref: contentOverlayRef })] })] }) }));
|
|
591
|
+
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
592
|
};
|
|
547
593
|
const BottomSheet = forwardRef(BottomSheetRenderFunc);
|
|
548
|
-
BottomSheet.displayName = 'BottomSheet';
|
|
549
594
|
|
|
550
595
|
export { BottomSheet };
|
|
551
596
|
//# sourceMappingURL=BottomSheet.js.map
|