Skip to content

Commit 7e5c486

Browse files
committed
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
1 parent f9fc668 commit 7e5c486

File tree

2 files changed

+86
-40
lines changed

2 files changed

+86
-40
lines changed

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
1212
import { useAppTranslation } from "@/i18n/TranslationContext"
1313
import {
1414
ContextMenuOptionType,
15+
ContextMenuQueryItem,
1516
getContextMenuOptions,
1617
insertMention,
1718
removeMention,
@@ -506,9 +507,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
506507

507508
// Announce menu state changes for screen readers
508509
if (showMenu && !wasMenuVisible) {
509-
setScreenReaderAnnouncement("File insertion menu opened")
510+
setScreenReaderAnnouncement(t("chat:contextMenu.menuOpened"))
510511
} else if (!showMenu && wasMenuVisible) {
511-
setScreenReaderAnnouncement("File insertion menu closed")
512+
setScreenReaderAnnouncement(t("chat:contextMenu.menuClosed"))
512513
}
513514

514515
if (showMenu) {
@@ -559,7 +560,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
559560
setFileSearchResults([]) // Clear file search results.
560561
}
561562
},
562-
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange, showContextMenu],
563+
[
564+
setInputValue,
565+
setSearchRequestId,
566+
setFileSearchResults,
567+
setSearchLoading,
568+
resetOnInputChange,
569+
showContextMenu,
570+
],
563571
)
564572

565573
useEffect(() => {
@@ -568,9 +576,52 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
568576
}
569577
}, [showContextMenu])
570578

571-
// Announce selected menu item for screen readers
579+
// Helper function to get announcement text for screen readers
580+
const getAnnouncementText = useCallback(
581+
(option: ContextMenuQueryItem, index: number, total: number) => {
582+
const position = t("chat:contextMenu.position", { current: index + 1, total })
583+
584+
switch (option.type) {
585+
case ContextMenuOptionType.File:
586+
case ContextMenuOptionType.OpenedFile:
587+
return t("chat:contextMenu.announceFile", {
588+
name: option.value || option.label,
589+
position,
590+
})
591+
case ContextMenuOptionType.Folder:
592+
return t("chat:contextMenu.announceFolder", {
593+
name: option.value || option.label,
594+
position,
595+
})
596+
case ContextMenuOptionType.Problems:
597+
return t("chat:contextMenu.announceProblems", { position })
598+
case ContextMenuOptionType.Terminal:
599+
return t("chat:contextMenu.announceTerminal", { position })
600+
case ContextMenuOptionType.Git:
601+
return t("chat:contextMenu.announceGit", {
602+
name: option.label || option.value,
603+
position,
604+
})
605+
case ContextMenuOptionType.Mode:
606+
return t("chat:contextMenu.announceMode", {
607+
name: option.label,
608+
position,
609+
})
610+
default:
611+
return t("chat:contextMenu.announceGeneric", {
612+
name: option.label || option.value,
613+
position,
614+
})
615+
}
616+
},
617+
[t],
618+
)
619+
620+
// Announce selected menu item for screen readers with debouncing
572621
useEffect(() => {
573-
if (showContextMenu && selectedMenuIndex >= 0) {
622+
if (!showContextMenu || selectedMenuIndex < 0) return
623+
624+
const timeoutId = setTimeout(() => {
574625
const options = getContextMenuOptions(
575626
searchQuery,
576627
inputValue,
@@ -581,34 +632,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
581632
)
582633
const selectedOption = options[selectedMenuIndex]
583634
if (selectedOption && selectedOption.type !== ContextMenuOptionType.NoResults) {
584-
let announcement = ""
585-
switch (selectedOption.type) {
586-
case ContextMenuOptionType.File:
587-
case ContextMenuOptionType.OpenedFile:
588-
announcement = `File: ${selectedOption.value || selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}`
589-
break
590-
case ContextMenuOptionType.Folder:
591-
announcement = `Folder: ${selectedOption.value || selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}`
592-
break
593-
case ContextMenuOptionType.Problems:
594-
announcement = `Problems, ${selectedMenuIndex + 1} of ${options.length}`
595-
break
596-
case ContextMenuOptionType.Terminal:
597-
announcement = `Terminal, ${selectedMenuIndex + 1} of ${options.length}`
598-
break
599-
case ContextMenuOptionType.Git:
600-
announcement = `Git: ${selectedOption.label || selectedOption.value}, ${selectedMenuIndex + 1} of ${options.length}`
601-
break
602-
case ContextMenuOptionType.Mode:
603-
announcement = `Mode: ${selectedOption.label}, ${selectedMenuIndex + 1} of ${options.length}`
604-
break
605-
default:
606-
announcement = `${selectedOption.label || selectedOption.value}, ${selectedMenuIndex + 1} of ${options.length}`
607-
}
635+
const announcement = getAnnouncementText(selectedOption, selectedMenuIndex, options.length)
608636
setScreenReaderAnnouncement(announcement)
609637
}
610-
}
611-
}, [showContextMenu, selectedMenuIndex, searchQuery, inputValue, selectedType, queryItems, fileSearchResults, allModes])
638+
}, 100) // Small delay to avoid rapid announcements
639+
640+
return () => clearTimeout(timeoutId)
641+
}, [
642+
showContextMenu,
643+
selectedMenuIndex,
644+
searchQuery,
645+
inputValue,
646+
selectedType,
647+
queryItems,
648+
fileSearchResults,
649+
allModes,
650+
getAnnouncementText,
651+
])
612652

613653
const handleBlur = useCallback(() => {
614654
// Only hide the context menu if the user didn't click on it.
@@ -1308,20 +1348,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
13081348
<div
13091349
aria-live="polite"
13101350
aria-atomic="true"
1311-
className="sr-only"
1312-
style={{
1313-
position: "absolute",
1314-
left: "-10000px",
1315-
width: "1px",
1316-
height: "1px",
1317-
overflow: "hidden",
1318-
}}>
1351+
className="sr-only absolute -left-[10000px] w-px h-px overflow-hidden">
13191352
{screenReaderAnnouncement}
13201353
</div>
13211354

13221355
{/* Instructions for screen readers */}
13231356
<div id="context-menu-instructions" className="sr-only">
1324-
Type @ to open file insertion menu. Use arrow keys to navigate, Enter to select, Escape to close.
1357+
{t("chat:contextMenu.instructions")}
13251358
</div>
13261359

13271360
{renderTextAreaSection()}

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,5 +324,18 @@
324324
},
325325
"versionIndicator": {
326326
"ariaLabel": "Version {{version}} - Click to view release notes"
327+
},
328+
"contextMenu": {
329+
"instructions": "Type @ to open file insertion menu. Use arrow keys to navigate, Enter to select, Escape to close.",
330+
"menuOpened": "File insertion menu opened",
331+
"menuClosed": "File insertion menu closed",
332+
"position": "{{current}} of {{total}}",
333+
"announceFile": "File: {{name}}, {{position}}",
334+
"announceFolder": "Folder: {{name}}, {{position}}",
335+
"announceProblems": "Problems, {{position}}",
336+
"announceTerminal": "Terminal, {{position}}",
337+
"announceGit": "Git: {{name}}, {{position}}",
338+
"announceMode": "Mode: {{name}}, {{position}}",
339+
"announceGeneric": "{{name}}, {{position}}"
327340
}
328341
}

0 commit comments

Comments
 (0)