@hh.ru/magritte-ui-bottom-sheet 8.2.9 → 8.2.11
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 +146 -141
- package/BottomSheet.js.map +1 -1
- package/BottomSheetFooter.js +1 -1
- package/BottomSheetIFrameAdapter.js +1 -1
- package/bottom-sheet-Blf76yQK.js +5 -0
- package/bottom-sheet-Blf76yQK.js.map +1 -0
- package/index.css +43 -37
- package/index.js +1 -2
- package/index.js.map +1 -1
- package/package.json +6 -7
- package/bottom-sheet-C6FIprRU.js +0 -5
- package/bottom-sheet-C6FIprRU.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 { isNavigationBarComponent, NavigationBarComponent } from '@hh.ru/magritte-ui-navigation-bar';
|
|
20
19
|
import { TextAreaGrowLimiter } from '@hh.ru/magritte-ui-textarea';
|
|
21
20
|
import { ThemeWrapper } from '@hh.ru/magritte-ui-theme-wrapper';
|
|
22
21
|
import { isValidTreeSelectorWrapper } from '@hh.ru/magritte-ui-tree-selector';
|
|
23
|
-
import { s as styles } from './bottom-sheet-
|
|
22
|
+
import { s as styles } from './bottom-sheet-Blf76yQK.js';
|
|
24
23
|
|
|
25
24
|
const CSS_VAR_ENTER_ANIMATION_DURATION = '--enter-animation-duration';
|
|
26
25
|
const CSS_VAR_EXIT_ANIMATION_DURATION = '--exit-animation-duration';
|
|
@@ -69,13 +68,13 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
69
68
|
const SWIPE_THRESHOLD = useRef({ max: Infinity });
|
|
70
69
|
const VIEWPORT_HEIGHT = useRef(0);
|
|
71
70
|
const contentRef = useRef(null);
|
|
71
|
+
const headerWrapperRef = useRef(null);
|
|
72
72
|
const contentOverlayRef = useRef(null);
|
|
73
73
|
const cssVariablesContainerRef = useRef(null);
|
|
74
74
|
const dividerRef = useRef(null);
|
|
75
75
|
const focusedElementRef = useRef(null);
|
|
76
76
|
const footerRef = useRef(null);
|
|
77
77
|
const grabberRef = useRef(null);
|
|
78
|
-
const headerRef = useRef(null);
|
|
79
78
|
const overlayRef = useRef(null);
|
|
80
79
|
const scrollContainerRef = useRef(null);
|
|
81
80
|
const swipeContainerRef = useRef(null);
|
|
@@ -84,7 +83,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
84
83
|
const scrollContextProviderRef = useRef(null);
|
|
85
84
|
const GrowLimitWrapper = isContentSizedFullHeight(children) ? Fragment : TextAreaGrowLimiter;
|
|
86
85
|
const growLimiterRef = useRef(null);
|
|
87
|
-
const
|
|
86
|
+
const growLimiterContextValue = useState(() => {
|
|
88
87
|
let height = 'auto';
|
|
89
88
|
let limiterFlex = '1 0';
|
|
90
89
|
return {
|
|
@@ -102,7 +101,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
102
101
|
growLimiterRef.current && (growLimiterRef.current.style.flex = limiterFlex);
|
|
103
102
|
},
|
|
104
103
|
};
|
|
105
|
-
});
|
|
104
|
+
})[0];
|
|
106
105
|
const growLimiterProps = useMemo(() => GrowLimitWrapper === TextAreaGrowLimiter
|
|
107
106
|
? {
|
|
108
107
|
...growLimiterContextValue,
|
|
@@ -158,9 +157,13 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
158
157
|
}, [setHasTouchSupport]);
|
|
159
158
|
const bindEscapeToClose = useEscapeToClose(() => currentVisible && onCloseRef.current(null));
|
|
160
159
|
useEffect(() => bindEscapeToClose(document.body), [bindEscapeToClose]);
|
|
161
|
-
|
|
160
|
+
// TODO: replace by useInitOnce
|
|
161
|
+
const LayoutMetrics = useState(() => {
|
|
162
162
|
let cache = {};
|
|
163
163
|
return {
|
|
164
|
+
get scrollTop() {
|
|
165
|
+
return Math.max(-stateRef.current.scrollOffset, 0);
|
|
166
|
+
},
|
|
164
167
|
get bottomSheetHeight() {
|
|
165
168
|
if (!('bottomSheetHeight' in cache) && visualContainerRef.current !== null) {
|
|
166
169
|
cache.bottomSheetHeight = visualContainerRef.current.clientHeight;
|
|
@@ -182,8 +185,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
182
185
|
*/
|
|
183
186
|
get visibleContentHeight() {
|
|
184
187
|
if (!('visibleContentHeight' in cache) && scrollContainerRef.current !== null) {
|
|
185
|
-
|
|
186
|
-
cache.visibleContentHeight = scrollContainerRef.current.clientHeight - layoutHeaderHeight;
|
|
188
|
+
cache.visibleContentHeight = scrollContainerRef.current.clientHeight - LayoutMetrics.headerHeight;
|
|
187
189
|
}
|
|
188
190
|
return cache.visibleContentHeight ?? 0;
|
|
189
191
|
},
|
|
@@ -191,9 +193,9 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
191
193
|
* Положение верхнего края видимой части контента относительно вьюпорта
|
|
192
194
|
*/
|
|
193
195
|
get contentTop() {
|
|
194
|
-
if (!('contentTop' in cache) && visualContainerRef.current !== null) {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
if (!('contentTop' in cache) && visualContainerRef.current !== null && contentRef.current !== null) {
|
|
197
|
+
cache.contentTop =
|
|
198
|
+
contentRef.current.offsetTop - LayoutMetrics.scrollTop + visualContainerRef.current.offsetTop;
|
|
197
199
|
}
|
|
198
200
|
return cache.contentTop ?? 0;
|
|
199
201
|
},
|
|
@@ -208,8 +210,8 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
208
210
|
* Может отличаться от видимой высоты NavigationBar
|
|
209
211
|
*/
|
|
210
212
|
get headerHeight() {
|
|
211
|
-
if (!('headerHeight' in cache) &&
|
|
212
|
-
cache.headerHeight =
|
|
213
|
+
if (!('headerHeight' in cache) && headerWrapperRef.current !== null) {
|
|
214
|
+
cache.headerHeight = headerWrapperRef.current.offsetHeight;
|
|
213
215
|
}
|
|
214
216
|
return cache.headerHeight ?? 0;
|
|
215
217
|
},
|
|
@@ -229,7 +231,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
229
231
|
return cache.initialOffset ?? 0;
|
|
230
232
|
},
|
|
231
233
|
get maxScrollTop() {
|
|
232
|
-
return LayoutMetrics.
|
|
234
|
+
return LayoutMetrics.contentHeight - LayoutMetrics.visibleContentHeight;
|
|
233
235
|
},
|
|
234
236
|
/**
|
|
235
237
|
* Расстояние между верхним краем боттомшита и границей вьюпорта
|
|
@@ -244,7 +246,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
244
246
|
cache = {};
|
|
245
247
|
},
|
|
246
248
|
};
|
|
247
|
-
})
|
|
249
|
+
})[0];
|
|
248
250
|
const setCSSVariable = (name, value) => cssVariablesContainerRef.current !== null && cssVariablesContainerRef.current.style.setProperty(name, value);
|
|
249
251
|
useEffect(() => {
|
|
250
252
|
if (!currentVisible) {
|
|
@@ -301,43 +303,41 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
301
303
|
}, [currentVisible, isSafari]);
|
|
302
304
|
const fixOverscroll = useCallback(() => {
|
|
303
305
|
if (hasTouchSupport) {
|
|
304
|
-
const isOverscrolled = LayoutMetrics.
|
|
305
|
-
LayoutMetrics.current.visibleContentHeight;
|
|
306
|
+
const isOverscrolled = LayoutMetrics.contentHeight + stateRef.current.scrollOffset < LayoutMetrics.visibleContentHeight;
|
|
306
307
|
if (isOverscrolled) {
|
|
307
|
-
stateRef.current.scrollOffset =
|
|
308
|
-
LayoutMetrics.current.visibleContentHeight - LayoutMetrics.current.contentHeight;
|
|
308
|
+
stateRef.current.scrollOffset = LayoutMetrics.visibleContentHeight - LayoutMetrics.contentHeight;
|
|
309
309
|
if (contentRef.current !== null) {
|
|
310
310
|
contentRef.current.style.transform = translateY(stateRef.current.scrollOffset);
|
|
311
311
|
}
|
|
312
|
-
scrollContextProviderRef.current?.
|
|
312
|
+
scrollContextProviderRef.current?.notifyScroll({
|
|
313
313
|
scrollTop: -stateRef.current.scrollOffset,
|
|
314
|
-
maxScrollTop: LayoutMetrics.
|
|
314
|
+
maxScrollTop: LayoutMetrics.maxScrollTop,
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
-
}, [hasTouchSupport]);
|
|
318
|
+
}, [hasTouchSupport, LayoutMetrics]);
|
|
319
319
|
const resetScrollPosition = useCallback(() => {
|
|
320
320
|
if (hasTouchSupport) {
|
|
321
|
-
stateRef.current.scrollOffset = LayoutMetrics.
|
|
321
|
+
stateRef.current.scrollOffset = LayoutMetrics.initialOffset;
|
|
322
322
|
stateRef.current.swipeOffset = 0;
|
|
323
323
|
if (contentRef.current !== null) {
|
|
324
324
|
contentRef.current.style.transform = translateY(0);
|
|
325
325
|
}
|
|
326
326
|
if (swipeContainerRef.current !== null) {
|
|
327
|
-
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.
|
|
327
|
+
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.initialOffset);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
else {
|
|
331
331
|
scrollContainerRef.current?.scrollTo({ top: 0 });
|
|
332
332
|
}
|
|
333
|
-
}, [hasTouchSupport]);
|
|
333
|
+
}, [hasTouchSupport, LayoutMetrics]);
|
|
334
334
|
const recalcScrollFlags = useCallback(() => {
|
|
335
335
|
const scrollOffset = hasTouchSupport
|
|
336
336
|
? stateRef.current.scrollOffset
|
|
337
337
|
: -(scrollContainerRef.current?.scrollTop ?? 0);
|
|
338
338
|
if (dividerRef.current !== null) {
|
|
339
339
|
const prevDividerVisible = stateRef.current.dividerVisible;
|
|
340
|
-
const isNotScrolledToEnd = LayoutMetrics.
|
|
340
|
+
const isNotScrolledToEnd = LayoutMetrics.contentHeight + scrollOffset > LayoutMetrics.visibleContentHeight;
|
|
341
341
|
stateRef.current.dividerVisible =
|
|
342
342
|
showDivider === 'always' || (showDivider === 'with-scroll' && isNotScrolledToEnd);
|
|
343
343
|
if (stateRef.current.dividerVisible !== prevDividerVisible) {
|
|
@@ -348,12 +348,12 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
348
348
|
const prevGrabberUnsafe = stateRef.current.grabberUnsafe;
|
|
349
349
|
stateRef.current.grabberUnsafe =
|
|
350
350
|
Math.round(Math.max(scrollOffset, 0) + stateRef.current.swipeOffset) ===
|
|
351
|
-
LayoutMetrics.
|
|
351
|
+
LayoutMetrics.remainingAvailableHeight;
|
|
352
352
|
if (stateRef.current.grabberUnsafe !== prevGrabberUnsafe) {
|
|
353
353
|
grabberRef.current.classList.toggle(styles.grabberEnsureSafe, stateRef.current.grabberUnsafe);
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
|
-
}, [hasTouchSupport, showDivider]);
|
|
356
|
+
}, [hasTouchSupport, showDivider, LayoutMetrics]);
|
|
357
357
|
// терминология: https://developer.chrome.com/blog/viewport-resize-behavior/
|
|
358
358
|
//
|
|
359
359
|
// при открытии виртуальной клавиатуры браузеры двигают Visual Viewport и Layout Viewport
|
|
@@ -367,7 +367,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
367
367
|
return;
|
|
368
368
|
}
|
|
369
369
|
// разворачиваем боттомшит с height=half-screen на всю доступную высоту
|
|
370
|
-
if (LayoutMetrics.
|
|
370
|
+
if (LayoutMetrics.initialOffset > 0 && stateRef.current.scrollOffset > 0) {
|
|
371
371
|
stateRef.current.scrollOffset = 0;
|
|
372
372
|
contentRef.current.style.transform = translateY(0);
|
|
373
373
|
swipeContainerRef.current.style.transform = translateY(0);
|
|
@@ -375,7 +375,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
375
375
|
footerRef.current.style.transform = translateY(0);
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
|
-
LayoutMetrics.
|
|
378
|
+
LayoutMetrics.invalidateCache();
|
|
379
379
|
const isKeyboardVisible = visualViewport.height !== VIEWPORT_HEIGHT.current;
|
|
380
380
|
if (stateRef.current.hasFocus && focusedElementRef.current !== null && isKeyboardVisible) {
|
|
381
381
|
const overlayDOMRect = overlayRef.current.getBoundingClientRect();
|
|
@@ -399,20 +399,20 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
399
399
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${topOffset}px`);
|
|
400
400
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${bottomOffset}px`);
|
|
401
401
|
}
|
|
402
|
-
LayoutMetrics.
|
|
402
|
+
LayoutMetrics.invalidateCache();
|
|
403
403
|
// если фокусируемый элемент лежит внутри скроллящегося контейнера и не виден, скроллим к нему
|
|
404
404
|
if (contentRef.current.contains(focusedElementRef.current)) {
|
|
405
405
|
let focusedElementOffset = 0;
|
|
406
406
|
for (let offsetElement = focusedElementRef.current.closest('[data-interactive]') ?? focusedElementRef.current; offsetElement !== contentRef.current && offsetElement !== null; offsetElement = offsetElement.offsetParent) {
|
|
407
407
|
focusedElementOffset += offsetElement.offsetTop;
|
|
408
408
|
}
|
|
409
|
-
const newScrollOffset = -Math.min(Math.max(focusedElementOffset - 10, 0), LayoutMetrics.
|
|
409
|
+
const newScrollOffset = -Math.min(Math.max(focusedElementOffset - 10, 0), LayoutMetrics.maxScrollTop);
|
|
410
410
|
if (stateRef.current.scrollOffset !== newScrollOffset) {
|
|
411
411
|
stateRef.current.scrollOffset = newScrollOffset;
|
|
412
412
|
contentRef.current.style.transform = translateY(stateRef.current.scrollOffset);
|
|
413
|
-
scrollContextProviderRef.current?.
|
|
413
|
+
scrollContextProviderRef.current?.notifyScroll({
|
|
414
414
|
scrollTop: -stateRef.current.scrollOffset,
|
|
415
|
-
maxScrollTop: LayoutMetrics.
|
|
415
|
+
maxScrollTop: LayoutMetrics.maxScrollTop,
|
|
416
416
|
});
|
|
417
417
|
}
|
|
418
418
|
}
|
|
@@ -423,7 +423,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
423
423
|
else {
|
|
424
424
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
425
425
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
426
|
-
LayoutMetrics.
|
|
426
|
+
LayoutMetrics.invalidateCache();
|
|
427
427
|
fixOverscroll();
|
|
428
428
|
recalcScrollFlags();
|
|
429
429
|
virtualKeyboardOffsetsRef.current.top = 0;
|
|
@@ -432,7 +432,15 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
432
432
|
stateRef.current.skipHeightAnimation = true;
|
|
433
433
|
setTimeout(() => (stateRef.current.skipHeightAnimation = false), 100);
|
|
434
434
|
bottomSheetContext.keyboardResizeHandlers.forEach((handler) => handler());
|
|
435
|
-
}, [
|
|
435
|
+
}, [
|
|
436
|
+
fixOverscroll,
|
|
437
|
+
hasTouchSupport,
|
|
438
|
+
isSafari,
|
|
439
|
+
keyboardOverlaysFooter,
|
|
440
|
+
recalcScrollFlags,
|
|
441
|
+
bottomSheetContext,
|
|
442
|
+
LayoutMetrics,
|
|
443
|
+
]);
|
|
436
444
|
const handleFocus = useCallback((event) => {
|
|
437
445
|
const focusedElement = event.target;
|
|
438
446
|
const initialViewportHeight = visualViewport?.height;
|
|
@@ -496,38 +504,38 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
496
504
|
if (contentOverlayRef.current !== null &&
|
|
497
505
|
scrollContainerRef.current !== null &&
|
|
498
506
|
visualContainerRef.current !== null) {
|
|
499
|
-
contentOverlayRef.current.style.top = `${LayoutMetrics.
|
|
500
|
-
contentOverlayRef.current.style.height = `${LayoutMetrics.
|
|
507
|
+
contentOverlayRef.current.style.top = `${LayoutMetrics.contentTop}px`;
|
|
508
|
+
contentOverlayRef.current.style.height = `${LayoutMetrics.visibleContentHeight}px`;
|
|
501
509
|
}
|
|
502
|
-
}, []);
|
|
510
|
+
}, [LayoutMetrics]);
|
|
503
511
|
// помещает боттомшит в позицию вне экрана снизу, которая может быть начальной либо конечной точкой анимации
|
|
504
512
|
const setTransformToInvisible = useCallback(() => {
|
|
505
|
-
LayoutMetrics.
|
|
513
|
+
LayoutMetrics.invalidateCache();
|
|
506
514
|
if (overlayRef.current !== null) {
|
|
507
515
|
overlayRef.current.style.opacity = `0`;
|
|
508
516
|
}
|
|
509
517
|
if (swipeContainerRef.current !== null) {
|
|
510
|
-
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.
|
|
518
|
+
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.bottomSheetHeight);
|
|
511
519
|
}
|
|
512
520
|
requestAnimationFrame(recalcContentOverlayPosition);
|
|
513
521
|
requestAnimationFrame(recalcScrollFlags);
|
|
514
|
-
}, [recalcContentOverlayPosition, recalcScrollFlags]);
|
|
522
|
+
}, [recalcContentOverlayPosition, recalcScrollFlags, LayoutMetrics]);
|
|
515
523
|
// помещает боттомшит в дефолтную позицию на экране, которая может быть начальной либо конечной точкой анимации
|
|
516
524
|
const setTransformToVisible = useCallback(() => {
|
|
517
|
-
LayoutMetrics.
|
|
518
|
-
stateRef.current.scrollOffset = LayoutMetrics.
|
|
525
|
+
LayoutMetrics.invalidateCache();
|
|
526
|
+
stateRef.current.scrollOffset = LayoutMetrics.initialOffset;
|
|
519
527
|
if (overlayRef.current !== null) {
|
|
520
528
|
overlayRef.current.style.opacity = `1`;
|
|
521
529
|
}
|
|
522
530
|
if (swipeContainerRef.current !== null) {
|
|
523
|
-
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.
|
|
531
|
+
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.initialOffset + stateRef.current.swipeOffset);
|
|
524
532
|
}
|
|
525
533
|
if (footerRef.current !== null) {
|
|
526
|
-
footerRef.current.style.transform = translateY(-LayoutMetrics.
|
|
534
|
+
footerRef.current.style.transform = translateY(-LayoutMetrics.initialOffset);
|
|
527
535
|
}
|
|
528
536
|
requestAnimationFrame(recalcContentOverlayPosition);
|
|
529
537
|
requestAnimationFrame(recalcScrollFlags);
|
|
530
|
-
}, [recalcContentOverlayPosition, recalcScrollFlags]);
|
|
538
|
+
}, [recalcContentOverlayPosition, recalcScrollFlags, LayoutMetrics]);
|
|
531
539
|
const handleExitAnimationStart = useCallback(() => {
|
|
532
540
|
setTransformToVisible();
|
|
533
541
|
onBeforeExit?.();
|
|
@@ -542,15 +550,15 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
542
550
|
onAfterExit?.();
|
|
543
551
|
}, [onAfterExit]);
|
|
544
552
|
const handleHeightAnimationStart = useCallback(() => {
|
|
545
|
-
LayoutMetrics.
|
|
553
|
+
LayoutMetrics.invalidateCache();
|
|
546
554
|
if (stateRef.current.heightAnimationDiff !== null && visualContainerRef.current !== null) {
|
|
547
|
-
visualContainerRef.current.style.height = `${LayoutMetrics.
|
|
555
|
+
visualContainerRef.current.style.height = `${LayoutMetrics.bottomSheetHeight + stateRef.current.heightAnimationDiff}px`;
|
|
548
556
|
}
|
|
549
|
-
}, []);
|
|
557
|
+
}, [LayoutMetrics]);
|
|
550
558
|
const handleHeightAnimationEnd = useCallback(() => {
|
|
551
|
-
LayoutMetrics.
|
|
559
|
+
LayoutMetrics.invalidateCache();
|
|
552
560
|
if (visualContainerRef.current !== null) {
|
|
553
|
-
visualContainerRef.current.style.height = `${LayoutMetrics.
|
|
561
|
+
visualContainerRef.current.style.height = `${LayoutMetrics.bottomSheetHeight}px`;
|
|
554
562
|
}
|
|
555
563
|
stateRef.current.heightAnimationDiff = null;
|
|
556
564
|
stateRef.current.heightAnimationCallback && requestAnimationFrame(stateRef.current.heightAnimationCallback);
|
|
@@ -558,11 +566,11 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
558
566
|
setHeightAnimationRunning(false);
|
|
559
567
|
requestAnimationFrame(recalcContentOverlayPosition);
|
|
560
568
|
requestAnimationFrame(recalcScrollFlags);
|
|
561
|
-
scrollContextProviderRef.current?.
|
|
569
|
+
scrollContextProviderRef.current?.notifyScroll({
|
|
562
570
|
scrollTop: 0,
|
|
563
|
-
maxScrollTop: LayoutMetrics.
|
|
571
|
+
maxScrollTop: LayoutMetrics.maxScrollTop,
|
|
564
572
|
});
|
|
565
|
-
}, [setHeightAnimationRunning, recalcContentOverlayPosition, recalcScrollFlags]);
|
|
573
|
+
}, [setHeightAnimationRunning, recalcContentOverlayPosition, recalcScrollFlags, LayoutMetrics]);
|
|
566
574
|
const handleSwipeMove = useCallback((event) => {
|
|
567
575
|
if ((stateRef.current.touchAction !== null && stateRef.current.touchAction !== 'swipe') ||
|
|
568
576
|
hasSelectedText()) {
|
|
@@ -620,69 +628,72 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
620
628
|
onSwipeEnd: handleSwipeEnd,
|
|
621
629
|
onSwipeCancel: handleSwipeCancel,
|
|
622
630
|
});
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (roundedNewScrollOffset >= LayoutMetrics.current.initialOffset) {
|
|
635
|
-
// скролла нет (touchAction is null)
|
|
636
|
-
// либо контент проскроллен в начало, тогда не даем скроллить дальше
|
|
637
|
-
newScrollOffset = LayoutMetrics.current.initialOffset;
|
|
638
|
-
if (stateRef.current.touchAction === 'scroll') {
|
|
639
|
-
stateRef.current.touchAction = 'complete';
|
|
640
|
-
}
|
|
631
|
+
const handleScroll = useCallback((delta) => {
|
|
632
|
+
if (LayoutMetrics.initialOffset !== 0 || LayoutMetrics.contentHeight > LayoutMetrics.visibleContentHeight) {
|
|
633
|
+
// храним неокругленное значение для translateY, чтобы анимация была плавнее
|
|
634
|
+
let newScrollOffset = stateRef.current.scrollOffset + delta;
|
|
635
|
+
const roundedNewScrollOffset = Math.round(newScrollOffset);
|
|
636
|
+
if (roundedNewScrollOffset >= LayoutMetrics.initialOffset) {
|
|
637
|
+
// скролла нет (touchAction is null)
|
|
638
|
+
// либо контент проскроллен в начало, тогда не даем скроллить дальше
|
|
639
|
+
newScrollOffset = LayoutMetrics.initialOffset;
|
|
640
|
+
if (stateRef.current.touchAction === 'scroll') {
|
|
641
|
+
stateRef.current.touchAction = 'complete';
|
|
641
642
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
// скролл в процессе
|
|
653
|
-
stateRef.current.touchAction = 'scroll';
|
|
643
|
+
}
|
|
644
|
+
else if (LayoutMetrics.contentHeight + roundedNewScrollOffset <= LayoutMetrics.visibleContentHeight) {
|
|
645
|
+
// скролла нет (touchAction is null)
|
|
646
|
+
// либо контент проскроллен до конца, тогда не даем скроллить дальше
|
|
647
|
+
newScrollOffset = LayoutMetrics.visibleContentHeight - LayoutMetrics.contentHeight;
|
|
648
|
+
if (stateRef.current.touchAction === 'scroll') {
|
|
649
|
+
stateRef.current.touchAction = 'complete';
|
|
654
650
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
// скролл в процессе
|
|
654
|
+
stateRef.current.touchAction = 'scroll';
|
|
655
|
+
}
|
|
656
|
+
if (stateRef.current.scrollOffset !== newScrollOffset) {
|
|
657
|
+
const offsetWasPositive = stateRef.current.scrollOffset > 0;
|
|
658
|
+
stateRef.current.scrollOffset = newScrollOffset;
|
|
659
|
+
if (contentRef.current !== null && swipeContainerRef.current !== null) {
|
|
660
|
+
if (newScrollOffset > 0) {
|
|
661
|
+
if (!offsetWasPositive) {
|
|
662
|
+
contentRef.current.style.transform = translateY(0);
|
|
667
663
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
664
|
+
swipeContainerRef.current.style.transform = translateY(newScrollOffset);
|
|
665
|
+
if (footerRef.current !== null) {
|
|
666
|
+
footerRef.current.style.transform = translateY(-newScrollOffset);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
contentRef.current.style.transform = translateY(newScrollOffset);
|
|
671
|
+
if (offsetWasPositive) {
|
|
672
|
+
swipeContainerRef.current.style.transform = translateY(0);
|
|
673
|
+
if (footerRef.current !== null) {
|
|
674
|
+
footerRef.current.style.transform = translateY(0);
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
677
|
}
|
|
678
|
-
recalcScrollFlags();
|
|
679
|
-
scrollContextProviderRef.current?.notify({
|
|
680
|
-
scrollTop: Math.max(-stateRef.current.scrollOffset, 0),
|
|
681
|
-
maxScrollTop: LayoutMetrics.current.maxScrollTop,
|
|
682
|
-
});
|
|
683
678
|
}
|
|
679
|
+
recalcScrollFlags();
|
|
680
|
+
scrollContextProviderRef.current?.notifyScroll({
|
|
681
|
+
scrollTop: Math.max(-stateRef.current.scrollOffset, 0),
|
|
682
|
+
maxScrollTop: LayoutMetrics.maxScrollTop,
|
|
683
|
+
});
|
|
684
684
|
}
|
|
685
|
-
}
|
|
685
|
+
}
|
|
686
|
+
}, [LayoutMetrics, recalcScrollFlags]);
|
|
687
|
+
const scrollContextProps = useState(() => ({
|
|
688
|
+
getMaxScrollTop: () => LayoutMetrics.maxScrollTop,
|
|
689
|
+
getScrollTop: () => LayoutMetrics.scrollTop,
|
|
690
|
+
setScrollTop: (pos) => handleScroll(-(pos - LayoutMetrics.scrollTop)),
|
|
691
|
+
}))[0];
|
|
692
|
+
const initTransformHandlers = useCallback(() => {
|
|
693
|
+
const visualContainer = visualContainerRef.current;
|
|
694
|
+
if (!visualContainer) {
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
686
697
|
let focusedElementTouchY = null;
|
|
687
698
|
const onTouchMove = (event) => {
|
|
688
699
|
if ((!allowScrollWhileFocused && stateRef.current.hasFocus) ||
|
|
@@ -702,6 +713,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
702
713
|
if (stateRef.current.touchAction === 'scroll') {
|
|
703
714
|
stateRef.current.touchAction = null;
|
|
704
715
|
}
|
|
716
|
+
scrollContextProviderRef.current?.notifyTouchEnd();
|
|
705
717
|
};
|
|
706
718
|
const handleTouchStart = (event) => {
|
|
707
719
|
if (stateRef.current.touchAction === 'complete') {
|
|
@@ -715,6 +727,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
715
727
|
swipeHandlers.onTouchStart(event);
|
|
716
728
|
}
|
|
717
729
|
visualContainer.classList.add(styles.noCaret);
|
|
730
|
+
scrollContextProviderRef.current?.notifyTouchStart();
|
|
718
731
|
};
|
|
719
732
|
const handleTouchMove = (event) => {
|
|
720
733
|
if (interceptTouchHandlers) {
|
|
@@ -754,7 +767,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
754
767
|
visualContainer.removeEventListener('touchend', handleTouchEnd);
|
|
755
768
|
visualContainer.removeEventListener('touchcancel', handleTouchCancel);
|
|
756
769
|
};
|
|
757
|
-
}, [allowScrollWhileFocused,
|
|
770
|
+
}, [allowScrollWhileFocused, swipeHandlers, interceptTouchHandlers, handleScroll]);
|
|
758
771
|
// при изменении высоты контента анимируем ее
|
|
759
772
|
// задаем боттомшиту фиксированную высоту и пересчитываем ее самостоятельно,
|
|
760
773
|
// чтобы не было мерцания, когда новый контент отрисовался до срабатывания колбека ResizeObserver
|
|
@@ -770,15 +783,13 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
770
783
|
let skipFirstResizeCallback = true;
|
|
771
784
|
let collapseResizeCallbacks = false;
|
|
772
785
|
const handleHeightChange = () => {
|
|
773
|
-
LayoutMetrics.
|
|
786
|
+
LayoutMetrics.invalidateCache();
|
|
774
787
|
if (skipFirstResizeCallback || stateRef.current.skipHeightAnimation) {
|
|
775
|
-
visualContainer.style.height = skipFirstResizeCallback
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
prevFooterHeight = LayoutMetrics.current.footerHeight;
|
|
781
|
-
prevVisibleContentHeight = LayoutMetrics.current.visibleContentHeight;
|
|
788
|
+
visualContainer.style.height = skipFirstResizeCallback ? `${LayoutMetrics.bottomSheetHeight}px` : '';
|
|
789
|
+
prevHeaderHeight = LayoutMetrics.headerHeight;
|
|
790
|
+
prevContentHeight = LayoutMetrics.contentHeight;
|
|
791
|
+
prevFooterHeight = LayoutMetrics.footerHeight;
|
|
792
|
+
prevVisibleContentHeight = LayoutMetrics.visibleContentHeight;
|
|
782
793
|
skipFirstResizeCallback = false;
|
|
783
794
|
stateRef.current.skipHeightAnimation = false;
|
|
784
795
|
return;
|
|
@@ -790,19 +801,16 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
790
801
|
}
|
|
791
802
|
}
|
|
792
803
|
else {
|
|
793
|
-
const contentHeightDiff = LayoutMetrics.
|
|
794
|
-
const containersHeightDiff = LayoutMetrics.
|
|
795
|
-
prevHeaderHeight +
|
|
796
|
-
LayoutMetrics.current.footerHeight -
|
|
797
|
-
prevFooterHeight;
|
|
804
|
+
const contentHeightDiff = LayoutMetrics.contentHeight - prevContentHeight;
|
|
805
|
+
const containersHeightDiff = LayoutMetrics.headerHeight - prevHeaderHeight + LayoutMetrics.footerHeight - prevFooterHeight;
|
|
798
806
|
// предположим, что scrollContainer останется таким же или станет меньше
|
|
799
807
|
// тогда можем рассчитать минимальную видимую высоту контента как min(scrollContainer.height, contentHeight)
|
|
800
808
|
const _prevVisibleContentHeight = Math.min(prevVisibleContentHeight, prevContentHeight);
|
|
801
|
-
const newMinVisibleContentHeight = Math.min(prevVisibleContentHeight - containersHeightDiff, LayoutMetrics.
|
|
809
|
+
const newMinVisibleContentHeight = Math.min(prevVisibleContentHeight - containersHeightDiff, LayoutMetrics.contentHeight);
|
|
802
810
|
const minVisibleContentHeightDiff = newMinVisibleContentHeight - _prevVisibleContentHeight;
|
|
803
811
|
// предположим, что scrollContainer станет больше
|
|
804
812
|
// тогда контент не может увеличиться больше, чем на расстояние между боттомшитом и верхним краем экрана
|
|
805
|
-
const maxVisibleContentHeightDiff = LayoutMetrics.
|
|
813
|
+
const maxVisibleContentHeightDiff = LayoutMetrics.remainingAvailableHeight - containersHeightDiff;
|
|
806
814
|
const visibleContentHeightDiff = Math.min(Math.max(contentHeightDiff, minVisibleContentHeightDiff), maxVisibleContentHeightDiff);
|
|
807
815
|
if (visibleContentHeightDiff !== 0) {
|
|
808
816
|
resetScrollPosition();
|
|
@@ -820,7 +828,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
820
828
|
// но в инлайн-стилях боттомшита остается старая высота
|
|
821
829
|
stateRef.current.heightAnimationDiff = heightAnimationDiff;
|
|
822
830
|
stateRef.current.heightAnimationCallback = () => {
|
|
823
|
-
prevVisibleContentHeight = LayoutMetrics.
|
|
831
|
+
prevVisibleContentHeight = LayoutMetrics.visibleContentHeight;
|
|
824
832
|
};
|
|
825
833
|
setHeightAnimationRunning(true);
|
|
826
834
|
collapseResizeCallbacks = true;
|
|
@@ -829,32 +837,30 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
829
837
|
});
|
|
830
838
|
}
|
|
831
839
|
else {
|
|
832
|
-
prevVisibleContentHeight = LayoutMetrics.
|
|
840
|
+
prevVisibleContentHeight = LayoutMetrics.visibleContentHeight;
|
|
833
841
|
recalcContentOverlayPosition();
|
|
834
842
|
recalcScrollFlags();
|
|
835
843
|
}
|
|
836
844
|
}
|
|
837
|
-
prevHeaderHeight = LayoutMetrics.
|
|
838
|
-
prevContentHeight = LayoutMetrics.
|
|
839
|
-
prevFooterHeight = LayoutMetrics.
|
|
845
|
+
prevHeaderHeight = LayoutMetrics.headerHeight;
|
|
846
|
+
prevContentHeight = LayoutMetrics.contentHeight;
|
|
847
|
+
prevFooterHeight = LayoutMetrics.footerHeight;
|
|
840
848
|
};
|
|
841
849
|
const resizeObserver = new ResizeObserver(handleHeightChange);
|
|
842
850
|
const content = contentRef.current;
|
|
843
851
|
const footer = footerRef.current;
|
|
844
|
-
const header =
|
|
852
|
+
const header = headerWrapperRef.current;
|
|
845
853
|
content !== null && resizeObserver.observe(content);
|
|
846
854
|
footer !== null && resizeObserver.observe(footer);
|
|
847
|
-
header !== null &&
|
|
848
|
-
return () =>
|
|
849
|
-
resizeObserver.disconnect();
|
|
850
|
-
header !== null && header.removeHeightObserver(handleHeightChange);
|
|
851
|
-
};
|
|
855
|
+
header !== null && resizeObserver.observe(header);
|
|
856
|
+
return () => resizeObserver.disconnect();
|
|
852
857
|
}, [
|
|
853
858
|
fixOverscroll,
|
|
854
859
|
recalcContentOverlayPosition,
|
|
855
860
|
recalcScrollFlags,
|
|
856
861
|
resetScrollPosition,
|
|
857
862
|
setHeightAnimationRunning,
|
|
863
|
+
LayoutMetrics,
|
|
858
864
|
]);
|
|
859
865
|
const handleAppearAnimationEnd = useCallback(() => {
|
|
860
866
|
const removeHeightObserver = initHeightObserver();
|
|
@@ -872,18 +878,17 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
872
878
|
}
|
|
873
879
|
const containerDataQa = `bottom-sheet-container${dataQA ? ` ${dataQA}` : ''}`;
|
|
874
880
|
const renderFunc = (appearTransition, heightTransition) => {
|
|
875
|
-
const navigationBar = header && isNavigationBarComponent(header) ? (jsx(NavigationBarComponent, { ...header.props, forwardedRef: headerRef })) : null;
|
|
876
881
|
const content = (jsx("div", { className: classnames(styles.content, {
|
|
877
882
|
[styles.contentFullScreen]: height === 'full-screen',
|
|
878
883
|
[styles.contentWithPaddings]: withContentPaddings,
|
|
879
884
|
[styles.contentWithoutHeader]: !header,
|
|
880
885
|
[styles.contentSizedFullScreen]: isContentSizedFullHeight(children),
|
|
881
886
|
}), ref: contentRef, children: jsx(BottomSheetContext.Provider, { value: bottomSheetContext.value, children: children }) }));
|
|
882
|
-
const scrollContainer = hasTouchSupport ? (jsx(CustomScrollContextProvider, { ref: scrollContextProviderRef, children: jsx("div", { className: classnames(styles.scrollContainer, {
|
|
887
|
+
const scrollContainer = hasTouchSupport ? (jsx(CustomScrollContextProvider, { ref: scrollContextProviderRef, wrapperRef: scrollContainerRef, ...scrollContextProps, children: jsx("div", { className: classnames(styles.scrollContainer, {
|
|
883
888
|
[styles.scrollContainerWithTopPadding]: withTopPadding,
|
|
884
|
-
}), onFocus: handleFocus, ref: scrollContainerRef, children: jsxs(GrowLimitWrapper, { ...growLimiterProps, children: [
|
|
889
|
+
}), onFocus: handleFocus, ref: scrollContainerRef, children: jsxs(GrowLimitWrapper, { ...growLimiterProps, children: [jsx("div", { className: styles.headerWrapper, ref: headerWrapperRef, children: header }), content] }) }) })) : (jsx("div", { className: classnames(styles.scrollContainer, styles.nativeScrollContainer, {
|
|
885
890
|
[styles.scrollContainerWithTopPadding]: withTopPadding,
|
|
886
|
-
}), onFocus: handleFocus, onScroll: recalcScrollFlags, ref: scrollContainerRef, children: jsxs(GrowLimitWrapper, { ...growLimiterProps, children: [
|
|
891
|
+
}), onFocus: handleFocus, onScroll: recalcScrollFlags, ref: scrollContainerRef, children: jsxs(GrowLimitWrapper, { ...growLimiterProps, children: [jsx("div", { className: styles.headerWrapper, ref: headerWrapperRef, children: header }), content] }) }));
|
|
887
892
|
const clonedFooter = footer && isActionBarComponent(footer)
|
|
888
893
|
? cloneElement(footer, {
|
|
889
894
|
type: footer.props.type || 'vertical',
|