fix: Positioning of editing toolbar on mobile devices (#6279)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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")};
|
||||||
|
|||||||
21
app/components/MobileScrollWrapper.tsx
Normal file
21
app/components/MobileScrollWrapper.tsx
Normal 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;
|
||||||
@@ -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")`
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user