diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx index 47d59e453c..def7b64b5b 100644 --- a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx @@ -1,39 +1,148 @@ import "@blocknote/core/fonts/inter.css"; -import { - ExperimentalMobileFormattingToolbarController, - useCreateBlockNote, -} from "@blocknote/react"; +import { useCreateBlockNote } from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import "./style.css"; +import { useEffect, useState } from "react"; +import { StaticText, NavBar } from "./DummyUI"; +import { createPortal } from "react-dom"; + +// Enough content that the editor actually overflows, so scrolling is testable. +const initialContent = [ + { type: "paragraph" as const, content: "Welcome to this demo!" }, + { + type: "paragraph" as const, + content: + "Select some text to bring up the keyboard, then scroll — the bar stays " + + "pinned above the keyboard because the document itself doesn't scroll.", + }, + ...Array.from({ length: 20 }, (_, i) => ({ + type: "paragraph" as const, + content: + `Filler paragraph ${i + 1}. Select some text here and bring up the ` + + "keyboard to see the bar sit above it.", + })), +]; + +type VisualViewportRect = { + top: number; + left: number; + width: number; + height: number; + scale: number; +}; + +function readVisualViewport(): VisualViewportRect { + const vp = visualViewport; + return { + top: vp?.offsetTop ?? 0, + left: vp?.offsetLeft ?? 0, + width: vp?.width ?? window.innerWidth, + height: vp?.height ?? window.innerHeight, + scale: vp?.scale ?? 1, + }; +} + +/** + * Owns everything about the visual viewport: + * + * - Locks the document so it never scrolls — a `.scroll-host` element does (see + * the CSS). With a non-scrolling document, content scrolling is an element + * scroll that never moves the visual viewport, so anything pinned to it stays + * put during scroll with no per-frame work. + * - Tracks the viewport rectangle + pinch-zoom scale and publishes it two ways: + * as the returned object (for JS, e.g. keyboard detection) and as CSS custom + * properties on the root (`--app-top/left/width/height/scale`) so elements can + * position themselves off the viewport without a React re-render. + */ +function useVisualViewport(): VisualViewportRect { + const [rect, setRect] = useState(readVisualViewport); + + useEffect(() => { + const html = document.documentElement; + const body = document.body; + + // Original values only saved to be able to restore when the parent component unmounts. + const prevHtmlOverflow = html.style.overflow; + const prevBodyOverflow = body.style.overflow; + const prevHtmlOverscroll = html.style.overscrollBehavior; + + // Disables scrolling on `document` & `document.body`. + html.style.overflow = "hidden"; + body.style.overflow = "hidden"; + // TODO: Manually test if necessary. + // html.style.overscrollBehavior = "none"; + + const vp = visualViewport; + const update = () => { + const next = readVisualViewport(); + + setRect(next); + + html.style.setProperty("--app-top", `${next.top}px`); + html.style.setProperty("--app-left", `${next.left}px`); + html.style.setProperty("--app-width", `${next.width}px`); + html.style.setProperty("--app-height", `${next.height}px`); + html.style.setProperty("--app-scale", `${next.scale}`); + }; + update(); + + // These fire on keyboard open/close and zoom/pan — never on (element) content scroll, since + // the document itself can't scroll. + vp?.addEventListener("resize", update); + vp?.addEventListener("scroll", update); + window.addEventListener("resize", update); + + return () => { + html.style.overflow = prevHtmlOverflow; + body.style.overflow = prevBodyOverflow; + html.style.overscrollBehavior = prevHtmlOverscroll; + + html.style.removeProperty("--app-top"); + html.style.removeProperty("--app-left"); + html.style.removeProperty("--app-width"); + html.style.removeProperty("--app-height"); + html.style.removeProperty("--app-scale"); + + vp?.removeEventListener("resize", update); + vp?.removeEventListener("scroll", update); + window.removeEventListener("resize", update); + }; + }, []); + + return rect; +} + +let maxLayoutViewportHeight = 0; +function isVirtualKeyboardOpen(viewport: VisualViewportRect): boolean { + const layoutHeight = viewport.height * viewport.scale; + maxLayoutViewportHeight = Math.max(maxLayoutViewportHeight, layoutHeight); + return maxLayoutViewportHeight - layoutHeight > 150; +} + +function VirtualKeyboardToolbar() { + return createPortal( +
+ Elit ipsum qui deserunt deserunt. Qui labore eu esse veniam excepteur. + Aute ipsum qui dolore in ipsum commodo adipisicing velit. Qui + consectetur et cupidatat consectetur sunt anim excepteur reprehenderit + sunt quis magna aliqua laborum. Lorem irure est ipsum ea nisi incididunt + culpa qui consequat eiusmod deserunt ipsum nostrud velit laboris. +
++ Culpa quis id ipsum enim proident dolore non. Ad occaecat nostrud + eiusmod pariatur occaecat nisi voluptate nulla. Nisi quis ut esse ex + reprehenderit Lorem tempor ex tempor id sit officia. Commodo sunt sint + aliqua quis reprehenderit. Occaecat id ad dolor officia qui sunt dolor. + Consectetur magna excepteur in minim pariatur qui elit in sit consequat + aliquip voluptate laboris. Reprehenderit et eu dolor ex cupidatat aliqua + in elit anim eiusmod et adipisicing. Cupidatat fugiat fugiat amet duis. +
++ Voluptate quis dolor ipsum commodo fugiat sit tempor tempor non aliqua + qui. Veniam consectetur mollit consequat exercitation sit ad. Lorem amet + deserunt qui sint et. Sint aute cillum aliqua pariatur cillum id. + Consectetur proident Lorem qui laborum id in sit. Aute aute irure nisi + est veniam Lorem. Anim labore irure ut sit mollit velit et duis veniam + ipsum aliquip. +
++ Occaecat dolore excepteur qui proident laborum. Dolor deserunt cillum + veniam nulla minim eu in est aute nulla anim incididunt ea. Anim aliquip + aute duis aliqua eu pariatur est dolor magna Lorem dolore do sunt + aliquip est. Laborum pariatur fugiat do reprehenderit tempor cupidatat + proident ipsum ad dolor laboris. +
+