Mijn aanvragen: list enter/leave animations + 204 null-body fix

- animate.enter/leave on aanvraag cards and upload rows (native Angular, no @angular/animations)
- reduced-motion: skip animation → instant removal
- api-client: null-body statuses (204/205/304) must pass null to Response()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 17:14:29 +02:00
parent 0f14239f68
commit 5027f099cf
4 changed files with 24 additions and 2 deletions

View File

@@ -39,7 +39,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<app-heading [level]="2" i18n="@@dashboard.mijnAanvragen">Mijn aanvragen</app-heading> <app-heading [level]="2" i18n="@@dashboard.mijnAanvragen">Mijn aanvragen</app-heading>
<div class="app-stack app-section"> <div class="app-stack app-section">
@for (a of aanvragen(); track a.id) { @for (a of aanvragen(); track a.id) {
<app-aanvraag-block [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" /> <app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" />
} }
</div> </div>
</section> </section>

View File

@@ -40,7 +40,9 @@ function httpClientFetch(http: HttpClient) {
}) })
.pipe(timeout(REQUEST_TIMEOUT_MS)), .pipe(timeout(REQUEST_TIMEOUT_MS)),
); );
return new Response(res.body ?? '', { status: res.status || 200 }); // 204/205/304 are null-body statuses — new Response(body, …) throws for any non-null body.
const nullBody = res.status === 204 || res.status === 205 || res.status === 304;
return new Response(nullBody ? null : (res.body ?? ''), { status: res.status || 200 });
} catch (e) { } catch (e) {
if (e instanceof TimeoutError) return new Response('', { status: 504 }); if (e instanceof TimeoutError) return new Response('', { status: 504 });
const err = e as HttpErrorResponse; const err = e as HttpErrorResponse;

View File

@@ -50,6 +50,8 @@ import { SingleUploadComponent } from '../single-upload/single-upload.component'
<div class="uploads"> <div class="uploads">
@for (u of uploads(); track u.localId) { @for (u of uploads(); track u.localId) {
<app-single-upload <app-single-upload
animate.enter="app-item-enter"
animate.leave="app-item-leave"
[upload]="u" [upload]="u"
[previewUrlFor]="previewUrlFor()" [previewUrlFor]="previewUrlFor()"
(remove)="onRemove(u)" (remove)="onRemove(u)"

View File

@@ -86,8 +86,26 @@ app-site-footer { view-transition-name: site-footer; }
animation-timing-function: ease; animation-timing-function: ease;
} }
/* List-item enter/leave for lists that add/remove at runtime (aanvraag cards, upload
rows). Used via Angular's native `animate.enter`/`animate.leave` — no @angular/animations. */
@keyframes app-item-enter { from { opacity: 0; transform: translateY(-0.5rem); } to { opacity: 1; transform: none; } }
@keyframes app-item-leave {
from { opacity: 1; max-block-size: 50rem; }
to { opacity: 0; transform: translateY(-0.25rem); max-block-size: 0; margin-block: 0; padding-block: 0; }
}
.app-item-enter { animation: app-item-enter 220ms ease both; }
.app-item-leave {
display: block; /* collapse needs block formatting (host is inline/flex by default) */
overflow: hidden;
pointer-events: none;
/* ponytail: 50rem max-height hack; if an item is taller the collapse eases slightly late — measure height in JS then. */
animation: app-item-leave 260ms ease both;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
::view-transition-group(*), ::view-transition-group(*),
::view-transition-old(*), ::view-transition-old(*),
::view-transition-new(*) { animation: none !important; } ::view-transition-new(*) { animation: none !important; }
/* No animation → Angular removes the leaving node immediately (instant, no motion). */
.app-item-enter, .app-item-leave { animation: none; }
} }