'use client'; /* eslint-disable jsdoc/require-jsdoc */ import { type ComponentType, type KeyboardEvent, useCallback } from 'react'; import { classNames, noop } from '@vkontakte/vkjs'; import { mergeStyle } from '../../helpers/mergeStyle'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { isSizeXRegularFallback, useAdaptivityWithJSMediaQueries, } from '../../hooks/useAdaptivityWithJSMediaQueries'; import { useExternRef } from '../../hooks/useExternRef'; import { useVirtualKeyboardState } from '../../hooks/useVirtualKeyboardState'; import { Keys, pressedKey } from '../../lib/accessibility'; import { useCSSTransition, type UseCSSTransitionState } from '../../lib/animation'; import { type SnapPoint, type SnapPointChange, useBottomSheet } from '../../lib/sheet'; import type { CSSCustomProperties } from '../../types'; import { useScrollLock } from '../AppRoot/ScrollContext'; import { useConfigProvider } from '../ConfigProvider/ConfigProviderContext'; import { FocusTrap } from '../FocusTrap/FocusTrap'; import { ModalOutlet } from '../ModalOutlet/ModalOutlet'; import { ModalOverlay as ModalOverlayDefault, type ModalOverlayProps, } from '../ModalOverlay/ModalOverlay'; import { RootComponent } from '../RootComponent/RootComponent'; import { ModalPageBase } from './ModalPageBase'; import type { ModalPageProps } from './types'; import styles from './ModalPage.module.css'; const transitionStateClassNames: Partial> = { appear: styles.documentStateEnter, appearing: styles.documentStateEntering, enter: styles.documentStateEnter, entering: styles.documentStateEntering, exiting: styles.documentStateExiting, exited: styles.documentStateExited, }; export interface ModalPageInternalProps extends Omit { snapPoint: SnapPoint; ModalOverlay?: ComponentType | undefined; onSnapPointChange?: SnapPointChange | undefined; } /** * В компоненте заложена вся логика модального окна. * * @private */ export const ModalPageInternal = ({ getRootRef, open, header, footer, size: desktopMaxWidth, height, children, className, style, snapPoint, onSnapPointChange, getModalContentRef, ModalOverlay = ModalOverlayDefault, modalOverlayTestId, modalContentTestId, modalDismissButtonTestId, modalDismissButtonLabel = 'Закрыть', outsideButtons, noFocusToDialog, hideCloseButton, preventClose, disableContentPanningGesture, restoreFocus, onOpen, onOpened, onClose = noop, onClosed, disableFocusTrap, disableModalOverlay, disableOpenAnimation = false, disableCloseAnimation = false, ...restProps }: ModalPageInternalProps) => { const { hasCustomPanelHeaderAfter } = useConfigProvider(); const rootRef = useExternRef(getRootRef); const [transitionState, { ref, onTransitionEnd }] = useCSSTransition(open, { enableAppear: !disableOpenAnimation, enableEnter: !disableOpenAnimation, enableExit: !disableCloseAnimation, onEnter() { onOpen?.(); }, onEntered() { onOpened?.(); }, onExited() { onClosed?.(); }, }); const opened = transitionState === 'appeared' || transitionState === 'entered'; const hidden = transitionState === 'exited'; const closable = !preventClose && opened; const { sizeX: legacySizeXContext } = useAdaptivity(); const { isDesktop, sizeX: legacySizeX, viewWidth } = useAdaptivityWithJSMediaQueries(); const bottomSheetEnabled = !isDesktop && !preventClose && transitionState !== 'exited'; const { opened: keyboardOpened } = useVirtualKeyboardState(bottomSheetEnabled); const [{ initialStyle, setSheetEl, setSheetScrollEl, setBackdropEl }, bottomSheetEventHandlers] = useBottomSheet(bottomSheetEnabled, { blocked: keyboardOpened, snapPoint, sheetCSSProperty: '--vkui_internal_ModalPageDocument--snapPoint', backdropCSSProperty: '--vkui_internal--modal-overlay--opacity', onSnapPointChange, onDismiss() { onClose('swipe-down'); }, }); const documentStyle = keyboardOpened ? { '--vkui_internal_ModalPageDocument--safeAreaInsetBottom': '0px', ...initialStyle, } : initialStyle; const handleSheetRef = useExternRef(setSheetEl, ref); const handleSheetScrollRef = useExternRef(setSheetScrollEl, getModalContentRef); const [desktopMaxWidthClassName, desktopMaxWidthStyle] = isDesktop ? resolveDesktopMaxWidth(desktopMaxWidth) : [undefined, undefined]; const modalOverlay = !disableModalOverlay && ( ); const handleEscKeyDown = useCallback( (event: KeyboardEvent) => { if (closable && pressedKey(event) === Keys.ESCAPE) { onClose('escape-key'); } }, [closable, onClose], ); useScrollLock(!hidden); return ( ); }; const desktopMaxWidthClassNames: Record = { s: styles.hostDesktopMaxWidthS, m: styles.hostDesktopMaxWidthM, l: styles.hostDesktopMaxWidthL, }; function resolveDesktopMaxWidth( desktopMaxWidth: ModalPageInternalProps['size'] = 's', ): [string | undefined, CSSCustomProperties | undefined] { if (typeof desktopMaxWidth === 'number') { return [undefined, { '--vkui_internal_ModalPage--desktopMaxWidth': `${desktopMaxWidth}px` }]; } const className = desktopMaxWidthClassNames[desktopMaxWidth]; const style = className ? undefined : { '--vkui_internal_ModalPage--desktopMaxWidth': desktopMaxWidth }; return [className, style]; } function getHeightCSSVariable(height?: number | string): CSSCustomProperties | undefined { return height !== undefined ? { '--vkui_internal_ModalPage--userHeight': typeof height === 'number' ? `${height}px` : height, } : undefined; }