Improve accessibility across components

FileTree:
- Add ARIA labels to new file button
- Add role="button", tabIndex, and keyboard navigation (Enter/Space) for file/folder items
- Add aria-label for expand/collapse folder states
- Add aria-expanded attribute for folders
- Add focus ring styles (focus:ring-2 focus:ring-accent)
- Add aria-hidden to decorative icons

SearchInFilesPanel:
- Add ARIA labels to close button and search button
- Add aria-label to search input
- Add aria-live="polite" to results count badge
- Add keyboard navigation (Enter/Space) to search results
- Add focus ring styles to search results
- Add role="button" to clickable result items
- Add aria-label to case sensitive checkbox
- Add aria-hidden to decorative icons

AIChat:
- Add aria-live="polite" to chat messages area
- Add role="log" to messages container
- Add aria-label to message input textarea
- Add aria-label to send button
- Add role="note" to keyboard shortcut hint
- Add aria-hidden to decorative icons
This commit is contained in:
Claude 2026-01-17 22:31:23 +00:00
parent 394000f5ad
commit 640a8836b6
No known key found for this signature in database
3 changed files with 44 additions and 16 deletions

View file

@ -73,12 +73,12 @@ export function AIChat({ currentCode }: AIChatProps) {
return ( return (
<div className="flex flex-col h-full bg-card border-l border-border min-w-[260px] max-w-[340px]"> <div className="flex flex-col h-full bg-card border-l border-border min-w-[260px] max-w-[340px]">
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-card/80"> <div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-card/80">
<Sparkle className="text-accent" weight="fill" /> <Sparkle className="text-accent" weight="fill" aria-hidden="true" />
<h2 className="font-semibold text-xs tracking-wide uppercase text-muted-foreground">AI Assistant</h2> <h2 className="font-semibold text-xs tracking-wide uppercase text-muted-foreground">AI Assistant</h2>
</div> </div>
<ScrollArea className="flex-1 px-2 py-2"> <ScrollArea className="flex-1 px-2 py-2" aria-live="polite" aria-label="Chat messages">
<div className="space-y-2"> <div className="space-y-2" role="log">
{messages.map((message, index) => ( {messages.map((message, index) => (
<div <div
key={index} key={index}
@ -122,18 +122,20 @@ export function AIChat({ currentCode }: AIChatProps) {
placeholder="Ask about your code..." placeholder="Ask about your code..."
className="resize-none min-h-[36px] max-h-24 bg-background text-xs px-2 py-1" className="resize-none min-h-[36px] max-h-24 bg-background text-xs px-2 py-1"
disabled={isLoading} disabled={isLoading}
aria-label="Chat message input"
/> />
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={!input.trim() || isLoading} disabled={!input.trim() || isLoading}
className="bg-accent text-accent-foreground hover:bg-accent/90 btn-accent-hover self-end h-8 w-8 p-0" className="bg-accent text-accent-foreground hover:bg-accent/90 btn-accent-hover self-end h-8 w-8 p-0"
tabIndex={-1} tabIndex={-1}
title="Send" title="Send message"
aria-label="Send message"
> >
<PaperPlaneRight weight="fill" size={16} /> <PaperPlaneRight weight="fill" size={16} aria-hidden="true" />
</Button> </Button>
</div> </div>
<p className="text-[10px] text-muted-foreground mt-1"> <p className="text-[10px] text-muted-foreground mt-1" role="note">
Press Enter to send, Shift+Enter for new line Press Enter to send, Shift+Enter for new line
</p> </p>
</div> </div>

View file

@ -147,13 +147,27 @@ export function FileTree({
return ( return (
<div key={node.id}> <div key={node.id}>
<div <div
role={node.type === 'folder' ? 'button' : 'button'}
tabIndex={0}
aria-label={node.type === 'folder' ? `${isExpanded ? 'Collapse' : 'Expand'} folder ${node.name}` : `Open file ${node.name}`}
aria-expanded={node.type === 'folder' ? isExpanded : undefined}
draggable={!isEditing} draggable={!isEditing}
onDragStart={(e) => handleDragStart(e, node)} onDragStart={(e) => handleDragStart(e, node)}
onDragOver={(e) => handleDragOver(e, node)} onDragOver={(e) => handleDragOver(e, node)}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, node)} onDrop={(e) => handleDrop(e, node)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
className={`flex items-center gap-1 px-2 py-1.5 md:py-1 hover:bg-muted/60 cursor-pointer group rounded-sm transition-colors touch-manipulation ${ onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (node.type === 'folder') {
toggleFolder(node.id);
} else {
onFileSelect(node);
}
}
}}
className={`flex items-center gap-1 px-2 py-1.5 md:py-1 hover:bg-muted/60 cursor-pointer group rounded-sm transition-colors touch-manipulation focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1 ${
isSelected ? 'bg-accent/30 text-accent font-semibold' : 'text-foreground' isSelected ? 'bg-accent/30 text-accent font-semibold' : 'text-foreground'
} ${isDragging ? 'opacity-50' : ''} ${isDropTarget && node.type === 'folder' ? 'bg-blue-500/20 border-2 border-blue-500 border-dashed' : ''}`} } ${isDragging ? 'opacity-50' : ''} ${isDropTarget && node.type === 'folder' ? 'bg-blue-500/20 border-2 border-blue-500 border-dashed' : ''}`}
style={{ paddingLeft: `${depth * 10 + 8}px` }} style={{ paddingLeft: `${depth * 10 + 8}px` }}
@ -234,6 +248,7 @@ export function FileTree({
size="icon" size="icon"
className="h-6 w-6" className="h-6 w-6"
title="New File" title="New File"
aria-label="Create new file"
onClick={() => { onClick={() => {
const name = prompt('Enter file name:'); const name = prompt('Enter file name:');
if (name) { if (name) {
@ -241,7 +256,7 @@ export function FileTree({
} }
}} }}
> >
<Plus size={14} /> <Plus size={14} aria-hidden="true" />
</Button> </Button>
</div> </div>
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">

View file

@ -119,16 +119,16 @@ export function SearchInFilesPanel({
<div className="h-[300px] md:h-[400px] bg-card border-t border-border flex flex-col"> <div className="h-[300px] md:h-[400px] bg-card border-t border-border flex flex-col">
<div className="flex items-center justify-between px-4 py-2 border-b border-border"> <div className="flex items-center justify-between px-4 py-2 border-b border-border">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MagnifyingGlass size={18} weight="bold" /> <MagnifyingGlass size={18} weight="bold" aria-hidden="true" />
<span className="text-sm font-semibold">Search in Files</span> <span className="text-sm font-semibold">Search in Files</span>
{results.length > 0 && ( {results.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs" aria-live="polite">
{results.length} results {results.length} result{results.length !== 1 ? 's' : ''}
</Badge> </Badge>
)} )}
</div> </div>
<Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6"> <Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6" aria-label="Close search panel">
<X size={16} /> <X size={16} aria-hidden="true" />
</Button> </Button>
</div> </div>
@ -142,9 +142,10 @@ export function SearchInFilesPanel({
if (e.key === 'Enter') handleSearch(); if (e.key === 'Enter') handleSearch();
}} }}
className="flex-1" className="flex-1"
aria-label="Search query"
/> />
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}> <Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()} aria-label="Search in files">
<MagnifyingGlass size={16} className="mr-1" /> <MagnifyingGlass size={16} className="mr-1" aria-hidden="true" />
Search Search
</Button> </Button>
</div> </div>
@ -156,6 +157,7 @@ export function SearchInFilesPanel({
checked={caseSensitive} checked={caseSensitive}
onChange={(e) => setCaseSensitive(e.target.checked)} onChange={(e) => setCaseSensitive(e.target.checked)}
className="rounded" className="rounded"
aria-label="Case sensitive search"
/> />
<span>Case sensitive</span> <span>Case sensitive</span>
</label> </label>
@ -179,8 +181,17 @@ export function SearchInFilesPanel({
{results.map((result, index) => ( {results.map((result, index) => (
<div <div
key={`${result.fileId}-${result.line}-${index}`} key={`${result.fileId}-${result.line}-${index}`}
role="button"
tabIndex={0}
onClick={() => handleResultClick(result)} onClick={() => handleResultClick(result)}
className="p-2 rounded hover:bg-muted/60 cursor-pointer group transition-colors border border-transparent hover:border-accent/30" onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleResultClick(result);
}
}}
aria-label={`Open ${result.fileName} at line ${result.line}`}
className="p-2 rounded hover:bg-muted/60 cursor-pointer group transition-colors border border-transparent hover:border-accent/30 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1"
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<FileText size={14} className="text-muted-foreground flex-shrink-0" /> <FileText size={14} className="text-muted-foreground flex-shrink-0" />