@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 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-C6FIprRU.js';
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 [growLimiterContextValue, _] = useState(() => {
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
- const LayoutMetrics = useRef((() => {
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
- const layoutHeaderHeight = headerRef.current?.layoutHeight ?? 0;
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
- const visibleHeaderHeight = headerRef.current?.visibleHeight ?? 0;
196
- cache.contentTop = visualContainerRef.current.offsetTop + visibleHeaderHeight;
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) && headerRef.current !== null) {
212
- cache.headerHeight = headerRef.current.layoutHeight;
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.current.contentHeight - LayoutMetrics.current.visibleContentHeight;
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.current.contentHeight + stateRef.current.scrollOffset <
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?.notify({
312
+ scrollContextProviderRef.current?.notifyScroll({
313
313
  scrollTop: -stateRef.current.scrollOffset,
314
- maxScrollTop: LayoutMetrics.current.maxScrollTop,
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.current.initialOffset;
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.current.initialOffset);
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.current.contentHeight + scrollOffset > LayoutMetrics.current.visibleContentHeight;
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.current.remainingAvailableHeight;
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.current.initialOffset > 0 && stateRef.current.scrollOffset > 0) {
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.current.invalidateCache();
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.current.invalidateCache();
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.current.maxScrollTop);
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?.notify({
413
+ scrollContextProviderRef.current?.notifyScroll({
414
414
  scrollTop: -stateRef.current.scrollOffset,
415
- maxScrollTop: LayoutMetrics.current.maxScrollTop,
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.current.invalidateCache();
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
- }, [fixOverscroll, hasTouchSupport, isSafari, keyboardOverlaysFooter, recalcScrollFlags, bottomSheetContext]);
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.current.contentTop}px`;
500
- contentOverlayRef.current.style.height = `${LayoutMetrics.current.visibleContentHeight}px`;
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.current.invalidateCache();
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.current.bottomSheetHeight);
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.current.invalidateCache();
518
- stateRef.current.scrollOffset = LayoutMetrics.current.initialOffset;
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.current.initialOffset + stateRef.current.swipeOffset);
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.current.initialOffset);
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.current.invalidateCache();
553
+ LayoutMetrics.invalidateCache();
546
554
  if (stateRef.current.heightAnimationDiff !== null && visualContainerRef.current !== null) {
547
- visualContainerRef.current.style.height = `${LayoutMetrics.current.bottomSheetHeight + stateRef.current.heightAnimationDiff}px`;
555
+ visualContainerRef.current.style.height = `${LayoutMetrics.bottomSheetHeight + stateRef.current.heightAnimationDiff}px`;
548
556
  }
549
- }, []);
557
+ }, [LayoutMetrics]);
550
558
  const handleHeightAnimationEnd = useCallback(() => {
551
- LayoutMetrics.current.invalidateCache();
559
+ LayoutMetrics.invalidateCache();
552
560
  if (visualContainerRef.current !== null) {
553
- visualContainerRef.current.style.height = `${LayoutMetrics.current.bottomSheetHeight}px`;
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?.notify({
569
+ scrollContextProviderRef.current?.notifyScroll({
562
570
  scrollTop: 0,
563
- maxScrollTop: LayoutMetrics.current.maxScrollTop,
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 initTransformHandlers = useCallback(() => {
624
- const visualContainer = visualContainerRef.current;
625
- if (!visualContainer) {
626
- return void 0;
627
- }
628
- const handleScroll = (delta) => {
629
- if (LayoutMetrics.current.initialOffset !== 0 ||
630
- LayoutMetrics.current.contentHeight > LayoutMetrics.current.visibleContentHeight) {
631
- // храним неокругленное значение для translateY, чтобы анимация была плавнее
632
- let newScrollOffset = stateRef.current.scrollOffset + delta;
633
- const roundedNewScrollOffset = Math.round(newScrollOffset);
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
- else if (LayoutMetrics.current.contentHeight + roundedNewScrollOffset <=
643
- LayoutMetrics.current.visibleContentHeight) {
644
- // скролла нет (touchAction is null)
645
- // либо контент проскроллен до конца, тогда не даем скроллить дальше
646
- newScrollOffset = LayoutMetrics.current.visibleContentHeight - LayoutMetrics.current.contentHeight;
647
- if (stateRef.current.touchAction === 'scroll') {
648
- stateRef.current.touchAction = 'complete';
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
- if (stateRef.current.scrollOffset !== newScrollOffset) {
656
- const offsetWasPositive = stateRef.current.scrollOffset > 0;
657
- stateRef.current.scrollOffset = newScrollOffset;
658
- if (contentRef.current !== null && swipeContainerRef.current !== null) {
659
- if (newScrollOffset > 0) {
660
- if (!offsetWasPositive) {
661
- contentRef.current.style.transform = translateY(0);
662
- }
663
- swipeContainerRef.current.style.transform = translateY(newScrollOffset);
664
- if (footerRef.current !== null) {
665
- footerRef.current.style.transform = translateY(-newScrollOffset);
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
- else {
669
- contentRef.current.style.transform = translateY(newScrollOffset);
670
- if (offsetWasPositive) {
671
- swipeContainerRef.current.style.transform = translateY(0);
672
- if (footerRef.current !== null) {
673
- footerRef.current.style.transform = translateY(0);
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, recalcScrollFlags, swipeHandlers, interceptTouchHandlers]);
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.current.invalidateCache();
786
+ LayoutMetrics.invalidateCache();
774
787
  if (skipFirstResizeCallback || stateRef.current.skipHeightAnimation) {
775
- visualContainer.style.height = skipFirstResizeCallback
776
- ? `${LayoutMetrics.current.bottomSheetHeight}px`
777
- : '';
778
- prevHeaderHeight = LayoutMetrics.current.headerHeight;
779
- prevContentHeight = LayoutMetrics.current.contentHeight;
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.current.contentHeight - prevContentHeight;
794
- const containersHeightDiff = LayoutMetrics.current.headerHeight -
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.current.contentHeight);
809
+ const newMinVisibleContentHeight = Math.min(prevVisibleContentHeight - containersHeightDiff, LayoutMetrics.contentHeight);
802
810
  const minVisibleContentHeightDiff = newMinVisibleContentHeight - _prevVisibleContentHeight;
803
811
  // предположим, что scrollContainer станет больше
804
812
  // тогда контент не может увеличиться больше, чем на расстояние между боттомшитом и верхним краем экрана
805
- const maxVisibleContentHeightDiff = LayoutMetrics.current.remainingAvailableHeight - containersHeightDiff;
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.current.visibleContentHeight;
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.current.visibleContentHeight;
840
+ prevVisibleContentHeight = LayoutMetrics.visibleContentHeight;
833
841
  recalcContentOverlayPosition();
834
842
  recalcScrollFlags();
835
843
  }
836
844
  }
837
- prevHeaderHeight = LayoutMetrics.current.headerHeight;
838
- prevContentHeight = LayoutMetrics.current.contentHeight;
839
- prevFooterHeight = LayoutMetrics.current.footerHeight;
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 = headerRef.current;
852
+ const header = headerWrapperRef.current;
845
853
  content !== null && resizeObserver.observe(content);
846
854
  footer !== null && resizeObserver.observe(footer);
847
- header !== null && header.addHeightObserver(handleHeightChange);
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: [navigationBar, content] }) }) })) : (jsx("div", { className: classnames(styles.scrollContainer, styles.nativeScrollContainer, {
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: [navigationBar, content] }) }));
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',