diff --git a/src/app/shared/infrastructure/scenario.ts b/src/app/shared/infrastructure/scenario.ts index 9bcd86b..33e78dc 100644 --- a/src/app/shared/infrastructure/scenario.ts +++ b/src/app/shared/infrastructure/scenario.ts @@ -1,6 +1,15 @@ -export type Scenario = 'default' | 'slow' | 'loading' | 'empty' | 'error'; +export type Scenario = + | 'default' + | 'slow' + | 'loading' + | 'empty' + | 'error' + // upload-only (the multipart POST is hand-written XHR, so it bypasses the HTTP + // interceptor — these are simulated in upload.adapter.ts instead): + | 'upload-slow' + | 'upload-fail'; -const VALID: Scenario[] = ['default', 'slow', 'loading', 'empty', 'error']; +const VALID: Scenario[] = ['default', 'slow', 'loading', 'empty', 'error', 'upload-slow', 'upload-fail']; /** Reads ?scenario= from the URL so a demo can force each async state. */ export function currentScenario(): Scenario { diff --git a/src/app/shared/ui/upload/document-category/document-category.component.ts b/src/app/shared/ui/upload/document-category/document-category.component.ts index 77473e3..56f5520 100644 --- a/src/app/shared/ui/upload/document-category/document-category.component.ts +++ b/src/app/shared/ui/upload/document-category/document-category.component.ts @@ -1,4 +1,4 @@ -import { Component, input, output } from '@angular/core'; +import { Component, computed, input, output } from '@angular/core'; import type { DeliveryChannel, DocumentCategory, Upload } from '@shared/upload/upload.machine'; import { DeliveryChannelToggleComponent } from '../delivery-channel-toggle/delivery-channel-toggle.component'; import { FileInputComponent } from '../file-input/file-input.component'; @@ -42,6 +42,7 @@ import { SingleUploadComponent } from '../single-upload/single-upload.component' @if (channel() === 'digital') { @@ -67,6 +68,9 @@ export class DocumentCategoryComponent { channel = input.required(); rejection = input(); + /** Accessible name for the file picker, e.g. "Bestand kiezen voor Diploma". */ + protected fileInputLabel = computed(() => $localize`:@@upload.fileInput.labelFor:Bestand kiezen voor ${this.category().label}:category:`); + fileSelected = output(); removeUpload = output(); retryUpload = output(); diff --git a/src/app/shared/ui/upload/file-input/file-input.component.ts b/src/app/shared/ui/upload/file-input/file-input.component.ts index 2e17aef..caebe5e 100644 --- a/src/app/shared/ui/upload/file-input/file-input.component.ts +++ b/src/app/shared/ui/upload/file-input/file-input.component.ts @@ -30,6 +30,7 @@ import { Component, ElementRef, input, output, viewChild } from '@angular/core'; #fileInput type="file" [id]="inputId()" + [attr.aria-label]="label()" [accept]="accept().join(',')" [multiple]="multiple()" [disabled]="disabled()" @@ -38,7 +39,7 @@ import { Component, ElementRef, input, output, viewChild } from '@angular/core'; class="utrecht-button rhc-button utrecht-button--secondary-action label" [class.utrecht-button--disabled]="disabled()"> - Bestand kiezen + {{ label() }} `, @@ -48,6 +49,8 @@ export class FileInputComponent { multiple = input(false); disabled = input(false); inputId = input.required(); + /** Accessible name + button text; the domain caller supplies a per-category label. */ + label = input($localize`:@@upload.fileInput.label:Bestand kiezen`); filesSelected = output(); diff --git a/src/app/shared/upload/upload.adapter.ts b/src/app/shared/upload/upload.adapter.ts index ef7ef85..9946ceb 100644 --- a/src/app/shared/upload/upload.adapter.ts +++ b/src/app/shared/upload/upload.adapter.ts @@ -5,6 +5,7 @@ import { UploadStatusItemDto, } from '@shared/infrastructure/api-client'; import { problemDetail } from '@shared/infrastructure/api-error'; +import { currentScenario } from '@shared/infrastructure/scenario'; import { environment } from '../../../environments/environment'; import { DocumentCategory } from './upload.machine'; @@ -63,6 +64,9 @@ export class UploadAdapter { * transport behind UploadTransport for true background sync. */ xhrUpload(req: XhrUploadRequest, onProgress: (pct: number) => void): XhrUploadHandle { + const scenario = currentScenario(); + if (scenario === 'upload-slow' || scenario === 'upload-fail') return simulateUpload(scenario, onProgress); + const xhr = new XMLHttpRequest(); const form = new FormData(); form.append('file', req.file); @@ -98,6 +102,34 @@ export class UploadAdapter { const UPLOAD_FAILED = $localize`:@@upload.failed:Uploaden is niet gelukt. Probeer het opnieuw.`; const genericError = (): string => UPLOAD_FAILED; + +/** + * Demo-only (dev): the real XHR POST finishes instantly for metadata, so progress + * and failure can't otherwise be shown. Drives the progress bar over ~2.5s, then + * succeeds (`upload-slow`) or fails (`upload-fail`). ponytail: timer-based, cancel + * via the returned handle; no network. + */ +function simulateUpload(scenario: 'upload-slow' | 'upload-fail', onProgress: (pct: number) => void): XhrUploadHandle { + let pct = 0; + let cancelled = false; + const done = new Promise<{ documentId: string }>((resolve, reject) => { + const tick = () => { + if (cancelled) { + reject(UPLOAD_ABORTED); + } else if (pct < 100) { + pct += 10; + onProgress(Math.min(pct, 100)); + setTimeout(tick, 250); + } else if (scenario === 'upload-fail') { + reject(UPLOAD_FAILED); + } else { + resolve({ documentId: `demo-${crypto.randomUUID()}` }); + } + }; + setTimeout(tick, 250); + }); + return { done, cancel: () => void (cancelled = true) }; +} const parseError = (body: string): string => { try { return problemDetail(JSON.parse(body), UPLOAD_FAILED);