@hh.ru/magritte-ui-bottom-sheet 6.0.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BottomSheet.js +172 -124
- package/BottomSheet.js.map +1 -1
- package/BottomSheetFooter.js +1 -1
- package/bottom-sheet-C4VJXwIv.js +5 -0
- package/bottom-sheet-C4VJXwIv.js.map +1 -0
- package/index.css +68 -64
- package/index.js +1 -1
- package/package.json +8 -8
- package/types.d.ts +1 -1
- package/bottom-sheet-BNgV4RDn.js +0 -5
- package/bottom-sheet-BNgV4RDn.js.map +0 -1
package/BottomSheet.js
CHANGED
|
@@ -18,18 +18,18 @@ import { Divider } from '@hh.ru/magritte-ui-divider';
|
|
|
18
18
|
import { Layer } from '@hh.ru/magritte-ui-layer';
|
|
19
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-C4VJXwIv.js';
|
|
22
22
|
|
|
23
23
|
const CSS_VAR_ENTER_ANIMATION_DURATION = '--enter-animation-duration';
|
|
24
24
|
const CSS_VAR_EXIT_ANIMATION_DURATION = '--exit-animation-duration';
|
|
25
25
|
const CSS_VAR_HEIGHT_ANIMATION_DURATION = '--height-transition-duration';
|
|
26
26
|
const CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET = '--virtual-keyboard-top-offset';
|
|
27
27
|
const CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET = '--virtual-keyboard-bottom-offset';
|
|
28
|
+
const checkSafari = () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
28
29
|
const hasSelectedText = () => {
|
|
29
30
|
const selection = document.getSelection();
|
|
30
31
|
return !!selection && !selection.isCollapsed;
|
|
31
32
|
};
|
|
32
|
-
const isSafari = () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
33
33
|
const toNumber = (value) => {
|
|
34
34
|
const result = parseInt(value, 10);
|
|
35
35
|
return Number.isInteger(result) ? result : 0;
|
|
@@ -56,7 +56,7 @@ const makeInitialState = () => ({
|
|
|
56
56
|
exitHandlers: [],
|
|
57
57
|
heightAnimationDiff: null,
|
|
58
58
|
});
|
|
59
|
-
const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, header, height = 'content', interceptClickHandlers = true,
|
|
59
|
+
const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, header, height = 'content', interceptClickHandlers = true, keyboardOverlaysFooter = true, onAppear, onBeforeExit, onAfterExit, onClose, showDivider = 'with-scroll', showOverlay = true, visible = false, withContentPaddings = true, }, ref) => {
|
|
60
60
|
const DOCUMENT_HEIGHT = useRef(0);
|
|
61
61
|
const SWIPE_THRESHOLD = useRef({ max: Infinity });
|
|
62
62
|
const contentRef = useRef(null);
|
|
@@ -83,13 +83,25 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
83
83
|
const [heightAnimationRunning, setHeightAnimationRunning] = useState(false);
|
|
84
84
|
const bottomSheetContext = useMemo(() => ({ contentOverlayRef }), [contentOverlayRef]);
|
|
85
85
|
const isContentSizedFullHeight = isValidTreeSelectorWrapper(children);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
const virtualKeyboardOffsetsRef = useRef({ top: 0, bottom: 0 });
|
|
87
|
+
const [isSafari, setIsSafari] = useState(null);
|
|
88
|
+
useEffect(() => setIsSafari(checkSafari()), [setIsSafari]);
|
|
89
|
+
const [hasTouchSupport, setHasTouchSupport] = useState(null);
|
|
90
|
+
const hasTouchSupportRef = useRef(null);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!('matchMedia' in window)) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const mediaQuery = window.matchMedia('(pointer:coarse)');
|
|
96
|
+
const handleMediaQueryChange = () => {
|
|
97
|
+
hasTouchSupportRef.current = mediaQuery.matches;
|
|
98
|
+
setHasTouchSupport(hasTouchSupportRef.current);
|
|
99
|
+
};
|
|
100
|
+
mediaQuery.addEventListener('change', handleMediaQueryChange);
|
|
101
|
+
hasTouchSupportRef.current = mediaQuery.matches;
|
|
102
|
+
setHasTouchSupport(hasTouchSupportRef.current);
|
|
103
|
+
return () => mediaQuery.removeEventListener('change', handleMediaQueryChange);
|
|
104
|
+
}, [setHasTouchSupport]);
|
|
93
105
|
const bindEscapeToClose = useEscapeToClose(() => currentVisible && onClose?.());
|
|
94
106
|
useEffect(() => bindEscapeToClose(document.body), [bindEscapeToClose]);
|
|
95
107
|
const LayoutMetrics = useRef((() => {
|
|
@@ -150,7 +162,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
150
162
|
get initialOffset() {
|
|
151
163
|
if (!('initialOffset' in cache)) {
|
|
152
164
|
if (height === 'half-screen' &&
|
|
153
|
-
|
|
165
|
+
hasTouchSupportRef.current &&
|
|
154
166
|
visualContainerRef.current !== null &&
|
|
155
167
|
visualViewport !== null) {
|
|
156
168
|
const halfScreenOffset = visualContainerRef.current.clientHeight - visualViewport.height / 2;
|
|
@@ -162,6 +174,9 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
162
174
|
}
|
|
163
175
|
return cache.initialOffset ?? 0;
|
|
164
176
|
},
|
|
177
|
+
get maxScrollTop() {
|
|
178
|
+
return LayoutMetrics.current.contentHeight - LayoutMetrics.current.visibleContentHeight;
|
|
179
|
+
},
|
|
165
180
|
/**
|
|
166
181
|
* Расстояние между верхним краем боттомшита и границей вьюпорта
|
|
167
182
|
*/
|
|
@@ -204,7 +219,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
204
219
|
setAnimationTimeout({ appear: { enter, exit }, height: { enter: height, exit } });
|
|
205
220
|
}, [setAnimationTimeout]);
|
|
206
221
|
useEffect(() => {
|
|
207
|
-
if (!currentVisible ||
|
|
222
|
+
if (!currentVisible || isSafari) {
|
|
208
223
|
return;
|
|
209
224
|
}
|
|
210
225
|
// используем Virtual Keyboard API через meta-тег вместо navigator.virtualKeyboard,
|
|
@@ -219,73 +234,129 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
219
234
|
const attributes = (attributesStr !== null
|
|
220
235
|
? Object.fromEntries(attributesStr.split(',').map((keyValuePairStr) => keyValuePairStr.split('=')))
|
|
221
236
|
: {});
|
|
222
|
-
attributes['interactive-widget'] =
|
|
237
|
+
attributes['interactive-widget'] = keyboardOverlaysFooter ? 'resizes-visual' : 'resizes-content';
|
|
223
238
|
const attributesStrUpdated = Object.entries(attributes)
|
|
224
239
|
.map((keyValuePair) => keyValuePair.join('='))
|
|
225
240
|
.join(',');
|
|
226
241
|
meta.setAttribute('content', attributesStrUpdated);
|
|
227
|
-
}, [currentVisible,
|
|
242
|
+
}, [currentVisible, isSafari, keyboardOverlaysFooter]);
|
|
243
|
+
const fixOverscroll = useCallback(() => {
|
|
244
|
+
if (hasTouchSupport) {
|
|
245
|
+
const isOverscrolled = LayoutMetrics.current.contentHeight + stateRef.current.scrollOffset <
|
|
246
|
+
LayoutMetrics.current.visibleContentHeight;
|
|
247
|
+
if (isOverscrolled) {
|
|
248
|
+
stateRef.current.scrollOffset =
|
|
249
|
+
LayoutMetrics.current.visibleContentHeight - LayoutMetrics.current.contentHeight;
|
|
250
|
+
if (contentRef.current !== null) {
|
|
251
|
+
contentRef.current.style.transform = translateY(stateRef.current.scrollOffset);
|
|
252
|
+
}
|
|
253
|
+
scrollContextProviderRef.current?.notify({
|
|
254
|
+
scrollTop: -stateRef.current.scrollOffset,
|
|
255
|
+
maxScrollTop: LayoutMetrics.current.maxScrollTop,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}, [hasTouchSupport]);
|
|
260
|
+
const resetScrollPosition = useCallback(() => {
|
|
261
|
+
if (hasTouchSupport) {
|
|
262
|
+
stateRef.current.scrollOffset = LayoutMetrics.current.initialOffset;
|
|
263
|
+
stateRef.current.swipeOffset = 0;
|
|
264
|
+
if (contentRef.current !== null) {
|
|
265
|
+
contentRef.current.style.transform = translateY(0);
|
|
266
|
+
}
|
|
267
|
+
if (swipeContainerRef.current !== null) {
|
|
268
|
+
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.current.initialOffset);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
scrollContainerRef.current?.scrollTo({ top: 0 });
|
|
273
|
+
}
|
|
274
|
+
}, [hasTouchSupport]);
|
|
275
|
+
const recalcScrollFlags = useCallback(() => {
|
|
276
|
+
const scrollOffset = hasTouchSupport
|
|
277
|
+
? stateRef.current.scrollOffset
|
|
278
|
+
: -(scrollContainerRef.current?.scrollTop ?? 0);
|
|
279
|
+
if (dividerRef.current !== null) {
|
|
280
|
+
const prevDividerVisible = stateRef.current.dividerVisible;
|
|
281
|
+
const isNotScrolledToEnd = LayoutMetrics.current.contentHeight + scrollOffset > LayoutMetrics.current.visibleContentHeight;
|
|
282
|
+
stateRef.current.dividerVisible =
|
|
283
|
+
showDivider === 'always' || (showDivider === 'with-scroll' && isNotScrolledToEnd);
|
|
284
|
+
if (stateRef.current.dividerVisible !== prevDividerVisible) {
|
|
285
|
+
dividerRef.current.classList.toggle(styles.dividerVisible, stateRef.current.dividerVisible);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (grabberRef.current !== null) {
|
|
289
|
+
const prevGrabberUnsafe = stateRef.current.grabberUnsafe;
|
|
290
|
+
stateRef.current.grabberUnsafe =
|
|
291
|
+
Math.round(Math.max(scrollOffset, 0) + stateRef.current.swipeOffset) ===
|
|
292
|
+
LayoutMetrics.current.remainingAvailableHeight;
|
|
293
|
+
if (stateRef.current.grabberUnsafe !== prevGrabberUnsafe) {
|
|
294
|
+
grabberRef.current.classList.toggle(styles.grabberEnsureSafe, stateRef.current.grabberUnsafe);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}, [hasTouchSupport, showDivider]);
|
|
298
|
+
// терминология: https://developer.chrome.com/blog/viewport-resize-behavior/
|
|
299
|
+
//
|
|
300
|
+
// при открытии виртуальной клавиатуры браузеры двигают Visual Viewport и Layout Viewport
|
|
301
|
+
// с помощью двух отступов делаем так, чтобы overlay боттомшита совпадал с видимой частью экрана
|
|
228
302
|
const recalcKeyboardOffsets = useCallback(() => {
|
|
229
|
-
if (!overlayRef.current || !visualViewport) {
|
|
303
|
+
if (!hasTouchSupport || !contentRef.current || !overlayRef.current || !visualViewport) {
|
|
230
304
|
return;
|
|
231
305
|
}
|
|
306
|
+
LayoutMetrics.current.invalidateCache();
|
|
232
307
|
if (stateRef.current.hasFocus && focusedElementRef.current !== null) {
|
|
233
|
-
// терминология: https://developer.chrome.com/blog/viewport-resize-behavior/
|
|
234
|
-
//
|
|
235
|
-
// делим браузеры на три группы в зависимости от поведения при открытии виртуальной клавиатуры:
|
|
236
|
-
// 1. Safari — ресайзит Visual Viewport, не меняет Layout Viewport.
|
|
237
|
-
// В нем не нужно ничего корректировать при keyboardOverlaysContent=true,
|
|
238
|
-
// а при keyboardOverlaysContent=false нужно сдвинуть НИЖНИЙ край контейнера ВВЕРХ,
|
|
239
|
-
// чтобы он совпал с границей Visual Viewport
|
|
240
|
-
// 2. Chrome < 108 & Chromium-based — ресайзит и Visual Viewport, и Layout Viewport.
|
|
241
|
-
// В нем не нужно ничего корректировать при keyboardOverlaysContent=false,
|
|
242
|
-
// а при keyboardOverlaysContent=true нужно сдвинуть НИЖНИЙ край контейнера ВНИЗ,
|
|
243
|
-
// чтобы футер уехал под клавиатуру
|
|
244
|
-
// 3. Chrome >= 108 — поддерживает Virtual Keyboard API и meta-тег interactive-widget.
|
|
245
|
-
// Используем поведение `resizes-visual` (как в Safari) в случае keyboardOverlaysContent=true
|
|
246
|
-
// и `resizes-content` (как в Chrome < 108 & Chromium-based) в случае keyboardOverlaysContent=false
|
|
247
|
-
// Таким образом в нем ничего не нужно корректировать
|
|
248
308
|
const overlayDOMRect = overlayRef.current.getBoundingClientRect();
|
|
249
309
|
// любой браузер может сдвинуть Visual Viewport вверх, если фокусируемый инпут находится близко к нижней границе
|
|
250
310
|
// из-за этого может возникнуть проблема, что ВЕРХНИЙ край контента уехал за границу Visual Viewport
|
|
251
311
|
// сдвигаем ВЕРХНИЙ край контейнера ВНИЗ, чтобы он совпал с границей Visual Viewport
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (keyboardOverlaysContent) {
|
|
256
|
-
// браузеры из этой группы меняют Layout Viewport
|
|
257
|
-
const layoutViewportDiff = Math.round(DOCUMENT_HEIGHT.current - document.documentElement.clientHeight);
|
|
258
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
259
|
-
if (layoutViewportDiff > 0) {
|
|
260
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
261
|
-
// сдвигаем НИЖНИЙ край контейнера ВНИЗ, чтобы футер уехал под клавиатуру
|
|
262
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${-layoutViewportDiff}px`);
|
|
263
|
-
// при этом может возникнуть проблема, что клавиатура перекрыла инпут
|
|
264
|
-
// проверяем это и компенсируем величину перекрытия при необходимости
|
|
265
|
-
const focusedElementOutOfViewportHeight = Math.round(focusedElementRef.current.getBoundingClientRect().bottom +
|
|
266
|
-
layoutViewportDiff -
|
|
267
|
-
DOCUMENT_HEIGHT.current);
|
|
268
|
-
if (focusedElementOutOfViewportHeight > 0) {
|
|
269
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${focusedElementOutOfViewportHeight - layoutViewportDiff}px`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// keyboardOverlaysContent=false, клавиатура ПОД контентом
|
|
274
|
-
// этот кейс нужно корректировать только в Safari
|
|
275
|
-
if (!keyboardOverlaysContent && deviceFlagsRef.current.isSafari) {
|
|
312
|
+
const topOffset = Math.round(-overlayDOMRect.top);
|
|
313
|
+
let bottomOffset = 0;
|
|
314
|
+
if (isSafari) {
|
|
276
315
|
const visualViewportDiff = Math.round(overlayDOMRect.bottom - visualViewport.height);
|
|
277
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${visualViewportShift}px`);
|
|
278
316
|
if (visualViewportDiff > 0) {
|
|
279
317
|
// сдвигаем НИЖНИЙ край контейнера ВВЕРХ, чтобы он совпал с границей Visual Viewport
|
|
280
|
-
|
|
318
|
+
bottomOffset = visualViewportDiff;
|
|
281
319
|
}
|
|
282
320
|
}
|
|
321
|
+
if (keyboardOverlaysFooter && footerRef.current !== null) {
|
|
322
|
+
bottomOffset -= footerRef.current.clientHeight;
|
|
323
|
+
}
|
|
324
|
+
if (topOffset !== virtualKeyboardOffsetsRef.current.top ||
|
|
325
|
+
bottomOffset !== virtualKeyboardOffsetsRef.current.bottom) {
|
|
326
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `${topOffset}px`);
|
|
327
|
+
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `${bottomOffset}px`);
|
|
328
|
+
}
|
|
329
|
+
LayoutMetrics.current.invalidateCache();
|
|
330
|
+
// если фокусируемый элемент лежит внутри скроллящегося контейнера и не виден, скроллим к нему
|
|
331
|
+
if (contentRef.current.contains(focusedElementRef.current)) {
|
|
332
|
+
let focusedElementOffset = 0;
|
|
333
|
+
for (let offsetElement = focusedElementRef.current.closest('[data-interactive]') ?? focusedElementRef.current; offsetElement !== contentRef.current && offsetElement !== null; offsetElement = offsetElement.offsetParent) {
|
|
334
|
+
focusedElementOffset += offsetElement.offsetTop;
|
|
335
|
+
}
|
|
336
|
+
const newScrollOffset = -Math.min(Math.max(focusedElementOffset - 10, 0), LayoutMetrics.current.maxScrollTop);
|
|
337
|
+
if (stateRef.current.scrollOffset !== newScrollOffset) {
|
|
338
|
+
stateRef.current.scrollOffset = newScrollOffset;
|
|
339
|
+
contentRef.current.style.transform = translateY(stateRef.current.scrollOffset);
|
|
340
|
+
scrollContextProviderRef.current?.notify({
|
|
341
|
+
scrollTop: -stateRef.current.scrollOffset,
|
|
342
|
+
maxScrollTop: LayoutMetrics.current.maxScrollTop,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
recalcScrollFlags();
|
|
347
|
+
virtualKeyboardOffsetsRef.current.top = topOffset;
|
|
348
|
+
virtualKeyboardOffsetsRef.current.bottom = bottomOffset;
|
|
283
349
|
}
|
|
284
350
|
else {
|
|
285
351
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
286
352
|
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
353
|
+
LayoutMetrics.current.invalidateCache();
|
|
354
|
+
fixOverscroll();
|
|
355
|
+
recalcScrollFlags();
|
|
356
|
+
virtualKeyboardOffsetsRef.current.top = 0;
|
|
357
|
+
virtualKeyboardOffsetsRef.current.bottom = 0;
|
|
287
358
|
}
|
|
288
|
-
}, [
|
|
359
|
+
}, [fixOverscroll, hasTouchSupport, isSafari, keyboardOverlaysFooter, recalcScrollFlags]);
|
|
289
360
|
const handleFocus = useCallback((event) => {
|
|
290
361
|
const focusedElement = event.target;
|
|
291
362
|
const initialViewportHeight = visualViewport?.height;
|
|
@@ -301,6 +372,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
301
372
|
recalcKeyboardOffsets();
|
|
302
373
|
if (!stateRef.current.hasFocus) {
|
|
303
374
|
visualViewport?.removeEventListener('resize', handleResize);
|
|
375
|
+
visualViewport?.removeEventListener('scroll', handleResize);
|
|
304
376
|
}
|
|
305
377
|
};
|
|
306
378
|
// если спамить фокус/блюр инпута, ивент visualViewport.resize может не долететь
|
|
@@ -308,6 +380,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
308
380
|
const waitForResize = () => {
|
|
309
381
|
if (performance.now() - resizeRAFStart > 1000 || visualViewport?.height !== initialViewportHeight) {
|
|
310
382
|
visualViewport?.removeEventListener('resize', handleResize);
|
|
383
|
+
visualViewport?.removeEventListener('scroll', handleResize);
|
|
311
384
|
stateRef.current.resizeRAFHandle = null;
|
|
312
385
|
recalcKeyboardOffsets();
|
|
313
386
|
}
|
|
@@ -316,10 +389,6 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
316
389
|
}
|
|
317
390
|
};
|
|
318
391
|
const handleBlur = () => {
|
|
319
|
-
if (deviceFlagsRef.current.isSafari) {
|
|
320
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_TOP_OFFSET, `0px`);
|
|
321
|
-
setCSSVariable(CSS_VAR_VIRTUAL_KEYBOARD_BOTTOM_OFFSET, `0px`);
|
|
322
|
-
}
|
|
323
392
|
stateRef.current.hasFocus = false;
|
|
324
393
|
if (stateRef.current.resizeRAFHandle !== null) {
|
|
325
394
|
cancelAnimationFrame(stateRef.current.resizeRAFHandle);
|
|
@@ -327,13 +396,22 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
327
396
|
}
|
|
328
397
|
focusedElement.removeEventListener('blur', handleBlur);
|
|
329
398
|
focusedElementRef.current = null;
|
|
399
|
+
if (isSafari) {
|
|
400
|
+
recalcKeyboardOffsets();
|
|
401
|
+
}
|
|
330
402
|
};
|
|
331
403
|
stateRef.current.hasFocus = true;
|
|
332
404
|
stateRef.current.resizeRAFHandle = requestAnimationFrame(waitForResize);
|
|
333
405
|
visualViewport?.addEventListener('resize', handleResize);
|
|
406
|
+
// событие scroll может прилететь, только если браузер сдвинул вьюпорт из-за появления виртуальной клавиатуры
|
|
407
|
+
// пользовательский скролл не вызывает событие scroll, т.к. нативный скролл отключен
|
|
408
|
+
// в Safari событие resize может прилететь до того как браузер сдвинул вьюпорт, поэтому слушаем scroll
|
|
409
|
+
if (isSafari) {
|
|
410
|
+
visualViewport?.addEventListener('scroll', handleResize);
|
|
411
|
+
}
|
|
334
412
|
focusedElement.addEventListener('blur', handleBlur);
|
|
335
413
|
focusedElementRef.current = focusedElement;
|
|
336
|
-
}, [recalcKeyboardOffsets]);
|
|
414
|
+
}, [isSafari, recalcKeyboardOffsets]);
|
|
337
415
|
// contentOverlay совпадает по границам с контентом боттомшита, но лежит вне боттомшита,
|
|
338
416
|
// чтобы чайлды contentOverlay не обрезались границами боттомшита
|
|
339
417
|
// например, снекбар, лежащий внутри contentOverlay, при смахивании может оказаться в любом месте экрана
|
|
@@ -346,29 +424,6 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
346
424
|
contentOverlayRef.current.style.height = `${LayoutMetrics.current.visibleContentHeight}px`;
|
|
347
425
|
}
|
|
348
426
|
}, []);
|
|
349
|
-
const recalcScrollFlags = useCallback(() => {
|
|
350
|
-
const scrollOffset = deviceFlagsRef.current.hasTouchSupport
|
|
351
|
-
? stateRef.current.scrollOffset
|
|
352
|
-
: -(scrollContainerRef.current?.scrollTop ?? 0);
|
|
353
|
-
if (dividerRef.current !== null) {
|
|
354
|
-
const prevDividerVisible = stateRef.current.dividerVisible;
|
|
355
|
-
const isNotScrolledToEnd = LayoutMetrics.current.contentHeight + scrollOffset > LayoutMetrics.current.visibleContentHeight;
|
|
356
|
-
stateRef.current.dividerVisible =
|
|
357
|
-
showDivider === 'always' || (showDivider === 'with-scroll' && isNotScrolledToEnd);
|
|
358
|
-
if (stateRef.current.dividerVisible !== prevDividerVisible) {
|
|
359
|
-
dividerRef.current.classList.toggle(styles.dividerVisible, stateRef.current.dividerVisible);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (grabberRef.current !== null) {
|
|
363
|
-
const prevGrabberUnsafe = stateRef.current.grabberUnsafe;
|
|
364
|
-
stateRef.current.grabberUnsafe =
|
|
365
|
-
Math.round(Math.max(scrollOffset, 0) + stateRef.current.swipeOffset) ===
|
|
366
|
-
LayoutMetrics.current.remainingAvailableHeight;
|
|
367
|
-
if (stateRef.current.grabberUnsafe !== prevGrabberUnsafe) {
|
|
368
|
-
grabberRef.current.classList.toggle(styles.grabberEnsureSafe, stateRef.current.grabberUnsafe);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}, [showDivider]);
|
|
372
427
|
// помещает боттомшит в позицию вне экрана снизу, которая может быть начальной либо конечной точкой анимации
|
|
373
428
|
const setTransformToInvisible = useCallback(() => {
|
|
374
429
|
LayoutMetrics.current.invalidateCache();
|
|
@@ -410,21 +465,6 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
410
465
|
stateRef.current.exitHandlers = [];
|
|
411
466
|
onAfterExit?.();
|
|
412
467
|
}, [onAfterExit]);
|
|
413
|
-
const resetScrollPosition = useCallback(() => {
|
|
414
|
-
if (deviceFlagsRef.current.hasTouchSupport) {
|
|
415
|
-
stateRef.current.scrollOffset = LayoutMetrics.current.initialOffset;
|
|
416
|
-
stateRef.current.swipeOffset = 0;
|
|
417
|
-
if (contentRef.current !== null) {
|
|
418
|
-
contentRef.current.style.transform = translateY(0);
|
|
419
|
-
}
|
|
420
|
-
if (swipeContainerRef.current !== null) {
|
|
421
|
-
swipeContainerRef.current.style.transform = translateY(LayoutMetrics.current.initialOffset);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
scrollContainerRef.current?.scrollTo({ top: 0 });
|
|
426
|
-
}
|
|
427
|
-
}, []);
|
|
428
468
|
const handleHeightAnimationStart = useCallback(() => {
|
|
429
469
|
LayoutMetrics.current.invalidateCache();
|
|
430
470
|
if (stateRef.current.heightAnimationDiff !== null && visualContainerRef.current !== null) {
|
|
@@ -444,7 +484,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
444
484
|
requestAnimationFrame(recalcScrollFlags);
|
|
445
485
|
scrollContextProviderRef.current?.notify({
|
|
446
486
|
scrollTop: 0,
|
|
447
|
-
maxScrollTop: LayoutMetrics.current.
|
|
487
|
+
maxScrollTop: LayoutMetrics.current.maxScrollTop,
|
|
448
488
|
});
|
|
449
489
|
}, [setHeightAnimationRunning, recalcContentOverlayPosition, recalcScrollFlags]);
|
|
450
490
|
const handleSwipeMove = useCallback((event) => {
|
|
@@ -555,7 +595,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
555
595
|
recalcScrollFlags();
|
|
556
596
|
scrollContextProviderRef.current?.notify({
|
|
557
597
|
scrollTop: Math.max(-stateRef.current.scrollOffset, 0),
|
|
558
|
-
maxScrollTop: LayoutMetrics.current.
|
|
598
|
+
maxScrollTop: LayoutMetrics.current.maxScrollTop,
|
|
559
599
|
});
|
|
560
600
|
}
|
|
561
601
|
}
|
|
@@ -565,7 +605,9 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
565
605
|
if ((!allowScrollWhileFocused && stateRef.current.hasFocus) ||
|
|
566
606
|
(stateRef.current.touchAction !== null && stateRef.current.touchAction !== 'scroll') ||
|
|
567
607
|
hasSelectedText() ||
|
|
568
|
-
(focusedElementRef.current !== null &&
|
|
608
|
+
(focusedElementRef.current !== null &&
|
|
609
|
+
focusedElementTouchY !== null &&
|
|
610
|
+
focusedElementRef.current.clientHeight !== focusedElementRef.current.scrollHeight)) {
|
|
569
611
|
return;
|
|
570
612
|
}
|
|
571
613
|
handleScroll(event.delta);
|
|
@@ -589,6 +631,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
589
631
|
focusedElementTouchY = null;
|
|
590
632
|
swipeHandlers.onTouchStart(event);
|
|
591
633
|
}
|
|
634
|
+
visualContainer.classList.add(styles.noCaret);
|
|
592
635
|
};
|
|
593
636
|
const handleTouchMove = (event) => {
|
|
594
637
|
event.preventDefault();
|
|
@@ -601,6 +644,14 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
601
644
|
swipeHandlers.onTouchMove(event);
|
|
602
645
|
}
|
|
603
646
|
};
|
|
647
|
+
const handleTouchEnd = (event) => {
|
|
648
|
+
swipeHandlers.onTouchEnd(event);
|
|
649
|
+
visualContainer.classList.remove(styles.noCaret);
|
|
650
|
+
};
|
|
651
|
+
const handleTouchCancel = (event) => {
|
|
652
|
+
swipeHandlers.onTouchCancel(event);
|
|
653
|
+
visualContainer.classList.remove(styles.noCaret);
|
|
654
|
+
};
|
|
604
655
|
const removeScrollHandlers = initScrollHandlers({
|
|
605
656
|
axis: 'vertical',
|
|
606
657
|
wrapperRef: visualContainerRef,
|
|
@@ -609,14 +660,14 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
609
660
|
});
|
|
610
661
|
visualContainer.addEventListener('touchstart', handleTouchStart);
|
|
611
662
|
visualContainer.addEventListener('touchmove', handleTouchMove);
|
|
612
|
-
visualContainer.addEventListener('touchend',
|
|
613
|
-
visualContainer.addEventListener('touchcancel',
|
|
663
|
+
visualContainer.addEventListener('touchend', handleTouchEnd);
|
|
664
|
+
visualContainer.addEventListener('touchcancel', handleTouchCancel);
|
|
614
665
|
return () => {
|
|
615
666
|
removeScrollHandlers();
|
|
616
667
|
visualContainer.removeEventListener('touchstart', handleTouchStart);
|
|
617
668
|
visualContainer.removeEventListener('touchmove', handleTouchMove);
|
|
618
|
-
visualContainer.removeEventListener('touchend',
|
|
619
|
-
visualContainer.removeEventListener('touchcancel',
|
|
669
|
+
visualContainer.removeEventListener('touchend', handleTouchEnd);
|
|
670
|
+
visualContainer.removeEventListener('touchcancel', handleTouchCancel);
|
|
620
671
|
};
|
|
621
672
|
}, [allowScrollWhileFocused, recalcScrollFlags, swipeHandlers]);
|
|
622
673
|
// при изменении высоты контента анимируем ее
|
|
@@ -668,20 +719,11 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
668
719
|
if (visibleContentHeightDiff !== 0) {
|
|
669
720
|
resetScrollPosition();
|
|
670
721
|
}
|
|
671
|
-
else
|
|
722
|
+
else {
|
|
672
723
|
// если высота видимой части контента не изменилась,
|
|
673
724
|
// но позиция скролла превысила максимально допустимую из-за уменьшения высоты контента,
|
|
674
725
|
// сбрасываем позицию скролла на максимально допустимую
|
|
675
|
-
|
|
676
|
-
LayoutMetrics.current.visibleContentHeight;
|
|
677
|
-
if (isOverscrolled) {
|
|
678
|
-
stateRef.current.scrollOffset =
|
|
679
|
-
LayoutMetrics.current.visibleContentHeight - LayoutMetrics.current.contentHeight;
|
|
680
|
-
if (contentRef.current !== null) {
|
|
681
|
-
contentRef.current.style.transform = translateY(stateRef.current.scrollOffset);
|
|
682
|
-
}
|
|
683
|
-
scrollContextProviderRef.current?.notify({ scrollTop: -stateRef.current.scrollOffset });
|
|
684
|
-
}
|
|
726
|
+
fixOverscroll();
|
|
685
727
|
}
|
|
686
728
|
const heightAnimationDiff = visibleContentHeightDiff + containersHeightDiff;
|
|
687
729
|
if (heightAnimationDiff !== 0) {
|
|
@@ -718,14 +760,20 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
718
760
|
resizeObserver.disconnect();
|
|
719
761
|
header !== null && header.removeHeightObserver(handleHeightChange);
|
|
720
762
|
};
|
|
721
|
-
}, [
|
|
763
|
+
}, [
|
|
764
|
+
fixOverscroll,
|
|
765
|
+
recalcContentOverlayPosition,
|
|
766
|
+
recalcScrollFlags,
|
|
767
|
+
resetScrollPosition,
|
|
768
|
+
setHeightAnimationRunning,
|
|
769
|
+
]);
|
|
722
770
|
const handleAppearAnimationEnd = useCallback(() => {
|
|
723
771
|
const removeHeightObserver = initHeightObserver();
|
|
724
772
|
removeHeightObserver && stateRef.current.exitHandlers.push(removeHeightObserver);
|
|
725
|
-
const removeTransformHandlers =
|
|
773
|
+
const removeTransformHandlers = hasTouchSupport && initTransformHandlers();
|
|
726
774
|
removeTransformHandlers && stateRef.current.exitHandlers.push(removeTransformHandlers);
|
|
727
775
|
onAppearRef.current?.();
|
|
728
|
-
}, [initHeightObserver, initTransformHandlers]);
|
|
776
|
+
}, [hasTouchSupport, initHeightObserver, initTransformHandlers]);
|
|
729
777
|
const { onTouchEnd, ...eventHandlers } = useNoBubbling();
|
|
730
778
|
if (!animationTimeout) {
|
|
731
779
|
return null;
|
|
@@ -738,7 +786,7 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
738
786
|
[styles.contentWithoutHeader]: !header,
|
|
739
787
|
[styles.contentSizedFullScreen]: isContentSizedFullHeight,
|
|
740
788
|
}), ref: contentRef, children: jsx(BottomSheetContext.Provider, { value: bottomSheetContext, children: children }) }));
|
|
741
|
-
const scrollContainer =
|
|
789
|
+
const scrollContainer = hasTouchSupport ? (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] }));
|
|
742
790
|
const clonedFooter = footer && isActionBarComponent(footer)
|
|
743
791
|
? cloneElement(footer, { type: footer.props.type || 'mobile', showDivider: false })
|
|
744
792
|
: footer;
|
|
@@ -759,9 +807,9 @@ const BottomSheetRenderFunc = ({ allowScrollWhileFocused, children, footer, head
|
|
|
759
807
|
[styles.heightTransitionAnimation]: heightTransition === 'entering',
|
|
760
808
|
}), "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, {
|
|
761
809
|
[styles.dividerVisible]: stateRef.current.dividerVisible,
|
|
762
|
-
}), ref: dividerRef, children: jsx(Divider, {}) })), interceptClickHandlers &&
|
|
810
|
+
}), ref: dividerRef, children: jsx(Divider, {}) })), interceptClickHandlers && hasTouchSupport ? (jsx(ClickInterceptor, { children: clonedFooter })) : (clonedFooter)] })] }), jsx("div", { className: styles.contentOverlay, ref: contentOverlayRef })] })] }) }));
|
|
763
811
|
};
|
|
764
|
-
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);
|
|
812
|
+
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) })) }, `hasTouchSupport:${hasTouchSupport}`) }), document.body);
|
|
765
813
|
};
|
|
766
814
|
const BottomSheet = forwardRef(BottomSheetRenderFunc);
|
|
767
815
|
|