From c9585c88064cdec31c444f15f0481372e0463ee4 Mon Sep 17 00:00:00 2001 From: PR Batch Tester Date: Sat, 24 Oct 2026 08:18:11 +0000 Subject: [PATCH] fix(mobile): taps near the end of the composer's text focus it instead of opening the model picker On phones the icon-only model button's leading padding started right where the text field ended, so a tap in the blank gap before the provider icon opened the model sheet. The text field's own slot also ignored taps on the few points around the native editor. The icon-only model button now starts just before its icon, the editor slot takes that gap and focuses the editor when tapped, and the editor ends a bit earlier so the text keeps the same visual gap to the icon. Fixes #24303 Co-Authored-By: Claude Opus 5.5 --- .../mobile/src/components/ComposerToolbar.tsx | 7 +++++- .../src/features/threads/ThreadComposer.tsx | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/components/ComposerToolbar.tsx b/apps/mobile/src/components/ComposerToolbar.tsx index 8963a4039d5f3..93ec96ca1b19e 100644 --- a/apps/mobile/src/components/ComposerToolbar.tsx +++ b/apps/mobile/src/components/ComposerToolbar.tsx @@ -60,7 +60,12 @@ export function ComposerInlineControl(props: { accessibilityState={ props.static ? undefined : { disabled: props.disabled, selected: props.selected } } - className="h-[44px] flex-row items-center gap-2 rounded-xl px-2 active:bg-subtle" + // Icon-only, the control sits right after the composer's text, where + // leading padding would take taps meant for the text field. + className={cn( + "h-[44px] flex-row items-center gap-2 rounded-xl pr-2 active:bg-subtle", + props.iconOnly ? "pl-1" : "pl-2", + )} disabled={props.disabled || props.static} onPress={props.onPress} style={{ opacity: props.disabled ? 0.45 : 1 }} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 024fc14179fd2..3c015a1ed4e01 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -32,7 +32,7 @@ import { import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import type { ReactNode } from "react"; import { useEffect, useEffectEvent, useRef, useState, type RefObject } from "react"; -import { Keyboard, Platform, View, type ViewStyle } from "react-native"; +import { Keyboard, Platform, Pressable, View, type ViewStyle } from "react-native"; import { composerImageGallery } from "../../lib/composerImageGallery"; import { filePreviewGallery } from "../../lib/filePreviewGallery"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; @@ -123,6 +123,9 @@ export const COMPOSER_COLLAPSED_CHROME = 60; const NO_MACHINES: ReadonlyArray = []; const COLLAPSED_EDITOR_DEFAULT_INSET = 6; +// Space between the compact editor and the icon-only model control. It belongs +// to the editor slot, so a tap that lands short of the icon focuses the editor. +const COMPACT_EDITOR_TRAILING_GAP = 12; // iOS puts line-height slack above glyphs. DM Sans's lowercase midline is // descender (0.31em) + half x-height (0.504em) above the line bottom. const LOWERCASE_MIDLINE_EM = 0.562; @@ -162,7 +165,9 @@ function useCompactComposerEditor(text: ScaledTextRole, value: string) { overflow: "hidden", height, left: isExpanded ? 14 : editorX.value + 4, - right: isExpanded ? 14 : rowWidth.value - editorX.value - editorWidth.value + 4, + right: isExpanded + ? 14 + : rowWidth.value - editorX.value - editorWidth.value + COMPACT_EDITOR_TRAILING_GAP, // The full-width editor must move above the controls in the same frame it widens. bottom: isExpanded ? 52 : (compactRowHeight - minimum) / 2, })); @@ -179,7 +184,9 @@ function useCompactComposerEditor(text: ScaledTextRole, value: string) { if (!isExpanded) { editorX.set(event.nativeEvent.layout.x); editorWidth.set(event.nativeEvent.layout.width); - setCompactWidth(Math.max(0, event.nativeEvent.layout.width - 8)); + setCompactWidth( + Math.max(0, event.nativeEvent.layout.width - 4 - COMPACT_EDITOR_TRAILING_GAP), + ); } }, editorProps: { @@ -905,7 +912,14 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps compactEditor.containerStyle, compactEditor.isExpanded ? { flex: 0, width: 0 } : undefined, ]} - > + > + {/* The editor overlay is inset from this slot; taps on the inset still focus it. */} + inputRef.current?.focus()} + /> + {!isExpanded && stripAttachments.length > 0 ? ( {stripAttachments.slice(0, compactAttachmentLimit).map((attachment) => (