diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx index 107b645f1..94567c9da 100644 --- a/app/components/AuthenticatedLayout.tsx +++ b/app/components/AuthenticatedLayout.tsx @@ -26,6 +26,7 @@ import { matchDocumentInsights, } from "~/utils/routeHelpers"; import Fade from "./Fade"; +import { PortalContext } from "./Portal"; const DocumentComments = lazyWithRetry( () => import("~/scenes/Document/components/Comments") @@ -45,6 +46,7 @@ type Props = { const AuthenticatedLayout: React.FC = ({ children }: Props) => { const { ui, auth } = useStores(); const location = useLocation(); + const layoutRef = React.useRef(null); const can = usePolicy(ui.activeCollectionId); const team = useCurrentTeam(); const documentContext = useLocalStore(() => ({ @@ -120,15 +122,22 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); }; diff --git a/app/components/Button.tsx b/app/components/Button.tsx index c072238fd..4c4d9b96c 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -171,7 +171,7 @@ const Button = ( danger, ...rest } = props; - const hasText = children !== undefined || value !== undefined; + const hasText = !!children || value !== undefined; const ic = hideIcon ? undefined : action?.icon ?? icon; const hasIcon = ic !== undefined; diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index 4d4f8954f..990727062 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -22,12 +22,10 @@ type Props = { sidebarRight?: React.ReactNode; }; -const Layout: React.FC = ({ - title, - children, - sidebar, - sidebarRight, -}: Props) => { +const Layout = React.forwardRef(function Layout_( + { title, children, sidebar, sidebarRight }: Props, + ref: React.RefObject +) { const { ui } = useStores(); const sidebarCollapsed = !sidebar || ui.sidebarIsClosed; @@ -40,7 +38,7 @@ const Layout: React.FC = ({ }); return ( - + {title ? title : env.APP_NAME} @@ -75,7 +73,7 @@ const Layout: React.FC = ({ ); -}; +}); const Container = styled(Flex)` background: ${s("background")}; diff --git a/app/components/MobileScrollWrapper.tsx b/app/components/MobileScrollWrapper.tsx new file mode 100644 index 000000000..14dbed872 --- /dev/null +++ b/app/components/MobileScrollWrapper.tsx @@ -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 ? {children} : <>{children}; +}; + +export default MobileScrollWrapper; diff --git a/app/components/Sidebar/Right.tsx b/app/components/Sidebar/Right.tsx index 339db2bed..2ebf8c28f 100644 --- a/app/components/Sidebar/Right.tsx +++ b/app/components/Sidebar/Right.tsx @@ -134,7 +134,7 @@ const Sidebar = styled(m.div)<{ top: 0; right: 0; bottom: 0; - z-index: ${depths.sidebar}; + z-index: ${depths.mobileSidebar}; `} ${breakpoint("tablet")` diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 78db3a391..23e1edc57 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -1,6 +1,5 @@ import { observer } from "mobx-react"; import * as React from "react"; -import { Portal } from "react-portal"; import { useLocation } from "react-router-dom"; import styled, { css, useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; @@ -192,11 +191,6 @@ const Sidebar = React.forwardRef(function _Sidebar( onPointerLeave={handlePointerLeave} column > - {ui.mobileSidebarVisible && ( - - - - )} {children} {user && ( @@ -235,6 +229,7 @@ const Sidebar = React.forwardRef(function _Sidebar( onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset} /> + {ui.mobileSidebarVisible && } ); }); @@ -247,7 +242,7 @@ const Backdrop = styled.a` bottom: 0; right: 0; cursor: default; - z-index: ${depths.sidebar - 1}; + z-index: ${depths.mobileSidebar - 1}; background: ${s("backdrop")}; `; @@ -288,7 +283,7 @@ const Container = styled(Flex)` transform: translateX( ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")} ); - z-index: ${depths.sidebar}; + z-index: ${depths.mobileSidebar}; max-width: 80%; min-width: 280px; ${fadeOnDesktopBackgrounded()} @@ -303,6 +298,7 @@ const Container = styled(Flex)` } ${breakpoint("tablet")` + z-index: ${depths.sidebar}; margin: 0; min-width: 0; transform: translateX(${(props: ContainerProps) => diff --git a/app/components/Tooltip.tsx b/app/components/Tooltip.tsx index 67489fd28..64bf73544 100644 --- a/app/components/Tooltip.tsx +++ b/app/components/Tooltip.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import styled, { createGlobalStyle } from "styled-components"; import { roundArrow } from "tippy.js"; import { s } from "@shared/styles"; +import useMobile from "~/hooks/useMobile"; export type Props = Omit & { tooltip?: React.ReactChild | React.ReactChild[]; @@ -10,9 +11,11 @@ export type Props = Omit & { }; function Tooltip({ shortcut, tooltip, delay = 50, ...rest }: Props) { + const isMobile = useMobile(); + let content = <>{tooltip}; - if (!tooltip) { + if (!tooltip || isMobile) { return rest.children ?? null; } diff --git a/app/editor/components/FloatingToolbar.tsx b/app/editor/components/FloatingToolbar.tsx index 26c13cf32..12895400c 100644 --- a/app/editor/components/FloatingToolbar.tsx +++ b/app/editor/components/FloatingToolbar.tsx @@ -1,6 +1,7 @@ import { NodeSelection } from "prosemirror-state"; import { CellSelection, selectedRect } from "prosemirror-tables"; import * as React from "react"; +import { Portal as ReactPortal } from "react-portal"; import styled, { css } from "styled-components"; import { isCode } from "@shared/editor/lib/isCode"; import { findParentNode } from "@shared/editor/queries/findParentNode"; @@ -8,6 +9,8 @@ import { depths, s } from "@shared/styles"; import { Portal } from "~/components/Portal"; import useComponentSize from "~/hooks/useComponentSize"; import useEventListener from "~/hooks/useEventListener"; +import useMobile from "~/hooks/useMobile"; +import useWindowSize from "~/hooks/useWindowSize"; import Logger from "~/utils/Logger"; 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 ( + + + {props.children} + + + ); + } + + return null; + } + return ( ` : ""; +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` will-change: opacity, transform; padding: 6px; diff --git a/app/hooks/useEventListener.ts b/app/hooks/useEventListener.ts index 37627f6e6..66ff46a40 100644 --- a/app/hooks/useEventListener.ts +++ b/app/hooks/useEventListener.ts @@ -12,7 +12,7 @@ import * as React from "react"; export default function useEventListener( eventName: string, handler: T, - element: Window | Node = window, + element: Window | VisualViewport | Node | null = window, options: AddEventListenerOptions = {} ) { const savedHandler = React.useRef(); diff --git a/app/hooks/useWindowSize.ts b/app/hooks/useWindowSize.ts index 01c4715a4..a607a97ee 100644 --- a/app/hooks/useWindowSize.ts +++ b/app/hooks/useWindowSize.ts @@ -10,24 +10,25 @@ import useThrottledCallback from "./useThrottledCallback"; */ export default function useWindowSize() { const [windowSize, setWindowSize] = React.useState({ - width: window.innerWidth, - height: window.innerHeight, + width: window.visualViewport?.width || window.innerWidth, + height: window.visualViewport?.height || window.innerHeight, }); const handleResize = useThrottledCallback(() => { + const width = window.visualViewport?.width || window.innerWidth; + const height = window.visualViewport?.height || window.innerHeight; + setWindowSize((state) => { - if ( - window.innerWidth === state.width && - window.innerHeight === state.height - ) { + if (width === state.width && height === state.height) { return state; } - return { width: window.innerWidth, height: window.innerHeight }; + return { width, height }; }); }, 100); useEventListener("resize", handleResize); + useEventListener("resize", handleResize, window.visualViewport); // Call handler right away so state gets updated with initial window size React.useEffect(() => { diff --git a/app/index.tsx b/app/index.tsx index 5b10151b0..bfd845f8b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -20,6 +20,7 @@ import env from "~/env"; import { initI18n } from "~/utils/i18n"; import Desktop from "./components/DesktopEventHandler"; import LazyPolyfill from "./components/LazyPolyfills"; +import MobileScrollWrapper from "./components/MobileScrollWrapper"; import Routes from "./routes"; import Logger from "./utils/Logger"; import history from "./utils/history"; @@ -60,7 +61,7 @@ if (element) { - <> + @@ -68,7 +69,7 @@ if (element) { - + diff --git a/app/scenes/Document/components/SidebarLayout.tsx b/app/scenes/Document/components/SidebarLayout.tsx index 6b0f94675..0e909b902 100644 --- a/app/scenes/Document/components/SidebarLayout.tsx +++ b/app/scenes/Document/components/SidebarLayout.tsx @@ -2,11 +2,11 @@ import { observer } from "mobx-react"; import { BackIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import { Portal } from "react-portal"; import styled from "styled-components"; import { depths, s, ellipsis } from "@shared/styles"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; +import { Portal } from "~/components/Portal"; import Scrollable from "~/components/Scrollable"; import Tooltip from "~/components/Tooltip"; import useMobile from "~/hooks/useMobile"; @@ -66,7 +66,7 @@ const Backdrop = styled.a` bottom: 0; right: 0; cursor: default; - z-index: ${depths.sidebar - 1}; + z-index: ${depths.mobileSidebar - 1}; background: ${s("backdrop")}; `; diff --git a/shared/styles/depths.ts b/shared/styles/depths.ts index d87360013..399a1d0a7 100644 --- a/shared/styles/depths.ts +++ b/shared/styles/depths.ts @@ -2,6 +2,7 @@ const depths = { header: 800, sidebar: 900, editorToolbar: 925, + mobileSidebar: 930, hoverPreview: 950, // Note: editor lightbox is z-index 999 modalOverlay: 2000,