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:
parent
394000f5ad
commit
640a8836b6
3 changed files with 44 additions and 16 deletions
|
|
@ -73,12 +73,12 @@ export function AIChat({ currentCode }: AIChatProps) {
|
|||
return (
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 px-2 py-2">
|
||||
<div className="space-y-2">
|
||||
<ScrollArea className="flex-1 px-2 py-2" aria-live="polite" aria-label="Chat messages">
|
||||
<div className="space-y-2" role="log">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
@ -122,18 +122,20 @@ export function AIChat({ currentCode }: AIChatProps) {
|
|||
placeholder="Ask about your code..."
|
||||
className="resize-none min-h-[36px] max-h-24 bg-background text-xs px-2 py-1"
|
||||
disabled={isLoading}
|
||||
aria-label="Chat message input"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
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"
|
||||
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>
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -147,13 +147,27 @@ export function FileTree({
|
|||
return (
|
||||
<div key={node.id}>
|
||||
<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}
|
||||
onDragStart={(e) => handleDragStart(e, node)}
|
||||
onDragOver={(e) => handleDragOver(e, node)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, node)}
|
||||
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'
|
||||
} ${isDragging ? 'opacity-50' : ''} ${isDropTarget && node.type === 'folder' ? 'bg-blue-500/20 border-2 border-blue-500 border-dashed' : ''}`}
|
||||
style={{ paddingLeft: `${depth * 10 + 8}px` }}
|
||||
|
|
@ -234,6 +248,7 @@ export function FileTree({
|
|||
size="icon"
|
||||
className="h-6 w-6"
|
||||
title="New File"
|
||||
aria-label="Create new file"
|
||||
onClick={() => {
|
||||
const name = prompt('Enter file name:');
|
||||
if (name) {
|
||||
|
|
@ -241,7 +256,7 @@ export function FileTree({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<Plus size={14} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
|
|
|
|||
|
|
@ -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="flex items-center justify-between px-4 py-2 border-b border-border">
|
||||
<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>
|
||||
{results.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{results.length} results
|
||||
<Badge variant="secondary" className="text-xs" aria-live="polite">
|
||||
{results.length} result{results.length !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6">
|
||||
<X size={16} />
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6" aria-label="Close search panel">
|
||||
<X size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -142,9 +142,10 @@ export function SearchInFilesPanel({
|
|||
if (e.key === 'Enter') handleSearch();
|
||||
}}
|
||||
className="flex-1"
|
||||
aria-label="Search query"
|
||||
/>
|
||||
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
|
||||
<MagnifyingGlass size={16} className="mr-1" />
|
||||
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()} aria-label="Search in files">
|
||||
<MagnifyingGlass size={16} className="mr-1" aria-hidden="true" />
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -156,6 +157,7 @@ export function SearchInFilesPanel({
|
|||
checked={caseSensitive}
|
||||
onChange={(e) => setCaseSensitive(e.target.checked)}
|
||||
className="rounded"
|
||||
aria-label="Case sensitive search"
|
||||
/>
|
||||
<span>Case sensitive</span>
|
||||
</label>
|
||||
|
|
@ -179,8 +181,17 @@ export function SearchInFilesPanel({
|
|||
{results.map((result, index) => (
|
||||
<div
|
||||
key={`${result.fileId}-${result.line}-${index}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
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">
|
||||
<FileText size={14} className="text-muted-foreground flex-shrink-0" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue