mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
fix(a11y): add accessibility and UX polish to DirectoryPicker
- Add role="dialog", aria-modal, aria-label to the modal panel - Add aria-label to close button, home button - Add aria-hidden to decorative icons (chevrons, backdrop) - Add role="status" to loading spinner with sr-only label - Add role="alert" to error state - Add aria-current="location" to active breadcrumb segment - Wrap breadcrumb in nav landmark with aria-label - Add Escape key handler to dismiss the modal - Auto-focus the modal panel on open - Add focus-visible ring styles to all interactive elements (matches existing focus-visible:ring-2 ring-accent/40 pattern) - Increase breadcrumb button padding (px-1.5 py-1) for better touch targets - Increase directory entry padding (py-2.5) for touch comfort - Add active:bg-hover/70 pressed state on directory entries - Add active:bg-accent/80 pressed state on select button
This commit is contained in:
parent
50c9defb34
commit
f25ce8db73
1 changed files with 46 additions and 14 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Folder, ChevronRight, Home, X, Loader2, AlertCircle, Check } from '@/lib/lucide-icons';
|
||||
import { listDirectories, type DirEntry } from '../services/backend-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -17,6 +17,7 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchEntries = useCallback(async (dir: string) => {
|
||||
setLoading(true);
|
||||
|
|
@ -41,6 +42,24 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
}
|
||||
}, [open, initialDir, fetchEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [open, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && panelRef.current) {
|
||||
panelRef.current.focus();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const segments = currentDir.split('/').filter(Boolean);
|
||||
|
|
@ -59,11 +78,17 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
<div
|
||||
className={`absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-200 ${isVisible ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={panelRef}
|
||||
data-testid="directory-picker-modal"
|
||||
className={`relative mx-4 flex w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-border-subtle bg-surface shadow-2xl transition-all duration-200 ${isVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t('repoAnalyzer.directoryPicker.title')}
|
||||
tabIndex={-1}
|
||||
className={`relative mx-4 flex w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-border-subtle bg-surface shadow-2xl transition-all duration-200 outline-none ${isVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`}
|
||||
style={{ maxHeight: '70vh' }}
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -73,27 +98,33 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 text-text-muted transition-colors hover:text-text-primary"
|
||||
aria-label="Close"
|
||||
className="rounded-md p-1.5 text-text-muted transition-colors hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:outline-none"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 overflow-x-auto border-b border-border-subtle bg-elevated/50 px-5 py-2.5">
|
||||
<nav
|
||||
aria-label="Directory breadcrumb"
|
||||
className="flex items-center gap-1 overflow-x-auto border-b border-border-subtle bg-elevated/50 px-5 py-2"
|
||||
>
|
||||
<button
|
||||
data-testid="directory-picker-home"
|
||||
onClick={() => navigateTo('/')}
|
||||
className="shrink-0 rounded p-0.5 text-text-muted transition-colors hover:text-accent"
|
||||
aria-label="Root directory"
|
||||
className="shrink-0 rounded-md p-1.5 text-text-muted transition-colors hover:text-accent focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:outline-none"
|
||||
>
|
||||
<Home className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{segments.map((seg, i) => (
|
||||
<span key={i} className="flex shrink-0 items-center gap-1">
|
||||
<ChevronRight className="h-3 w-3 text-text-muted/50" />
|
||||
<ChevronRight className="h-3 w-3 text-text-muted/50" aria-hidden="true" />
|
||||
<button
|
||||
onClick={() => handleBreadcrumbClick(i)}
|
||||
className={`rounded px-1 py-0.5 font-mono text-xs transition-colors ${
|
||||
aria-current={i === segments.length - 1 ? 'location' : undefined}
|
||||
className={`rounded-md px-1.5 py-1 font-mono text-xs transition-colors focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:outline-none ${
|
||||
i === segments.length - 1
|
||||
? 'font-medium text-text-primary'
|
||||
: 'text-text-muted hover:text-accent'
|
||||
|
|
@ -103,25 +134,26 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Directory listing */}
|
||||
<div data-testid="directory-listing" className="min-h-[200px] flex-1 overflow-y-auto px-2 py-2">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="flex items-center justify-center py-12" role="status">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-text-muted" />
|
||||
<span className="sr-only">Loading directories...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="flex flex-col items-center gap-2 py-12 text-center">
|
||||
<div className="flex flex-col items-center gap-2 py-12 text-center" role="alert">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<p className="text-xs text-red-400">{error}</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
navigateTo(currentDir === '/' ? '/' : currentDir.replace(/\/[^/]+$/, '') || '/')
|
||||
}
|
||||
className="mt-1 text-xs text-text-muted underline hover:text-text-secondary"
|
||||
className="mt-1 rounded-md px-2 py-1 text-xs text-text-muted underline transition-colors hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:outline-none"
|
||||
>
|
||||
{t('repoAnalyzer.directoryPicker.goBack')}
|
||||
</button>
|
||||
|
|
@ -144,13 +176,13 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
key={entry.name}
|
||||
data-testid={`dir-entry-${entry.name}`}
|
||||
onClick={() => navigateTo(target)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left transition-colors hover:bg-hover"
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-hover active:bg-hover/70 focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:outline-none"
|
||||
>
|
||||
<Folder className="h-4 w-4 shrink-0 text-accent/70" />
|
||||
<span className="truncate font-mono text-xs text-text-secondary">
|
||||
{entry.name}
|
||||
</span>
|
||||
<ChevronRight className="ml-auto h-3 w-3 shrink-0 text-text-muted/40" />
|
||||
<ChevronRight className="ml-auto h-3 w-3 shrink-0 text-text-muted/40" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -164,7 +196,7 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo
|
|||
<button
|
||||
data-testid="directory-picker-select"
|
||||
onClick={() => onSelect(currentDir)}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-lg bg-accent px-3.5 py-2 text-xs font-medium text-white transition-colors hover:bg-accent/90"
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-lg bg-accent px-3.5 py-2 text-xs font-medium text-white transition-colors hover:bg-accent/90 active:bg-accent/80 focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-surface focus-visible:outline-none"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{t('repoAnalyzer.directoryPicker.select')}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue