fix(dashboard): move Annuleren inside the keuzelijst card

"Annuleren" rendered as a separate line below the grey card — visually disconnected
from the choice it belongs to. Fixing this properly means the card can't just be one
big <a> anymore: a <button> can't nest inside an anchor (invalid HTML, broken a11y),
and choice-link's [choiceActions] slot needs Annuleren to sit inside the same box.

choice-link.component.ts now makes the card a <div> always, with the title wrapped in
a vendored Bootstrap `.stretched-link` (its ::after overlay keeps the whole card
clickable, same as before) instead of the whole box being the anchor. The projected
action gets its own `position:relative;z-index:2` (in aanvraag-block.component.ts,
which owns that markup) to stay clickable above the stretched-link overlay. Added
`:focus-within` on the card to restore the focus-accent CIBG's `:focus` rule would
have given the card itself, since focus now lands on the inner title link.

Verified: lint/check:tokens/test/build green; drove it end-to-end — clicking anywhere
on a card body still resumes the wizard, clicking Annuleren cancels without navigating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:15:22 +02:00
parent 7ac14557dd
commit a2ed3ae5b8
2 changed files with 39 additions and 25 deletions

View File

@@ -13,13 +13,18 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
/** Organism: one application on the dashboard's "Mijn aanvragen" list, rendered as
a CIBG Huisstijl "keuzelijst" choice (choice-link). Which text/actions it shows
is driven by the pure `blockActions(status)` and the status tag; the block only
renders. Only a resumable Concept is clickable — a resolved status has nothing
to navigate to, so it renders as a plain (non-anchor) row. */
renders. Only a resumable Concept is clickable (stretched-link over the whole
card) — a resolved status has nothing to navigate to, so it renders as a plain
(non-interactive) card. */
@Component({
selector: 'app-aanvraag-block',
imports: [ButtonComponent, ChoiceLinkComponent],
// display:contents — see choice-link.component.ts (keeps <li> a direct <ul> child).
styles: [`:host{display:contents}`],
styles: [`
:host{display:contents} /* see choice-link.component.ts (keeps <li> a direct <ul> child) */
/* Sits inside the same card as the stretched-link title — needs its own
stacking context above the stretched-link overlay (z-index:1) to stay clickable. */
.actions{position:relative;z-index:2;margin-block-start:var(--rhc-space-max-sm)}
`],
template: `
<app-choice-link
[heading]="typeLabel()"
@@ -27,7 +32,7 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
[clickable]="actions().includes('resume')"
(activate)="resume.emit()">
@if (actions().includes('cancel')) {
<div choiceActions>
<div choiceActions class="actions">
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.annuleren">Annuleren</app-button>
</div>
}

View File

@@ -1,40 +1,49 @@
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">`
/** Molecule: one choice in a CIBG Huisstijl "keuzelijst" — `<li><div 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.
The title is a Bootstrap "stretched-link" (`.stretched-link`, vendored) rather than
the whole box being an `<a>`: a `[choiceActions]` slot needs to project a sibling
action (e.g. "Annuleren") *inside* the same card, and a `<button>` can't nest
inside an `<a>` (invalid HTML, broken a11y). stretched-link keeps the entire card
clickable via its `::after` overlay; the projected action sits above that overlay
(see its own `position:relative;z-index:2` at the call site) so it stays clickable.
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
class — keuzelijst assumes every item IS a link — so a non-interactive 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. */
suppresses both for that case. `:focus-within` restores the focus accent that
`:focus` would have given the (no longer directly focused) card. */
@Component({
selector: 'app-choice-link',
imports: [RouterLink, NgTemplateOutlet],
imports: [RouterLink],
styles: [`
:host{display:contents}
.keuzelijst__link{position:relative}
.keuzelijst__link:focus-within{background-color:var(--rhc-color-cool-grey-100);box-shadow:inset 4px 0 0 0 var(--rhc-color-lintblauw-500)}
.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}
.keuzelijst__link--static:hover{background-color:var(--rhc-color-cool-grey-200);box-shadow:none}
`],
template: `
<li class="keuzelijst__list-item">
<div class="keuzelijst__link" [class.keuzelijst__link--static]="!to() && !clickable()">
<h3 class="keuzelijst__header">
@if (to()) {
<a class="keuzelijst__link" [routerLink]="to()"><ng-container [ngTemplateOutlet]="body" /></a>
<a class="stretched-link" [routerLink]="to()">{{ heading() }}</a>
} @else if (clickable()) {
<a href="#" class="keuzelijst__link" (click)="onActivate($event)"><ng-container [ngTemplateOutlet]="body" /></a>
<a class="stretched-link" href="#" (click)="onActivate($event)">{{ heading() }}</a>
} @else {
<div class="keuzelijst__link keuzelijst__link--static"><ng-container [ngTemplateOutlet]="body" /></div>
{{ heading() }}
}
<ng-content select="[choiceActions]" />
</li>
<ng-template #body>
<h3 class="keuzelijst__header">{{ heading() }}</h3>
</h3>
@if (instructions()) { <p class="keuzelijst__instructions">{{ instructions() }}</p> }
</ng-template>
<ng-content select="[choiceActions]" />
</div>
</li>
`,
})
export class ChoiceLinkComponent {
@@ -43,7 +52,7 @@ export class ChoiceLinkComponent {
/** 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. */
row still renders as a clickable card, but `activate` decides what happens. */
clickable = input(false);
activate = output<void>();