2021-07-05 11:23:23 +01:00
|
|
|
import styled, { css, keyframes } from "styled-components";
|
|
|
|
|
2021-06-19 18:46:05 +01:00
|
|
|
import classNames from "classnames";
|
|
|
|
import { createPortal, useEffect } from "preact/compat";
|
2021-07-05 11:23:23 +01:00
|
|
|
|
|
|
|
import { Children } from "../../types/Preact";
|
|
|
|
import Button from "./Button";
|
2021-06-19 18:46:05 +01:00
|
|
|
|
|
|
|
const open = keyframes`
|
|
|
|
0% {opacity: 0;}
|
|
|
|
70% {opacity: 0;}
|
|
|
|
100% {opacity: 1;}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const zoomIn = keyframes`
|
|
|
|
0% {transform: scale(0.5);}
|
|
|
|
98% {transform: scale(1.01);}
|
|
|
|
100% {transform: scale(1);}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const ModalBase = styled.div`
|
2021-07-05 11:25:20 +01:00
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
z-index: 9999;
|
|
|
|
position: fixed;
|
|
|
|
max-height: 100%;
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
animation-name: ${open};
|
|
|
|
animation-duration: 0.2s;
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
overflow-y: auto;
|
|
|
|
place-items: center;
|
|
|
|
|
|
|
|
color: var(--foreground);
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
2021-06-19 18:46:05 +01:00
|
|
|
`;
|
|
|
|
|
|
|
|
const ModalContainer = styled.div`
|
2021-07-05 11:25:20 +01:00
|
|
|
overflow: hidden;
|
|
|
|
border-radius: 8px;
|
|
|
|
max-width: calc(100vw - 20px);
|
2021-06-19 18:46:05 +01:00
|
|
|
|
2021-07-05 11:25:20 +01:00
|
|
|
animation-name: ${zoomIn};
|
|
|
|
animation-duration: 0.25s;
|
|
|
|
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
|
2021-06-19 18:46:05 +01:00
|
|
|
`;
|
|
|
|
|
2021-07-05 11:23:23 +01:00
|
|
|
const ModalContent = styled.div<
|
2021-07-05 11:25:20 +01:00
|
|
|
{ [key in "attachment" | "noBackground" | "border" | "padding"]?: boolean }
|
2021-07-05 11:23:23 +01:00
|
|
|
>`
|
2021-07-05 11:25:20 +01:00
|
|
|
border-radius: 8px;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
margin-top: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
form {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
}
|
|
|
|
|
|
|
|
${(props) =>
|
|
|
|
!props.noBackground &&
|
|
|
|
css`
|
|
|
|
background: var(--secondary-header);
|
|
|
|
`}
|
|
|
|
|
|
|
|
${(props) =>
|
|
|
|
props.padding &&
|
|
|
|
css`
|
|
|
|
padding: 1.5em;
|
|
|
|
`}
|
2021-07-05 11:23:23 +01:00
|
|
|
|
|
|
|
${(props) =>
|
2021-07-05 11:25:20 +01:00
|
|
|
props.attachment &&
|
|
|
|
css`
|
|
|
|
border-radius: 8px 8px 0 0;
|
|
|
|
`}
|
2021-07-05 11:23:23 +01:00
|
|
|
|
|
|
|
${(props) =>
|
2021-07-05 11:25:20 +01:00
|
|
|
props.border &&
|
|
|
|
css`
|
|
|
|
border-radius: 10px;
|
|
|
|
border: 2px solid var(--secondary-background);
|
|
|
|
`}
|
2021-06-19 18:46:05 +01:00
|
|
|
`;
|
|
|
|
|
|
|
|
const ModalActions = styled.div`
|
2021-07-05 11:25:20 +01:00
|
|
|
gap: 8px;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row-reverse;
|
2021-06-19 18:46:05 +01:00
|
|
|
|
2021-07-05 11:25:20 +01:00
|
|
|
padding: 1em 1.5em;
|
|
|
|
border-radius: 0 0 8px 8px;
|
|
|
|
background: var(--secondary-background);
|
2021-06-19 18:46:05 +01:00
|
|
|
`;
|
|
|
|
|
|
|
|
export interface Action {
|
2021-07-05 11:25:20 +01:00
|
|
|
text: Children;
|
|
|
|
onClick: () => void;
|
|
|
|
confirmation?: boolean;
|
|
|
|
contrast?: boolean;
|
|
|
|
error?: boolean;
|
2021-06-19 18:46:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Props {
|
2021-07-05 11:25:20 +01:00
|
|
|
children?: Children;
|
|
|
|
title?: Children;
|
|
|
|
|
|
|
|
disallowClosing?: boolean;
|
|
|
|
noBackground?: boolean;
|
|
|
|
dontModal?: boolean;
|
|
|
|
padding?: boolean;
|
|
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
actions?: Action[];
|
|
|
|
disabled?: boolean;
|
|
|
|
border?: boolean;
|
|
|
|
visible: boolean;
|
2021-06-19 18:46:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export default function Modal(props: Props) {
|
2021-07-05 11:25:20 +01:00
|
|
|
if (!props.visible) return null;
|
|
|
|
|
|
|
|
let content = (
|
|
|
|
<ModalContent
|
|
|
|
attachment={!!props.actions}
|
|
|
|
noBackground={props.noBackground}
|
|
|
|
border={props.border}
|
|
|
|
padding={props.padding ?? !props.dontModal}>
|
|
|
|
{props.title && <h3>{props.title}</h3>}
|
|
|
|
{props.children}
|
|
|
|
</ModalContent>
|
|
|
|
);
|
|
|
|
|
|
|
|
if (props.dontModal) {
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (props.disallowClosing) return;
|
|
|
|
|
|
|
|
function keyDown(e: KeyboardEvent) {
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
props.onClose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
document.body.addEventListener("keydown", keyDown);
|
|
|
|
return () => document.body.removeEventListener("keydown", keyDown);
|
|
|
|
}, [props.disallowClosing, props.onClose]);
|
|
|
|
|
|
|
|
let confirmationAction = props.actions?.find(
|
|
|
|
(action) => action.confirmation,
|
|
|
|
);
|
|
|
|
useEffect(() => {
|
|
|
|
if (!confirmationAction) return;
|
|
|
|
|
|
|
|
// ! FIXME: this may be done better if we
|
|
|
|
// ! can focus the button although that
|
|
|
|
// ! doesn't seem to work...
|
|
|
|
function keyDown(e: KeyboardEvent) {
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
confirmationAction!.onClick();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
document.body.addEventListener("keydown", keyDown);
|
|
|
|
return () => document.body.removeEventListener("keydown", keyDown);
|
|
|
|
}, [confirmationAction]);
|
|
|
|
|
|
|
|
return createPortal(
|
|
|
|
<ModalBase
|
|
|
|
onClick={(!props.disallowClosing && props.onClose) || undefined}>
|
|
|
|
<ModalContainer onClick={(e) => (e.cancelBubble = true)}>
|
|
|
|
{content}
|
|
|
|
{props.actions && (
|
|
|
|
<ModalActions>
|
|
|
|
{props.actions.map((x) => (
|
|
|
|
<Button
|
|
|
|
contrast={x.contrast ?? true}
|
|
|
|
error={x.error ?? false}
|
|
|
|
onClick={x.onClick}
|
|
|
|
disabled={props.disabled}>
|
|
|
|
{x.text}
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</ModalActions>
|
|
|
|
)}
|
|
|
|
</ModalContainer>
|
|
|
|
</ModalBase>,
|
|
|
|
document.body,
|
|
|
|
);
|
2021-06-19 18:46:05 +01:00
|
|
|
}
|