Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(
<div className="viewport-bar">Virtual Keyboard Toolbar</div>,
document.body,
);
}

export default function App() {
// Creates a new editor instance.
const editor = useCreateBlockNote({
initialContent: [
{
type: "paragraph",
content: "Welcome to this demo!",
},
{
type: "paragraph",
content:
"Check out the experimental mobile formatting toolbar by selecting some text (best experienced on a mobile device).",
},
],
});

// Renders the editor instance using a React component.
const editor = useCreateBlockNote({ initialContent });

const viewport = useVisualViewport();

return (
// Disables the default formatting toolbar and re-adds it without the
// `FormattingToolbarController` component. You may have seen
// `FormattingToolbarController` used in other examples, but we omit it here
// as we want to control the position and visibility ourselves. BlockNote
// also uses the `FormattingToolbarController` when displaying the
// Formatting Toolbar by default.
<BlockNoteView editor={editor} formattingToolbar={false}>
<ExperimentalMobileFormattingToolbarController />
</BlockNoteView>
<div className="scroll-host">
<NavBar />
<main className="app-main">
<StaticText />
<BlockNoteView editor={editor} formattingToolbar={false}>
{isVirtualKeyboardOpen(viewport) && <VirtualKeyboardToolbar />}
</BlockNoteView>
<StaticText />
</main>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useState } from "react";

function HamburgerMenu() {
const [open, setOpen] = useState(false);

return (
<div className="hamburger">
<button
type="button"
className="hamburger-button"
aria-label="Menu"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span />
<span />
<span />
</button>
{open && (
<nav className="hamburger-menu">
<a href="#">Home</a>
<a href="#">Documents</a>
<a href="#">Shared with me</a>
<a href="#">Settings</a>
</nav>
)}
</div>
);
}

export function NavBar() {
return (
<header className="top-nav">
<HamburgerMenu />
<span className="top-nav-title">Lorem Ipsum</span>
</header>
);
}

/** A block of static page text, to sit around the editor. */
export function StaticText() {
return (
<section className="prose">
<h2>Lorem Ipsum</h2>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
</section>
);
}
Loading
Loading