feat: Small confirmation dialogs (#3293)

* wip

* refinement
This commit is contained in:
Tom Moor
2022-03-30 17:11:19 -07:00
committed by GitHub
parent 7f3b602259
commit 6c25f8fc72
11 changed files with 148 additions and 62 deletions

View File

@@ -113,6 +113,10 @@ const RealButton = styled.button<{
&:disabled {
background: none;
}
&.focus-visible {
outline-color: ${darken(0.2, props.theme.danger)} !important;
}
`};
`;

View File

@@ -22,6 +22,7 @@ function Dialogs() {
<Modal
key={id}
isOpen={modal.isOpen}
isCentered={modal.isCentered}
onRequestClose={() => dialogs.closeModal(id)}
title={modal.title}
>

View File

@@ -9,6 +9,8 @@ import breakpoint from "styled-components-breakpoint";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import usePrevious from "~/hooks/usePrevious";
import useUnmount from "~/hooks/useUnmount";
import { fadeAndScaleIn } from "~/styles/animations";
@@ -16,6 +18,7 @@ import { fadeAndScaleIn } from "~/styles/animations";
let openModals = 0;
type Props = {
isOpen: boolean;
isCentered?: boolean;
title?: React.ReactNode;
onRequestClose: () => void;
};
@@ -23,6 +26,7 @@ type Props = {
const Modal: React.FC<Props> = ({
children,
isOpen,
isCentered,
title = "Untitled",
onRequestClose,
}) => {
@@ -31,6 +35,7 @@ const Modal: React.FC<Props> = ({
});
const [depth, setDepth] = React.useState(0);
const wasOpen = usePrevious(isOpen);
const isMobile = useMobile();
const { t } = useTranslation();
React.useEffect(() => {
@@ -58,37 +63,59 @@ const Modal: React.FC<Props> = ({
return (
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop {...props}>
<Backdrop $isCentered={isCentered} {...props}>
<Dialog
{...dialog}
preventBodyScroll
hideOnEsc
hideOnClickOutside={false}
hideOnClickOutside={isCentered}
hide={onRequestClose}
>
{(props) => (
<Scene
$nested={!!depth}
style={{
marginLeft: `${depth * 12}px`,
}}
{...props}
>
<Content>
{(props) =>
isCentered && !isMobile ? (
<Small {...props}>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
<Header>
{title && (
<Text as="span" size="large">
{title}
</Text>
)}
<NudeButton onClick={onRequestClose}>
<CloseIcon color="currentColor" />
</NudeButton>
</Header>
<SmallContent shadow>{children}</SmallContent>
</Centered>
</Content>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
<Text>{t("Back")}</Text>
</Back>
<Close onClick={onRequestClose}>
<CloseIcon size={32} color="currentColor" />
</Close>
</Scene>
)}
</Small>
) : (
<Fullscreen
$nested={!!depth}
style={
isMobile
? undefined
: {
marginLeft: `${depth * 12}px`,
}
}
{...props}
>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</Centered>
</Content>
<Close onClick={onRequestClose}>
<CloseIcon size={32} color="currentColor" />
</Close>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
<Text as="span">{t("Back")} </Text>
</Back>
</Fullscreen>
)
}
</Dialog>
</Backdrop>
)}
@@ -96,14 +123,16 @@ const Modal: React.FC<Props> = ({
);
};
const Backdrop = styled.div`
const Backdrop = styled(Flex)<{ $isCentered?: boolean }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) =>
transparentize(0.25, props.theme.background)} !important;
props.$isCentered
? props.theme.modalBackdrop
: transparentize(0.25, props.theme.background)} !important;
z-index: ${(props) => props.theme.depths.modalOverlay};
transition: opacity 50ms ease-in-out;
opacity: 0;
@@ -113,7 +142,7 @@ const Backdrop = styled.div`
}
`;
const Scene = styled.div<{ $nested: boolean }>`
const Fullscreen = styled.div<{ $nested: boolean }>`
animation: ${fadeAndScaleIn} 250ms ease;
position: absolute;
@@ -142,10 +171,10 @@ const Scene = styled.div<{ $nested: boolean }>`
const Content = styled(Scrollable)`
width: 100%;
padding: 8vh 2rem 2rem;
padding: 8vh 32px;
${breakpoint("tablet")`
padding-top: 13vh;
padding: 13vh 2rem 2rem;
`};
`;
@@ -156,13 +185,6 @@ const Centered = styled(Flex)`
margin: 0 auto;
`;
const Text = styled.span`
font-size: 16px;
font-weight: 500;
padding-right: 12px;
user-select: none;
`;
const Close = styled(NudeButton)`
position: absolute;
display: block;
@@ -191,6 +213,7 @@ const Back = styled(NudeButton)`
left: 2rem;
opacity: 0.75;
color: ${(props) => props.theme.text};
font-weight: 500;
width: auto;
height: auto;
@@ -203,4 +226,40 @@ const Back = styled(NudeButton)`
`};
`;
const Header = styled(Flex)`
color: ${(props) => props.theme.textSecondary};
align-items: center;
justify-content: space-between;
font-weight: 600;
padding: 24px 24px 4px;
`;
const Small = styled.div`
animation: ${fadeAndScaleIn} 250ms ease;
margin: auto auto;
min-width: 350px;
max-width: 30vw;
z-index: ${(props) => props.theme.depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
background: ${(props) => props.theme.modalBackground};
transition: ${(props) => props.theme.backgroundTransition};
box-shadow: ${(props) => props.theme.modalShadow};
border-radius: 8px;
outline: none;
${NudeButton} {
&:hover,
&[aria-expanded="true"] {
background: ${(props) => props.theme.sidebarControlHoverBackground};
}
}
`;
const SmallContent = styled(Scrollable)`
padding: 12px 24px 24px;
`;
export default observer(Modal);

View File

@@ -50,6 +50,7 @@ function TrashLink() {
})}
onRequestClose={() => setDocument(undefined)}
isOpen
isCentered
>
<DocumentDelete
document={document}

View File

@@ -54,7 +54,7 @@ function CollectionMenu({
});
const [renderModals, setRenderModals] = React.useState(false);
const team = useCurrentTeam();
const { documents } = useStores();
const { documents, dialogs } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const history = useHistory();
@@ -64,7 +64,6 @@ function CollectionMenu({
setShowCollectionPermissions,
] = React.useState(false);
const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
const handleOpen = React.useCallback(() => {
@@ -139,6 +138,19 @@ function CollectionMenu({
[collection, menu]
);
const handleDelete = React.useCallback(() => {
dialogs.openModal({
isCentered: true,
title: t("Delete collection"),
content: (
<CollectionDelete
collection={collection}
onSubmit={dialogs.closeAllModals}
/>
),
});
}, [dialogs, t, collection]);
const alphabeticalSort = collection.sort.field === "title";
const can = usePolicy(collection.id);
const canUserInTeam = usePolicy(team.id);
@@ -214,7 +226,7 @@ function CollectionMenu({
title: `${t("Delete")}`,
dangerous: true,
visible: !!(collection && can.delete),
onClick: () => setShowCollectionDelete(true),
onClick: handleDelete,
icon: <TrashIcon />,
},
],
@@ -226,6 +238,7 @@ function CollectionMenu({
handleChangeSort,
handleNewDocument,
handleImportDocument,
handleDelete,
collection,
canUserInTeam.export,
]
@@ -282,16 +295,6 @@ function CollectionMenu({
collectionId={collection.id}
/>
</Modal>
<Modal
title={t("Delete collection")}
isOpen={showCollectionDelete}
onRequestClose={() => setShowCollectionDelete(false)}
>
<CollectionDelete
onSubmit={() => setShowCollectionDelete(false)}
collection={collection}
/>
</Modal>
<Modal
title={t("Export collection")}
isOpen={showCollectionExport}

View File

@@ -493,6 +493,7 @@ function DocumentMenu({
})}
onRequestClose={() => setShowDeleteModal(false)}
isOpen={showDeleteModal}
isCentered
>
<DocumentDelete
document={document}
@@ -507,6 +508,7 @@ function DocumentMenu({
})}
onRequestClose={() => setShowPermanentDeleteModal(false)}
isOpen={showPermanentDeleteModal}
isCentered
>
<DocumentPermanentDelete
document={document}
@@ -519,6 +521,7 @@ function DocumentMenu({
title={t("Create template")}
onRequestClose={() => setShowTemplateModal(false)}
isOpen={showTemplateModal}
isCentered
>
<DocumentTemplatize
documentId={document.id}

View File

@@ -39,6 +39,7 @@ function GroupMenu({ group, onMembers }: Props) {
title={t("Delete group")}
onRequestClose={() => setDeleteModalOpen(false)}
isOpen={deleteModalOpen}
isCentered
>
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
</Modal>

View File

@@ -2,23 +2,19 @@ import { observable, action } from "mobx";
import * as React from "react";
import { v4 as uuidv4 } from "uuid";
type DialogDefinition = {
title: string;
content: React.ReactNode;
isOpen: boolean;
isCentered?: boolean;
};
export default class DialogsStore {
@observable
guide: {
title: string;
content: React.ReactNode;
isOpen: boolean;
};
guide: DialogDefinition;
@observable
modalStack = new Map<
string,
{
title: string;
content: React.ReactNode;
isOpen: boolean;
}
>();
modalStack = new Map<string, DialogDefinition>();
openGuide = ({
title,
@@ -49,9 +45,11 @@ export default class DialogsStore {
openModal = ({
title,
content,
isCentered,
replace,
}: {
title: string;
isCentered?: boolean;
content: React.ReactNode;
replace?: boolean;
}) => {
@@ -67,6 +65,7 @@ export default class DialogsStore {
title,
content,
isOpen: true,
isCentered,
});
}),
0

View File

@@ -156,6 +156,9 @@ declare module "styled-components" {
sidebarText: string;
backdrop: string;
shadow: string;
modalBackdrop: string;
modalBackground: string;
modalShadow: string;
menuItemSelected: string;
menuBackground: string;
menuShadow: string;

View File

@@ -234,6 +234,7 @@
"Path to document": "Path to document",
"Group member options": "Group member options",
"Remove": "Remove",
"Delete collection": "Delete collection",
"Sort in sidebar": "Sort in sidebar",
"Alphabetical sort": "Alphabetical sort",
"Manual sort": "Manual sort",
@@ -241,7 +242,6 @@
"Permissions": "Permissions",
"Delete": "Delete",
"Collection permissions": "Collection permissions",
"Delete collection": "Delete collection",
"Export collection": "Export collection",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",

View File

@@ -140,6 +140,12 @@ export const light = {
sidebarText: "rgb(78, 92, 110)",
backdrop: "rgba(0, 0, 0, 0.2)",
shadow: "rgba(0, 0, 0, 0.2)",
modalBackdrop: colors.black10,
modalBackground: colors.white,
modalShadow:
"0 4px 8px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 0%), 0 30px 40px rgb(0 0 0 / 8%)",
menuItemSelected: colors.warmGrey,
menuBackground: colors.white,
menuShadow:
@@ -191,6 +197,12 @@ export const dark = {
sidebarText: colors.slate,
backdrop: "rgba(255, 255, 255, 0.3)",
shadow: "rgba(0, 0, 0, 0.6)",
modalBackdrop: colors.black50,
modalBackground: "#1f2128",
modalShadow:
"0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08)",
menuItemSelected: lighten(0.1, "#1f2128"),
menuBackground: "#1f2128",
menuShadow: