Skip to content
Closed
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
Expand Up @@ -6,6 +6,10 @@ import { useLayoutEffect, useState } from "react";
// refreshing it from a render pass is safe.
let maxLayoutViewportHeight = 0;
let baselineLayoutWidth = 0;
// The last keyboard-open height measured away from the document's bottom,
// multiplied by the zoom scale. Safari clips its reported viewport to the
// document there, even though the keyboard hasn't moved.
let unclippedKeyboardHeight: number | undefined;

/**
* Whether the on-screen keyboard is open, from the current visual viewport. We
Expand Down Expand Up @@ -44,6 +48,7 @@ function isVirtualKeyboardOpen(): boolean {
if (Math.abs(layoutWidth - baselineLayoutWidth) > baselineLayoutWidth * 0.2) {
baselineLayoutWidth = layoutWidth;
maxLayoutViewportHeight = 0;
unclippedKeyboardHeight = undefined;
}

maxLayoutViewportHeight = Math.max(maxLayoutViewportHeight, layoutHeight);
Expand Down Expand Up @@ -115,23 +120,36 @@ export function useVirtualKeyboard(): boolean {
!keyboardOpen && container.scrollTop <= 0,
);

// iOS Safari lets the document scroll past its layout maximum while the
// keyboard is open (by its accessory bar, 98px measured) and clips the
// visual viewport there, so the toolbar pinned to that edge floats above
// the keyboard. Holding the document at the maximum keeps the end
// reachable through the viewport pan. A no-op with a pinned scroll
// container, or where the keyboard resizes the layout viewport.
// How-to-test: without it, iOS Safari, pinned scroll container off, focus
// the editor and drag the page past its end: the toolbar sits about 100px
// above the keyboard with an empty band below it (no emulated instance
// reproduces the range; on the release checklist).
const clampDocumentScroll = () => {
const max = html.scrollHeight - html.clientHeight;
// Safari went past the layout maximum: pull the document back to it.
// Safari adds its accessory-bar inset to the native scroll range while
// the keyboard is open: https://bugs.webkit.org/show_bug.cgi?id=292603.
// Limit only that blank region. The visual viewport, not the unchanged
// layout viewport, determines how far the user must scroll to reach the
// document's end. Using clientHeight snaps back before the end is visible.
function clampDocumentScroll() {
const height = vp?.height ?? window.innerHeight;
const scale = vp?.scale ?? 1;
const scaledHeight = height * scale;

// Accept a smaller height when the keyboard resizes the layout (Android),
// or leaves space below the visual viewport before the document's end.
// Safari's clipped measurements at the bottom satisfy neither condition.
const keyboardResized =
html.clientHeight <= scaledHeight + 1 ||
(window.innerHeight > height + 1 &&
Math.max(window.scrollY, vp?.pageTop ?? 0) + height <
html.scrollHeight - 1);

unclippedKeyboardHeight = keyboardResized
? scaledHeight
: Math.max(unclippedKeyboardHeight ?? 0, scaledHeight);
const max = Math.max(
0,
html.scrollHeight - unclippedKeyboardHeight / scale,
);
Comment on lines +145 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to simplify this logic, it's getting to be a bit of a mess, maybe it can be simpler to cache the maximum instead of of the unclippedKeyboarHeight instead. Presumably you want the absolute maximum and for it to go no further? Unsure, what we can do here

if (window.scrollY > max + 1) {
window.scrollTo(0, max);
window.scrollTo(window.scrollX, max);
}
};
}

const update = () => {
const keyboardOpen = isVirtualKeyboardOpen();
Expand All @@ -141,9 +159,9 @@ export function useVirtualKeyboard(): boolean {
markPullToRefresh(container, keyboardOpen);
}
if (keyboardOpen) {
// The keyboard resized or panned the viewport: keep the document within
// its layout maximum (see `clampDocumentScroll`).
clampDocumentScroll();
} else {
unclippedKeyboardHeight = undefined;
}
};
viewportPublishers++;
Expand Down Expand Up @@ -198,6 +216,7 @@ export function useVirtualKeyboard(): boolean {
// what it measures itself, not from a maximum seen on another page.
maxLayoutViewportHeight = 0;
baselineLayoutWidth = 0;
unclippedKeyboardHeight = undefined;
}
};
}, []);
Expand Down