From 6c25f8fc72dfdf25f84beaba2b424268a3b1dd75 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 30 Mar 2022 17:11:19 -0700 Subject: [PATCH] feat: Small confirmation dialogs (#3293) * wip * refinement --- app/components/Button.tsx | 4 + app/components/Dialogs.tsx | 1 + app/components/Modal.tsx | 129 +++++++++++++----- .../Sidebar/components/TrashLink.tsx | 1 + app/menus/CollectionMenu.tsx | 29 ++-- app/menus/DocumentMenu.tsx | 3 + app/menus/GroupMenu.tsx | 1 + app/stores/DialogsStore.ts | 25 ++-- app/typings/styled-components.d.ts | 3 + shared/i18n/locales/en_US/translation.json | 2 +- shared/theme.ts | 12 ++ 11 files changed, 148 insertions(+), 62 deletions(-) diff --git a/app/components/Button.tsx b/app/components/Button.tsx index e8171c2f5..8f4a22527 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -113,6 +113,10 @@ const RealButton = styled.button<{ &:disabled { background: none; } + + &.focus-visible { + outline-color: ${darken(0.2, props.theme.danger)} !important; + } `}; `; diff --git a/app/components/Dialogs.tsx b/app/components/Dialogs.tsx index 57e42ddce..8cf15fe4e 100644 --- a/app/components/Dialogs.tsx +++ b/app/components/Dialogs.tsx @@ -22,6 +22,7 @@ function Dialogs() { dialogs.closeModal(id)} title={modal.title} > diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx index 3f81ecc16..d54ff4915 100644 --- a/app/components/Modal.tsx +++ b/app/components/Modal.tsx @@ -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 = ({ children, isOpen, + isCentered, title = "Untitled", onRequestClose, }) => { @@ -31,6 +35,7 @@ const Modal: React.FC = ({ }); 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 = ({ return ( {(props) => ( - + - {(props) => ( - - + {(props) => + isCentered && !isMobile ? ( + ev.stopPropagation()} column> - {title &&

{title}

} - {children} +
+ {title && ( + + {title} + + )} + + + +
+ {children}
-
- - - {t("Back")} - - - - -
- )} + + ) : ( + + + ev.stopPropagation()} column> + {title &&

{title}

} + {children} +
+
+ + + + + + {t("Back")} + +
+ ) + }
)} @@ -96,14 +123,16 @@ const Modal: React.FC = ({ ); }; -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); diff --git a/app/components/Sidebar/components/TrashLink.tsx b/app/components/Sidebar/components/TrashLink.tsx index 5dbdf6ba7..5ba9bee61 100644 --- a/app/components/Sidebar/components/TrashLink.tsx +++ b/app/components/Sidebar/components/TrashLink.tsx @@ -50,6 +50,7 @@ function TrashLink() { })} onRequestClose={() => setDocument(undefined)} isOpen + isCentered > { @@ -139,6 +138,19 @@ function CollectionMenu({ [collection, menu] ); + const handleDelete = React.useCallback(() => { + dialogs.openModal({ + isCentered: true, + title: t("Delete collection"), + content: ( + + ), + }); + }, [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: , }, ], @@ -226,6 +238,7 @@ function CollectionMenu({ handleChangeSort, handleNewDocument, handleImportDocument, + handleDelete, collection, canUserInTeam.export, ] @@ -282,16 +295,6 @@ function CollectionMenu({ collectionId={collection.id} />
- setShowCollectionDelete(false)} - > - setShowCollectionDelete(false)} - collection={collection} - /> - setShowDeleteModal(false)} isOpen={showDeleteModal} + isCentered > setShowPermanentDeleteModal(false)} isOpen={showPermanentDeleteModal} + isCentered > setShowTemplateModal(false)} isOpen={showTemplateModal} + isCentered > setDeleteModalOpen(false)} isOpen={deleteModalOpen} + isCentered > setDeleteModalOpen(false)} /> diff --git a/app/stores/DialogsStore.ts b/app/stores/DialogsStore.ts index de0483a10..a509a5ed6 100644 --- a/app/stores/DialogsStore.ts +++ b/app/stores/DialogsStore.ts @@ -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(); 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 diff --git a/app/typings/styled-components.d.ts b/app/typings/styled-components.d.ts index 51fe0effe..042a141de 100644 --- a/app/typings/styled-components.d.ts +++ b/app/typings/styled-components.d.ts @@ -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; diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index f78d40d43..b2ee70dfe 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -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", diff --git a/shared/theme.ts b/shared/theme.ts index 3be9638a1..d55546b30 100644 --- a/shared/theme.ts +++ b/shared/theme.ts @@ -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: