Appearance
[UX] members — Data explorer
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch:
develop@2c22f5a· Files reviewed: 7 Patterns: none consulted (baseline covers every finding)
Summary
The data explorer is one of the better-built admin surfaces: filter/operator/value controls are real <button>/<select> elements (not the mouse-only div@click rows of TableData), the row-edit dialog is server-guarded and audited, and the results header reports truncation honestly (rows.length / total), so it does not fall into the batch's "aggregate from one page" trap. The gaps are accessibility and labeling: the saved-query cards are click-only <div>s that keyboard users cannot open, form inputs lean on placeholders instead of associated labels, and several icon-only controls have neither an accessible name nor a 24px target.
Files reviewed: views/DataExplorer.vue, composables/useDataExplorer.ts, components/app/data-explorer/{ConditionRow,FieldTreePicker,SavedQueriesPanel}.vue, components/ui/PageHeader.vue, lib/apiRequest.ts.
Findings
1. Saved-query cards are keyboard-unreachable — High · A11Y-03
Where: components/app/data-explorer/SavedQueriesPanel.vue:27-33What: Each saved query is loaded by clicking a <div class="cursor-pointer" @click="$emit('load', q)">. It has no tabindex, no role, and no key handler, so it is not in the tab order and cannot be activated from the keyboard. The only real <button> inside it is the delete (trash) control — so a keyboard user can delete a favorite but cannot open one, which is the panel's primary action. Why it matters: Loading a saved query is unreachable for keyboard and switch users (WCAG 2.1.1). This is the members instance of the project-wide click-only-row theme (A11Y-03), but it is feature-local here — not TableData — so it needs its own fix. Fix: Make the card a <button> (or add role="button", tabindex="0", and @keydown.enter/@keydown.space). Move the delete control out of the clickable region or keep it as the nested @click.stop button it already is.
2. Query error and status changes are silent to screen readers — Medium · MSG-01
Where: views/DataExplorer.vue:191-195 (error), :184-189 (running), :196-201 (empty) What: A failed query renders <div class="text-red-600">{{ error }}</div> with no role="alert"; the "Exécution…" running state and "Aucun résultat" empty state carry no role="status"/aria-live. The onMounted collections-load failure surfaces through the same unannounced block. All three are silent DOM swaps. Why it matters: A screen-reader user pressing "Exécuter" gets no feedback on whether the query ran, failed, or returned nothing (WCAG 4.1.3). Fix: Add role="alert" to the error block and role="status"/aria-live="polite" to the running and empty states (toasts from useToast presumably already announce — verify separately).
3. Inputs use placeholders as their only label — Medium · FORM-01
Where: ConditionRow.vue:53-147 (value inputs), DataExplorer.vue:27 + :277-290 + :320-326 (unassociated <label>s) What: Every condition value control names itself only through placeholder — "Valeur", "Min"/"Max", "Valeurs séparées par des virgules", "Rechercher…" — with no associated <label>. Where a visible <label> does exist (Collection at :27, the edit-dialog fields at :277, the save-dialog name at :320), it is a bare <label> with no for/id pairing to its Bcc* input, so it is not programmatically associated either. Why it matters: Placeholder text disappears on focus and is not reliably exposed as an accessible name, so screen-reader users and anyone who has started typing lose the field's purpose (FORM-01). Fix: Give each Bcc* control an id and point a <label for> at it, or pass the label through the component's own label/aria-label prop. Check whether @bcc-code/component-library-vue already wires a label prop before hand-rolling — the fix may belong at the call site only.
4. Column-chip remove buttons have no accessible name — Medium · A11Y-05
Where: views/DataExplorer.vue:100-106What: The "×" that removes a selected output column is <button class="hover:text-brand-900"><i class="pi pi-times"></button> — icon-only, with no aria-label, title, or text. (By contrast the row pencil at :239, condition-remove at :150, and saved-query delete all carry a title, so the omission here is inconsistent.) It also lacks type="button". Why it matters: A screen reader announces an unnamed button; the user cannot tell which column it removes (A11Y-05). Fix: Add :aria-label="\Retirer ${col.label}`"andtype="button"`.
5. Icon-only remove/add targets are far under 24px — Medium · A11Y-02
Where: views/DataExplorer.vue:100-106 (pi-times text-[0.6rem], ~10px) and :104 chip, plus the text-2xs/text-[0.6rem] "Ajouter une colonne" pill at :111-117What: The column-chip remove glyph is a ~10px icon in a chip with px-2 py-0.5, giving a hit target well below the 24×24 CSS-px floor; the dashed "add column" pill is similarly small. Why it matters: Small faint targets are hard to hit for motor-impaired and touch users (WCAG 2.5.8), the exact case the baseline calls out as routine. Fix: Pad the interactive area to ≥24px (e.g. p-1 on the button with a larger tap region) without necessarily enlarging the glyph.
6. Raw backend / network error strings reach the user — Medium · MSG-02
Where: views/DataExplorer.vue:194 + :463/:477/:541/:586/:610, via lib/apiRequest.ts:24-26 and useDataExplorer.ts:73What: Errors are shown as e.message. apiRequest sets that message to the server's error/message field or, failing that, HTTP <status>; exportCsv throws HTTP ${response.status} directly. On a network failure the browser's "Failed to fetch" becomes the message. All of these land verbatim in the red results block and in toast detail. Why it matters: Users see technical/untranslated strings ("HTTP 500", "Failed to fetch") instead of an actionable French sentence (MSG-02, MSG-03). This originates in the shared apiRequest, so it is not unique to this feature — but it is user-visible here. Fix: Map known code/status values to French copy and fall back to one generic sentence with a next step ("Réessayez ou contactez un administrateur"). Best fixed once in apiRequest/a shared error mapper.
7. Heading levels skip from h1 to h3 — Low · A11Y-04
Where: SavedQueriesPanel.vue:4 (<h3>) under PageHeader.vue:5 (<h1>); "Résultats" is a <span> at DataExplorer.vue:164What: The page <h1> (Exploration des données) is followed directly by the saved-queries <h3>, skipping h2, and the "Résultats" section title is a styled <span> rather than a heading, so the results region is not in the outline at all. Why it matters: Screen-reader users navigating by heading get a broken outline (WCAG 1.3.1). Fix: Make the sidebar title <h2> and promote "Résultats" to <h2>.
8. Save button is disabled while the name is empty — Low · FORM-05
Where: views/DataExplorer.vue:337-346What: The save-dialog primary buttons are :disabled="!saveName.trim()", so an empty name gives the user a dead, unexplained control rather than a validation message. Why it matters: Minor — the placeholder hints at the requirement — but it is the baseline's disabled-submit anti-pattern (FORM-05). Fix: Keep the button enabled and show "Donnez un nom au favori" on submit when blank.
Unverified
- A11Y-01 (contrast) — not assessable from code. Faint greys (
text-neutral-400,text-neutral-500) on white for placeholders, the "toutes les colonnes" hint, and disabled operator selects are the likely risk spots; verify with a contrast tool. - A11Y-06 (short viewport / mobile keyboard) — the
lg:flex-rowsidebar + builder +max-h-[60vh]scroll results region needs a rendered viewport to judge. - FORM-06 (focus drop on submit) — "Exécuter"/"Exporter"/"Enregistrer" use
:loading. Whether@bcc-code/component-library-vueBccButtonsets the nativedisabledattribute while loading (which would drop focus to<body>) cannot be read:node_modulesis not installed in this checkout.Enregistrer/Exporterare additionally:disabled="running||exporting"— the same concern applies if the active button becomes disabled mid-click.
Baseline additions
- A11Y-08 (proposed) — clickable non-interactive elements must be focusable and key-operable. A
<div>/<li>/<span>carrying@clickneedstabindex="0", an appropriaterole, and Enter/Space handling. (Finding 1 is an instance; overlaps the click-only-row theme already noted project-wide under A11Y-03 — fold in if A11Y-03 is broadened rather than adding a new ID.) - Otherwise none — every finding maps to an existing rule.
- CONTENT-01: not-applicable — see project-level i18n finding.
Cross-project note
- Finding 1 (click-only rows/cards) is the same shape confirmed in all four projects (see PROJECT-LEVEL.md cross-project table, A11Y-03 row) — customer-portal, playout and tt-time-tracker all have click-only
div/lirows or chips. This is a saved-query card rather than a table row, so it is a fresh instance of the same class. - Finding 6 (MSG-02, raw error strings) mirrors customer-portal's
getErrorMessagepreferring the raw backend string and members' widgetsapi.tssurfacing rawHTTP <status>; here it is the adminapiRequestdoing the same, so members fails MSG-02 on both client layers. - Finding 2 (missing
role="alert"/status) is a recurring MSG-01 gap across the members admin views that render ad-hoc result panels rather than going through a shared alert component.