Skip to content

[UX] customer-portal — Warehouse take-out (internal) ​

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

Summary ​

A well-built internal pick flow: real <button> rows (not click-only <li>), 44px tap targets, mapped command-error copy (no raw SDK strings), full en/nb i18n parity, and correct camera-stream teardown on unmount. The defects are a11y-flavoured polish on the two text inputs and the confirm affordance — nothing blocks the task. Most important: the product-filter search field has no programmatic label, so a screen-reader user reaches an unnamed edit box.

Findings ​

1. Product-filter search field has no label — Medium · FORM-01 ​

Where: apps/web/src/pages/internal/WarehouseTakeOutPage.vue:380-387What: The shelf product filter is an InputText inside an IconField with a pi-search decorative icon and a :placeholder only — no <label>, no aria-label, no aria-labelledby. (Contrast the sibling inputs, which are labelled correctly: manual-bin at :322-336 and the destination Select via input-id at TakeOutCart.vue:174-198.) Why it matters: A screen-reader user tabbing into the filter hears "edit text" with no name; once any text is typed the placeholder is gone, so nothing identifies the field. The forms/search-field pattern's own reference markup carries an explicit <label for=…>, and lists "search input + label or hint" as required anatomy. Fix: Add a visually-hidden <label for> (or aria-label="t('warehouse.takeOut.filterProducts')") to the filter InputText. The key already exists.

2. Confirm submit is disabled while invalid, and its reason isn't linked to it — Medium · FORM-05 ​

Where: apps/web/src/components/internal/warehouse/TakeOutCart.vue:207-222What: The confirm button is :disabled="!canSubmit || isSubmitting". The blockingHint ("Add at least one product…" / "Choose a destination…") is rendered as a plain <p> sibling below the button, not connected via aria-describedby. Why it matters: A disabled button is removed from the tab order, so a keyboard/AT user lands on nothing and never encounters the adjacent hint that explains the block — the exact "disabled submit gives the user nothing to act on" case the rule targets. Sighted users are fine (the hint is visible), which is why it survives. Fix: Keep the button enabled and surface the reason on activation, or at minimum tie the hint to the button with aria-describedby and give the button aria-disabled rather than the native disabled so it stays reachable.

3. Shelf loading state is a bare "…" that names nothing and isn't announced — Low · CONTENT-04 ​

Where: apps/web/src/pages/internal/WarehouseTakeOutPage.vue:390-395What: While the shelf's products load, the UI shows <p>…</p> — a literal ellipsis, no text, no aria-live. Why it matters: CONTENT-04 wants waiting states to name what is being waited on; "…" names nothing, and with no live region a screen-reader user gets no signal that a fetch is in flight after opening a shelf. (The submit waiting state, by contrast, is done well: named copy + role="status" overlay at :260-275.) Fix: Replace with named copy (e.g. a "Loading products…" string) in a role="status" / aria-live="polite" container, mirroring the submit overlay.

4. Confirm button self-disables on activation; focus is never moved on state change — Low · FORM-06 ​

Where: apps/web/src/components/internal/warehouse/TakeOutCart.vue:209-210; apps/web/src/pages/internal/WarehouseTakeOutPage.vue:246-253What: On submit the same button becomes :disabled (via || isSubmitting and :loading), dropping focus to <body>. When the mutation resolves the whole pick UI is swapped for TakeOutSuccess (v-if="isSubmitted") but focus is not moved onto the success heading or its "New take-out" button. Why it matters: The FORM-06 focus-drop anti-pattern. Largely mitigated here — a full-surface role="status" overlay takes over during submit and announces progress — but a keyboard user finishes the flow with focus orphaned on <body> behind/after the swap, having to tab from the top to reach "New take-out". Fix: Guard the double-submit in the handler (it already checks canSubmit) instead of via disabled, and move focus to the TakeOutSuccess <h2> when it mounts.

5. Shelf load error offers no retry — Low · MSG-04 ​

Where: apps/web/src/pages/internal/WarehouseTakeOutPage.vue:396-402What: On isError the shelf shows a static <Message severity="error">Could not load the shelf.</Message> with no retry control, even though useShelfProducts exposes an unused refetch() (warehouse.collection.ts:41). Why it matters: Not a hard dead end — the "Scan another shelf" header button (:369-376) lets the user restart — but recovering the same shelf means re-scanning/re-typing its code rather than one tap. Says what went wrong, not what to do. Fix: Add a "Try again" button wired to the exposed refetch().

Unverified ​

  • A11Y-01 (contrast) — the amber low-stock treatment (text-status-warn-fg on the dot + label, WarehouseTakeOutPage.vue:444-452) and muted text-text-2/3 copy need a contrast tool. Not colour-only (the "On hand: N" text is always present), so no A11Y concern beyond contrast itself.
  • A11Y-06 (responsive / short viewport) — needs a rendered viewport; the mobile bottom-sheet + SelectionActionBar summary bar path (:511-563) is where to check.
  • MSG-01 on the two PrimeVue Message blocks (shelf error :396, camera-unsupported :302) — whether they carry role="alert" is a PrimeVue internal; node_modules is not installed, so Unverified. The submit-error path is fine — CommandErrorNotice.vue sets role="alert" explicitly.

Baseline additions ​

  • MSG-ASYNC-ACK (proposed; likely folds into the batch's MSG-06 family) — a fire-and-forget durable command shown as "registered/on its way" must give the user some later path to learn if the write ultimately failed. Here the take-out is enqueued and the cart cleared on the enqueue ack (mutations.ts + page :180-194); a worker-stage failure surfaces nowhere (command status is admin-only, no polling by design). Not filed as a firm finding: the copy is deliberately honest — "Take-out registered" / "on their way" / "Stock levels update shortly", never "completed" — which is the right mitigation, but the silent-worker-failure gap is real.

Cross-project note ​

  • NAV-03 fails project-wide (one static document title) — see PROJECT-LEVEL.md; not re-filed here.
  • FORM-01 placeholder-as-label on filter/search inputs is worth grepping across the other internal list features (charging, shipping-orders) and the four projects — the same "IconField + placeholder, no label" shape recurs wherever a client-side filter is hand-placed above a list.
  • The disabled-submit + unlinked-hint shape (FORM-05) is a candidate to check on any customer-portal confirm/wizard step that gates its primary button.