mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-26 21:27:48 -05:00
Fix opening phone keyboard closing the dropdown
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,4 +40,5 @@ notepad.txt
|
||||
|
||||
*.mp4
|
||||
*.mkv
|
||||
*.webm
|
||||
*.webm
|
||||
*.mov
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
4
changelog/2026-09-18-keyboard-closing-selects.md
Normal file
4
changelog/2026-09-18-keyboard-closing-selects.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
type: bug
|
||||
---
|
||||
Opening the phone keyboard no longer closes the dropdown you are typing into
|
||||
Reference in New Issue
Block a user