From 517de7228a9d4da09a04704bdeb411b297b9acf4 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 74708eff8d8c0..966258208a971 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 0385a6b64e126..1b1d8bc8133d5 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -30,7 +30,7 @@ import { import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import type { ReactNode } from "react"; import { useEffect, useLayoutEffect, 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"; @@ -127,6 +127,9 @@ import { export const COMPOSER_COLLAPSED_CHROME = 60; 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; @@ -196,7 +199,9 @@ export function useCompactComposerEditor(text: ScaledTextRole, value: string) { overflow: "hidden", height: animatedHeight.value, 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, bottom: (compactRowHeight - minimum) / 2 + expansion.value * (52 - (compactRowHeight - minimum) / 2), })); @@ -213,7 +218,9 @@ export 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: { @@ -979,7 +986,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) => (