diff --git a/app/components/Modal.js b/app/components/Modal.js index 03b8518dc..05caa8ca2 100644 --- a/app/components/Modal.js +++ b/app/components/Modal.js @@ -4,15 +4,17 @@ import { CloseIcon, BackIcon } from "outline-icons"; import { transparentize } from "polished"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import ReactModal from "react-modal"; -import styled, { createGlobalStyle } from "styled-components"; +import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog"; +import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { fadeAndScaleIn } from "shared/styles/animations"; import Flex from "components/Flex"; import NudeButton from "components/NudeButton"; import Scrollable from "components/Scrollable"; +import usePrevious from "hooks/usePrevious"; +import useUnmount from "hooks/useUnmount"; -ReactModal.setAppElement("#root"); +let openModals = 0; type Props = {| children?: React.Node, @@ -21,44 +23,6 @@ type Props = {| onRequestClose: () => void, |}; -const GlobalStyles = createGlobalStyle` - .ReactModal__Overlay { - background-color: ${(props) => - transparentize(0.25, props.theme.background)} !important; - z-index: ${(props) => props.theme.depths.modalOverlay}; - } - - ${breakpoint("tablet")` - .ReactModalPortal + .ReactModalPortal, - .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal { - .ReactModal__Overlay { - margin-left: 12px; - box-shadow: 0 -2px 10px ${(props) => props.theme.shadow}; - border-radius: 8px 0 0 8px; - overflow: hidden; - } - } - - .ReactModalPortal + .ReactModalPortal + .ReactModalPortal, - .ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal { - .ReactModal__Overlay { - margin-left: 24px; - } - } - - .ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal, - .ReactModalPortal + .ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal { - .ReactModal__Overlay { - margin-left: 36px; - } - } - `}; - - .ReactModal__Body--open { - overflow: hidden; - } -`; - const Modal = ({ children, isOpen, @@ -66,36 +30,112 @@ const Modal = ({ onRequestClose, ...rest }: Props) => { + const dialog = useDialogState({ animated: 250 }); + const [depth, setDepth] = React.useState(0); + const wasOpen = usePrevious(isOpen); const { t } = useTranslation(); + + React.useEffect(() => { + if (!wasOpen && isOpen) { + setDepth(openModals++); + dialog.show(); + } + if (wasOpen && !isOpen) { + setDepth(openModals--); + dialog.hide(); + } + }, [dialog, wasOpen, isOpen]); + + useUnmount(() => { + if (isOpen) { + openModals--; + } + }); + if (!isOpen) return null; return ( - <> - - - - ev.stopPropagation()} column> - {title &&

{title}

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

{title}

} + {children} +
+
+ + + {t("Back")} + + + + +
+ )} +
+
+ )} +
); }; +const Backdrop = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${(props) => + transparentize(0.25, props.theme.background)} !important; + z-index: ${(props) => props.theme.depths.modalOverlay}; + transition: opacity 50ms ease-in-out; + opacity: 0; + + &[data-enter] { + opacity: 1; + } +`; + +const Scene = styled.div` + animation: ${fadeAndScaleIn} 250ms ease; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: ${(props) => props.theme.depths.modal}; + display: flex; + justify-content: center; + align-items: flex-start; + background: ${(props) => props.theme.background}; + transition: ${(props) => props.theme.backgroundTransition}; + outline: none; + + ${breakpoint("tablet")` + ${(props) => + props.$nested && + ` + box-shadow: 0 -2px 10px ${props.theme.shadow}; + border-radius: 8px 0 0 8px; + overflow: hidden; + `} +`} +`; + const Content = styled(Scrollable)` width: 100%; padding: 8vh 2rem 2rem; @@ -112,23 +152,6 @@ const Centered = styled(Flex)` margin: 0 auto; `; -const StyledModal = styled(ReactModal)` - animation: ${fadeAndScaleIn} 250ms ease; - - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - z-index: ${(props) => props.theme.depths.modal}; - display: flex; - justify-content: center; - align-items: flex-start; - background: ${(props) => props.theme.background}; - transition: ${(props) => props.theme.backgroundTransition}; - outline: none; -`; - const Text = styled.span` font-size: 16px; font-weight: 500; diff --git a/app/hooks/useUnmount.js b/app/hooks/useUnmount.js new file mode 100644 index 000000000..18d556c03 --- /dev/null +++ b/app/hooks/useUnmount.js @@ -0,0 +1,16 @@ +// @flow +import * as React from "react"; + +const useUnmount = (callback: Function) => { + const ref = React.useRef(callback); + + ref.current = callback; + + React.useEffect(() => { + return () => { + ref.current(); + }; + }, []); +}; + +export default useUnmount; diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 92e53d8ed..e58f568a5 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -8,6 +8,7 @@ import { VisuallyHidden } from "reakit/VisuallyHidden"; import styled from "styled-components"; import Document from "models/Document"; import DocumentDelete from "scenes/DocumentDelete"; +import DocumentMove from "scenes/DocumentMove"; import DocumentShare from "scenes/DocumentShare"; import DocumentTemplatize from "scenes/DocumentTemplatize"; import CollectionIcon from "components/CollectionIcon"; @@ -21,7 +22,6 @@ import useStores from "hooks/useStores"; import getDataTransferFiles from "utils/getDataTransferFiles"; import { documentHistoryUrl, - documentMoveUrl, documentUrl, editDocumentUrl, newDocumentUrl, @@ -64,6 +64,7 @@ function DocumentMenu({ const { t } = useTranslation(); const [renderModals, setRenderModals] = React.useState(false); const [showDeleteModal, setShowDeleteModal] = React.useState(false); + const [showMoveModal, setShowMoveModal] = React.useState(false); const [showTemplateModal, setShowTemplateModal] = React.useState(false); const [showShareModal, setShowShareModal] = React.useState(false); const file = React.useRef(); @@ -351,7 +352,7 @@ function DocumentMenu({ }, { title: `${t("Move")}…`, - to: documentMoveUrl(document), + onClick: () => setShowMoveModal(true), visible: !!can.move, }, { @@ -379,6 +380,18 @@ function DocumentMenu({ {renderModals && ( <> + setShowMoveModal(false)} + isOpen={showMoveModal} + > + setShowMoveModal(false)} + /> + { @observable isPublishing: boolean = false; @observable isDirty: boolean = false; @observable isEmpty: boolean = true; - @observable moveModalOpen: boolean = false; @observable lastRevision: number = this.props.document.revision; @observable title: string = this.props.document.title; getEditorText: () => string = () => this.props.document.text; @@ -186,9 +186,6 @@ class DocumentScene extends React.Component { } } - handleCloseMoveModal = () => (this.moveModalOpen = false); - handleOpenMoveModal = () => (this.moveModalOpen = true); - onSave = async ( options: { done?: boolean, @@ -337,7 +334,16 @@ class DocumentScene extends React.Component { ( - + + + )} /> { }; render() { - const { document, collections, onRequestClose } = this.props; + const { document, collections } = this.props; const data = this.results; - return ( - - {document && collections.isLoaded && ( - -
- - {this.renderPathToCurrentDocument()} - -
+ if (!document || !collections.isLoaded) { + return null; + } -
- - - - - - - - {({ width, height }) => ( - - data[index].id} - > - {this.row} - - - )} - - - -
-
- )} -
+ return ( + +
+ + {this.renderPathToCurrentDocument()} + +
+ +
+ + + + + + + + {({ width, height }) => ( + + data[index].id} + > + {this.row} + + + )} + + + +
+
); } } diff --git a/flow-typed/npm/react-modal_v3.1.x.js b/flow-typed/npm/react-modal_v3.1.x.js deleted file mode 100644 index 5cf3f3ff1..000000000 --- a/flow-typed/npm/react-modal_v3.1.x.js +++ /dev/null @@ -1,51 +0,0 @@ -// flow-typed signature: 096d0e067d7269fca81b46322149e3f5 -// flow-typed version: c6154227d1/react-modal_v3.1.x/flow_>=v0.104.x - -declare module 'react-modal' { - declare type DefaultProps = { - isOpen?: boolean, - portalClassName?: string, - bodyOpenClassName?: string, - ariaHideApp?: boolean, - closeTimeoutMS?: number, - shouldFocusAfterRender?: boolean, - shouldCloseOnEsc?: boolean, - shouldCloseOnOverlayClick?: boolean, - shouldReturnFocusAfterClose?: boolean, - parentSelector?: () => HTMLElement, - ... - }; - - declare type Props = DefaultProps & { - style?: { - content?: { [key: string]: string | number, ... }, - overlay?: { [key: string]: string | number, ... }, - ... - }, - className?: string | { - base: string, - afterOpen: string, - beforeClose: string, - ... - }, - overlayClassName?: string | { - base: string, - afterOpen: string, - beforeClose: string, - ... - }, - appElement?: HTMLElement | string | null, - onAfterOpen?: () => void | Promise, - onRequestClose?: (SyntheticEvent<>) => void, - aria?: { [key: string]: string, ... }, - role?: string, - contentLabel?: string, - ... - }; - - declare class Modal extends React$Component { - static setAppElement(element: HTMLElement | string | null): void; - } - - declare module.exports: typeof Modal; -} diff --git a/package.json b/package.json index 509c04217..e57f4fe97 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,6 @@ "react-helmet": "^5.2.0", "react-i18next": "^11.7.3", "react-keydown": "^1.7.3", - "react-modal": "^3.1.2", "react-portal": "^4.0.0", "react-router-dom": "^5.2.0", "react-virtualized-auto-sizer": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 5dba516ae..66467781c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5379,11 +5379,6 @@ execa@^4.0.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -exenv@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" - integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= - exif-parser@^0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" @@ -10701,26 +10696,11 @@ react-keydown@^1.7.3: dependencies: core-js "^3.1.2" -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - react-medium-image-zoom@^3.0.16: version "3.1.2" resolved "https://registry.yarnpkg.com/react-medium-image-zoom/-/react-medium-image-zoom-3.1.2.tgz#5ac4441f1d424bd9680a25bfc2591be3d7704a42" integrity sha512-werjufn5o4ytdyvJNzfqXCilovDhMyREH0qeJhCjV5brNAyfV7anZmvpFc3FApbuVXwBkzHMuQkV2z/GyEQatg== -react-modal@^3.1.2: - version "3.11.2" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4" - integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w== - dependencies: - exenv "^1.2.0" - prop-types "^15.5.10" - react-lifecycles-compat "^3.0.0" - warning "^4.0.3" - react-portal@^4.0.0, react-portal@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6" @@ -13464,13 +13444,6 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"