fix(dashboard): don't preventDefault on chat composer touchstart on Android

ChatView and QuickChatFAB both had an iOS-specific onTouchStart on the
textarea that called event.preventDefault() and then programmatically
re-focused the input — meant to suppress iOS's visualViewport auto-scroll
on re-focus. On Android, preventDefault on a textarea touchstart blocks
the soft keyboard from opening (programmatic focus() alone does not raise
the Android keyboard — only the default touch action does), so tapping
the main chat or quick chat composer focused the input but the keyboard
never appeared, looking like an instant dismiss.

Gate the touchstart workaround to iOS via isIOS().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-22 16:00:02 -07:00
parent a7e55e3d7a
commit 3fbebd44e9
3 changed files with 24 additions and 4 deletions

View File

@@ -2,8 +2,10 @@
"@fusion/dashboard": patch "@fusion/dashboard": patch
--- ---
fix(dashboard): stop ChatView's body scroll-lock from instantly dismissing the Android soft keyboard fix(dashboard): stop main-chat and quick-chat composers from instantly dismissing the Android soft keyboard
The body scroll-lock applied while the keyboard is open in main chat was an iOS-specific workaround for visualViewport drift. On Android Chrome it does the opposite of what we want — mutating `body { position: fixed; ... }` while the keyboard is opening causes Chrome to treat it as a focus-target relayout and immediately dismisses the keyboard, making the main chat composer unusable on Android. Two layered Android-specific fixes for the chat composers:
`useMobileScrollLock` is now gated to iOS UAs. Android Chrome doesn't need it (with `interactive-widget=resizes-content` the layout viewport shrinks with the keyboard, so no drift compensation is required). 1. The body scroll-lock applied while the keyboard is open in main chat was an iOS-specific workaround for visualViewport drift. On Android Chrome it does the opposite of what we want — mutating `body { position: fixed; ... }` while the keyboard is opening causes Chrome to treat it as a focus-target relayout and immediately dismisses the keyboard. `useMobileScrollLock` is now gated to iOS UAs.
2. ChatView and QuickChatFAB both had an iOS-specific `onTouchStart` on the textarea that called `event.preventDefault()` and then programmatically refocused the input (to suppress iOS's visualViewport auto-scroll on re-focus). On Android, `preventDefault` on a textarea touchstart prevents the soft keyboard from opening — programmatic `focus()` alone does not raise the Android keyboard. Result: tapping the composer focused the input but the keyboard never appeared, looking like an instant dismiss. The touchstart workaround is now gated to iOS UAs via `isIOS()`.

View File

@@ -42,7 +42,7 @@ import { useModelsCache } from "../hooks/useModelsCache";
import { useDiscoveredSkillsCache } from "../hooks/useDiscoveredSkillsCache"; import { useDiscoveredSkillsCache } from "../hooks/useDiscoveredSkillsCache";
import { useAgentsMapCache } from "../hooks/useAgentsMapCache"; import { useAgentsMapCache } from "../hooks/useAgentsMapCache";
import { useMobileKeyboard } from "../hooks/useMobileKeyboard"; import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useMobileScrollLock, isIOS } from "../hooks/useMobileScrollLock";
import { matchesAgentMentionFilter } from "./mentionMatching"; import { matchesAgentMentionFilter } from "./mentionMatching";
import { useNavigationHistoryContext } from "../hooks/useNavigationHistory"; import { useNavigationHistoryContext } from "../hooks/useNavigationHistory";
import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify";
@@ -2988,6 +2988,13 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
onTouchStart={(event) => { onTouchStart={(event) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (window.innerWidth > 768) return; if (window.innerWidth > 768) return;
// iOS-only: preventDefault + programmatic focus avoids
// iOS's visual-viewport scroll on re-focus. On Android,
// preventDefault here blocks the soft keyboard from
// opening at all (programmatic focus() does not raise
// the keyboard on Android), so the input "focuses" but
// the keyboard never appears.
if (!isIOS()) return;
if (document.activeElement === event.currentTarget) return; if (document.activeElement === event.currentTarget) return;
event.preventDefault(); event.preventDefault();
event.currentTarget.focus({ preventScroll: true }); event.currentTarget.focus({ preventScroll: true });
@@ -3313,6 +3320,10 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
onTouchStart={(event) => { onTouchStart={(event) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (window.innerWidth > 768) return; if (window.innerWidth > 768) return;
// iOS-only: see comment on the other chat-input touchstart
// handler above. On Android, preventDefault blocks the
// soft keyboard from opening.
if (!isIOS()) return;
if (document.activeElement === event.currentTarget) return; if (document.activeElement === event.currentTarget) return;
event.preventDefault(); event.preventDefault();
event.currentTarget.focus({ preventScroll: true }); event.currentTarget.focus({ preventScroll: true });

View File

@@ -27,6 +27,7 @@ import { useDiscoveredSkillsCache } from "../hooks/useDiscoveredSkillsCache";
import { FileMentionPopup } from "./FileMentionPopup"; import { FileMentionPopup } from "./FileMentionPopup";
import { useFileMention } from "../hooks/useFileMention"; import { useFileMention } from "../hooks/useFileMention";
import { useMobileKeyboard } from "../hooks/useMobileKeyboard"; import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
import { isIOS } from "../hooks/useMobileScrollLock";
import { useViewportMode } from "../hooks/useViewportMode"; import { useViewportMode } from "../hooks/useViewportMode";
import { useAppSettings } from "../hooks/useAppSettings"; import { useAppSettings } from "../hooks/useAppSettings";
import { useChatRooms } from "../hooks/useChatRooms"; import { useChatRooms } from "../hooks/useChatRooms";
@@ -2962,6 +2963,12 @@ export function QuickChatFAB({
onTouchStart={(event) => { onTouchStart={(event) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return; if (window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT) return;
// iOS-only workaround. On Android, preventDefault on
// textarea touchstart prevents the soft keyboard from
// opening at all (programmatic focus() does not raise
// the keyboard on Android — only the default touch
// action does), so the tap silently dismisses.
if (!isIOS()) return;
if (document.activeElement === event.currentTarget) return; if (document.activeElement === event.currentTarget) return;
event.preventDefault(); event.preventDefault();
event.currentTarget.focus({ preventScroll: true }); event.currentTarget.focus({ preventScroll: true });