Skip to content

Commit 21c0db9

Browse files
committed
Overlay branch overlay/web-android-enter on top of origin/main via Merge
• 26131fb6fa Overlay branch overlay/web-android-enter on top of origin/main via Merge |\ | • a808491 fix(web): detect keyboards that resize the layout viewport • | a8dba9c fix(mobile): the empty composer row shows only the provider icon (pingdotgg#23570) • | 09bb9f1 fix(server): wake PR watches when remaining CI checks finish (pingdotgg#23904) • | 05cd55d fix(mobile): center Android composer text and pad wrapped drafts (pingdotgg#23829) • | 50e151c docs(connect): correct the relay webhook endpoint key (pingdotgg#23526) • | 18e2742 fix(mobile): undo toasts keep their action on one row (pingdotgg#23910) • | 5127302 feat(settings): add thread title instructions (pingdotgg#21882)
2 parents a8dba9c + a808491 commit 21c0db9

2 files changed

Lines changed: 77 additions & 5 deletions

File tree

‎apps/web/src/lib/softwareKeyboard.test.ts‎

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ import { describe, expect, it, vi } from "vite-plus/test";
22

33
import { createSoftwareKeyboardStore } from "./softwareKeyboard";
44

5-
function fixture(options: { native?: boolean; overlay?: boolean; desktop?: boolean } = {}) {
5+
function fixture(
6+
options: { native?: boolean; overlay?: boolean; desktop?: boolean; coarse?: boolean } = {},
7+
) {
68
const viewport = Object.assign(new EventTarget(), { width: 390, height: 844, scale: 1 });
79
const keyboard = Object.assign(new EventTarget(), {
810
boundingRect: { height: 0 },
911
overlaysContent: options.overlay ?? false,
1012
});
1113
const win = {
14+
innerWidth: 390,
1215
innerHeight: 844,
1316
visualViewport: viewport,
17+
matchMedia: (query: string) => ({
18+
matches: query === "(pointer: coarse)" && (options.coarse ?? true),
19+
}),
1420
navigator: options.native ? { virtualKeyboard: keyboard } : {},
1521
};
1622
const store = createSoftwareKeyboardStore(win as unknown as Window, options.desktop ?? false);
@@ -91,6 +97,15 @@ describe("software keyboard detection", () => {
9197
unsubscribe();
9298
});
9399

100+
it("ignores window resizing even with a large height change", () => {
101+
const { store, resize, win } = fixture({ coarse: false });
102+
const unsubscribe = store.subscribe(() => {});
103+
win.innerHeight = 500;
104+
resize(500);
105+
expect(store.getSnapshot()).toBe(false);
106+
unsubscribe();
107+
});
108+
94109
it("ignores pinch zoom and recognizes keyboard changes at the new scale", () => {
95110
const { store, resize } = fixture();
96111
const unsubscribe = store.subscribe(() => {});
@@ -106,12 +121,49 @@ describe("software keyboard detection", () => {
106121
it("does not mistake rotation for an open keyboard", () => {
107122
const { store, resize, win } = fixture();
108123
const unsubscribe = store.subscribe(() => {});
109-
win.innerHeight = 390;
124+
Object.assign(win, { innerWidth: 844, innerHeight: 390 });
110125
resize(390, 844);
111126
expect(store.getSnapshot()).toBe(false);
112127
unsubscribe();
113128
});
114129

130+
it("detects a keyboard that resizes the layout viewport with the visual viewport", () => {
131+
const { store, resize, viewport, win } = fixture();
132+
const layoutResize = (height: number, width = viewport.width, scale = 1) => {
133+
win.innerHeight = height;
134+
resize(height / scale, width, scale);
135+
};
136+
const unsubscribe = store.subscribe(() => {});
137+
138+
layoutResize(500);
139+
expect(store.getSnapshot()).toBe(true);
140+
layoutResize(480);
141+
expect(store.getSnapshot()).toBe(true);
142+
layoutResize(480, 195, 2);
143+
expect(store.getSnapshot()).toBe(true);
144+
layoutResize(844, 195, 2);
145+
expect(store.getSnapshot()).toBe(false);
146+
layoutResize(800, 390);
147+
expect(store.getSnapshot()).toBe(false);
148+
layoutResize(460);
149+
expect(store.getSnapshot()).toBe(true);
150+
layoutResize(800);
151+
expect(store.getSnapshot()).toBe(false);
152+
unsubscribe();
153+
});
154+
155+
it("detects a layout-resizing keyboard that opened before subscribing", () => {
156+
const { store, resize, win } = fixture();
157+
win.innerHeight = 500;
158+
resize(500);
159+
const unsubscribe = store.subscribe(() => {});
160+
expect(store.getSnapshot()).toBe(true);
161+
win.innerHeight = 844;
162+
resize(844);
163+
expect(store.getSnapshot()).toBe(false);
164+
unsubscribe();
165+
});
166+
115167
it("recognizes keyboard closure despite viewport rounding at high zoom", () => {
116168
const { store, resize } = fixture();
117169
resize(211, 98, 4);

‎apps/web/src/lib/softwareKeyboard.ts‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ type VirtualKeyboard = EventTarget & {
33
readonly overlaysContent: boolean;
44
};
55

6-
type KeyboardWindow = Pick<Window, "innerHeight" | "visualViewport"> & {
6+
type KeyboardWindow = Pick<
7+
Window,
8+
"innerWidth" | "innerHeight" | "visualViewport" | "matchMedia"
9+
> & {
710
readonly navigator: Navigator & { readonly virtualKeyboard?: VirtualKeyboard };
811
};
912

@@ -23,6 +26,19 @@ export function createSoftwareKeyboardStore(win: KeyboardWindow | undefined, isD
2326
const viewport = isDesktop ? undefined : win?.visualViewport;
2427
const keyboard = isDesktop ? undefined : win?.navigator.virtualKeyboard;
2528
let previous = viewport ? viewportSnapshot(viewport) : undefined;
29+
// Our interactive-widget=resizes-content makes browsers that honor it (Firefox Android)
30+
// shrink the layout viewport with the visual one, so compare against the tallest layout
31+
// seen at this width. A fine pointer means a desktop window resize, not a keyboard.
32+
const tracksLayout = viewport !== undefined && win?.matchMedia("(pointer: coarse)").matches;
33+
let fullLayout = win ? { width: win.innerWidth, height: win.innerHeight } : undefined;
34+
35+
const layoutShrunkByKeyboard = () => {
36+
if (!tracksLayout || !win || !fullLayout) return false;
37+
if (fullLayout.width !== win.innerWidth || win.innerHeight > fullLayout.height) {
38+
fullLayout = { width: win.innerWidth, height: win.innerHeight };
39+
}
40+
return fullLayout.height - win.innerHeight > KEYBOARD_THRESHOLD;
41+
};
2642

2743
const getSnapshot = () => {
2844
// Chromium can expose the API while withholding geometry outside overlay mode.
@@ -45,7 +61,9 @@ export function createSoftwareKeyboardStore(win: KeyboardWindow | undefined, isD
4561
const scaledHeightDifference = previous
4662
? (previous.height - current.height) * current.scale
4763
: 0;
48-
if (Math.abs(current.height - win.innerHeight / current.scale) <= 1) {
64+
if (layoutShrunkByKeyboard()) {
65+
viewportOpen = true;
66+
} else if (Math.abs(current.height - win.innerHeight / current.scale) <= 1) {
4967
viewportOpen = false;
5068
} else if (
5169
!viewportOpen &&
@@ -64,7 +82,9 @@ export function createSoftwareKeyboardStore(win: KeyboardWindow | undefined, isD
6482
if (viewport) {
6583
previous = viewportSnapshot(viewport);
6684
// A composer can mount after the keyboard has already opened.
67-
viewportOpen = win.innerHeight - previous.height * previous.scale > KEYBOARD_THRESHOLD;
85+
viewportOpen =
86+
layoutShrunkByKeyboard() ||
87+
win.innerHeight - previous.height * previous.scale > KEYBOARD_THRESHOLD;
6888
viewport.addEventListener("resize", onViewportResize);
6989
}
7090
keyboard?.addEventListener("geometrychange", notify);

0 commit comments

Comments
 (0)