@hh.ru/magritte-ui-bottom-sheet 5.3.29 → 5.5.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 +203 -182
- package/BottomSheet.js.map +1 -1
- package/BottomSheetFooter.js +1 -1
- package/bottom-sheet-CPDqyiXV.js +5 -0
- package/bottom-sheet-CPDqyiXV.js.map +1 -0
- package/index.css +38 -39
- package/index.js +1 -1
- package/package.json +6 -6
- package/bottom-sheet-W0jm1DWc.js +0 -5
- package/bottom-sheet-W0jm1DWc.js.map +0 -1
package/BottomSheet.js
CHANGED
|
@@ -16,11 +16,10 @@ import { ClickInterceptor } from './ClickInterceptor.js';
|
|
|
16
16
|
import { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';
|
|
17
17
|
import { Divider } from '@hh.ru/magritte-ui-divider';
|
|
18
18
|
import { Layer } from '@hh.ru/magritte-ui-layer';
|
|
19
|
-
import {
|
|
19
|
+
import { isNavigationBarComponent, NavigationBarComponent } from '@hh.ru/magritte-ui-navigation-bar';
|
|
20
20
|
import { isValidTreeSelectorWrapper } from '@hh.ru/magritte-ui-tree-selector';
|
|
21
|
-
import { s as styles } from './bottom-sheet-
|
|
21
|
+
import { s as styles } from './bottom-sheet-CPDqyiXV.js';
|
|
22
22
|
|
|
23
|
-
const NAVIGATION_BAR_CONTEXT_PROPS = { insideBottomSheet: true };
|
|
24
23
|
const CSS_VAR_ENTER_ANIMATION_DURATION = '--enter-animation-duration';
|
|
25
24
|
const CSS_VAR_EXIT_ANIMATION_DURATION = '--exit-animation-duration';
|
|
26
25
|
const CSS_VAR_HEIGHT_ANIMATION_DURATION = '--height-transition-duration';
|
|
@@ -30,15 +29,7 @@ const hasSelectedText = () => {
|
|
|
30
29
|
const selection = document.getSelection();
|
|
31
30
|
return !!selection && !selection.isCollapsed;
|
|
32
31
|
};
|
|
33
|
-
const
|
|
34
|
-
let lazyValue = null;
|
|
35
|
-
return () => {
|
|
36
|
-
if (lazyValue === null) {
|
|
37
|
-
lazyValue = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
38
|
-
}
|
|
39
|
-
return lazyValue;
|
|
40
|
-
};
|
|
41
|
-
};
|
|
32
|
+
const isSafari = () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
42
33
|
const toNumber = (value) => {
|
|
43
34
|
const result = parseInt(value, 10);
|
|
44
35
|
return Number.isInteger(result) ? result : 0;
|
|
@@ -52,6 +43,7 @@ const INITIAL_STATE = {
|
|
|
52
43
|
scrollOffset: 0,
|
|
53
44
|
swipeOffset: 0,
|
|
54
45
|
touchAction: null,
|
|
46
|
+
exitHandlers: [],
|
|
55
47
|
heightAnimationDiff: null,
|
|
56
48
|
};
|
|
57
49
|
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) => {
|
|
@@ -69,8 +61,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
69
61
|
const swipeContainerRef = useRef(null);
|
|
70
62
|
const visualContainerRef = useRef(null);
|
|
71
63
|
const bottomSheetRef = useMultipleRefs(ref, visualContainerRef);
|
|
72
|
-
const
|
|
73
|
-
const currentVisible = isMobile && visible;
|
|
64
|
+
const scrollContextProviderRef = useRef(null);
|
|
65
|
+
const currentVisible = useBreakpoint().isMobile && visible;
|
|
74
66
|
const onAppearRef = useRef(onAppear);
|
|
75
67
|
onAppearRef.current = onAppear;
|
|
76
68
|
const onCloseRef = useRef(onClose);
|
|
@@ -78,75 +70,72 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
78
70
|
const stateRef = useRef({ ...INITIAL_STATE });
|
|
79
71
|
const [animationTimeout, setAnimationTimeout] = useState(null);
|
|
80
72
|
const [heightAnimationRunning, setHeightAnimationRunning] = useState(false);
|
|
81
|
-
const
|
|
82
|
-
const isSafari = useRef(isSafariFunc()).current;
|
|
83
|
-
const scrollContextProviderRef = useRef(null);
|
|
84
|
-
const exitHandlersRef = useRef([]);
|
|
73
|
+
const bottomSheetContext = useMemo(() => ({ contentOverlayRef }), [contentOverlayRef]);
|
|
85
74
|
const isContentSizedFullHeight = isValidTreeSelectorWrapper(children);
|
|
75
|
+
const deviceFlagsRef = useRef({});
|
|
76
|
+
if (typeof deviceFlagsRef.current.isSafari !== 'boolean' && typeof navigator !== 'undefined') {
|
|
77
|
+
deviceFlagsRef.current.isSafari = isSafari();
|
|
78
|
+
}
|
|
79
|
+
if (typeof deviceFlagsRef.current.useCustomScroll !== 'boolean' && typeof window !== 'undefined') {
|
|
80
|
+
deviceFlagsRef.current.useCustomScroll = 'ontouchstart' in window;
|
|
81
|
+
}
|
|
86
82
|
const LayoutMetrics = useRef((() => {
|
|
87
|
-
let
|
|
88
|
-
let cachedContentHeight = null;
|
|
89
|
-
let cachedFooterHeight = null;
|
|
90
|
-
let cachedHeaderHeight = null;
|
|
91
|
-
let cachedInitialOffset = null;
|
|
92
|
-
let cachedRemainingAvailableHeight = null;
|
|
93
|
-
let cachedScrollContainerHeight = null;
|
|
83
|
+
let cache = {};
|
|
94
84
|
return {
|
|
85
|
+
get availableScrollHeight() {
|
|
86
|
+
if (!('availableScrollHeight' in cache) && scrollContainerRef.current !== null) {
|
|
87
|
+
cache.availableScrollHeight =
|
|
88
|
+
scrollContainerRef.current.clientHeight - LayoutMetrics.current.headerHeight;
|
|
89
|
+
}
|
|
90
|
+
return cache.availableScrollHeight ?? 0;
|
|
91
|
+
},
|
|
95
92
|
get bottomSheetHeight() {
|
|
96
|
-
if (
|
|
97
|
-
|
|
93
|
+
if (!('bottomSheetHeight' in cache) && visualContainerRef.current !== null) {
|
|
94
|
+
cache.bottomSheetHeight = visualContainerRef.current.clientHeight;
|
|
98
95
|
}
|
|
99
|
-
return
|
|
96
|
+
return cache.bottomSheetHeight ?? 0;
|
|
100
97
|
},
|
|
101
98
|
get contentHeight() {
|
|
102
|
-
if (
|
|
103
|
-
|
|
99
|
+
if (!('contentHeight' in cache) && contentRef.current !== null) {
|
|
100
|
+
cache.contentHeight = contentRef.current.clientHeight;
|
|
104
101
|
}
|
|
105
|
-
return
|
|
102
|
+
return cache.contentHeight ?? 0;
|
|
106
103
|
},
|
|
107
104
|
get footerHeight() {
|
|
108
|
-
if (
|
|
109
|
-
|
|
105
|
+
if (!('footerHeight' in cache) && footerRef.current !== null) {
|
|
106
|
+
cache.footerHeight = footerRef.current.clientHeight;
|
|
110
107
|
}
|
|
111
|
-
return
|
|
108
|
+
return cache.footerHeight ?? 0;
|
|
112
109
|
},
|
|
113
110
|
get headerHeight() {
|
|
114
|
-
if (
|
|
115
|
-
|
|
111
|
+
if (!('headerHeight' in cache) && headerRef.current !== null) {
|
|
112
|
+
cache.headerHeight = headerRef.current.layoutHeight;
|
|
116
113
|
}
|
|
117
|
-
return
|
|
114
|
+
return cache.headerHeight ?? 0;
|
|
118
115
|
},
|
|
119
116
|
get initialOffset() {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
if (!('initialOffset' in cache)) {
|
|
118
|
+
if (height === 'half-screen' &&
|
|
119
|
+
deviceFlagsRef.current.useCustomScroll &&
|
|
120
|
+
visualContainerRef.current !== null &&
|
|
121
|
+
visualViewport !== null) {
|
|
122
|
+
const halfScreenOffset = visualContainerRef.current.clientHeight - visualViewport.height / 2;
|
|
123
|
+
cache.initialOffset = Math.max(Math.round(halfScreenOffset), 0);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
cache.initialOffset = 0;
|
|
127
|
+
}
|
|
127
128
|
}
|
|
128
|
-
return
|
|
129
|
+
return cache.initialOffset ?? 0;
|
|
129
130
|
},
|
|
130
131
|
get remainingAvailableHeight() {
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
return cachedRemainingAvailableHeight ?? 0;
|
|
135
|
-
},
|
|
136
|
-
get scrollContainerHeight() {
|
|
137
|
-
if (cachedScrollContainerHeight === null && scrollContainerRef.current !== null) {
|
|
138
|
-
cachedScrollContainerHeight = scrollContainerRef.current.clientHeight;
|
|
132
|
+
if (!('remainingAvailableHeight' in cache) && grabberRef.current !== null) {
|
|
133
|
+
cache.remainingAvailableHeight = grabberRef.current.offsetTop;
|
|
139
134
|
}
|
|
140
|
-
return
|
|
135
|
+
return cache.remainingAvailableHeight ?? 0;
|
|
141
136
|
},
|
|
142
137
|
invalidateCache() {
|
|
143
|
-
|
|
144
|
-
cachedContentHeight = null;
|
|
145
|
-
cachedFooterHeight = null;
|
|
146
|
-
cachedHeaderHeight = null;
|
|
147
|
-
cachedInitialOffset = null;
|
|
148
|
-
cachedRemainingAvailableHeight = null;
|
|
149
|
-
cachedScrollContainerHeight = null;
|
|
138
|
+
cache = {};
|
|
150
139
|
},
|
|
151
140
|
};
|
|
152
141
|
})());
|
|
@@ -177,75 +166,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
177
166
|
document.body.removeChild(animationTimeoutElement);
|
|
178
167
|
setAnimationTimeout({ appear: { enter, exit }, height: { enter: height, exit } });
|
|
179
168
|
}, [setAnimationTimeout]);
|
|
180
|
-
// при изменении высоты контента анимируем ее
|
|
181
|
-
// задаем боттомшиту фиксированную высоту и пересчитываем ее самостоятельно,
|
|
182
|
-
// чтобы не было мерцания, когда новый контент отрисовался до срабатывания колбека ResizeObserver
|
|
183
|
-
const initHeightObserver = useCallback(() => {
|
|
184
|
-
if (!contentRef.current || !footerRef.current || !headerRef.current || !visualContainerRef.current) {
|
|
185
|
-
return void 0;
|
|
186
|
-
}
|
|
187
|
-
const visualContainer = visualContainerRef.current;
|
|
188
|
-
let prevContentHeight = 0;
|
|
189
|
-
let prevHeaderHeight = 0;
|
|
190
|
-
let prevFooterHeight = 0;
|
|
191
|
-
let prevScrollContainerHeight = 0;
|
|
192
|
-
let skipFirst = true;
|
|
193
|
-
const observer = new ResizeObserver(() => {
|
|
194
|
-
LayoutMetrics.current.invalidateCache();
|
|
195
|
-
if (skipFirst) {
|
|
196
|
-
visualContainer.style.height = `${LayoutMetrics.current.bottomSheetHeight}px`;
|
|
197
|
-
prevHeaderHeight = LayoutMetrics.current.headerHeight;
|
|
198
|
-
prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
199
|
-
prevFooterHeight = LayoutMetrics.current.footerHeight;
|
|
200
|
-
prevScrollContainerHeight = LayoutMetrics.current.scrollContainerHeight;
|
|
201
|
-
skipFirst = false;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (stateRef.current.heightAnimationDiff !== null) {
|
|
205
|
-
// если предыдущая анимация не завершилась, без анимации сбрасываем высоту на вычисленную браузером
|
|
206
|
-
visualContainer.style.height = ``;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
const contentHeightDiff = LayoutMetrics.current.contentHeight - prevContentHeight;
|
|
210
|
-
const containersHeightDiff = LayoutMetrics.current.headerHeight -
|
|
211
|
-
prevHeaderHeight +
|
|
212
|
-
LayoutMetrics.current.footerHeight -
|
|
213
|
-
prevFooterHeight;
|
|
214
|
-
// предположим, что scrollContainer останется таким же или станет меньше
|
|
215
|
-
// тогда можем рассчитать минимальную видимую высоту контента как min(scrollContainer.height, contentHeight)
|
|
216
|
-
const prevVisibleContentHeight = Math.min(prevScrollContainerHeight, prevContentHeight);
|
|
217
|
-
const newMinVisibleContentHeight = Math.min(prevScrollContainerHeight - containersHeightDiff, LayoutMetrics.current.contentHeight);
|
|
218
|
-
const minVisibleContentHeightDiff = newMinVisibleContentHeight - prevVisibleContentHeight;
|
|
219
|
-
// предположим, что scrollContainer станет больше
|
|
220
|
-
// тогда контент не может увеличиться больше, чем на расстояние между боттомшитом и верхним краем экрана
|
|
221
|
-
const maxVisibleContentHeightDiff = LayoutMetrics.current.remainingAvailableHeight - containersHeightDiff;
|
|
222
|
-
const visibleContentHeightDiff = Math.min(Math.max(contentHeightDiff, minVisibleContentHeightDiff), maxVisibleContentHeightDiff);
|
|
223
|
-
if (visibleContentHeightDiff !== 0) {
|
|
224
|
-
scrollContextProviderRef.current?.notify({ scrollTop: 0 });
|
|
225
|
-
}
|
|
226
|
-
const heightAnimationDiff = visibleContentHeightDiff + containersHeightDiff;
|
|
227
|
-
if (heightAnimationDiff !== 0) {
|
|
228
|
-
// запоминаем высоту scrollContainer после того, как боттомшиту будет присвоена новая высота.
|
|
229
|
-
// до этого значение некорректно, т.к. новый контент уже был отрендерен,
|
|
230
|
-
// но в инлайн-стилях боттомшита остается старая высота
|
|
231
|
-
stateRef.current.heightAnimationDiff = heightAnimationDiff;
|
|
232
|
-
stateRef.current.heightAnimationCallback = () => {
|
|
233
|
-
prevScrollContainerHeight = LayoutMetrics.current.scrollContainerHeight;
|
|
234
|
-
};
|
|
235
|
-
setHeightAnimationRunning(true);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
prevHeaderHeight = LayoutMetrics.current.headerHeight;
|
|
239
|
-
prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
240
|
-
prevFooterHeight = LayoutMetrics.current.footerHeight;
|
|
241
|
-
});
|
|
242
|
-
observer.observe(headerRef.current);
|
|
243
|
-
observer.observe(contentRef.current);
|
|
244
|
-
observer.observe(footerRef.current);
|
|
245
|
-
return () => observer.disconnect();
|
|
246
|
-
}, [setHeightAnimationRunning]);
|
|
247
169
|
useEffect(() => {
|
|
248
|
-
if (!currentVisible || isSafari
|
|
170
|
+
if (!currentVisible || deviceFlagsRef.current.isSafari) {
|
|
249
171
|
return;
|
|
250
172
|
}
|
|
251
173
|
// используем Virtual Keyboard API через meta-тег вместо navigator.virtualKeyboard,
|
|
@@ -265,7 +187,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
265
187
|
.map((keyValuePair) => keyValuePair.join('='))
|
|
266
188
|
.join(',');
|
|
267
189
|
meta.setAttribute('content', attributesStrUpdated);
|
|
268
|
-
}, [currentVisible,
|
|
190
|
+
}, [currentVisible, keyboardOverlaysContent]);
|
|
269
191
|
const recalcKeyboardOffsets = useCallback(() => {
|
|
270
192
|
if (!headerRef.current || !overlayRef.current || !visualViewport) {
|
|
271
193
|
return;
|
|
@@ -302,8 +224,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
302
224
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${-layoutViewportDiff}px`);
|
|
303
225
|
// при этом может возникнуть проблема, что клавиатура перекрыла хедер
|
|
304
226
|
// проверяем это и компенсируем величину перекрытия при необходимости
|
|
305
|
-
const
|
|
306
|
-
const headerOutOfViewportHeight = Math.round(headerDOMRect.bottom + layoutViewportDiff - DOCUMENT_HEIGHT.current);
|
|
227
|
+
const headerOutOfViewportHeight = Math.round(headerRef.current.bottom + layoutViewportDiff - DOCUMENT_HEIGHT.current);
|
|
307
228
|
if (headerOutOfViewportHeight > 0) {
|
|
308
229
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${headerOutOfViewportHeight - layoutViewportDiff}px`);
|
|
309
230
|
}
|
|
@@ -311,7 +232,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
311
232
|
}
|
|
312
233
|
// keyboardOverlaysContent=false, клавиатура ПОД контентом
|
|
313
234
|
// этот кейс нужно корректировать только в Safari
|
|
314
|
-
if (!keyboardOverlaysContent && isSafari
|
|
235
|
+
if (!keyboardOverlaysContent && deviceFlagsRef.current.isSafari) {
|
|
315
236
|
const visualViewportDiff = Math.round(overlayDOMRect.bottom - visualViewport.height);
|
|
316
237
|
if (visualViewportDiff > 0) {
|
|
317
238
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
@@ -324,7 +245,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
324
245
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
325
246
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
326
247
|
}
|
|
327
|
-
}, [
|
|
248
|
+
}, [keyboardOverlaysContent]);
|
|
328
249
|
const handleFocus = useCallback((event) => {
|
|
329
250
|
const focusedElement = event.target;
|
|
330
251
|
const initialViewportHeight = visualViewport?.height;
|
|
@@ -355,7 +276,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
355
276
|
}
|
|
356
277
|
};
|
|
357
278
|
const handleBlur = () => {
|
|
358
|
-
if (isSafari
|
|
279
|
+
if (deviceFlagsRef.current.isSafari) {
|
|
359
280
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
360
281
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
361
282
|
}
|
|
@@ -370,7 +291,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
370
291
|
stateRef.current.resizeRAFHandle = requestAnimationFrame(waitForResize);
|
|
371
292
|
visualViewport?.addEventListener('resize', handleResize);
|
|
372
293
|
focusedElement.addEventListener('blur', handleBlur);
|
|
373
|
-
}, [
|
|
294
|
+
}, [recalcKeyboardOffsets]);
|
|
374
295
|
// contentOverlay совпадает по границам с контентом боттомшита, но лежит вне боттомшита,
|
|
375
296
|
// чтобы чайлды contentOverlay не обрезались границами боттомшита
|
|
376
297
|
// например, снекбар, лежащий внутри contentOverlay, при смахивании может оказаться в любом месте экрана
|
|
@@ -379,15 +300,18 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
379
300
|
if (contentOverlayRef.current !== null &&
|
|
380
301
|
scrollContainerRef.current !== null &&
|
|
381
302
|
visualContainerRef.current !== null) {
|
|
382
|
-
|
|
383
|
-
contentOverlayRef.current.style.
|
|
303
|
+
const visibleHeaderHeight = headerRef.current?.visibleHeight ?? 0;
|
|
304
|
+
contentOverlayRef.current.style.top = `${scrollContainerRef.current.offsetTop + visualContainerRef.current.offsetTop + visibleHeaderHeight}px`;
|
|
305
|
+
contentOverlayRef.current.style.height = `${LayoutMetrics.current.availableScrollHeight}px`;
|
|
384
306
|
}
|
|
385
307
|
}, []);
|
|
386
308
|
const recalcScrollFlags = useCallback(() => {
|
|
309
|
+
const scrollOffset = deviceFlagsRef.current.useCustomScroll
|
|
310
|
+
? stateRef.current.scrollOffset
|
|
311
|
+
: -(scrollContainerRef.current?.scrollTop ?? 0);
|
|
387
312
|
if (dividerRef.current !== null) {
|
|
388
313
|
const prevDividerVisible = stateRef.current.dividerVisible;
|
|
389
|
-
const isNotScrolledToEnd = LayoutMetrics.current.contentHeight +
|
|
390
|
-
LayoutMetrics.current.scrollContainerHeight;
|
|
314
|
+
const isNotScrolledToEnd = LayoutMetrics.current.contentHeight + scrollOffset > LayoutMetrics.current.availableScrollHeight;
|
|
391
315
|
stateRef.current.dividerVisible =
|
|
392
316
|
showDivider === 'always' || (showDivider === 'with-scroll' && isNotScrolledToEnd);
|
|
393
317
|
if (stateRef.current.dividerVisible !== prevDividerVisible) {
|
|
@@ -397,13 +321,21 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
397
321
|
if (grabberRef.current !== null) {
|
|
398
322
|
const prevGrabberUnsafe = stateRef.current.grabberUnsafe;
|
|
399
323
|
stateRef.current.grabberUnsafe =
|
|
400
|
-
Math.round(Math.max(
|
|
324
|
+
Math.round(Math.max(scrollOffset, 0) + stateRef.current.swipeOffset) ===
|
|
401
325
|
LayoutMetrics.current.remainingAvailableHeight;
|
|
402
326
|
if (stateRef.current.grabberUnsafe !== prevGrabberUnsafe) {
|
|
403
327
|
grabberRef.current.classList.toggle(styles.grabberEnsureSafe, stateRef.current.grabberUnsafe);
|
|
404
328
|
}
|
|
405
329
|
}
|
|
406
330
|
}, [showDivider]);
|
|
331
|
+
const resetScrollPosition = useCallback(() => {
|
|
332
|
+
if (deviceFlagsRef.current.useCustomScroll) {
|
|
333
|
+
scrollContextProviderRef.current?.notify({ scrollTop: 0 });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
scrollContainerRef.current?.scrollTo({ top: 0 });
|
|
337
|
+
}
|
|
338
|
+
}, []);
|
|
407
339
|
// помещает боттомшит в позицию вне экрана снизу, которая может быть начальной либо конечной точкой анимации
|
|
408
340
|
const setTransformToInvisible = useCallback(() => {
|
|
409
341
|
LayoutMetrics.current.invalidateCache();
|
|
@@ -431,8 +363,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
431
363
|
}
|
|
432
364
|
requestAnimationFrame(recalcContentOverlayPosition);
|
|
433
365
|
requestAnimationFrame(recalcScrollFlags);
|
|
434
|
-
|
|
435
|
-
}, [recalcContentOverlayPosition, recalcScrollFlags]);
|
|
366
|
+
resetScrollPosition();
|
|
367
|
+
}, [recalcContentOverlayPosition, recalcScrollFlags, resetScrollPosition]);
|
|
436
368
|
const handleExitAnimationStart = useCallback(() => {
|
|
437
369
|
setTransformToVisible();
|
|
438
370
|
onBeforeExit?.();
|
|
@@ -442,8 +374,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
442
374
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
443
375
|
stateRef.current.resizeRAFHandle !== null && cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
444
376
|
stateRef.current = { ...INITIAL_STATE };
|
|
445
|
-
|
|
446
|
-
|
|
377
|
+
stateRef.current.exitHandlers.forEach((handler) => handler());
|
|
378
|
+
stateRef.current.exitHandlers = [];
|
|
447
379
|
onAfterExit?.();
|
|
448
380
|
}, [onAfterExit]);
|
|
449
381
|
const handleHeightAnimationStart = useCallback(() => {
|
|
@@ -471,7 +403,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
471
403
|
setHeightAnimationRunning(false);
|
|
472
404
|
requestAnimationFrame(recalcContentOverlayPosition);
|
|
473
405
|
requestAnimationFrame(recalcScrollFlags);
|
|
474
|
-
|
|
406
|
+
resetScrollPosition();
|
|
407
|
+
}, [setHeightAnimationRunning, recalcContentOverlayPosition, recalcScrollFlags, resetScrollPosition]);
|
|
475
408
|
const handleSwipeMove = useCallback((event) => {
|
|
476
409
|
if ((stateRef.current.touchAction !== null && stateRef.current.touchAction !== 'swipe') ||
|
|
477
410
|
hasSelectedText()) {
|
|
@@ -523,12 +456,13 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
523
456
|
onSwipeCancel: handleSwipeCancel,
|
|
524
457
|
});
|
|
525
458
|
const initTransformHandlers = useCallback(() => {
|
|
526
|
-
|
|
459
|
+
const visualContainer = visualContainerRef.current;
|
|
460
|
+
if (!visualContainer) {
|
|
527
461
|
return void 0;
|
|
528
462
|
}
|
|
529
463
|
const handleScroll = (delta) => {
|
|
530
464
|
if (LayoutMetrics.current.initialOffset !== 0 ||
|
|
531
|
-
LayoutMetrics.current.contentHeight > LayoutMetrics.current.
|
|
465
|
+
LayoutMetrics.current.contentHeight > LayoutMetrics.current.availableScrollHeight) {
|
|
532
466
|
// храним неокругленное значение для translateY, чтобы анимация была плавнее
|
|
533
467
|
let newScrollOffset = stateRef.current.scrollOffset + delta;
|
|
534
468
|
const roundedNewScrollOffset = Math.round(newScrollOffset);
|
|
@@ -541,10 +475,10 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
541
475
|
}
|
|
542
476
|
}
|
|
543
477
|
else if (LayoutMetrics.current.contentHeight + roundedNewScrollOffset <=
|
|
544
|
-
LayoutMetrics.current.
|
|
478
|
+
LayoutMetrics.current.availableScrollHeight) {
|
|
545
479
|
// скролла нет (touchAction is null)
|
|
546
480
|
// либо контент проскроллен до конца, тогда не даем скроллить дальше
|
|
547
|
-
newScrollOffset = LayoutMetrics.current.
|
|
481
|
+
newScrollOffset = LayoutMetrics.current.availableScrollHeight - LayoutMetrics.current.contentHeight;
|
|
548
482
|
if (stateRef.current.touchAction === 'scroll') {
|
|
549
483
|
stateRef.current.touchAction = 'complete';
|
|
550
484
|
}
|
|
@@ -591,12 +525,6 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
591
525
|
}
|
|
592
526
|
handleScroll(event.delta);
|
|
593
527
|
};
|
|
594
|
-
const onWheelMove = (event) => {
|
|
595
|
-
if (!allowScrollWhileFocused && stateRef.current.hasFocus) {
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
handleScroll(event.delta);
|
|
599
|
-
};
|
|
600
528
|
const onTouchEnd = () => {
|
|
601
529
|
if (stateRef.current.touchAction === 'scroll') {
|
|
602
530
|
stateRef.current.touchAction = null;
|
|
@@ -618,9 +546,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
618
546
|
wrapperRef: visualContainerRef,
|
|
619
547
|
onTouchMove,
|
|
620
548
|
onTouchEnd,
|
|
621
|
-
onWheelMove,
|
|
622
549
|
});
|
|
623
|
-
const visualContainer = visualContainerRef.current;
|
|
624
550
|
visualContainer.addEventListener('touchstart', handleTouchStart);
|
|
625
551
|
visualContainer.addEventListener('touchmove', handleTouchMove);
|
|
626
552
|
visualContainer.addEventListener('touchend', swipeHandlers.onTouchEnd);
|
|
@@ -633,38 +559,133 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
633
559
|
visualContainer.removeEventListener('touchcancel', swipeHandlers.onTouchCancel);
|
|
634
560
|
};
|
|
635
561
|
}, [allowScrollWhileFocused, recalcScrollFlags, swipeHandlers]);
|
|
562
|
+
// при изменении высоты контента анимируем ее
|
|
563
|
+
// задаем боттомшиту фиксированную высоту и пересчитываем ее самостоятельно,
|
|
564
|
+
// чтобы не было мерцания, когда новый контент отрисовался до срабатывания колбека ResizeObserver
|
|
565
|
+
const initHeightObserver = useCallback(() => {
|
|
566
|
+
const visualContainer = visualContainerRef.current;
|
|
567
|
+
if (!visualContainer) {
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
let prevContentHeight = 0;
|
|
571
|
+
let prevHeaderHeight = 0;
|
|
572
|
+
let prevFooterHeight = 0;
|
|
573
|
+
let prevAvailableScrollHeight = 0;
|
|
574
|
+
let skipFirstResizeCallback = true;
|
|
575
|
+
let collapseResizeCallbacks = false;
|
|
576
|
+
const handleHeightChange = () => {
|
|
577
|
+
LayoutMetrics.current.invalidateCache();
|
|
578
|
+
if (skipFirstResizeCallback) {
|
|
579
|
+
visualContainer.style.height = `${LayoutMetrics.current.bottomSheetHeight}px`;
|
|
580
|
+
prevHeaderHeight = LayoutMetrics.current.headerHeight;
|
|
581
|
+
prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
582
|
+
prevFooterHeight = LayoutMetrics.current.footerHeight;
|
|
583
|
+
prevAvailableScrollHeight = LayoutMetrics.current.availableScrollHeight;
|
|
584
|
+
skipFirstResizeCallback = false;
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (stateRef.current.heightAnimationDiff !== null) {
|
|
588
|
+
if (!collapseResizeCallbacks) {
|
|
589
|
+
// если предыдущая анимация не завершилась, без анимации сбрасываем высоту на вычисленную браузером
|
|
590
|
+
visualContainer.style.height = ``;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const contentHeightDiff = LayoutMetrics.current.contentHeight - prevContentHeight;
|
|
595
|
+
const containersHeightDiff = LayoutMetrics.current.headerHeight -
|
|
596
|
+
prevHeaderHeight +
|
|
597
|
+
LayoutMetrics.current.footerHeight -
|
|
598
|
+
prevFooterHeight;
|
|
599
|
+
// предположим, что scrollContainer останется таким же или станет меньше
|
|
600
|
+
// тогда можем рассчитать минимальную видимую высоту контента как min(scrollContainer.height, contentHeight)
|
|
601
|
+
const prevVisibleContentHeight = Math.min(prevAvailableScrollHeight, prevContentHeight);
|
|
602
|
+
const newMinVisibleContentHeight = Math.min(prevAvailableScrollHeight - containersHeightDiff, LayoutMetrics.current.contentHeight);
|
|
603
|
+
const minVisibleContentHeightDiff = newMinVisibleContentHeight - prevVisibleContentHeight;
|
|
604
|
+
// предположим, что scrollContainer станет больше
|
|
605
|
+
// тогда контент не может увеличиться больше, чем на расстояние между боттомшитом и верхним краем экрана
|
|
606
|
+
const maxVisibleContentHeightDiff = LayoutMetrics.current.remainingAvailableHeight - containersHeightDiff;
|
|
607
|
+
const visibleContentHeightDiff = Math.min(Math.max(contentHeightDiff, minVisibleContentHeightDiff), maxVisibleContentHeightDiff);
|
|
608
|
+
if (visibleContentHeightDiff !== 0) {
|
|
609
|
+
resetScrollPosition();
|
|
610
|
+
}
|
|
611
|
+
const heightAnimationDiff = visibleContentHeightDiff + containersHeightDiff;
|
|
612
|
+
if (heightAnimationDiff !== 0) {
|
|
613
|
+
// запоминаем высоту scrollContainer после того, как боттомшиту будет присвоена новая высота.
|
|
614
|
+
// до этого значение некорректно, т.к. новый контент уже был отрендерен,
|
|
615
|
+
// но в инлайн-стилях боттомшита остается старая высота
|
|
616
|
+
stateRef.current.heightAnimationDiff = heightAnimationDiff;
|
|
617
|
+
stateRef.current.heightAnimationCallback = () => {
|
|
618
|
+
prevAvailableScrollHeight = LayoutMetrics.current.availableScrollHeight;
|
|
619
|
+
};
|
|
620
|
+
setHeightAnimationRunning(true);
|
|
621
|
+
collapseResizeCallbacks = true;
|
|
622
|
+
requestAnimationFrame(() => {
|
|
623
|
+
collapseResizeCallbacks = false;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
recalcContentOverlayPosition();
|
|
628
|
+
recalcScrollFlags();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
prevHeaderHeight = LayoutMetrics.current.headerHeight;
|
|
632
|
+
prevContentHeight = LayoutMetrics.current.contentHeight;
|
|
633
|
+
prevFooterHeight = LayoutMetrics.current.footerHeight;
|
|
634
|
+
};
|
|
635
|
+
const resizeObserver = new ResizeObserver(handleHeightChange);
|
|
636
|
+
const content = contentRef.current;
|
|
637
|
+
const footer = footerRef.current;
|
|
638
|
+
const header = headerRef.current;
|
|
639
|
+
content !== null && resizeObserver.observe(content);
|
|
640
|
+
footer !== null && resizeObserver.observe(footer);
|
|
641
|
+
header !== null && header.addHeightObserver(handleHeightChange);
|
|
642
|
+
return () => {
|
|
643
|
+
resizeObserver.disconnect();
|
|
644
|
+
header !== null && header.removeHeightObserver(handleHeightChange);
|
|
645
|
+
};
|
|
646
|
+
}, [setHeightAnimationRunning, recalcScrollFlags, recalcContentOverlayPosition, resetScrollPosition]);
|
|
636
647
|
const handleAppearAnimationEnd = useCallback(() => {
|
|
637
|
-
|
|
648
|
+
const removeHeightObserver = initHeightObserver();
|
|
649
|
+
removeHeightObserver && stateRef.current.exitHandlers.push(removeHeightObserver);
|
|
650
|
+
const removeTransformHandlers = deviceFlagsRef.current.useCustomScroll && initTransformHandlers();
|
|
651
|
+
removeTransformHandlers && stateRef.current.exitHandlers.push(removeTransformHandlers);
|
|
638
652
|
onAppearRef.current?.();
|
|
639
653
|
}, [initHeightObserver, initTransformHandlers]);
|
|
640
654
|
const { onTouchEnd, ...eventHandlers } = useNoBubbling();
|
|
641
655
|
if (!animationTimeout) {
|
|
642
656
|
return null;
|
|
643
657
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
658
|
+
const renderFunc = (appearTransition, heightTransition) => {
|
|
659
|
+
const navigationBar = header && isNavigationBarComponent(header) ? (jsx(NavigationBarComponent, { ...header.props, forwardedRef: headerRef })) : null;
|
|
660
|
+
const content = (jsx("div", { className: classnames(styles.content, {
|
|
661
|
+
[styles.contentFullScreen]: height === 'full-screen',
|
|
662
|
+
[styles.contentWithPaddings]: withContentPaddings,
|
|
663
|
+
[styles.contentWithoutHeader]: !header,
|
|
664
|
+
[styles.contentSizedFullScreen]: isContentSizedFullHeight,
|
|
665
|
+
}), ref: contentRef, children: jsx(BottomSheetContext.Provider, { value: bottomSheetContext, children: children }) }));
|
|
666
|
+
const scrollContainer = deviceFlagsRef.current.useCustomScroll ? (jsx(CustomScrollContextProvider, { ref: scrollContextProviderRef, children: jsxs("div", { className: styles.scrollContainer, onFocus: handleFocus, ref: scrollContainerRef, children: [navigationBar, content] }) })) : (jsxs("div", { className: classnames(styles.scrollContainer, styles.nativeScrollContainer), onFocus: handleFocus, onScroll: recalcScrollFlags, ref: scrollContainerRef, children: [navigationBar, content] }));
|
|
667
|
+
const clonedFooter = footer && isActionBarComponent(footer)
|
|
668
|
+
? cloneElement(footer, { type: footer.props.type || 'mobile', showDivider: false })
|
|
669
|
+
: footer;
|
|
670
|
+
return (jsx(Layer, { layer: InternalLayerName.BottomSheet, children: jsxs("div", { ...eventHandlers, className: styles.overlay, children: [jsx("div", { className: classnames(styles.overlayBackground, {
|
|
671
|
+
[styles.overlayBackgroundVisible]: showOverlay,
|
|
672
|
+
[styles.appearAnimation]: appearTransition === 'entering',
|
|
673
|
+
[styles.disappearAnimation]: appearTransition === 'exiting',
|
|
674
|
+
}), ...(appearTransition === 'entered'
|
|
675
|
+
? { 'data-qa': 'bottom-sheet-overlay', onClick: onCloseRef.current }
|
|
676
|
+
: {}), ref: overlayRef }), jsxs("div", { className: classnames(styles.swipeContainer, {
|
|
677
|
+
[styles.appearAnimation]: appearTransition === 'entering',
|
|
678
|
+
[styles.closeBySwipeAnimation]: appearTransition === 'exiting' && stateRef.current.touchAction === 'swipe',
|
|
679
|
+
[styles.disappearAnimation]: appearTransition === 'exiting',
|
|
680
|
+
}), "data-qa": appearTransition === 'entered' ? 'bottom-sheet-container' : undefined, ref: swipeContainerRef, children: [jsx("div", { className: classnames(styles.grabber, styles.grabberTransitionAnimation, {
|
|
681
|
+
[styles.grabberEnsureSafe]: stateRef.current.grabberUnsafe,
|
|
682
|
+
}), ref: grabberRef }), jsxs("div", { className: classnames(styles.visualContainer, {
|
|
683
|
+
[styles.visualContainerFullScreen]: height === 'full-screen',
|
|
684
|
+
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
685
|
+
}), "data-qa": appearTransition === 'entered' ? 'bottom-sheet-content' : undefined, ref: bottomSheetRef, children: [scrollContainer, jsxs("div", { className: styles.footer, ref: footerRef, children: [footer && (jsx("div", { className: classnames(styles.divider, {
|
|
666
686
|
[styles.dividerVisible]: stateRef.current.dividerVisible,
|
|
667
|
-
}), ref: dividerRef, children: jsx(Divider, {}) })), interceptClickHandlers ? (jsx(ClickInterceptor, { children: clonedFooter })) : (clonedFooter)] })] })
|
|
687
|
+
}), ref: dividerRef, children: jsx(Divider, {}) })), interceptClickHandlers ? (jsx(ClickInterceptor, { children: clonedFooter })) : (clonedFooter)] })] }), jsx("div", { className: styles.contentOverlay, ref: contentOverlayRef })] })] }) }));
|
|
688
|
+
};
|
|
668
689
|
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: handleAppearAnimationEnd, onExit: handleExitAnimationStart, onExiting: setTransformToInvisible, onExited: handleExitAnimationEnd, timeout: animationTimeout.appear, unmountOnExit: true, nodeRef: contentRef, children: (appearTransition) => (jsx(Transition, { in: heightAnimationRunning, onEnter: handleHeightAnimationStart, onEntered: handleHeightAnimationEnd, timeout: animationTimeout.height, nodeRef: contentRef, children: (heightTransition) => renderFunc(appearTransition, heightTransition) })) }) }), document.body);
|
|
669
690
|
};
|
|
670
691
|
const BottomSheet = forwardRef(BottomSheetRenderFunc);
|