Files
familienarchiv/frontend/src/lib/components/document/UploadZone.svelte
Marcel f7ed154e4d fix(a11y): bump progress bar text to text-xs minimum, add motion-safe to upload animation
- text-[9px]/text-[10px] in required-fields bar raised to text-xs (12px),
  meeting the project minimum for the 60+ audience (WCAG 1.4.4)
- Upload animation now uses motion-safe: prefix so it stops for users
  with prefers-reduced-motion set (WCAG 2.1 SC 2.3.3)
- Strengthened UploadZone tests: onCancel uses [role=status] button
  selector instead of first-button heuristic; added positive file
  selection test (valid PDF calls onFile), file-too-large test, and
  MIME rejection now also asserts the error message is visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:36:31 +02:00

103 lines
3.0 KiB
Svelte

<script lang="ts">
const ALLOWED_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/png', 'image/tiff']);
const MAX_SIZE_BYTES = 50 * 1024 * 1024;
let {
filename,
isUploading,
isDragging = $bindable(false),
error,
onFile,
onCancel
}: {
filename: string;
isUploading: boolean;
isDragging?: boolean;
error: string | null;
onFile?: (file: File) => void;
onCancel?: () => void;
} = $props();
let validationError = $state<string | null>(null);
const displayError = $derived(error ?? validationError);
function handleFile(file: File | undefined) {
if (!file) return;
validationError = null;
if (!ALLOWED_TYPES.has(file.type)) {
validationError = 'Dieser Dateityp wird nicht unterstützt (PDF, JPG, PNG, TIFF).';
return;
}
if (file.size > MAX_SIZE_BYTES) {
validationError = 'Die Datei ist zu groß (max. 50 MB).';
return;
}
onFile?.(file);
}
function handleChange(e: Event) {
const input = e.currentTarget as HTMLInputElement;
handleFile(input.files?.[0]);
}
function handleDrop(e: DragEvent) {
e.preventDefault();
isDragging = false;
handleFile(e.dataTransfer?.files[0]);
}
</script>
<div
class="flex flex-1 items-center justify-center bg-pdf-bg"
aria-live="polite"
aria-label={isUploading ? 'Datei wird hochgeladen' : 'Dateiupload-Bereich'}
>
{#if isUploading}
<div role="status" class="flex flex-col items-center gap-3 text-center">
<div class="h-0.5 w-48 overflow-hidden rounded-full bg-white/10">
<div
class="h-full bg-brand-mint/70 motion-safe:animate-[slide_1.4s_ease-in-out_infinite]"
></div>
</div>
<p class="max-w-[200px] truncate text-xs font-medium text-brand-mint/70">{filename}</p>
<p class="text-xs text-white/40">Wird hochgeladen …</p>
<button
class="min-h-[44px] px-3 text-xs text-white/40 transition-colors hover:text-white/60"
onclick={onCancel}
>
Abbrechen
</button>
</div>
{:else}
<div
role="region"
aria-label="Datei ablegen"
class="flex flex-col items-center gap-3 rounded-sm border border-dashed p-8 text-center transition-colors
{isDragging ? 'border-brand-mint bg-brand-mint/5' : 'border-white/20'}"
ondragover={(e) => {
e.preventDefault();
isDragging = true;
}}
ondragleave={() => (isDragging = false)}
ondrop={handleDrop}
>
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white/40">
</div>
<p class="max-w-[200px] truncate text-xs font-medium text-white/50">{filename}</p>
<p class="text-xs text-white/30">Noch keine Datei hochgeladen</p>
{#if displayError}
<p class="text-xs text-red-400">{displayError}</p>
{/if}
<label
class="flex min-h-[44px] cursor-pointer items-center rounded-sm bg-brand-navy px-4 py-1.5 text-xs font-bold tracking-widest text-white/90 uppercase"
aria-label="Datei auswählen"
>
Datei auswählen
<input type="file" class="sr-only" onchange={handleChange} />
</label>
<p class="text-xs text-white/20">oder Datei hier ablegen</p>
</div>
{/if}
</div>