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:
parent
07425f4243
commit
25023fb086
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
|
|
27
yarn.lock
27
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"
|
||||
|
|
Reference in New Issue