origam 2.18.4 → 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
@@ -89,6 +89,83 @@ les ruptures, elles, ne le sont pas — voir "Work priorities and versioning"
89
89
  du `CLAUDE.md`. `origam` n'a aucun consommateur : les deux ruptures
90
90
  ci-dessous partent en **mineure**, sans shim ni période de grâce.
91
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
+
92
169
  ### ⚠️ BREAKING — `click:prepend` / `click:append` retirés d'`IBtnEmits` (#443, #577)
93
170
 
94
171
  `<OrigamBtn>` n'émet plus `click:prepend` ni `click:append` — ni au niveau du
@@ -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}"
@@ -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
  });
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "origam",
3
- "version": "2.18.4",
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": [