textmachine/frontend/src/ui/Modal.tsx

57 lines
1.9 KiB
TypeScript

import { X } from 'lucide-react';
import type { ReactNode } from 'react';
import { Dialog, Heading, Modal as AriaModal, ModalOverlay } from 'react-aria-components';
import { useText } from '../i18n/text';
import { Button } from './Button';
import { icon } from './icon';
import styles from './Modal.module.css';
interface Props {
title: string;
isOpen: boolean;
onClose: () => void;
/** The wide form — for the go-to palette: an input line plus a list of results. */
size?: 'regular' | 'wide';
/** The action row nailed to the bottom; the palette does not have one. */
footer?: ReactNode;
children: ReactNode;
}
/**
* A modal window over the shell (remark 11): settings and adding a book open with it, and not
* with a document tab. A document tab gives the screen the whole width of the centre and demands
* closing with the cross — for a short form that is an extra trace in the row of tabs.
*
* The focus trap, the return of focus, Escape and a click outside are the primitive's work, not
* ours.
*/
export function Modal({ title, isOpen, onClose, size = 'regular', footer, children }: Props) {
const text = useText();
return (
<ModalOverlay
className={styles.overlay}
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
isDismissable
>
<AriaModal className={styles.window} data-size={size}>
<Dialog className={styles.dialog}>
<div className={styles.header}>
<Heading className={styles.title} slot="title">
{title}
</Heading>
<Button look="icon" aria-label={text('modal.close')} onPress={onClose}>
<X {...icon} />
</Button>
</div>
<div className={styles.body}>{children}</div>
{footer && <div className={styles.footer}>{footer}</div>}
</Dialog>
</AriaModal>
</ModalOverlay>
);
}