2021-04-13 14:20:02 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-08-06 00:17:05 +00:00
|
|
|
import React, { useRef, useState, ReactElement, ReactNode } from 'react';
|
|
|
|
import Measure, { ContentRect, MeasuredComponentProps } from 'react-measure';
|
2021-04-13 14:20:02 +00:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { noop } from 'lodash';
|
|
|
|
|
|
|
|
import { LocalizerType } from '../types/Util';
|
|
|
|
import { ModalHost } from './ModalHost';
|
2021-04-27 19:29:59 +00:00
|
|
|
import { Theme } from '../util/theme';
|
2021-05-11 00:50:43 +00:00
|
|
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
2021-05-27 20:17:05 +00:00
|
|
|
import { useHasWrapped } from '../util/hooks';
|
2021-04-13 14:20:02 +00:00
|
|
|
|
|
|
|
type PropsType = {
|
|
|
|
children: ReactNode;
|
2021-08-06 00:17:05 +00:00
|
|
|
hasStickyButtons?: boolean;
|
2021-04-13 14:20:02 +00:00
|
|
|
hasXButton?: boolean;
|
|
|
|
i18n: LocalizerType;
|
2021-04-21 16:31:12 +00:00
|
|
|
moduleClassName?: string;
|
2021-05-28 16:15:17 +00:00
|
|
|
noMouseClose?: boolean;
|
2021-04-13 14:20:02 +00:00
|
|
|
onClose?: () => void;
|
|
|
|
title?: ReactNode;
|
2021-04-27 19:29:59 +00:00
|
|
|
theme?: Theme;
|
2021-04-13 14:20:02 +00:00
|
|
|
};
|
|
|
|
|
2021-05-11 00:50:43 +00:00
|
|
|
const BASE_CLASS_NAME = 'module-Modal';
|
|
|
|
|
2021-04-13 14:20:02 +00:00
|
|
|
export function Modal({
|
|
|
|
children,
|
2021-08-06 00:17:05 +00:00
|
|
|
hasStickyButtons,
|
2021-04-13 14:20:02 +00:00
|
|
|
hasXButton,
|
|
|
|
i18n,
|
2021-04-21 16:31:12 +00:00
|
|
|
moduleClassName,
|
2021-05-28 16:15:17 +00:00
|
|
|
noMouseClose,
|
2021-04-13 14:20:02 +00:00
|
|
|
onClose = noop,
|
|
|
|
title,
|
2021-04-27 19:29:59 +00:00
|
|
|
theme,
|
2021-04-13 14:20:02 +00:00
|
|
|
}: Readonly<PropsType>): ReactElement {
|
2021-08-06 00:17:05 +00:00
|
|
|
const modalRef = useRef<HTMLDivElement | null>(null);
|
2021-04-13 14:20:02 +00:00
|
|
|
const [scrolled, setScrolled] = useState(false);
|
2021-08-06 00:17:05 +00:00
|
|
|
const [hasOverflow, setHasOverflow] = useState(false);
|
2021-04-13 14:20:02 +00:00
|
|
|
|
|
|
|
const hasHeader = Boolean(hasXButton || title);
|
2021-05-11 00:50:43 +00:00
|
|
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
2021-04-13 14:20:02 +00:00
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 14:20:02 +00:00
|
|
|
return (
|
2021-05-28 16:15:17 +00:00
|
|
|
<ModalHost noMouseClose={noMouseClose} onClose={onClose} theme={theme}>
|
2021-09-02 19:35:23 +00:00
|
|
|
{/* We don't want the click event to propagate to its container node. */}
|
|
|
|
{/* eslint-disable jsx-a11y/no-static-element-interactions */}
|
|
|
|
{/* eslint-disable jsx-a11y/click-events-have-key-events */}
|
2021-04-13 14:20:02 +00:00
|
|
|
<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-04-13 14:20:02 +00:00
|
|
|
)}
|
2021-08-06 00:17:05 +00:00
|
|
|
ref={modalRef}
|
2021-09-02 19:35:23 +00:00
|
|
|
onClick={event => {
|
|
|
|
event.stopPropagation();
|
|
|
|
}}
|
2021-04-13 14:20:02 +00:00
|
|
|
>
|
2021-09-02 19:35:23 +00:00
|
|
|
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
|
|
|
|
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
|
2021-04-13 14:20:02 +00:00
|
|
|
{hasHeader && (
|
2021-05-11 00:50:43 +00:00
|
|
|
<div className={getClassName('__header')}>
|
2021-04-13 14:20:02 +00:00
|
|
|
{hasXButton && (
|
|
|
|
<button
|
|
|
|
aria-label={i18n('close')}
|
|
|
|
type="button"
|
2021-05-11 00:50:43 +00:00
|
|
|
className={getClassName('__close-button')}
|
2021-05-06 00:09:29 +00:00
|
|
|
tabIndex={0}
|
2021-04-13 14:20:02 +00:00
|
|
|
onClick={() => {
|
|
|
|
onClose();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
2021-05-06 00:09:29 +00:00
|
|
|
{title && (
|
|
|
|
<h1
|
|
|
|
className={classNames(
|
2021-05-11 00:50:43 +00:00
|
|
|
getClassName('__title'),
|
|
|
|
hasXButton ? getClassName('__title--with-x-button') : null
|
2021-05-06 00:09:29 +00:00
|
|
|
)}
|
|
|
|
>
|
|
|
|
{title}
|
|
|
|
</h1>
|
|
|
|
)}
|
2021-04-13 14:20:02 +00:00
|
|
|
</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
|
|
|
|
)}
|
|
|
|
onScroll={event => {
|
|
|
|
setScrolled((event.target as HTMLDivElement).scrollTop > 2);
|
|
|
|
}}
|
|
|
|
ref={measureRef}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</div>
|
2021-05-11 00:50:43 +00:00
|
|
|
)}
|
2021-08-06 00:17:05 +00:00
|
|
|
</Measure>
|
2021-04-13 14:20:02 +00:00
|
|
|
</div>
|
|
|
|
</ModalHost>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:17:05 +00:00
|
|
|
Modal.ButtonFooter = function ButtonFooter({
|
2021-04-13 14:20:02 +00:00
|
|
|
children,
|
2021-05-11 00:50:43 +00:00
|
|
|
moduleClassName,
|
|
|
|
}: Readonly<{
|
|
|
|
children: ReactNode;
|
|
|
|
moduleClassName?: string;
|
2021-05-27 20:17:05 +00:00
|
|
|
}>): 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>
|
|
|
|
);
|
|
|
|
};
|