57 lines
1.9 KiB
TypeScript
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>
|
|
);
|
|
}
|