feat(a11y): apply ARIA roles, landmarks, focus management, touch targets and contrast fixes across components

This commit is contained in:
DrSmoothl
2026-03-05 21:57:36 +08:00
parent c12d1ca42a
commit c658b2314d
32 changed files with 365 additions and 156 deletions

View File

@@ -418,7 +418,7 @@ export function AdapterConfigPage() {
</CardHeader>
<CollapsibleContent>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4" role="radiogroup" aria-label="部署模式选择">
{/* 预设模式 */}
<div
className={`border-2 rounded-lg p-3 md:p-4 cursor-pointer transition-all ${
@@ -426,7 +426,11 @@ export function AdapterConfigPage() {
? 'border-primary bg-primary/5'
: 'border-muted hover:border-primary/50 active:border-primary/70'
}`}
role="radio"
aria-checked={mode === 'preset'}
tabIndex={0}
onClick={() => handleModeChange('preset')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleModeChange('preset') } }}
>
<div className="flex items-start gap-2 md:gap-3">
<Package className="h-4 w-4 md:h-5 md:w-5 mt-0.5 flex-shrink-0" />
@@ -446,7 +450,11 @@ export function AdapterConfigPage() {
? 'border-primary bg-primary/5'
: 'border-muted hover:border-primary/50 active:border-primary/70'
}`}
role="radio"
aria-checked={mode === 'upload'}
tabIndex={0}
onClick={() => handleModeChange('upload')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleModeChange('upload') } }}
>
<div className="flex items-start gap-2 md:gap-3">
<Upload className="h-4 w-4 md:h-5 md:w-5 mt-0.5 flex-shrink-0" />
@@ -466,7 +474,11 @@ export function AdapterConfigPage() {
? 'border-primary bg-primary/5'
: 'border-muted hover:border-primary/50 active:border-primary/70'
}`}
role="radio"
aria-checked={mode === 'path'}
tabIndex={0}
onClick={() => handleModeChange('path')}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleModeChange('path') } }}
>
<div className="flex items-start gap-2 md:gap-3">
<FolderOpen className="h-4 w-4 md:h-5 md:w-5 mt-0.5 flex-shrink-0" />
@@ -496,10 +508,14 @@ export function AdapterConfigPage() {
? 'border-primary bg-primary/5'
: 'border-muted hover:border-primary/50'
}`}
role="radio"
aria-checked={isSelected}
tabIndex={0}
onClick={() => {
setSelectedPreset(key as PresetKey)
handleLoadFromPreset(key as PresetKey)
}}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedPreset(key as PresetKey); handleLoadFromPreset(key as PresetKey) } }}
>
<div className="flex items-start gap-3">
<Icon className="h-5 w-5 mt-0.5 flex-shrink-0" />

View File

@@ -54,7 +54,7 @@ export const ModelTable = React.memo(function ModelTable({
return (
<div className="hidden md:block rounded-lg border bg-card overflow-hidden">
<div className="overflow-x-auto">
<Table>
<Table aria-label="模型列表">
<TableHeader>
<TableRow>
<TableHead className="w-12">

View File

@@ -209,10 +209,12 @@ export function ProviderForm({
}
}}
placeholder="例如: DeepSeek, SiliconFlow"
aria-invalid={formErrors.name ? true : undefined}
aria-describedby={formErrors.name ? 'name-error' : undefined}
className={formErrors.name ? 'border-destructive focus-visible:ring-destructive' : ''}
/>
{formErrors.name && (
<p className="text-xs text-destructive">{formErrors.name}</p>
<p id="name-error" role="alert" className="text-xs text-destructive">{formErrors.name}</p>
)}
</div>
@@ -249,10 +251,12 @@ export function ProviderForm({
}}
placeholder="https://api.example.com/v1"
disabled={isUsingTemplate}
aria-invalid={formErrors.base_url ? true : undefined}
aria-describedby={formErrors.base_url ? 'base-url-error' : undefined}
className={`${isUsingTemplate ? 'bg-muted cursor-not-allowed' : ''} ${formErrors.base_url ? 'border-destructive focus-visible:ring-destructive' : ''}`}
/>
{formErrors.base_url && (
<p className="text-xs text-destructive">{formErrors.base_url}</p>
<p id="base-url-error" role="alert" className="text-xs text-destructive">{formErrors.base_url}</p>
)}
{isUsingTemplate && !formErrors.base_url && (
<p className="text-xs text-muted-foreground">
@@ -295,6 +299,8 @@ export function ProviderForm({
}
}}
placeholder="sk-..."
aria-invalid={formErrors.api_key ? true : undefined}
aria-describedby={formErrors.api_key ? 'api-key-error' : undefined}
className={`flex-1 ${formErrors.api_key ? 'border-destructive focus-visible:ring-destructive' : ''}`}
/>
<Button
@@ -321,7 +327,7 @@ export function ProviderForm({
</Button>
</div>
{formErrors.api_key && (
<p className="text-xs text-destructive">{formErrors.api_key}</p>
<p id="api-key-error" role="alert" className="text-xs text-destructive">{formErrors.api_key}</p>
)}
</div>

View File

@@ -170,7 +170,7 @@ export function ProviderList({
{/* 桌面端表格视图 */}
<div className="hidden md:block rounded-lg border bg-card overflow-hidden">
<div className="overflow-x-auto">
<Table>
<Table aria-label="AI 模型提供商列表">
<TableHeader>
<TableRow>
<TableHead className="w-12">

View File

@@ -400,7 +400,7 @@ export default function PackDetailPage() {
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<Table aria-label="API 提供商配置列表">
<TableHeader>
<TableRow>
<TableHead></TableHead>
@@ -435,7 +435,7 @@ export default function PackDetailPage() {
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<Table aria-label="模型配置列表">
<TableHeader>
<TableRow>
<TableHead></TableHead>