Appearance
[UX] members — Objectives & group comparison
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch:
develop@2c22f5a· Files reviewed: 7 Patterns: data-display/comparison-table (consulted)
Summary
The objectifs list is a thin TableData wrapper and is mostly clean (its shared-table defects are the project-level TableData.vue cluster, not re-filed here). The custom bactive-groupes comparison screen is where the real defects sit: it deletes shared group definitions with no confirmation, silently swallows score-computation failures, ships label-less inputs and icon-only buttons with no accessible names, and carries a dead colour-coding branch that never runs. None are blockers, but the delete-without-confirm and the swallowed-error path both have real consequences for a treasurer acting on these numbers.
Findings
1. Deleting a group is instant and irreversible, with no confirmation — High · MSG-05
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:112-121 (trash button) → :339 (deleteGroupe) What: The trash icon calls groupes.remove.mutateAsync({ id }) directly. There is no confirm step and no undo. These Groupes_Comparaison records are Directus-backed and shared across all admins (:275 comment), and each carries a Conditions expression that can be non-trivial to rebuild. Why it matters: A single misclick permanently destroys a group definition every other admin relies on, including its filter conditions. The button sits directly beside the (also destructive-adjacent) edit pencil, at a 24px-ish target, making the misclick plausible. Fix: Gate deletion behind a confirm dialog that names the group ("Supprimer le groupe « X » ?") and states it is shared and permanent, per MSG-05. The ModalTransferSurplus confirm/processing/error pattern already in this feature is a reusable model.
2. Score-computation failure is swallowed to the console — Medium · MSG-01 (+ project MSG-06)
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:395-399What: computeScores's catch does console.error(...) only; isComputing resets to false and groupeScores is left holding its previous value. So a failed /api/groupes-comparaison/:id/objectifs-stats call renders either stale scores from a prior tab/year (which look authoritative) or the "Sélectionnez au moins un groupe" empty state — never an error. No role="alert" / aria-live surface exists anywhere in the right pane. Why it matters: The percentages drive treasurer decisions about which group is behind on camp payments; a silently stale or blank comparison is acted on as if current. This is the members instance of the cross-project MSG-06 theme (see PROJECT-LEVEL.md). Fix: Track an error ref, render a visible error state with a retry in the results pane, and mark it role="alert". Do not leave the previous groupeScores on screen after a failed recompute.
3. Row edit/delete and popover triggers are icon-only with no accessible name — Medium · A11Y-05
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:102-111 (pencil) and :112-121 (trash); admin/src/client/components/app/tables/objectifs/CellObjectifProgress.vue:8-17 (info/warning popover trigger) What: Each <button> contains only an icon (<component :is="piIcon(...)"> or a bare <i class="pi ...">) with no aria-label, title, or visually-hidden text. A screen-reader user hears "button" with no indication of edit vs delete vs "show transactions". Why it matters: The primary management actions of the comparison screen, and the per-row detail affordance in the objectives table, are unusable non-visually. @bcc-code BccButton uses in this feature pass because they set label; only these hand-rolled <button>s miss it. Fix: Add aria-label="Modifier le groupe" / "Supprimer le groupe" / "Voir les transactions" (interpolate the group/member name where available).
4. Group-name input has no label — placeholder is the only cue — Medium · FORM-01
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:61-66 (edit) and :129-134 (new) What: A bare <input v-model="editDraft.Nom" placeholder="Nom du groupe"> with no associated <label>. The placeholder disappears the moment the user types, and there is no programmatic label for assistive tech. @keydown.enter.prevent also swallows Enter, so the expected "type name, press Enter to save" does nothing. Why it matters: FORM-01 — placeholder-as-label. On edit of an existing group the field is pre-filled, so a screen-reader user tabbing in hears the value with no field name; on the new-group draft the label vanishes on first keystroke. The FormConditionsBuilder below it takes a label prop, so the name field is the only unlabeled control. Fix: Add a <label> (or the Field UI primitive) for the name input; let Enter trigger save instead of preventing it.
5. Score colour-coding is dead code — the intended low/high signal never renders — Low · A11Y-07(e) (selection/severity not colour-only)
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:410-415What: scoreColor unconditionally return "text-brand-800"; on its first line, so the pct >= 80 / >= 50 / else branches are unreachable. Every non-leader score renders the same brand colour regardless of value; the intended "a struggling group looks different" cue is silently absent. Why it matters: The screen is a comparison whose whole job is surfacing which groups lag, yet the only per-score differentiation (colour) is disabled. The leader card is distinguished by size + a rank number as well, so this is not colour-alone-fatal, but the design intent is lost. The comparison-table pattern explicitly warns against relying on colour alone for severity/completion — here even that colour is missing. Fix: Remove the leading return, or delete the dead branches and decide whether colour coding is wanted; if kept, pair it with a non-colour cue.
6. One pending delete disables every row's delete button — Low · FORM-06
Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:114 (:disabled="groupes.remove.isPending.value"); similarly :104/:106 disable all edit buttons while editingId !== nullWhat: The disabled flag is not scoped to the row being mutated, so activating one delete disables the trash button on every row. Disabling the just-clicked control drops keyboard focus to <body> (FORM-06). Why it matters: Minor with typically few groups, but a keyboard user loses their place after each delete, and unrelated rows briefly become non-interactive. Fix: Scope the pending/disabled state to the affected row id; prefer aria-busy over disabled on the activated control.
Unverified
- A11Y-01 (contrast): not assessed from code. Candidates to check on a rendered page:
text-neutral-400empty-state and select-all copy (:45-47,:158-174),text-white/40rank andtext-white/60count on thebg-brand-800leader card (:220-241), andtext-brand-500/600onbg-brand-400runner-up cards. - A11Y-02 (target size): the
p-1pencil/trash buttons (:102-121) and thetext-xs"Tout sélectionner / désélectionner" text buttons (:162-174) look under 24×24 / closely packed but need measurement on a rendered viewport. - A11Y-06 (short viewport / responsive): the
lg:grid-cols-[360px_1fr]two-pane layout (:28) and the score cards need a rendered check; not assessable from code. - Progress semantics:
BarProgresswraps@bcc-codeBccProgressBar; whether it emitsrole="progressbar"/aria-valuenowis innode_modules, which is not installed — the visiblevalue / totaland%text (BarProgress.vue:4-6) do give SR users the numbers regardless.
Baseline additions
- A11Y-07(e) (referenced above) — selection/severity/completion state must not be conveyed by colour alone; already proposed in PROJECT-LEVEL.md's A11Y-07 collision list. No new ID proposed here.
Not-applicable
- CONTENT-01 — not-applicable; see project-level i18n finding (members has no i18n layer by design).
Cross-project references (not re-filed)
objectifsrenders throughTableData.vue— inherits the project-level shared-table cluster (isErrornever read so a failed load shows « Aucun élement »; mouse-only sort/edit/row-actions). See PROJECT-LEVEL.md.- NAV-03 — fails project-wide (static « BCC Nancy Admin » title); see PROJECT-LEVEL.md.
Cross-project note
Finding 2 (non-throwing/failed load swallowed, shown as stale or empty rather than an error) is the MSG-06 shape confirmed in all four projects — playout, customer-portal, tt-time-tracker and members. Findings 3 and 4 (icon-only controls, placeholder-as-label) are recurring A11Y-05/FORM-01 shapes worth grepping for in the other three admin surfaces, especially any hand-rolled <button><i></button> and <input placeholder=…> outside the component library. Finding 1 (destructive action with no confirm) should be checked against the other projects' definition/config CRUD screens.