// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactElement, ReactNode } from 'react'; import React, { useRef, useState } from 'react'; import type { ContentRect, MeasuredComponentProps } from 'react-measure'; import Measure from 'react-measure'; import classNames from 'classnames'; import { noop } from 'lodash'; import { animated } from '@react-spring/web'; import type { LocalizerType } from '../types/Util'; import { ModalHost } from './ModalHost'; import type { Theme } from '../util/theme'; import { getClassNamesFor } from '../util/getClassNamesFor'; import { useAnimated } from '../hooks/useAnimated'; import { useHasWrapped } from '../hooks/useHasWrapped'; import { useRefMerger } from '../hooks/useRefMerger'; type PropsType = { children: ReactNode; hasStickyButtons?: boolean; hasXButton?: boolean; i18n: LocalizerType; moduleClassName?: string; onClose?: () => void; title?: ReactNode; useFocusTrap?: boolean; }; type ModalPropsType = PropsType & { noMouseClose?: boolean; theme?: Theme; }; const BASE_CLASS_NAME = 'module-Modal'; export function Modal({ children, hasStickyButtons, hasXButton, i18n, moduleClassName, noMouseClose, onClose = noop, title, theme, useFocusTrap, }: Readonly): ReactElement { const { close, modalStyles, overlayStyles } = useAnimated(onClose, { getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }), getTo: isOpen => isOpen ? { opacity: 1, transform: 'translateY(0px)' } : { opacity: 0, transform: 'translateY(48px)' }, }); return ( {children} ); } export function ModalWindow({ children, hasStickyButtons, hasXButton, i18n, moduleClassName, onClose = noop, title, }: Readonly): JSX.Element { const modalRef = useRef(null); const refMerger = useRefMerger(); const bodyRef = useRef(null); const [scrolled, setScrolled] = useState(false); const [hasOverflow, setHasOverflow] = useState(false); const hasHeader = Boolean(hasXButton || title); const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName); function handleResize({ scroll }: ContentRect) { const modalNode = modalRef?.current; if (!modalNode) { return; } if (scroll) { setHasOverflow(scroll.height > modalNode.clientHeight); } } return ( <> {/* We don't want the click event to propagate to its container node. */} {/* eslint-disable-next-line max-len */} {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
{ event.stopPropagation(); }} > {hasHeader && (
{hasXButton && (
)} {({ measureRef }: MeasuredComponentProps) => (
{ const scrollTop = bodyRef.current?.scrollTop || 0; setScrolled(scrollTop > 2); }} ref={refMerger(measureRef, bodyRef)} > {children}
)}
); } Modal.ButtonFooter = function ButtonFooter({ children, moduleClassName, }: Readonly<{ children: ReactNode; moduleClassName?: string; }>): ReactElement { const [ref, hasWrapped] = useHasWrapped(); const className = getClassNamesFor( BASE_CLASS_NAME, moduleClassName )('__button-footer'); return (
{children}
); };