Skip to content

[UX] customer-portal — Contact Aadalen ​

Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Adalen-Truck/customer-portal · Branch: develop @ 693a76c · Files reviewed: 4 Patterns: forms/form-validation

Summary ​

A small, clean single-purpose contact form (subject + message → POST /api/v1/contact) built on PrimeVue InputText/Textarea/Message/Button inside @aadalen/uiPageLayout. Both labels are correctly associated, an idempotency key is sent, and copy has full en/nb parity, so CONTENT-01 passes here. The two real defects are a disabled submit button that explains nothing when empty (FORM-05) and a success confirmation that swaps in silently for screen-reader users (MSG-01).

Findings ​

1. Submit button is disabled while the form is invalid, with no explanation — Medium · FORM-05 ​

Where: apps/web/src/pages/ContactAadalenPage.vue:129-135 (:disabled="!subject.trim() || !message.trim()") What: The Send button is disabled until both fields are non-empty. There is no validation message explaining the block; the native required attributes on the inputs (:101, :116) never fire because the disabled button plus @submit.prevent guard (:24) means the form is never submitted while empty. Why it matters: A user who lands on the page and clicks the only obvious control gets a dead, greyed-out button and nothing to act on — no hint that both fields are required or which one is missing. The baseline's forms/form-validation pattern prefers on-submit validation that names the missing field over a disabled submit. Fix: Keep the button enabled; on submit, if a field is empty, set an inline validation message tied to that field (aria-describedby) and move focus to it. The existing send() early-return already guards the empty case — surface it instead of hiding the button.

2. Success confirmation replaces the form with no live-region announcement — Medium · MSG-01 ​

Where: apps/web/src/pages/ContactAadalenPage.vue:62-83 (the v-if="isSent" block) What: On a successful POST the whole form is swapped out for a centred confirmation card (<h2> "Message sent" + body). The container carries no role="status" / aria-live="polite", so the DOM change is silent (WCAG 4.1.3). Why it matters: A screen-reader user who submits hears nothing — the form simply vanishes and focus is left where it was. They get no confirmation their message was sent. The forms/form-validation pattern's reference markup explicitly wires a role="status" aria-live="polite" status element for exactly this completion signal. Fix: Give the confirmation container role="status" (or wrap the heading/body in an aria-live="polite" region) and move focus to the <h2> on isSent so the completion is both announced and reachable.

3. First field is not focused on mount — Low · FORM-11 ​

Where: apps/web/src/pages/ContactAadalenPage.vue:96-102 (subject InputText) What: This is a single-purpose form but the subject field is not autofocused on mount, so keyboard users must tab past the page chrome to start typing. Why it matters: Minor added friction on a form whose only job is to be typed into. Fix: Autofocus the subject input on mount (e.g. autofocus or a ref + onMounted).

Cross-references (already logged — not re-filed) ​

  • NAV-03 — one static document title app-wide; fails project-wide, see PROJECT-LEVEL.md. This route sets no title.
  • MSG-02 — the error path calls getErrorMessage(error, t("contactAadalen.error")) (:40), and the shared getErrorMessage helper prefers the raw backend string over the localised fallback; helper-level, see PROJECT-LEVEL.md. So a network/SDK failure can still surface untranslated prose here despite the good nb fallback string being passed.

Unverified ​

  • PrimeVue Message role="alert" (MSG-01, error branch). The error box at :121-127 relies on PrimeVue Message carrying an assertive role. PrimeVue internals are not readable (node_modules not installed) — cannot confirm the error is announced.
  • PrimeVue Button :loading → disabled focus drop (FORM-06). :loading="isSending" (:133) — if PrimeVue disables the button during load, focus drops to <body>. The send() handler already guards re-entry (isSending check at :24), so the double-submit risk is covered; only the focus behaviour is unverifiable.
  • A11Y-01 contrast — text-text-3 on the direct-line paragraph (:137) and the text-accent mailto link need a rendered contrast check.
  • A11Y-06 responsive / short viewport — needs a rendered viewport; PageLayout with sticky-mobile-title and auto-resize Textarea (rows="7") not verified against a short viewport with the mobile keyboard open.

Baseline additions ​

None. All findings map to existing rules.

Cross-project note ​

The disabled-submit-while-invalid shape (FORM-05) is a common convenience pattern and is worth checking on the simple write forms in playout, tt-time-tracker and members. The silent success/state swap (MSG-01) — replacing a form with a confirmation with no live region — is equally generic and likely recurs on any "submit → thank-you" surface across all three.