fix: preserve composer taps while the keyboard dismisses

Keeps mobile composer controls from missing taps during keyboard blur/reflow
Applies the deferred blur behavior to mobile browsers and installed PWAs
Leaves Capacitor behavior unchanged
This commit is contained in:
Bohdan Triapitsyn
2026-07-07 10:20:14 +03:00
parent 624be556f6
commit 9ceb027e75
+13 -10
View File
@@ -1016,13 +1016,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
// so the chat compensates keyboard + composer height in a single motion. // so the chat compensates keyboard + composer height in a single motion.
const [mobileComposerExpanded, setMobileComposerExpanded] = React.useState(false); const [mobileComposerExpanded, setMobileComposerExpanded] = React.useState(false);
const [mobileTextareaFocused, setMobileTextareaFocused] = React.useState(false); const [mobileTextareaFocused, setMobileTextareaFocused] = React.useState(false);
// Installed PWA (standalone): tapping a composer control while the keyboard // Mobile browser / installed PWA: tapping a composer control while the
// is up blurs the textarea first, and the keyboard-resize reflow moves the // keyboard is up blurs the textarea first, and the keyboard-resize reflow
// control out from under the finger BEFORE iOS synthesizes the click — the // moves the control out from under the finger BEFORE the browser
// tap dismisses the keyboard but the control's onClick never fires. Defer // synthesizes the click — the tap dismisses the keyboard but the control's
// the blur-driven state flip so the pinned composer holds still through the // onClick never fires. Defer the blur-driven state flip so the pinned
// tap; a refocus cancels it. Browser (pan mode) and Capacitor keep the // composer holds still through the tap; a refocus cancels it. Capacitor
// immediate flip. // keeps the immediate flip.
const mobileBlurTimerRef = React.useRef<number | null>(null); const mobileBlurTimerRef = React.useRef<number | null>(null);
React.useEffect(() => () => { React.useEffect(() => () => {
if (mobileBlurTimerRef.current !== null) { if (mobileBlurTimerRef.current !== null) {
@@ -5204,9 +5204,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
} }
} }
lastMobileBlurAtRef.current = Date.now(); lastMobileBlurAtRef.current = Date.now();
const standalone = !isCapacitorApp() // Mobile browsers and installed PWAs share the
&& window.matchMedia?.('(display-mode: standalone)')?.matches; // blur race: the keyboard-dismiss reflow moves
if (!standalone) { // composer buttons before the tap's synthesized
// click lands, so the click misses its target.
// Capacitor's WebView does not need the hold.
if (isCapacitorApp()) {
setMobileTextareaFocused(false); setMobileTextareaFocused(false);
return; return;
} }