Fix opening phone keyboard closing the dropdown

This commit is contained in:
Kalle
2026-09-18 07:56:57 +03:00
parent bfcd3db322
commit 75c7f397ba
4 changed files with 80 additions and 2 deletions

3
.gitignore vendored
View File

@@ -40,4 +40,5 @@ notepad.txt
*.mp4
*.mkv
*.webm
*.webm
*.mov

View File

@@ -8,6 +8,7 @@ const PAGE_HEIGHT = 5000;
afterEach(() => {
window.scrollTo(0, 0);
closeKeyboard();
});
function Overlay({
@@ -66,6 +67,29 @@ function ScrollingOverlay({
const settle = () => new Promise((resolve) => setTimeout(resolve, 150));
const KEYBOARD_HEIGHT = 300;
/** Shrinks the visual viewport the way the virtual keyboard opening does. */
function openKeyboard() {
const viewport = window.visualViewport;
invariant(viewport);
const shrunk = viewport.height - KEYBOARD_HEIGHT;
Object.defineProperty(viewport, "height", {
configurable: true,
get: () => shrunk,
});
viewport.dispatchEvent(new Event("resize"));
}
function closeKeyboard() {
const viewport = window.visualViewport;
invariant(viewport);
Reflect.deleteProperty(viewport, "height");
viewport.dispatchEvent(new Event("resize"));
}
describe("useCloseOnScrollClip", () => {
test("closes once scrolling clips a popover that was fully visible", async () => {
const close = vi.fn();
@@ -100,6 +124,32 @@ describe("useCloseOnScrollClip", () => {
expect(close).not.toHaveBeenCalled();
});
test("never closes over the scroll the virtual keyboard opening causes", async () => {
const close = vi.fn();
await render(<Overlay top={200} height={100} close={close} />);
await settle();
openKeyboard();
window.scrollTo(0, 250);
await settle();
expect(close).not.toHaveBeenCalled();
});
test("forgets a scroll the keyboard only lands after", async () => {
const close = vi.fn();
await render(<Overlay top={200} height={100} close={close} />);
await settle();
window.scrollTo(0, 250);
// the scroll can reach the page before the keyboard has shrunk the viewport
window.dispatchEvent(new Event("scroll"));
openKeyboard();
await settle();
expect(close).not.toHaveBeenCalled();
});
test("never closes over a scroll of the popover's own content", async () => {
const close = vi.fn();
const screen = await render(

View File

@@ -1,6 +1,8 @@
import * as React from "react";
const VISIBLE_RATIO_THRESHOLD = 0.98;
/** A visual viewport shorter than the window by more than this is the virtual keyboard, not collapsing browser chrome. */
const KEYBOARD_MIN_HEIGHT = 150;
/**
* Closes an open popover once scrolling clips it against the sticky header
@@ -8,7 +10,11 @@ const VISIBLE_RATIO_THRESHOLD = 0.98;
*
* Only scrolling may close: a popover clipped by its own content growing (the
* moment before anchor positioning flips it into view), one too tall to ever
* fit fully, or one measured before it is shown must not close itself.
* fit fully, or one measured before it is shown must not close itself. The
* virtual keyboard opening is not scrolling either, even though the browser
* scrolls the page to keep the focused field in view as it does: a popover
* left under the keyboard beats one that closes as its own search input is
* focused.
*/
export function useCloseOnScrollClip(
isOpen: boolean,
@@ -37,6 +43,7 @@ export function useCloseOnScrollClip(
if (event.target instanceof Node && element.contains(event.target)) {
return;
}
if (keyboardIsOpen()) return;
scrolledSinceFullyVisible = true;
};
window.addEventListener("scroll", onScroll, {
@@ -44,6 +51,14 @@ export function useCloseOnScrollClip(
passive: true,
});
// the keyboard can land after the scroll it causes, which then has to be forgotten
const onViewportResize = () => {
if (keyboardIsOpen()) {
scrolledSinceFullyVisible = false;
}
};
window.visualViewport?.addEventListener("resize", onViewportResize);
const observer = new IntersectionObserver(
(entries) => {
const entry = entries.at(-1);
@@ -64,7 +79,15 @@ export function useCloseOnScrollClip(
return () => {
window.removeEventListener("scroll", onScroll, { capture: true });
window.visualViewport?.removeEventListener("resize", onViewportResize);
observer.disconnect();
};
}, [isOpen, elementRef]);
}
function keyboardIsOpen() {
const viewport = window.visualViewport;
if (!viewport) return false;
return window.innerHeight - viewport.height > KEYBOARD_MIN_HEIGHT;
}

View File

@@ -0,0 +1,4 @@
---
type: bug
---
Opening the phone keyboard no longer closes the dropdown you are typing into