feat(dashboard): restyle Mijn aanvragen / Wat moet ik regelen as a CIBG keuzelijst

Both sections offer a set of choices the user picks between to proceed, matching
designsystem.cibg.nl/componenten/keuzelijst rather than the "aanvragen" row pattern
("Wat wilt u doen?" keeps that look — it's a static nav list, not a choice list).

- New shared/ui molecules: choice-list (heading + keuzelijst__list, wired via
  aria-labelledby per CIBG's a11y guidance) and choice-link (one keuzelijst__link
  choice; routerLink, imperative-clickable, or a plain non-interactive block).
- choice-link's non-interactive block needed a `--static` modifier: CIBG's
  `.keuzelijst__link:after`/`:hover`/`:focus` key off the bare class (keuzelijst
  assumes every item is a link), unlike `.applications li a::after` which is scoped
  to the anchor — without it, a non-actionable aanvraag row inherited a chevron and
  hover accent it shouldn't have.
- task-list.component.ts now composes choice-list/choice-link internally; public
  API unchanged except a new required `listHeading` input (the heading moves inside
  the list for the aria-labelledby link, so dashboard.page.ts stops rendering it
  separately — same fix applied to "Mijn aanvragen").
- aanvraag-block.component.ts moves from application-link to choice-link, combining
  its separate status/subtitle text into one instructions paragraph (keuzelijst has
  no cta field — the row itself is the action). Only a resumable Concept renders as
  a real choice; InBehandeling/Goedgekeurd/Afgewezen stay non-interactive, unchanged
  from before.

Verified: lint/check:tokens/build green, 178 tests pass, build-storybook succeeds,
and manually driven end-to-end (dashboard renders both sections as keuzelijst cards,
confirmed via screenshot that non-actionable rows have no chevron after the fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:54:02 +02:00
parent 6257d7ede3
commit 7ac14557dd
10 changed files with 2225 additions and 875 deletions

View File

@@ -0,0 +1,54 @@
import { Component, input, output } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
/** Molecule: one choice in a CIBG Huisstijl "keuzelijst" — `<li><a class="keuzelijst__link">`
with a title and optional instructions (see choice-list.component.ts). Renders a
plain (non-interactive) block when there's nothing to navigate to.
Unlike the "aanvragen" pattern's `.applications li a::after` (scoped to the `a`
tag), CIBG's `.keuzelijst__link:after`/`:hover`/`:focus` rules key off the bare
class — keuzelijst assumes every item IS a link — so a non-anchor row would
otherwise inherit the chevron and hover accent too. The `--static` modifier below
suppresses both for the non-interactive case. A `[choiceActions]` slot projects a
sibling action (e.g. "Annuleren") after the anchor — a button can't nest inside it. */
@Component({
selector: 'app-choice-link',
imports: [RouterLink, NgTemplateOutlet],
styles: [`
:host{display:contents}
.keuzelijst__link--static::after{content:none}
.keuzelijst__link--static:hover,.keuzelijst__link--static:focus{background-color:var(--rhc-color-cool-grey-200);box-shadow:none}
`],
template: `
<li class="keuzelijst__list-item">
@if (to()) {
<a class="keuzelijst__link" [routerLink]="to()"><ng-container [ngTemplateOutlet]="body" /></a>
} @else if (clickable()) {
<a href="#" class="keuzelijst__link" (click)="onActivate($event)"><ng-container [ngTemplateOutlet]="body" /></a>
} @else {
<div class="keuzelijst__link keuzelijst__link--static"><ng-container [ngTemplateOutlet]="body" /></div>
}
<ng-content select="[choiceActions]" />
</li>
<ng-template #body>
<h3 class="keuzelijst__header">{{ heading() }}</h3>
@if (instructions()) { <p class="keuzelijst__instructions">{{ instructions() }}</p> }
</ng-template>
`,
})
export class ChoiceLinkComponent {
heading = input.required<string>();
instructions = input('');
/** Set for a plain routerLink navigation. */
to = input('');
/** Set when the choice navigates imperatively (e.g. resume with query params) — the
row still renders as a clickable `<a>`, but `activate` decides what happens. */
clickable = input(false);
activate = output<void>();
protected onActivate(ev: Event) {
ev.preventDefault(); // fragment href resolves against <base href>, not the route
this.activate.emit();
}
}

View File

@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { applicationConfig } from '@storybook/angular';
import { provideRouter } from '@angular/router';
import { ChoiceLinkComponent } from './choice-link.component';
const meta: Meta<ChoiceLinkComponent> = {
title: 'Molecules/Choice Link',
component: ChoiceLinkComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
render: (args) => ({
props: args,
// Rows are <li>s — a real list gives them their normal layout in the story.
template: `<ul class="keuzelijst__list"><app-choice-link [heading]="heading" [instructions]="instructions" [to]="to" [clickable]="clickable" /></ul>`,
}),
};
export default meta;
type Story = StoryObj<ChoiceLinkComponent>;
export const Navigatie: Story = {
args: { heading: 'Ik heb een Nederlands diploma', instructions: 'U kunt direct uw registratie aanvragen.', to: '/registreren' },
};
export const Actie: Story = {
args: { heading: 'Inschrijving', instructions: 'Stap 2 van 3', clickable: true },
};
export const NietInteractief: Story = {
args: { heading: 'Herregistratie', instructions: 'Referentie 2024-00123 · ingediend op 12 mei 2024' },
};