From 7e5c4862cef45f28163ef49eff0967c5a25767f4 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 17:42:03 +0000 Subject: [PATCH] fix: address review feedback for accessibility improvements - Add translations for all announcement strings using helper function - Translate instruction text for screen readers - Add debouncing to useEffect to improve performance during keyboard navigation - Replace inline styles with Tailwind classes for live region - Import missing ContextMenuQueryItem type - Fix ESLint warning by adding t to dependency array --- .../src/components/chat/ChatTextArea.tsx | 113 +++++++++++------- webview-ui/src/i18n/locales/en/chat.json | 13 ++ 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 4e6f6eab95..f3c9ce37db 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -12,6 +12,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext" import { useAppTranslation } from "@/i18n/TranslationContext" import { ContextMenuOptionType, + ContextMenuQueryItem, getContextMenuOptions, insertMention, removeMention, @@ -506,9 +507,9 @@ const ChatTextArea = forwardRef( // Announce menu state changes for screen readers if (showMenu && !wasMenuVisible) { - setScreenReaderAnnouncement("File insertion menu opened") + setScreenReaderAnnouncement(t("chat:contextMenu.menuOpened")) } else if (!showMenu && wasMenuVisible) { - setScreenReaderAnnouncement("File insertion menu closed") + setScreenReaderAnnouncement(t("chat:contextMenu.menuClosed")) } if (showMenu) { @@ -559,7 +560,14 @@ const ChatTextArea = forwardRef( setFileSearchResults([]) // Clear file search results. } }, - [setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange, showContextMenu], + [ + setInputValue, + setSearchRequestId, + setFileSearchResults, + setSearchLoading, + resetOnInputChange, + showContextMenu, + ], ) useEffect(() => { @@ -568,9 +576,52 @@ const ChatTextArea = forwardRef( } }, [showContextMenu]) - // Announce selected menu item for screen readers + // Helper function to get announcement text for screen readers + const getAnnouncementText = useCallback( + (option: ContextMenuQueryItem, index: number, total: number) => { + const position = t("chat:contextMenu.position", { current: index + 1, total }) + + switch (option.type) { + case ContextMenuOptionType.File: + case ContextMenuOptionType.OpenedFile: + return t("chat:contextMenu.announceFile", { + name: option.value || option.label, + position, + }) + case ContextMenuOptionType.Folder: + return t("chat:contextMenu.announceFolder", { + name: option.value || option.label, + position, + }) + case ContextMenuOptionType.Problems: + return t("chat:contextMenu.announceProblems", { position }) + case ContextMenuOptionType.Terminal: + return t("chat:contextMenu.announceTerminal", { position }) + case ContextMenuOptionType.Git: + return t("chat:contextMenu.announceGit", { + name: option.label || option.value, + position, + }) + case ContextMenuOptionType.Mode: + return t("chat:contextMenu.announceMode", { + name: option.label, + position, + }) + default: + return t("chat:contextMenu.announceGeneric", { + name: option.label || option.value, + position, + }) + } + }, + [t], + ) + + // Announce selected menu item for screen readers with debouncing useEffect(() => { - if (showContextMenu && selectedMenuIndex >= 0) { + if (!showContextMenu || selectedMenuIndex < 0) return + + const timeoutId = setTimeout(() => { const options = getContextMenuOptions( searchQuery, inputValue, @@ -581,34 +632,23 @@ const ChatTextArea = forwardRef( ) const selectedOption = options[selectedMenuIndex] if (selectedOption && selectedOption.type !== ContextMenuOptionType.NoResults) { - let announcement = "" - switch (selectedOption.type) { - case ContextMenuOptionType.File: - case ContextMenuOptionType.OpenedFile: - announcement = `File: ${selectedOption.value || selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}` - break - case ContextMenuOptionType.Folder: - announcement = `Folder: ${selectedOption.value || selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}` - break - case ContextMenuOptionType.Problems: - announcement = `Problems, ${selectedMenuIndex + 1} of ${options.length}` - break - case ContextMenuOptionType.Terminal: - announcement = `Terminal, ${selectedMenuIndex + 1} of ${options.length}` - break - case ContextMenuOptionType.Git: - announcement = `Git: ${selectedOption.label || selectedOption.value}, ${selectedMenuIndex + 1} of ${options.length}` - break - case ContextMenuOptionType.Mode: - announcement = `Mode: ${selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}` - break - default: - announcement = `${selectedOption.label || selectedOption.value}, ${selectedMenuIndex + 1} of ${options.length}` - } + const announcement = getAnnouncementText(selectedOption, selectedMenuIndex, options.length) setScreenReaderAnnouncement(announcement) } - } - }, [showContextMenu, selectedMenuIndex, searchQuery, inputValue, selectedType, queryItems, fileSearchResults, allModes]) + }, 100) // Small delay to avoid rapid announcements + + return () => clearTimeout(timeoutId) + }, [ + showContextMenu, + selectedMenuIndex, + searchQuery, + inputValue, + selectedType, + queryItems, + fileSearchResults, + allModes, + getAnnouncementText, + ]) const handleBlur = useCallback(() => { // Only hide the context menu if the user didn't click on it. @@ -1308,20 +1348,13 @@ const ChatTextArea = forwardRef(
+ className="sr-only absolute -left-[10000px] w-px h-px overflow-hidden"> {screenReaderAnnouncement}
{/* Instructions for screen readers */}
- Type @ to open file insertion menu. Use arrow keys to navigate, Enter to select, Escape to close. + {t("chat:contextMenu.instructions")}
{renderTextAreaSection()} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index aed3bcfdc5..9619e51e33 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -324,5 +324,18 @@ }, "versionIndicator": { "ariaLabel": "Version {{version}} - Click to view release notes" + }, + "contextMenu": { + "instructions": "Type @ to open file insertion menu. Use arrow keys to navigate, Enter to select, Escape to close.", + "menuOpened": "File insertion menu opened", + "menuClosed": "File insertion menu closed", + "position": "{{current}} of {{total}}", + "announceFile": "File: {{name}}, {{position}}", + "announceFolder": "Folder: {{name}}, {{position}}", + "announceProblems": "Problems, {{position}}", + "announceTerminal": "Terminal, {{position}}", + "announceGit": "Git: {{name}}, {{position}}", + "announceMode": "Mode: {{name}}, {{position}}", + "announceGeneric": "{{name}}, {{position}}" } }