diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb..220e73e774 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -180,6 +180,7 @@ const ChatTextArea = forwardRef( const contextMenuContainerRef = useRef(null) const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) const [isFocused, setIsFocused] = useState(false) + const [screenReaderAnnouncement, setScreenReaderAnnouncement] = useState("") // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ @@ -500,8 +501,16 @@ const ChatTextArea = forwardRef( setCursorPosition(newCursorPosition) const showMenu = shouldShowContextMenu(newValue, newCursorPosition) + const wasMenuVisible = showContextMenu setShowContextMenu(showMenu) + // Announce menu state changes for screen readers + if (showMenu && !wasMenuVisible) { + setScreenReaderAnnouncement("File insertion menu opened") + } else if (!showMenu && wasMenuVisible) { + setScreenReaderAnnouncement("File insertion menu closed") + } + if (showMenu) { if (newValue.startsWith("/")) { // Handle slash command. @@ -559,6 +568,48 @@ const ChatTextArea = forwardRef( } }, [showContextMenu]) + // Announce selected menu item for screen readers + useEffect(() => { + if (showContextMenu && selectedMenuIndex >= 0) { + const options = getContextMenuOptions( + searchQuery, + inputValue, + selectedType, + queryItems, + fileSearchResults, + allModes, + ) + 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}` + } + setScreenReaderAnnouncement(announcement) + } + } + }, [showContextMenu, selectedMenuIndex, searchQuery, inputValue, selectedType, queryItems, fileSearchResults, allModes]) + const handleBlur = useCallback(() => { // Only hide the context menu if the user didn't click on it. if (!isMouseDownOnMenu) { @@ -1076,6 +1127,10 @@ const ChatTextArea = forwardRef( minRows={3} maxRows={15} autoFocus={true} + aria-expanded={showContextMenu} + aria-haspopup="listbox" + aria-controls={showContextMenu ? "context-menu" : undefined} + aria-describedby="context-menu-instructions" className={cn( "w-full", "text-vscode-input-foreground", @@ -1249,6 +1304,26 @@ const ChatTextArea = forwardRef( )} + {/* Live region for screen reader announcements */} +
+ {screenReaderAnnouncement} +
+ + {/* Instructions for screen readers */} +
+ Type @ to open file insertion menu. Use arrow keys to navigate, Enter to select, Escape to close. +
+ {renderTextAreaSection()} diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 1672c35ee3..55d13703ef 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -208,7 +208,11 @@ const ContextMenu: React.FC = ({ }} onMouseDown={onMouseDown}>
= 0 ? `context-menu-option-${selectedIndex}` : undefined} style={{ backgroundColor: "var(--vscode-dropdown-background)", border: "1px solid var(--vscode-editorGroup-border)", @@ -224,6 +228,10 @@ const ContextMenu: React.FC = ({ filteredOptions.map((option, index) => (
isOptionSelectable(option) && onSelect(option.type, option.value)} style={{ padding: "4px 6px",