From b441339bb45fb96f8063cfe0985ee48d0456e931 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 10 Oct 2026 04:29:33 -0300 Subject: [PATCH] feat(a11y): label icon-only buttons and inputs for screen readers Header, message action, send/stop, history, model selector and dark-mode controls had no accessible name (icon-only buttons). Adds roles/labels and selected/disabled state; no visual change. --- __tests__/ChatInput.test.tsx | 39 ++++++++++++++++++++++++++ app/chat/_layout.tsx | 6 ++++ app/chat/history.tsx | 11 +++++++- components/chat/ChatInput.tsx | 13 ++++++++- components/chat/CodeBlock.tsx | 6 +++- components/chat/MessageActions.tsx | 8 +++--- components/chat/ModelSelector.tsx | 7 +++++ components/settings/AppearanceCard.tsx | 1 + 8 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 __tests__/ChatInput.test.tsx diff --git a/__tests__/ChatInput.test.tsx b/__tests__/ChatInput.test.tsx new file mode 100644 index 0000000..2ece3b5 --- /dev/null +++ b/__tests__/ChatInput.test.tsx @@ -0,0 +1,39 @@ +import { fireEvent, render, screen } from '@testing-library/react-native'; +import React from 'react'; +import { ChatInput } from '../components/chat/ChatInput'; + +describe('ChatInput', () => { + it('sends trimmed text, clears the field and disables send while empty', () => { + const onSend = jest.fn(); + render(); + + expect(screen.getByLabelText('Send message').props.accessibilityState.disabled).toBe(true); + + fireEvent.changeText(screen.getByLabelText('Message'), ' hello '); + fireEvent.press(screen.getByLabelText('Send message')); + + expect(onSend).toHaveBeenCalledWith('hello'); + expect(screen.getByLabelText('Message').props.value).toBe(''); + }); + + it('shows a stop button instead of send while loading', () => { + const onStop = jest.fn(); + render(); + + expect(screen.queryByLabelText('Send message')).toBeNull(); + fireEvent.press(screen.getByLabelText('Stop generating')); + expect(onStop).toHaveBeenCalledTimes(1); + }); + + it('fills the field when editing and lets the user cancel', () => { + const onCancelEdit = jest.fn(); + render(); + + expect(screen.getByText('Editing message')).toBeTruthy(); + expect(screen.getByLabelText('Message').props.value).toBe('old text'); + + fireEvent.press(screen.getByLabelText('Cancel editing')); + expect(onCancelEdit).toHaveBeenCalledTimes(1); + expect(screen.getByLabelText('Message').props.value).toBe(''); + }); +}); diff --git a/app/chat/_layout.tsx b/app/chat/_layout.tsx index 291f819..288c55f 100644 --- a/app/chat/_layout.tsx +++ b/app/chat/_layout.tsx @@ -36,6 +36,8 @@ export default function ChatLayout() { router.push('/chat/history')} + accessibilityRole="button" + accessibilityLabel="Chat history" style={[styles.headerBtn, { backgroundColor: colors.cardBackground }]} activeOpacity={0.7} > @@ -43,6 +45,8 @@ export default function ChatLayout() { @@ -53,6 +57,8 @@ export default function ChatLayout() { headerRight: () => ( router.push('/settings')} + accessibilityRole="button" + accessibilityLabel="Settings" style={[styles.headerBtn, { backgroundColor: colors.cardBackground }]} activeOpacity={0.7} > diff --git a/app/chat/history.tsx b/app/chat/history.tsx index afca7a3..6d438fb 100644 --- a/app/chat/history.tsx +++ b/app/chat/history.tsx @@ -20,7 +20,12 @@ export default function HistoryScreen() { return ( - router.back()} style={styles.backButton}> + router.back()} + style={styles.backButton} + accessibilityRole="button" + accessibilityLabel="Back" + > Chat History @@ -43,6 +48,8 @@ export default function HistoryScreen() { { backgroundColor: colors.cardBackground, borderColor: colors.border } ]} onPress={() => handleLoadChat(item.id)} + accessibilityRole="button" + accessibilityLabel={`Open chat: ${item.title}`} > @@ -55,6 +62,8 @@ export default function HistoryScreen() { deleteChatSession(item.id)} + accessibilityRole="button" + accessibilityLabel={`Delete chat: ${item.title}`} > diff --git a/components/chat/ChatInput.tsx b/components/chat/ChatInput.tsx index 761ee70..8e19bda 100644 --- a/components/chat/ChatInput.tsx +++ b/components/chat/ChatInput.tsx @@ -65,7 +65,12 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit {isEditing && ( Editing message - + @@ -80,6 +85,7 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit @@ -106,6 +114,9 @@ export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit onPress={handleSend} disabled={!text.trim()} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel="Send message" + accessibilityState={{ disabled: !text.trim() }} > diff --git a/components/chat/CodeBlock.tsx b/components/chat/CodeBlock.tsx index e03bbca..18d866d 100644 --- a/components/chat/CodeBlock.tsx +++ b/components/chat/CodeBlock.tsx @@ -16,7 +16,11 @@ export function CodeBlock({ language, content, copied, onCopy }: CodeBlockProps) {language} - onCopy(content)} style={styles.copyButton}> + onCopy(content)} + style={styles.copyButton} + accessibilityRole="button" + > {copied ? 'Copied!' : 'Copy'} diff --git a/components/chat/MessageActions.tsx b/components/chat/MessageActions.tsx index 9e0804a..9f59837 100644 --- a/components/chat/MessageActions.tsx +++ b/components/chat/MessageActions.tsx @@ -50,24 +50,24 @@ export function MessageActions({ return ( - + {isUser && onEdit && ( - + )} {!isUser && onRegenerate && ( - + )} {onDelete && ( - + )} diff --git a/components/chat/ModelSelector.tsx b/components/chat/ModelSelector.tsx index ddefbce..a5f2acb 100644 --- a/components/chat/ModelSelector.tsx +++ b/components/chat/ModelSelector.tsx @@ -42,6 +42,8 @@ export function ModelSelector() { style={[styles.headerButton, { backgroundColor: colors.cardBackground }]} onPress={() => setModalVisible(true)} activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`Model: ${activeModelName}. Tap to change`} > @@ -70,6 +72,8 @@ export function ModelSelector() { onPress={handleRefresh} style={[styles.refreshButton, { backgroundColor: colors.cardBackground }]} disabled={isRefreshing} + accessibilityRole="button" + accessibilityLabel="Refresh models" > {isRefreshing ? ( @@ -108,6 +112,9 @@ export function ModelSelector() { isSelected && { borderColor: colors.tint } ]} onPress={() => handleSelect(item.id)} + accessibilityRole="button" + accessibilityLabel={item.id} + accessibilityState={{ selected: isSelected }} > diff --git a/components/settings/AppearanceCard.tsx b/components/settings/AppearanceCard.tsx index e86cda3..5acbf95 100644 --- a/components/settings/AppearanceCard.tsx +++ b/components/settings/AppearanceCard.tsx @@ -29,6 +29,7 @@ export function AppearanceCard() {