origam 2.18.3 → 2.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -18,12 +18,154 @@ This project follows [Semantic Versioning](https://semver.org).
18
18
 
19
19
  ## [Unreleased]
20
20
 
21
+ ### Fixed — #818 baseline shrink : `aria-allowed-attr`/`aria-prohibited-attr`/`aria-valid-attr-value` (12 des 25 entrées)
22
+
23
+ Recompté sur `a11y-violations.baseline.json` (pas repris d'un chiffre cité
24
+ ailleurs) : la baseline #818 portait **25 clés / 33 violations** sur 24
25
+ composants, dont **12 `aria-allowed-attr`** concentrées sur la famille
26
+ `OrigamDataTable*`. Cause commune, pas 12 défauts distincts :
27
+ `OrigamTextField` ne posait pas `inheritAttrs: false` alors qu'il
28
+ redistribue déjà explicitement chaque attr de fall-through
29
+ (`filterInputAttrs` → `rootAttrs` sur `<origam-input>`, `inputAttrs` sur le
30
+ vrai `<input>`). Sans ce flag, Vue appliquait EN PLUS le `$attrs` brut sur
31
+ la racine `<origam-input>`, qui ne s'y soustrayait pas non plus — doublant
32
+ `aria-haspopup`/`aria-expanded`/`aria-controls`/`aria-valuenow`/… sur le
33
+ `<div>` wrapper, qui ne porte aucun rôle les autorisant. `role` (prop
34
+ déclarée sur `OrigamTextField`) n'atteignait par ailleurs que
35
+ `<origam-field>`, jamais le vrai `<input>` — c'est ce défaut exact que le
36
+ commentaire `KNOWN_FAILURES` documentait déjà pour `OrigamSelect` (test
37
+ `fixme`).
38
+
39
+ **Correctif à la source** (`packages/ds/src/components/TextField/OrigamTextField.vue`) :
40
+ `defineOptions({ inheritAttrs: false })` + `role` désormais aussi lié sur le
41
+ `<input>` réel. Élimine, mesuré axe-core en navigateur réel (Playwright +
42
+ Histoire statique) :
43
+ - `OrigamSelect` — sort de `KNOWN_FAILURES` (`components.spec.ts`), test
44
+ `fixme` → vert.
45
+ - `OrigamDataTable`, `OrigamDataTableFooter`, `OrigamDataTableHeaderCell`,
46
+ `OrigamDataTableHeaders`, `OrigamDataTableHeadersCell`,
47
+ `OrigamDataTableRow` — `aria-allowed-attr` disparaît, entrée de baseline
48
+ retirée entièrement.
49
+ - `OrigamDataTableGroupHeaderRow`, `OrigamDataTableRows`, `OrigamNumberField`
50
+ — `aria-allowed-attr` disparaît, l'entrée reste pour leur AUTRE violation
51
+ (`button-name` / `color-contrast`), non touchée par ce lot.
52
+ - `OrigamDataTableHeadersCellMobile` — `aria-allowed-attr` ET
53
+ `aria-prohibited-attr` disparaissent (même fuite, deux règles axe
54
+ différentes), entrée retirée entièrement.
55
+
56
+ **Deuxième cause, distincte, deux composants** —
57
+ `packages/ds/src/components/ColorPickerField/OrigamColorPickerField.vue` et
58
+ `.../DatePickerField/OrigamDatePickerField.vue` posaient un
59
+ `aria-haspopup` avec une valeur inventée (`'colorpickerbox'` /
60
+ `'datepickerbox'`, jamais un token ARIA valide) sur l'élément
61
+ `.origam-field` (via `activator="parent"` d'`OrigamMenu`), sans `role` le
62
+ justifiant. Remplacé par `aria-haspopup: 'dialog'` (motif WAI-ARIA « Date
63
+ Picker Dialog ») + `role: 'combobox'` sur le même élément — même principe
64
+ que `comboboxAriaAttrs` d'`OrigamSelect`. `aria-allowed-attr` ET
65
+ `aria-valid-attr-value` disparaissent pour les deux ; `OrigamColorPickerField`
66
+ est intégralement retiré de la baseline, `OrigamDatePickerField` y reste
67
+ pour son `color-contrast` (non touché).
68
+
69
+ **Bilan mesuré** : baseline #818 25 clés/33 violations → **17 clés/18
70
+ violations**. Suite `pnpm -F @origam/tests test:a11y` complète (226 tests,
71
+ catalogue 218/218) : **226 passed**, aucune régression sur les 36
72
+ composants à baseline vide. Contrôle positif exécuté : réintroduire l'ancien
73
+ comportement fait échouer `a11y — OrigamSelect Default Variant` avec
74
+ `aria-allowed-attr`/`aria-prohibited-attr` NON baselinés — la porte capte
75
+ la régression, pas seulement l'édition du JSON.
76
+
77
+ **Laissé de côté, délibérément** (17 clés / 18 violations restent dans
78
+ `a11y-violations.baseline.json`, cf. #818) : `color-contrast` (8 instances —
79
+ plusieurs agents dédiés au contraste travaillent déjà sur ce périmètre),
80
+ `button-name` (3, boutons icône sans libellé — famille distincte),
81
+ `scrollable-region-focusable` (3), `aria-required-parent`/`aria-required-children`/
82
+ `listitem`/`aria-prohibited-attr` (`OrigamChartMap`, sans lien avec ce
83
+ correctif) — un défaut chacun, cause non partagée avec ce lot, non
84
+ instruit ici.
85
+
21
86
  Récolte de deux dépréciations posées "pour la prochaine majeure" avant que le
22
87
  `CLAUDE.md` n'acte que `3.0.0` est réservé à la séparation en modules et que
23
88
  les ruptures, elles, ne le sont pas — voir "Work priorities and versioning"
24
89
  du `CLAUDE.md`. `origam` n'a aucun consommateur : les deux ruptures
25
90
  ci-dessous partent en **mineure**, sans shim ni période de grâce.
26
91
 
92
+ ### Fixed — `button-name` et `scrollable-region-focusable` (6 des 18 violations laissées de côté par le lot précédent)
93
+
94
+ Recompté sur `a11y-violations.baseline.json` avant de commencer (pas repris
95
+ d'un chiffre cité ailleurs) : **18 violations / 17 clés**, dont deux familles
96
+ explicitement laissées de côté par le lot ci-dessus.
97
+
98
+ **`button-name` (3, cause commune)** — `OrigamDataTableGroupHeaderRow`,
99
+ `OrigamDatePicker`, `OrigamDatePickerControls` rendaient chacun un
100
+ `<origam-btn icon="…">` sans `aria-label`/`aria-labelledby`/`title` : un
101
+ bouton icône réel, annoncé "button" et rien d'autre par un lecteur d'écran
102
+ (`OrigamBtn` avertit déjà de ce cas précis en dev depuis #653, mais ne
103
+ fabrique jamais de libellé — cf. son commentaire `isUnnamedIconOnly`, à
104
+ raison : un libellé deviné tairait axe sans rien dire à l'utilisateur).
105
+ Corrigé à la source de chaque bouton (pas dans `OrigamBtn`, qui n'a pas
106
+ l'information sémantique) :
107
+ - `OrigamDataTableGroupHeaderRow` — le bouton d'expand/collapse du groupe
108
+ reçoit `aria-expanded` + `aria-label` dynamique (`origam.data_table_group_header_row.{expand_group,collapse_group}`),
109
+ même motif que le correctif `data_table_row.{expand_row,collapse_row}`
110
+ déjà en place sur `OrigamDataTableRow`.
111
+ - `OrigamDatePickerControls` — les 3 boutons icône (bascule mois/année,
112
+ précédent, suivant) reçoivent chacun un `aria-label` via `useLocale()`
113
+ (`origam.date_picker_controls.{toggle_year,prev,next}`).
114
+ - `OrigamDatePicker::default` en héritait sans bouton icône propre — sa
115
+ Variant par défaut rend `OrigamDatePickerControls`, donc le correctif
116
+ ci-dessus l'assainit aussi ; son AUTRE violation (`color-contrast`,
117
+ hors périmètre) reste dans la baseline.
118
+
119
+ Nouvelles clés ajoutées à `en.json` ET `fr.json` dans le même commit,
120
+ `snake_case` minuscule, aucune chaîne en dur dans les templates.
121
+
122
+ **`scrollable-region-focusable` (3, cause commune)** — `OrigamInfiniteScroll`,
123
+ `OrigamInfiniteScrollIntersect` (dont la story monte elle-même un
124
+ `OrigamInfiniteScroll`) et `OrigamSlideGroup` rendent chacun un conteneur
125
+ `overflow: auto` sans jamais être atteignables au clavier quand leur contenu
126
+ n'a aucun descendant focusable — axe-core l'exige (technique WAI WCAG
127
+ SCR29).
128
+
129
+ ⚠️ `tabindex="0"` inconditionnel a été essayé et écarté : `OrigamSlideGroup`
130
+ a un seul consommateur réel (`OrigamChipGroup`), qui slotte toujours des
131
+ puces focusables — y poser `tabindex` sans condition aurait inséré un arrêt
132
+ de tabulation redondant AVANT les items, dégradant le parcours clavier réel
133
+ pour corriger un cas de story synthétique. Le correctif retenu conditionne
134
+ donc le `tabindex` sur l'ABSENCE de descendant focusable (`focusableChildren()`,
135
+ déjà utilisé ailleurs dans le DS) :
136
+ - `OrigamInfiniteScroll` — `hasFocusableDescendant` (recalculé par
137
+ `MutationObserver` sur la racine, le contenu s'accumulant en continu via
138
+ `@load`) pilote un `:tabindex` conditionnel sur la racine.
139
+ - `OrigamSlideGroup` — `hasFocusableContent` (recalculé dans le même
140
+ `requestAnimationFrame` qui mesure déjà `isOverflowing`) pilote un
141
+ `:tabindex` conditionnel sur `.origam-slide-group__container`, l'élément
142
+ RÉELLEMENT scrollable — distinct de la racine, qui porte déjà son propre
143
+ `tabindex` conditionnel pour un mécanisme sans rapport (proxy de focus
144
+ vers le premier item, cf. `handleFocus`) qu'axe ne peut pas lire ici
145
+ puisque `__container` n'est pas un ancêtre de la racine.
146
+ - `OrigamInfiniteScrollIntersect::default` en héritait sans conteneur
147
+ scrollable propre — sa Variant par défaut monte un `OrigamInfiniteScroll`,
148
+ donc le correctif ci-dessus l'assainit aussi.
149
+
150
+ **Bilan mesuré** : baseline **18 clés/17 violations → 12 clés/12 violations**
151
+ (6 clés retirées : `OrigamDataTableGroupHeaderRow`, `OrigamDatePickerControls`,
152
+ `OrigamInfiniteScroll`, `OrigamInfiniteScrollIntersect`, `OrigamSlideGroup`,
153
+ et `button-name` retiré de l'entrée `OrigamDatePicker` qui garde son
154
+ `color-contrast`). Suite `pnpm -F @origam/tests test:a11y` complète (226
155
+ tests, catalogue 218/218) : **226 passed**, aucune régression sur les 36
156
+ composants à baseline vide. Contrôle positif exécuté sur
157
+ `OrigamDataTableGroupHeaderRow` : retirer `aria-expanded`/`aria-label` fait
158
+ échouer la porte avec `1 violation(s) a11y NOUVELLE(S) non baselinée(s) —
159
+ button-name` (`REAL_EXIT=1`) ; le correctif remis, la suite ciblée repasse
160
+ au vert. `pnpm -F origam guards` (28/28), `guards:self` (15/15),
161
+ `pnpm -F @origam/tests test:e2e:audit` (3/3) et `pnpm run lint`
162
+ (`--max-warnings 0`) verts sur le dernier commit.
163
+
164
+ **Laissé de côté, délibérément** (12 clés / 12 violations restent dans
165
+ `a11y-violations.baseline.json`) : `color-contrast` (8 instances, périmètre
166
+ d'autres agents), `aria-required-parent`/`aria-required-children`/`listitem`/
167
+ `aria-prohibited-attr` (un défaut chacun, cause non partagée avec ce lot).
168
+
27
169
  ### ⚠️ BREAKING — `click:prepend` / `click:append` retirés d'`IBtnEmits` (#443, #577)
28
170
 
29
171
  `<OrigamBtn>` n'émet plus `click:prepend` ni `click:append` — ni au niveau du
@@ -156,6 +298,105 @@ conclure qu'elle ne l'est plus par défaut.
156
298
 
157
299
  ### Fixed
158
300
 
301
+ - **#871 (clôture) — les 7 dernières violations AA étaient UN défaut de
302
+ palette répété, pas sept défauts de composant.** Mesuré avec
303
+ `packages/tests/audit/dark-contrast.audit.mjs` : **7 / 1 664 → 0 / 1 664**,
304
+ les 8 identités × 2 modes × 2 portées, quatre contrôles verts des deux
305
+ côtés. Exactement **7 lignes changent, aucune ne se dégrade**, et le ratio
306
+ minimum de toute la surface passe de 3.65 à **4.70**.
307
+
308
+ Les 7 se répartissaient en deux causes, toutes deux dans
309
+ `packages/marketing/src/themes` — **aucune dans le DS**, qui était déjà à
310
+ zéro :
311
+
312
+ | cause | marque · mode | couple | avant | après |
313
+ |---|---|---|---|---|
314
+ | encre héritée du mode clair | `apple` sombre | `#ffffff` → `#000000` sur `#0a84ff` | 3.65 | **5.76** |
315
+ | encre héritée du mode clair | `ecom` sombre | `#ffffff` → `#1a0f0a` sur `#f43f5e` | 3.67 | **5.12** |
316
+ | jeton de rôle inversé | `ecom` clair | `action.primary.bg` → `.fgSubtle` sur la crème | 4.43 | **5.93** |
317
+
318
+ **La cause commune.** Le mode sombre ÉCLAIRCIT l'accent de marque — c'est
319
+ la bonne pratique pour une grande surface (`#0071e3` → `#0a84ff` chez
320
+ `apple`, rose-600 → rose-500 chez `ecom`) — mais les deux palettes gardaient
321
+ l'encre `#ffffff` de leur jumeau clair. Or chacune applique **déjà** la règle
322
+ inverse à ses intentions `feedback` : les 4 d'`ecom` et 3 des 4 d'`apple`
323
+ encrent leur accent éclairci avec le `text.inverse` de la palette.
324
+ `action.primary` était la dernière exception. Le badge n'était pas en cause :
325
+ `--origam-badge__badge---{background-color,color}` lit exactement ce couple —
326
+ tout comme `--origam-btn--primary---*`, qui n'est pas dans la sonde et était
327
+ donc en défaut sans être mesuré.
328
+
329
+ ⚠️ **On n'a PAS foncé l'accent**, bien que ce soit l'autre levier : chez
330
+ `apple`, `action.primary.bg` sert aussi d'ENCRE (le bloc `components` passe
331
+ ce `var()` en `color` sur `origam-breadcrumb-item`). Le ramener à `#0071e3`
332
+ donnait 4.70 sous du blanc mais faisait tomber le fil d'Ariane à **4.47** sur
333
+ le fond noir — un défaut échangé contre un autre. Vérifié par calcul avant
334
+ d'écrire la moindre ligne.
335
+
336
+ La 3ᵉ cause est une erreur de RÔLE : `bg` est un jeton de surface, `fgSubtle`
337
+ la teinte prévue pour être LUE sur une surface neutre. `ecomDarkTheme` posait
338
+ déjà `fgSubtle` ; seul le bloc clair se trompait de jeton. Les deux modes sont
339
+ désormais symétriques.
340
+
341
+ - **#871 — une 8ᵉ violation, que la sonde ne pouvait pas voir** :
342
+ `material` sombre, `feedback.success`, `#1b5e20` sur `#81c784` = **3.91**
343
+ → `#0d3b10` = **6.32**. La sonde de `dark-contrast.audit.mjs` ne rend qu'UNE
344
+ intention par famille de composant, donc elle ne peint jamais
345
+ `feedback.success`. Trouvée par la spec statique ajoutée ci-dessous. C'est
346
+ l'encre qui était l'intruse, pas le conteneur : les trois autres intentions
347
+ sombres de `material` tiennent 5.79 / 7.70 / 7.71 avec des encres bespoke
348
+ très profondes, `success` était la seule à se contenter du green-900 de la
349
+ rampe.
350
+
351
+ - **#871 — l'audit lui-même ne démarrait plus.** Son `npx vite build` résolvait
352
+ vite par le PATH, via un lien `node_modules/.bin` qu'un peer auto-installé
353
+ ne garantit pas : sur un `pnpm install --frozen-lockfile` propre,
354
+ `packages/tests/node_modules/.bin/` ne contient que `playwright` et `vitest`,
355
+ et la commande mourait en `127 — sh: vite: command not found`. **L'audit ne
356
+ rendait plus aucun chiffre**, et rien ne le signalait puisque rien ne le
357
+ rejoue en CI. `resolveViteBin()` passe désormais par le realpath de
358
+ `@vitejs/plugin-vue`, ce qui ne dépend ni du PATH ni du hoisting et
359
+ sélectionne la copie de vite que le plugin utilise réellement (deux majeures
360
+ coexistent dans le magasin). Les commentaires qui affirmaient le contraire
361
+ dans les deux fichiers ont été corrigés.
362
+
363
+ - **#871 — l'état SURVOL était en défaut plus grave encore, et le correctif
364
+ d'encre l'a réglé sans qu'on y touche.** Le survol n'a pas d'encre à lui :
365
+ `--origam-btn--primary---background-color-hover` bascule la surface, mais il
366
+ n'existe **aucun** `--origam-btn--primary---color-hover` (ni de
367
+ `--origam-badge__badge---color-hover`) — l'encre reste `action.primary.fg`
368
+ dans les deux états. Sous l'encre blanche, `apple` sombre y tombait à
369
+ **2.19:1** et `ecom` sombre à **2.69:1** : *pires que n'importe laquelle des
370
+ 7 violations comptées*, et invisibles parce que la sonde de
371
+ `dark-contrast.audit.mjs` ne rend que l'état au repos.
372
+
373
+ **Aucun `bgHover` n'a été modifié.** La direction du survol n'était pas le
374
+ problème — l'encre l'était. Mesuré sur les deux marques :
375
+
376
+ | mode · encre | direction du survol | ratio repos → survol |
377
+ |---|---|---|
378
+ | clair · blanche | fonce | 4.70 → **6.95** / **6.29** ↑ |
379
+ | sombre · blanche *(avant)* | éclaircit | 3.65 → **2.19** / 3.67 → **2.69** ↓ |
380
+ | sombre · foncée *(après)* | éclaircit | 5.76 → **9.60** / 5.12 → **6.99** ↑ |
381
+
382
+ La règle n'est donc pas *« le survol doit foncer »* mais **« le survol doit
383
+ déplacer l'accent à l'opposé de la clarté de son encre »**. Les palettes la
384
+ respectaient déjà dans 3 quadrants sur 4 ; le seul cassé l'était par son
385
+ encre. L'idiome « le survol éclaircit », qui est le bon en mode sombre, est
386
+ conservé dans les deux marques.
387
+
388
+ - **#871 — non-régression : `packages/tests/TU/marketing/brand-palette-contrast.spec.ts`.**
389
+ Les couples `{bg, fg}` littéraux des 7 palettes de marque (14 thèmes) doivent
390
+ tenir AA. L'audit de #871 est un script qu'on lance à la main : rien ne le
391
+ rejoue, donc rien n'empêchait une retouche de palette de ramener ces couples
392
+ en silence. La spec lit les hex **dans l'objet `IOrigamTheme`**, avant toute
393
+ indirection CSS — la règle jsdom/`var()` du `CLAUDE.md` ne s'y applique pas,
394
+ et le navigateur reste l'arbitre de ce qui est peint. Elle embarque ses
395
+ propres garde-fous : les 3 témoins de la formule, un contrôle négatif sur les
396
+ 3 couples corrigés, et un plancher de couverture qui rend bruyante une dérive
397
+ du filtre. **Vérifiée ROUGE sur `origin/develop`** : 3 échecs / 14 succès.
398
+ C'est elle qui a trouvé la 8ᵉ violation ci-dessus.
399
+
159
400
  - **#871 — le mode sombre ne peignait qu'à moitié dans un sous-arbre thémé.**
160
401
  Les feuilles de tokens portent ~1 761 déclarations **dérivées**
161
402
  (`--origam-title---color: var(--origam-color__text---primary)`). Une custom
@@ -51,6 +51,11 @@
51
51
  "placeholder": "Enter date"
52
52
  }
53
53
  },
54
+ "date_picker_controls": {
55
+ "prev": "Previous",
56
+ "next": "Next",
57
+ "toggle_year": "Toggle year selection"
58
+ },
54
59
  "no_data_text": "No data available",
55
60
  "bracket": {
56
61
  "aria_label": "Tournament bracket",
@@ -199,6 +204,10 @@
199
204
  "expand_row": "Expand row",
200
205
  "collapse_row": "Collapse row"
201
206
  },
207
+ "data_table_group_header_row": {
208
+ "expand_group": "Expand group",
209
+ "collapse_group": "Collapse group"
210
+ },
202
211
  "rating": {
203
212
  "aria_label": {
204
213
  "item": "Rating {0} of {1}"
@@ -51,6 +51,11 @@
51
51
  "placeholder": "Entrer une date"
52
52
  }
53
53
  },
54
+ "date_picker_controls": {
55
+ "prev": "Précédent",
56
+ "next": "Suivant",
57
+ "toggle_year": "Basculer la sélection de l'année"
58
+ },
54
59
  "no_data_text": "Aucune donnée disponible",
55
60
  "bracket": {
56
61
  "aria_label": "Tableau du tournoi",
@@ -199,6 +204,10 @@
199
204
  "expand_row": "Développer la ligne",
200
205
  "collapse_row": "Réduire la ligne"
201
206
  },
207
+ "data_table_group_header_row": {
208
+ "expand_group": "Développer le groupe",
209
+ "collapse_group": "Réduire le groupe"
210
+ },
202
211
  "rating": {
203
212
  "aria_label": {
204
213
  "item": "Note de {0} sur {1}"
@@ -331,9 +331,31 @@ const menuProps = computed(() => {
331
331
  return {
332
332
  ...props.menuProps,
333
333
  activatorProps: {
334
+ /*********************************************************
335
+ * role + aria-haspopup — #818
336
+ *
337
+ * @description
338
+ * `'colorpickerbox'` was never a valid `aria-haspopup` token
339
+ * (spec allows `false|true|menu|listbox|tree|grid|dialog`) —
340
+ * axe-core's `aria-valid-attr-value` flagged it, measured
341
+ * against a real browser. The popup is a free-form panel
342
+ * (a color canvas + swatches), not a list of options, so
343
+ * `'dialog'` is the correct token — same family as the
344
+ * WAI-ARIA "Date Picker Dialog" pattern `OrigamDatePickerField`
345
+ * uses right below.
346
+ *
347
+ * @description
348
+ * `role: 'combobox'` is ALSO required here: `activator="parent"`
349
+ * (below) lands these ARIA attrs on the `.origam-field` wrapper
350
+ * `<div>`, which carries no role by default — `aria-allowed-attr`
351
+ * rejects `aria-expanded` on an element whose role doesn't
352
+ * support it. Same fix, same rationale as
353
+ * `OrigamSelect.comboboxAriaAttrs` (`role` MUST sit on the same
354
+ * element as `aria-haspopup`/`aria-expanded`).
355
+ ********************************************************/
356
+ role: "combobox",
334
357
  ...props.menuProps?.activatorProps || {},
335
- "aria-haspopup": "colorpickerbox"
336
- // Set aria-haspopup to 'listbox'
358
+ "aria-haspopup": "dialog"
337
359
  },
338
360
  contentProps: {
339
361
  ...consumerContentProps,
@@ -15,6 +15,8 @@
15
15
  >
16
16
  <origam-data-table-column-cell class="origam-data-table-group-header-row__column">
17
17
  <origam-btn
18
+ :aria-expanded="isGroupOpen(item)"
19
+ :aria-label="isGroupOpen(item) ? t('origam.data_table_group_header_row.collapse_group') : t('origam.data_table_group_header_row.expand_group')"
18
20
  :icon="groupIcon"
19
21
  size="small"
20
22
  variant="text"
@@ -54,6 +56,7 @@ import OrigamCheckboxBtn from "../Checkbox/OrigamCheckboxBtn.vue";
54
56
  import OrigamDataTableColumnCell from "./OrigamDataTableColumnCell.vue";
55
57
  import { useGroupBy } from "../../composables/DataTable/group.composable";
56
58
  import { useHeaders } from "../../composables/DataTable/headers.composable";
59
+ import { useLocale } from "../../composables/Commons/locale.composable";
57
60
  import { useProps } from "../../composables/Commons/props.composable";
58
61
  import { useSelection } from "../../composables/DataTable/select.composable";
59
62
  import { useStyle } from "../../composables/Commons/style.composable";
@@ -74,6 +77,7 @@ const props = defineProps({
74
77
  defineEmits([]);
75
78
  defineSlots();
76
79
  const { filterProps } = useProps(props);
80
+ const { t } = useLocale();
77
81
  const { isGroupOpen, toggleGroup, extractRows } = useGroupBy();
78
82
  const { isSelected, isSomeSelected, select } = useSelection();
79
83
  const { columns } = useHeaders();
@@ -17,6 +17,7 @@
17
17
 
18
18
  <origam-btn
19
19
  key="mode-btn"
20
+ :aria-label="t('origam.date_picker_controls.toggle_year')"
20
21
  :disabled="disableYear"
21
22
  :icon="modeIcon"
22
23
  class="origam-date-picker-controls__mode-btn"
@@ -35,6 +36,7 @@
35
36
  <origam-btn-group>
36
37
  <template #default>
37
38
  <origam-btn
39
+ :aria-label="t('origam.date_picker_controls.prev')"
38
40
  :disabled="disablePrev"
39
41
  :icon="prevIcon"
40
42
  :rounded="0"
@@ -42,6 +44,7 @@
42
44
  />
43
45
 
44
46
  <origam-btn
47
+ :aria-label="t('origam.date_picker_controls.next')"
45
48
  :disabled="disableNext"
46
49
  :icon="nextIcon"
47
50
  :rounded="0"
@@ -58,6 +61,7 @@ import OrigamBtn from "../Btn/OrigamBtn.vue";
58
61
  import OrigamBtnGroup from "../Btn/OrigamBtnGroup.vue";
59
62
  import OrigamSpacer from "../Grids/OrigamSpacer.vue";
60
63
  import { useUnsupportedProp } from "../../composables/Commons/unsupportedProp.composable";
64
+ import { useLocale } from "../../composables/Commons/locale.composable";
61
65
  import { useProps } from "../../composables/Commons/props.composable";
62
66
  import { useStyle } from "../../composables/Commons/style.composable";
63
67
  import { DATE_MODE } from "../../enums/DatePicker/date-picker.enum";
@@ -94,6 +98,7 @@ useUnsupportedProp(
94
98
  const emits = defineEmits(["click:year", "click:month", "click:prev", "click:next"]);
95
99
  defineSlots();
96
100
  const { filterProps } = useProps(props);
101
+ const { t } = useLocale();
97
102
  const disableMonth = computed(() => {
98
103
  return props.disabled || props.disabledMonth;
99
104
  });
@@ -405,9 +405,29 @@ const menuProps = computed(() => {
405
405
  return {
406
406
  ...props.menuProps,
407
407
  activatorProps: {
408
+ /*********************************************************
409
+ * role + aria-haspopup — #818
410
+ *
411
+ * @description
412
+ * `'datepickerbox'` was never a valid `aria-haspopup` token
413
+ * (spec allows `false|true|menu|listbox|tree|grid|dialog`) —
414
+ * axe-core's `aria-valid-attr-value` flagged it, measured
415
+ * against a real browser. `'dialog'` matches the WAI-ARIA
416
+ * "Date Picker Dialog" pattern: a button/combobox opening a
417
+ * calendar dialog.
418
+ *
419
+ * @description
420
+ * `role: 'combobox'` is ALSO required here: `activator="parent"`
421
+ * (below) lands these ARIA attrs on the `.origam-field` wrapper
422
+ * `<div>`, which carries no role by default — `aria-allowed-attr`
423
+ * rejects `aria-expanded` on an element whose role doesn't
424
+ * support it. Same fix, same rationale as
425
+ * `OrigamSelect.comboboxAriaAttrs` (`role` MUST sit on the same
426
+ * element as `aria-haspopup`/`aria-expanded`).
427
+ ********************************************************/
428
+ role: "combobox",
408
429
  ...props.menuProps?.activatorProps || {},
409
- "aria-haspopup": "datepickerbox"
410
- // Set aria-haspopup to 'listbox'
430
+ "aria-haspopup": "dialog"
411
431
  },
412
432
  contentProps: {
413
433
  ...consumerContentProps,
@@ -5,6 +5,7 @@
5
5
  ref="rootEl"
6
6
  :class="infiniteScrollClasses"
7
7
  :style="infiniteScrollStyles"
8
+ :tabindex="hasFocusableDescendant ? undefined : 0"
8
9
  >
9
10
  <div class="origam-infinite-scroll__side" role="status" aria-live="polite" :style="typographyStyles">
10
11
  <template v-if="hasStartIntersect">
@@ -129,6 +130,7 @@ import { IN_BROWSER } from "../../consts/Commons/commons.const";
129
130
  import { DIRECTION } from "../../enums/Commons/direction.enum";
130
131
  import { INFINITE_SCROLL_MODE, INFINITE_SCROLL_SIDE, INFINITE_SCROLL_STATUS } from "../../enums/InfiniteScroll/infinite-scroll.enum";
131
132
  import { PROGRESS_TYPE } from "../../enums/Progress/progress.enum";
133
+ import { focusableChildren } from "../../utils/Commons/commons.util";
132
134
  const props = defineProps({
133
135
  side: { type: null, required: false, default: INFINITE_SCROLL_SIDE.END },
134
136
  mode: { type: null, required: false, default: INFINITE_SCROLL_MODE.INTERSECT },
@@ -159,6 +161,21 @@ const { typographyStyles } = useTypography(props, "infinite-scroll__loader");
159
161
  const { colorClasses, colorStyles } = useBothColor(toRef(props, "bgColor"), toRef(props, "color"));
160
162
  const rootEl = ref();
161
163
  const isIntersecting = shallowRef(false);
164
+ const hasFocusableDescendant = shallowRef(false);
165
+ let focusableObserver;
166
+ const refreshHasFocusableDescendant = () => {
167
+ if (!rootEl.value) return;
168
+ hasFocusableDescendant.value = focusableChildren(rootEl.value).length > 0;
169
+ };
170
+ onMounted(() => {
171
+ if (!IN_BROWSER || !rootEl.value) return;
172
+ refreshHasFocusableDescendant();
173
+ focusableObserver = new MutationObserver(refreshHasFocusableDescendant);
174
+ focusableObserver.observe(rootEl.value, { childList: true, subtree: true });
175
+ });
176
+ onBeforeUnmount(() => {
177
+ focusableObserver?.disconnect();
178
+ });
162
179
  const propertyDirection = computed(() => {
163
180
  return props.direction === DIRECTION.VERTICAL ? "scrollTop" : "scrollLeft";
164
181
  });
@@ -25,6 +25,7 @@
25
25
  key="container"
26
26
  ref="containerRef"
27
27
  class="origam-slide-group__container"
28
+ :tabindex="isOverflowing && !hasFocusableContent ? 0 : undefined"
28
29
  @scroll="handleScroll"
29
30
  >
30
31
  <div
@@ -138,6 +139,7 @@ const { marginClasses, marginStyles } = useMargin(props);
138
139
  const { displayClasses, mobile } = useDisplay(props);
139
140
  const group = useGroup(props, ORIGAM_SLIDE_GROUP_KEY);
140
141
  const isOverflowing = shallowRef(false);
142
+ const hasFocusableContent = shallowRef(false);
141
143
  const scrollOffset = shallowRef(0);
142
144
  const containerSize = shallowRef(0);
143
145
  const contentSize = shallowRef(0);
@@ -172,6 +174,9 @@ if (IN_BROWSER) {
172
174
  contentSize.value = contentRect.value[sizeProperty];
173
175
  isOverflowing.value = containerSize.value + 1 < contentSize.value;
174
176
  }
177
+ if (contentRef.value) {
178
+ hasFocusableContent.value = focusableChildren(contentRef.value).length > 0;
179
+ }
175
180
  if (firstSelectedIndex.value >= 0 && contentRef.value) {
176
181
  const selectedElement = contentRef.value.children[lastSelectedIndex.value];
177
182
  scrollToChildren(selectedElement, props.centerActive);
@@ -91,6 +91,7 @@
91
91
  :readonly="isReadonly"
92
92
  :required="required || undefined"
93
93
  :aria-required="required ? 'true' : undefined"
94
+ :role="role"
94
95
  :size="1"
95
96
  :type="resolvedInputType"
96
97
  :value="displayValue"
@@ -208,6 +209,7 @@ import { applyMask } from "../../utils/Commons/apply-mask.util";
208
209
  import { filterInputAttrs } from "../../utils/Input/input.util";
209
210
  import { forwardRefs } from "../../utils/Commons/forwardRefs.util";
210
211
  import { resolveMaskConfig } from "../../utils/Commons/resolve-mask-config.util";
212
+ defineOptions({ inheritAttrs: false });
211
213
  const props = defineProps({
212
214
  autofocus: { type: Boolean, required: false },
213
215
  counter: { type: [Boolean, Number, String], required: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "origam",
3
- "version": "2.18.3",
3
+ "version": "2.18.5",
4
4
  "type": "module",
5
5
  "description": "origam — a Vue 3 design system with multi-theme design tokens, CSS-first components and TypeScript types.",
6
6
  "keywords": [