signal-desktop/ts/components/Modal.tsx

203 lines
5.4 KiB
TypeScript
Raw Normal View History

2022-03-04 21:14:52 +00:00
// 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';
2021-10-14 16:52:42 +00:00
import { animated } from '@react-spring/web';
import type { LocalizerType } from '../types/Util';
import { ModalHost } from './ModalHost';
import type { Theme } from '../util/theme';
2021-05-11 00:50:43 +00:00
import { getClassNamesFor } from '../util/getClassNamesFor';
import { useAnimated } from '../hooks/useAnimated';
2021-09-17 22:24:21 +00:00
import { useHasWrapped } from '../hooks/useHasWrapped';
import { useRefMerger } from '../hooks/useRefMerger';
type PropsType = {
children: ReactNode;
2021-08-06 00:17:05 +00:00
hasStickyButtons?: boolean;
hasXButton?: boolean;
i18n: LocalizerType;
2021-04-21 16:31:12 +00:00
moduleClassName?: string;
onClose?: () => void;
title?: ReactNode;
2022-03-04 21:14:52 +00:00
useFocusTrap?: boolean;
};
type ModalPropsType = PropsType & {
noMouseClose?: boolean;
theme?: Theme;
};
2021-05-11 00:50:43 +00:00
const BASE_CLASS_NAME = 'module-Modal';
export function Modal({
children,
2021-08-06 00:17:05 +00:00
hasStickyButtons,
hasXButton,
i18n,
2021-04-21 16:31:12 +00:00
moduleClassName,
2021-05-28 16:15:17 +00:00
noMouseClose,
onClose = noop,
title,
theme,
2022-03-04 21:14:52 +00:00
useFocusTrap,
}: Readonly<ModalPropsType>): ReactElement {
2021-10-14 16:52:42 +00:00
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 (
2021-10-14 16:52:42 +00:00
<ModalHost
2022-03-04 21:14:52 +00:00
moduleClassName={moduleClassName}
2021-10-14 16:52:42 +00:00
noMouseClose={noMouseClose}
onClose={close}
overlayStyles={overlayStyles}
theme={theme}
2022-03-04 21:14:52 +00:00
useFocusTrap={useFocusTrap}
2021-10-14 16:52:42 +00:00
>
<animated.div style={modalStyles}>
<ModalWindow
hasStickyButtons={hasStickyButtons}
hasXButton={hasXButton}
i18n={i18n}
moduleClassName={moduleClassName}
onClose={close}
title={title}
>
{children}
</ModalWindow>
2021-10-14 16:52:42 +00:00
</animated.div>
</ModalHost>
);
}
export function ModalWindow({
children,
hasStickyButtons,
hasXButton,
i18n,
moduleClassName,
onClose = noop,
title,
}: Readonly<PropsType>): JSX.Element {
2021-08-06 00:17:05 +00:00
const modalRef = useRef<HTMLDivElement | null>(null);
const refMerger = useRefMerger();
2021-09-21 16:25:21 +00:00
const bodyRef = useRef<HTMLDivElement | null>(null);
const [scrolled, setScrolled] = useState(false);
2021-08-06 00:17:05 +00:00
const [hasOverflow, setHasOverflow] = useState(false);
const hasHeader = Boolean(hasXButton || title);
2021-05-11 00:50:43 +00:00
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
2021-08-06 00:17:05 +00:00
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 */}
<div
className={classNames(
2021-05-11 00:50:43 +00:00
getClassName(''),
2021-08-06 00:17:05 +00:00
getClassName(hasHeader ? '--has-header' : '--no-header'),
hasStickyButtons && getClassName('--sticky-buttons')
)}
2021-08-06 00:17:05 +00:00
ref={modalRef}
onClick={event => {
event.stopPropagation();
}}
>
{hasHeader && (
2021-05-11 00:50:43 +00:00
<div className={getClassName('__header')}>
{hasXButton && (
<button
aria-label={i18n('close')}
type="button"
2021-05-11 00:50:43 +00:00
className={getClassName('__close-button')}
tabIndex={0}
onClick={onClose}
/>
)}
{title && (
<h1
className={classNames(
2021-05-11 00:50:43 +00:00
getClassName('__title'),
hasXButton ? getClassName('__title--with-x-button') : null
)}
>
{title}
</h1>
)}
</div>
)}
2021-08-06 00:17:05 +00:00
<Measure scroll onResize={handleResize}>
{({ measureRef }: MeasuredComponentProps) => (
<div
className={classNames(
getClassName('__body'),
scrolled ? getClassName('__body--scrolled') : null,
hasOverflow || scrolled
? getClassName('__body--overflow')
: null
)}
2021-09-21 16:25:21 +00:00
onScroll={() => {
const scrollTop = bodyRef.current?.scrollTop || 0;
setScrolled(scrollTop > 2);
}}
ref={refMerger(measureRef, bodyRef)}
2021-08-06 00:17:05 +00:00
>
{children}
</div>
2021-05-11 00:50:43 +00:00
)}
2021-08-06 00:17:05 +00:00
</Measure>
</div>
</>
);
}
Modal.ButtonFooter = function ButtonFooter({
children,
2021-05-11 00:50:43 +00:00
moduleClassName,
}: Readonly<{
children: ReactNode;
moduleClassName?: string;
}>): ReactElement {
const [ref, hasWrapped] = useHasWrapped<HTMLDivElement>();
const className = getClassNamesFor(
BASE_CLASS_NAME,
moduleClassName
)('__button-footer');
return (
<div
className={classNames(
className,
hasWrapped ? `${className}--one-button-per-line` : undefined
)}
ref={ref}
>
{children}
</div>
);
};