fix(mobile): label the reasoning toggle and give it a full touch target

The toggle now tells a screen reader what it is, "Reasoning: Thought
for 3s", as desktop's prefix does, and reaches a 44 pt target. The
shared English copy stays private to the module that formats it.
This commit is contained in:
Brennan Benson
2026-10-01 22:17:39 -07:00
parent 038a922c58
commit 213076b9bb
4 changed files with 15 additions and 6 deletions
@@ -307,6 +307,9 @@ describe('MobileNativeChatMessage', () => {
const tree = render(reasoning())
expect(textIn(tree.root)).toContain('Thought for 3s')
expect(toggleOf(tree).props.accessibilityState).toEqual({ expanded: false })
// Said with what it is, as desktop's screen-reader prefix does, on a 32 + 2 × 6 pt target.
expect(toggleOf(tree).props.accessibilityLabel).toBe('Reasoning: Thought for 3s')
expect(toggleOf(tree).props.hitSlop).toBe(6)
expect(markdownIn(tree)).toHaveLength(0)
})
@@ -328,9 +331,10 @@ describe('MobileNativeChatMessage', () => {
it('says only what the host saw', () => {
expect(textIn(render(reasoning({ state: 'running' })).root)).toContain('Thought')
expect(
textIn(render(reasoning({ state: undefined, completedAt: undefined })).root)
).toContain('Reasoning')
const unknown = render(reasoning({ state: undefined, completedAt: undefined }))
expect(textIn(unknown.root)).toContain('Reasoning')
// No "Reasoning: Reasoning".
expect(toggleOf(unknown).props.accessibilityLabel).toBe('Reasoning')
expect(textIn(render(reasoning({ completedAt: 1_300 })).root)).toContain('Thought for 1s')
})
})
@@ -24,6 +24,8 @@ export function MobileNativeChatReasoningRow({
onOpenFile?: (relativePath: string) => void
}): React.JSX.Element {
const [expanded, setExpanded] = useState(false)
const headline = nativeChatReasoningHeadlineText(nativeChatReasoningHeadline(message))
const label = nativeChatReasoningHeadlineText({ kind: 'reasoning' })
return (
<View>
<Pressable
@@ -32,9 +34,11 @@ export function MobileNativeChatReasoningRow({
hitSlop={6}
accessibilityRole="button"
accessibilityState={{ expanded }}
// Desktop's screen-reader prefix: the headline alone does not say what was thought.
accessibilityLabel={headline === label ? label : `${label}: ${headline}`}
>
<Text style={styles.reasoningHeadline} numberOfLines={1}>
{nativeChatReasoningHeadlineText(nativeChatReasoningHeadline(message))}
{headline}
</Text>
<View style={expanded ? styles.reasoningCaretOpen : undefined}>
<ChevronRight size={14} color={colors.textMuted} strokeWidth={2} />
@@ -36,7 +36,8 @@ export const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingVertical: 3
// With the toggle's 6 pt hitSlop above and below, a 44 pt touch target.
minHeight: 32
},
reasoningPressed: {
opacity: 0.6
+1 -1
View File
@@ -44,7 +44,7 @@ export function nativeChatReasoningHeadline(
}
/** English copy for clients without a translation catalog; desktop translates the same three. */
export const NATIVE_CHAT_REASONING_COPY = {
const NATIVE_CHAT_REASONING_COPY = {
reasoning: 'Reasoning',
thought: 'Thought',
thoughtFor: (duration: string) => `Thought for ${duration}`