fix: open the keyboard in the body when replying on Android

This commit is contained in:
Linus Rath
2026-10-31 13:52:38 +01:00
parent 66cdc07d31
commit 461e4da0b2
2 changed files with 49 additions and 1 deletions
+22 -1
View File
@@ -137,6 +137,19 @@ const RichTextEditor = React.forwardRef<RichTextEditorHandle, Props>(function Ri
// posts its first message, then replay in order.
const readyRef = React.useRef(false);
const queuedRef = React.useRef<string[]>([]);
// Focusing the editor in the page only moves the caret there. Android
// opens the keyboard, and takes key focus off the composer's first button
// (Close: a hardware Space shut the composer), only for a WebView that holds
// native focus. iOS has keyboardDisplayRequiresUserAction for this.
const wantsNativeFocusRef = React.useRef(false);
const requestNativeFocus = React.useCallback(() => {
if (Platform.OS !== 'android') return;
if (!readyRef.current) {
wantsNativeFocusRef.current = true;
return;
}
webViewRef.current?.requestFocus();
}, []);
const call = React.useCallback((script: string) => {
if (!readyRef.current) {
queuedRef.current.push(script);
@@ -203,15 +216,17 @@ const RichTextEditor = React.forwardRef<RichTextEditorHandle, Props>(function Ri
call(`window.__rne && window.__rne.getHtml(${id})`);
}),
focusEnd: () => {
requestNativeFocus();
call(`window.__rne && window.__rne.focusEnd()`);
},
focus: () => {
requestNativeFocus();
call(`window.__rne && window.__rne.focus()`);
},
blur: () => {
call(`document.activeElement && document.activeElement.blur()`);
},
}), [call]);
}), [call, requestNativeFocus]);
const onMessage = (event: WebViewMessageEvent) => {
let data: { type: string; payload: unknown };
@@ -225,6 +240,12 @@ const RichTextEditor = React.forwardRef<RichTextEditorHandle, Props>(function Ri
perfEndAfterPaint('compose-open');
const queued = queuedRef.current;
queuedRef.current = [];
// A focus asked for before the page was up (a reply opening) gets the
// view's native focus first, then the caret from the queued script.
if (wantsNativeFocusRef.current) {
wantsNativeFocusRef.current = false;
webViewRef.current?.requestFocus();
}
for (const script of queued) webViewRef.current?.injectJavaScript(`${script}; true;`);
}
switch (data.type) {
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
// Reply opened with no keyboard on Android and focus on the composer's
// Close button (a hardware Space shut the composer). 8bb7b607 focused the
// editor inside its page only; an Android WebView takes the keyboard and
// key focus only with native view focus, which nothing gave it. The suite
// has no React Native renderer, so this checks the editor's wiring.
const src = fs.readFileSync(path.resolve(__dirname, '..', '..', 'components/RichTextEditor.tsx'), 'utf8');
describe('rich text editor focus', () => {
it('gives the WebView native focus before focusing the editor in the page', () => {
const handle = src.slice(src.indexOf('focusEnd: () =>'), src.indexOf('blur: () =>'));
expect(handle).toMatch(/focusEnd: \(\) => \{\s+requestNativeFocus\(\);\s+call\(`window\.__rne && window\.__rne\.focusEnd\(\)`\);/);
expect(handle).toMatch(/focus: \(\) => \{\s+requestNativeFocus\(\);\s+call\(`window\.__rne && window\.__rne\.focus\(\)`\);/);
});
it('asks for native focus on Android, and holds the request until the page is up', () => {
const fn = src.slice(src.indexOf('const requestNativeFocus'), src.indexOf('const call = '));
expect(fn).toMatch(/if \(Platform\.OS !== 'android'\) return;/);
expect(fn).toMatch(/if \(!readyRef\.current\) \{\s+wantsNativeFocusRef\.current = true;\s+return;\s+\}\s+webViewRef\.current\?\.requestFocus\(\);/);
const ready = src.slice(src.indexOf('if (!readyRef.current) {\r\n readyRef.current = true;'.replace(/\r\n/g, src.includes('\r\n') ? '\r\n' : '\n')));
expect(ready).toMatch(/if \(wantsNativeFocusRef\.current\) \{\s+wantsNativeFocusRef\.current = false;\s+webViewRef\.current\?\.requestFocus\(\);\s+\}\s+for \(const script of queued\)/);
});
});