fix: Positioning of editing toolbar on mobile devices (#6279)

This commit is contained in:
Tom Moor
2023-12-13 19:22:06 -05:00
committed by GitHub
parent 04d4cb6d52
commit 53ff144f00
13 changed files with 121 additions and 40 deletions

View File

@@ -26,6 +26,7 @@ import {
matchDocumentInsights, matchDocumentInsights,
} from "~/utils/routeHelpers"; } from "~/utils/routeHelpers";
import Fade from "./Fade"; import Fade from "./Fade";
import { PortalContext } from "./Portal";
const DocumentComments = lazyWithRetry( const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments") () => import("~/scenes/Document/components/Comments")
@@ -45,6 +46,7 @@ type Props = {
const AuthenticatedLayout: React.FC = ({ children }: Props) => { const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const { ui, auth } = useStores(); const { ui, auth } = useStores();
const location = useLocation(); const location = useLocation();
const layoutRef = React.useRef<HTMLDivElement>(null);
const can = usePolicy(ui.activeCollectionId); const can = usePolicy(ui.activeCollectionId);
const team = useCurrentTeam(); const team = useCurrentTeam();
const documentContext = useLocalStore<DocumentContextValue>(() => ({ const documentContext = useLocalStore<DocumentContextValue>(() => ({
@@ -120,15 +122,22 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
return ( return (
<DocumentContext.Provider value={documentContext}> <DocumentContext.Provider value={documentContext}>
<Layout title={team.name} sidebar={sidebar} sidebarRight={sidebarRight}> <PortalContext.Provider value={layoutRef.current}>
<RegisterKeyDown trigger="n" handler={goToNewDocument} /> <Layout
<RegisterKeyDown trigger="t" handler={goToSearch} /> title={team.name}
<RegisterKeyDown trigger="/" handler={goToSearch} /> sidebar={sidebar}
{children} sidebarRight={sidebarRight}
<React.Suspense fallback={null}> ref={layoutRef}
<CommandBar /> >
</React.Suspense> <RegisterKeyDown trigger="n" handler={goToNewDocument} />
</Layout> <RegisterKeyDown trigger="t" handler={goToSearch} />
<RegisterKeyDown trigger="/" handler={goToSearch} />
{children}
<React.Suspense fallback={null}>
<CommandBar />
</React.Suspense>
</Layout>
</PortalContext.Provider>
</DocumentContext.Provider> </DocumentContext.Provider>
); );
}; };

View File

@@ -171,7 +171,7 @@ const Button = <T extends React.ElementType = "button">(
danger, danger,
...rest ...rest
} = props; } = props;
const hasText = children !== undefined || value !== undefined; const hasText = !!children || value !== undefined;
const ic = hideIcon ? undefined : action?.icon ?? icon; const ic = hideIcon ? undefined : action?.icon ?? icon;
const hasIcon = ic !== undefined; const hasIcon = ic !== undefined;

View File

@@ -22,12 +22,10 @@ type Props = {
sidebarRight?: React.ReactNode; sidebarRight?: React.ReactNode;
}; };
const Layout: React.FC<Props> = ({ const Layout = React.forwardRef(function Layout_(
title, { title, children, sidebar, sidebarRight }: Props,
children, ref: React.RefObject<HTMLDivElement>
sidebar, ) {
sidebarRight,
}: Props) => {
const { ui } = useStores(); const { ui } = useStores();
const sidebarCollapsed = !sidebar || ui.sidebarIsClosed; const sidebarCollapsed = !sidebar || ui.sidebarIsClosed;
@@ -40,7 +38,7 @@ const Layout: React.FC<Props> = ({
}); });
return ( return (
<Container column auto> <Container column auto ref={ref}>
<Helmet> <Helmet>
<title>{title ? title : env.APP_NAME}</title> <title>{title ? title : env.APP_NAME}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -75,7 +73,7 @@ const Layout: React.FC<Props> = ({
</Container> </Container>
</Container> </Container>
); );
}; });
const Container = styled(Flex)` const Container = styled(Flex)`
background: ${s("background")}; background: ${s("background")};

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import styled from "styled-components";
import useMobile from "~/hooks/useMobile";
type Props = {
children: React.ReactNode;
};
const MobileWrapper = styled.div`
width: 100vw;
height: 100vh;
overflow: auto;
-webkit-overflow-scrolling: touch;
`;
const MobileScrollWrapper = ({ children }: Props) => {
const isMobile = useMobile();
return isMobile ? <MobileWrapper>{children}</MobileWrapper> : <>{children}</>;
};
export default MobileScrollWrapper;

View File

@@ -134,7 +134,7 @@ const Sidebar = styled(m.div)<{
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: ${depths.sidebar}; z-index: ${depths.mobileSidebar};
`} `}
${breakpoint("tablet")` ${breakpoint("tablet")`

View File

@@ -1,6 +1,5 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Portal } from "react-portal";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import styled, { css, useTheme } from "styled-components"; import styled, { css, useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
@@ -192,11 +191,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
onPointerLeave={handlePointerLeave} onPointerLeave={handlePointerLeave}
column column
> >
{ui.mobileSidebarVisible && (
<Portal>
<Backdrop onClick={ui.toggleMobileSidebar} />
</Portal>
)}
{children} {children}
{user && ( {user && (
@@ -235,6 +229,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset} onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
/> />
</Container> </Container>
{ui.mobileSidebarVisible && <Backdrop onClick={ui.toggleMobileSidebar} />}
</> </>
); );
}); });
@@ -247,7 +242,7 @@ const Backdrop = styled.a`
bottom: 0; bottom: 0;
right: 0; right: 0;
cursor: default; cursor: default;
z-index: ${depths.sidebar - 1}; z-index: ${depths.mobileSidebar - 1};
background: ${s("backdrop")}; background: ${s("backdrop")};
`; `;
@@ -288,7 +283,7 @@ const Container = styled(Flex)<ContainerProps>`
transform: translateX( transform: translateX(
${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")} ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")}
); );
z-index: ${depths.sidebar}; z-index: ${depths.mobileSidebar};
max-width: 80%; max-width: 80%;
min-width: 280px; min-width: 280px;
${fadeOnDesktopBackgrounded()} ${fadeOnDesktopBackgrounded()}
@@ -303,6 +298,7 @@ const Container = styled(Flex)<ContainerProps>`
} }
${breakpoint("tablet")` ${breakpoint("tablet")`
z-index: ${depths.sidebar};
margin: 0; margin: 0;
min-width: 0; min-width: 0;
transform: translateX(${(props: ContainerProps) => transform: translateX(${(props: ContainerProps) =>

View File

@@ -3,6 +3,7 @@ import * as React from "react";
import styled, { createGlobalStyle } from "styled-components"; import styled, { createGlobalStyle } from "styled-components";
import { roundArrow } from "tippy.js"; import { roundArrow } from "tippy.js";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
export type Props = Omit<TippyProps, "content" | "theme"> & { export type Props = Omit<TippyProps, "content" | "theme"> & {
tooltip?: React.ReactChild | React.ReactChild[]; tooltip?: React.ReactChild | React.ReactChild[];
@@ -10,9 +11,11 @@ export type Props = Omit<TippyProps, "content" | "theme"> & {
}; };
function Tooltip({ shortcut, tooltip, delay = 50, ...rest }: Props) { function Tooltip({ shortcut, tooltip, delay = 50, ...rest }: Props) {
const isMobile = useMobile();
let content = <>{tooltip}</>; let content = <>{tooltip}</>;
if (!tooltip) { if (!tooltip || isMobile) {
return rest.children ?? null; return rest.children ?? null;
} }

View File

@@ -1,6 +1,7 @@
import { NodeSelection } from "prosemirror-state"; import { NodeSelection } from "prosemirror-state";
import { CellSelection, selectedRect } from "prosemirror-tables"; import { CellSelection, selectedRect } from "prosemirror-tables";
import * as React from "react"; import * as React from "react";
import { Portal as ReactPortal } from "react-portal";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { isCode } from "@shared/editor/lib/isCode"; import { isCode } from "@shared/editor/lib/isCode";
import { findParentNode } from "@shared/editor/queries/findParentNode"; import { findParentNode } from "@shared/editor/queries/findParentNode";
@@ -8,6 +9,8 @@ import { depths, s } from "@shared/styles";
import { Portal } from "~/components/Portal"; import { Portal } from "~/components/Portal";
import useComponentSize from "~/hooks/useComponentSize"; import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener"; import useEventListener from "~/hooks/useEventListener";
import useMobile from "~/hooks/useMobile";
import useWindowSize from "~/hooks/useWindowSize";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import { useEditor } from "./EditorContext"; import { useEditor } from "./EditorContext";
@@ -207,6 +210,32 @@ const FloatingToolbar = React.forwardRef(function FloatingToolbar_(
} }
}); });
const isMobile = useMobile();
const { height } = useWindowSize();
if (!props.children) {
return null;
}
if (isMobile) {
if (props.active) {
const rect = document.body.getBoundingClientRect();
return (
<ReactPortal>
<MobileWrapper
style={{
bottom: `calc(100% - ${height - rect.y}px)`,
}}
>
{props.children}
</MobileWrapper>
</ReactPortal>
);
}
return null;
}
return ( return (
<Portal> <Portal>
<Wrapper <Wrapper
@@ -253,6 +282,28 @@ const arrow = (props: WrapperProps) =>
` `
: ""; : "";
const MobileWrapper = styled.div`
position: absolute;
left: 0;
right: 0;
width: 100vw;
padding: 10px 6px;
background-color: ${s("menuBackground")};
border-top: 1px solid ${s("divider")};
box-sizing: border-box;
z-index: ${depths.editorToolbar};
&:after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 100px;
background-color: ${s("menuBackground")};
}
`;
const Wrapper = styled.div<WrapperProps>` const Wrapper = styled.div<WrapperProps>`
will-change: opacity, transform; will-change: opacity, transform;
padding: 6px; padding: 6px;

View File

@@ -12,7 +12,7 @@ import * as React from "react";
export default function useEventListener<T extends EventListener>( export default function useEventListener<T extends EventListener>(
eventName: string, eventName: string,
handler: T, handler: T,
element: Window | Node = window, element: Window | VisualViewport | Node | null = window,
options: AddEventListenerOptions = {} options: AddEventListenerOptions = {}
) { ) {
const savedHandler = React.useRef<T>(); const savedHandler = React.useRef<T>();

View File

@@ -10,24 +10,25 @@ import useThrottledCallback from "./useThrottledCallback";
*/ */
export default function useWindowSize() { export default function useWindowSize() {
const [windowSize, setWindowSize] = React.useState({ const [windowSize, setWindowSize] = React.useState({
width: window.innerWidth, width: window.visualViewport?.width || window.innerWidth,
height: window.innerHeight, height: window.visualViewport?.height || window.innerHeight,
}); });
const handleResize = useThrottledCallback(() => { const handleResize = useThrottledCallback(() => {
const width = window.visualViewport?.width || window.innerWidth;
const height = window.visualViewport?.height || window.innerHeight;
setWindowSize((state) => { setWindowSize((state) => {
if ( if (width === state.width && height === state.height) {
window.innerWidth === state.width &&
window.innerHeight === state.height
) {
return state; return state;
} }
return { width: window.innerWidth, height: window.innerHeight }; return { width, height };
}); });
}, 100); }, 100);
useEventListener("resize", handleResize); useEventListener("resize", handleResize);
useEventListener("resize", handleResize, window.visualViewport);
// Call handler right away so state gets updated with initial window size // Call handler right away so state gets updated with initial window size
React.useEffect(() => { React.useEffect(() => {

View File

@@ -20,6 +20,7 @@ import env from "~/env";
import { initI18n } from "~/utils/i18n"; import { initI18n } from "~/utils/i18n";
import Desktop from "./components/DesktopEventHandler"; import Desktop from "./components/DesktopEventHandler";
import LazyPolyfill from "./components/LazyPolyfills"; import LazyPolyfill from "./components/LazyPolyfills";
import MobileScrollWrapper from "./components/MobileScrollWrapper";
import Routes from "./routes"; import Routes from "./routes";
import Logger from "./utils/Logger"; import Logger from "./utils/Logger";
import history from "./utils/history"; import history from "./utils/history";
@@ -60,7 +61,7 @@ if (element) {
<LazyPolyfill> <LazyPolyfill>
<LazyMotion features={loadFeatures}> <LazyMotion features={loadFeatures}>
<Router history={history}> <Router history={history}>
<> <MobileScrollWrapper>
<PageTheme /> <PageTheme />
<ScrollToTop> <ScrollToTop>
<Routes /> <Routes />
@@ -68,7 +69,7 @@ if (element) {
<Toasts /> <Toasts />
<Dialogs /> <Dialogs />
<Desktop /> <Desktop />
</> </MobileScrollWrapper>
</Router> </Router>
</LazyMotion> </LazyMotion>
</LazyPolyfill> </LazyPolyfill>

View File

@@ -2,11 +2,11 @@ import { observer } from "mobx-react";
import { BackIcon } from "outline-icons"; import { BackIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Portal } from "react-portal";
import styled from "styled-components"; import styled from "styled-components";
import { depths, s, ellipsis } from "@shared/styles"; import { depths, s, ellipsis } from "@shared/styles";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import { Portal } from "~/components/Portal";
import Scrollable from "~/components/Scrollable"; import Scrollable from "~/components/Scrollable";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useMobile from "~/hooks/useMobile"; import useMobile from "~/hooks/useMobile";
@@ -66,7 +66,7 @@ const Backdrop = styled.a`
bottom: 0; bottom: 0;
right: 0; right: 0;
cursor: default; cursor: default;
z-index: ${depths.sidebar - 1}; z-index: ${depths.mobileSidebar - 1};
background: ${s("backdrop")}; background: ${s("backdrop")};
`; `;

View File

@@ -2,6 +2,7 @@ const depths = {
header: 800, header: 800,
sidebar: 900, sidebar: 900,
editorToolbar: 925, editorToolbar: 925,
mobileSidebar: 930,
hoverPreview: 950, hoverPreview: 950,
// Note: editor lightbox is z-index 999 // Note: editor lightbox is z-index 999
modalOverlay: 2000, modalOverlay: 2000,