fix(quick-260921-bi2): a11y - Rollen, Semantik, Tab-Reihenfolge (32er-Rest, Teil 1/2)
Vier der fuenf noch offenen Regeln aus Teillieferung B bereinigt:
- noRedundantRoles (4): ueberfluessige role-Angaben auf button/time/select
entfernt (maschineller --unsafe-Fix, gelesen).
- useAriaPropsForRole (1): entfaellt automatisch mit obigem Fix -- das
<select role="combobox"> in search-widget.tsx verlangte die fehlenden
ARIA-Attribute nur wegen der ueberfluessigen Rolle.
- useSemanticElements (4): admin-sidebar/settings-sidebar tragen role=
"navigation" jetzt am <nav> statt am <aside> (kein doppeltes Landmark
mehr); widget-wrapper.tsx ist jetzt ein echtes <article> statt
div role="article"; DropZone.tsx trennt die Datei-Entfernen-Schaltflaeche
als Geschwister ab, damit die Drop-Flaeche selbst ein echtes <button>
werden kann (ein <button> darf kein zweites <button> verschachteln).
Die Drop-Flaeche traegt darum jetzt Klick- UND Drag-Handler direkt am
<button>, sonst waere sie ein "statisches" Element mit Ereignis-Handlern
geworden (die zurueckgestellten Regeln noStaticElementInteractions /
noNoninteractiveElementInteractions waeren neu angeschlagen -- geprueft,
bleiben bei 5/11).
- noNoninteractiveTabindex (1): calculator-widget.tsx traegt jetzt
tabIndex={-1} statt {0}. Die Zifferntasten sind bereits echte <button>
und damit selbst Teil der Tab-Reihenfolge; Tastendruecke erreichen
handleKeyboard weiterhin per Bubbling, sobald eine Taste fokussiert ist.
Verhalten unveraendert, nur ein wirkungsloser Tab-Stopp auf dem Container
selbst entfaellt.
Verbleibend: noLabelWithoutControl (22), naechster Schritt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -15,7 +15,6 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
<button
|
||||
type="button"
|
||||
role="button"
|
||||
onClick={() => onChange('all')}
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
value === 'all'
|
||||
@@ -29,7 +28,6 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
role="button"
|
||||
onClick={() => onChange(cat)}
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
value === cat
|
||||
|
||||
@@ -28,7 +28,7 @@ export function DropZone({ onFile, accept, currentFile, onClear }: DropZoneProps
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const handleDragOver = (e: React.DragEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export function DropZone({ onFile, accept, currentFile, onClear }: DropZoneProps
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const handleDrop = (e: React.DragEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
@@ -55,43 +55,52 @@ export function DropZone({ onFile, accept, currentFile, onClear }: DropZoneProps
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(); }}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`rounded-lg border-2 border-dashed p-8 text-center cursor-pointer transition-colors ${
|
||||
isDragOver
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
{currentFile ? (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
{/* a11y/useSemanticElements: kein role="button"-div mehr -- die
|
||||
eigentliche Flaeche ist jetzt ein echtes <button> und traegt darum
|
||||
auch Klick UND Drag-Ereignisse (ein <div> ohne Rolle mit
|
||||
Ereignis-Handlern waere ein statisches, nicht interaktives Element
|
||||
und faellt unter die zurueckgestellten Regeln noStaticElement-
|
||||
Interactions/noNoninteractiveElementInteractions). Die "Entfernen"-
|
||||
Schaltflaeche liegt darum als Geschwister daneben, nicht mehr
|
||||
verschachtelt in der Flaechen-Schaltflaeche -- ein <button> darf
|
||||
kein weiteres <button> enthalten (ungueltiges Verschachteln). */}
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`block w-full cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${
|
||||
isDragOver
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
{currentFile ? (
|
||||
<span className="text-sm text-foreground">
|
||||
{currentFile.name}{' '}
|
||||
<span className="text-muted-foreground">
|
||||
({(currentFile.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onClear(); }}
|
||||
className="text-muted-foreground hover:text-foreground text-xs border border-border rounded px-2 py-0.5"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('dropZone.placeholder')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t('dropZone.formats')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('dropZone.placeholder')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t('dropZone.formats')}</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
{currentFile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onClear(); }}
|
||||
className="absolute right-2 top-2 rounded border border-border bg-card px-2 py-0.5 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user