chore: Fix modal nesting, remove react-modal (#1996)

* chore: Fix modal nesting, remove react-modal

* tweak

* fix: Janky route jump when accessing Document -> Move from non-document scene
This commit is contained in:
Tom Moor 2021-03-30 18:46:14 -07:00 committed by GitHub
parent 07425f4243
commit 25023fb086
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 215 deletions

View File

@ -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 (
<>
<GlobalStyles />
<StyledModal
contentLabel={title}
onRequestClose={onRequestClose}
isOpen={isOpen}
{...rest}
>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</Centered>
</Content>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
<Text>{t("Back")}</Text>
</Back>
<Close onClick={onRequestClose}>
<CloseIcon size={32} color="currentColor" />
</Close>
</StyledModal>
</>
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop {...props}>
<Dialog
{...dialog}
aria-label={title}
preventBodyScrollhideOnEsc
hide={onRequestClose}
>
{(props) => (
<Scene
$nested={!!depth}
style={{ marginLeft: `${depth * 12}px` }}
{...props}
>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</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>
)}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
};
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;

16
app/hooks/useUnmount.js Normal file
View File

@ -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;

View File

@ -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<?HTMLInputElement>();
@ -351,7 +352,7 @@ function DocumentMenu({
},
{
title: `${t("Move")}`,
to: documentMoveUrl(document),
onClick: () => setShowMoveModal(true),
visible: !!can.move,
},
{
@ -379,6 +380,18 @@ function DocumentMenu({
</ContextMenu>
{renderModals && (
<>
<Modal
title={t("Move {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowMoveModal(false)}
isOpen={showMoveModal}
>
<DocumentMove
document={document}
onRequestClose={() => setShowMoveModal(false)}
/>
</Modal>
<Modal
title={t("Delete {{ documentName }}", {
documentName: document.noun,

View File

@ -13,17 +13,18 @@ import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import Document from "models/Document";
import Revision from "models/Revision";
import DocumentMove from "scenes/DocumentMove";
import Branding from "components/Branding";
import ErrorBoundary from "components/ErrorBoundary";
import Flex from "components/Flex";
import LoadingIndicator from "components/LoadingIndicator";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import Modal from "components/Modal";
import Notice from "components/Notice";
import PageTitle from "components/PageTitle";
import Time from "components/Time";
import Container from "./Container";
import Contents from "./Contents";
import DocumentMove from "./DocumentMove";
import Editor from "./Editor";
import Header from "./Header";
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
@ -75,7 +76,6 @@ class DocumentScene extends React.Component<Props> {
@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<Props> {
}
}
handleCloseMoveModal = () => (this.moveModalOpen = false);
handleOpenMoveModal = () => (this.moveModalOpen = true);
onSave = async (
options: {
done?: boolean,
@ -337,7 +334,16 @@ class DocumentScene extends React.Component<Props> {
<Route
path={`${match.url}/move`}
component={() => (
<DocumentMove document={document} onRequestClose={this.goBack} />
<Modal
title={`Move ${document.noun}`}
onRequestClose={this.goBack}
isOpen
>
<DocumentMove
document={document}
onRequestClose={this.goBack}
/>
</Modal>
)}
/>
<PageTitle

View File

@ -14,7 +14,6 @@ import Document from "models/Document";
import Flex from "components/Flex";
import { Outline } from "components/Input";
import Labeled from "components/Labeled";
import Modal from "components/Modal";
import PathToDocument from "components/PathToDocument";
type Props = {|
@ -124,55 +123,55 @@ class DocumentMove extends React.Component<Props> {
};
render() {
const { document, collections, onRequestClose } = this.props;
const { document, collections } = this.props;
const data = this.results;
return (
<Modal isOpen onRequestClose={onRequestClose} title="Move document">
{document && collections.isLoaded && (
<Flex column>
<Section>
<Labeled label="Current location">
{this.renderPathToCurrentDocument()}
</Labeled>
</Section>
if (!document || !collections.isLoaded) {
return null;
}
<Section column>
<Labeled label="Choose a new location" />
<NewLocation>
<InputWrapper>
<Input
type="search"
placeholder="Search collections & documents…"
onChange={this.handleFilter}
required
autoFocus
/>
</InputWrapper>
<Results>
<AutoSizer>
{({ width, height }) => (
<Flex role="listbox" column>
<List
key={data.length}
width={width}
height={height}
itemData={data}
itemCount={data.length}
itemSize={40}
itemKey={(index, data) => data[index].id}
>
{this.row}
</List>
</Flex>
)}
</AutoSizer>
</Results>
</NewLocation>
</Section>
</Flex>
)}
</Modal>
return (
<Flex column>
<Section>
<Labeled label="Current location">
{this.renderPathToCurrentDocument()}
</Labeled>
</Section>
<Section column>
<Labeled label="Choose a new location" />
<NewLocation>
<InputWrapper>
<Input
type="search"
placeholder="Search collections & documents…"
onChange={this.handleFilter}
required
autoFocus
/>
</InputWrapper>
<Results>
<AutoSizer>
{({ width, height }) => (
<Flex role="listbox" column>
<List
key={data.length}
width={width}
height={height}
itemData={data}
itemCount={data.length}
itemSize={40}
itemKey={(index, data) => data[index].id}
>
{this.row}
</List>
</Flex>
)}
</AutoSizer>
</Results>
</NewLocation>
</Section>
</Flex>
);
}
}

View File

@ -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<void>,
onRequestClose?: (SyntheticEvent<>) => void,
aria?: { [key: string]: string, ... },
role?: string,
contentLabel?: string,
...
};
declare class Modal extends React$Component<Props> {
static setAppElement(element: HTMLElement | string | null): void;
}
declare module.exports: typeof Modal;
}

View File

@ -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",

View File

@ -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"