@cnamts/synapse 1.0.22 → 1.0.23

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.
Files changed (190) hide show
  1. package/dist/{DateFilter-B5n-ZkLi.js → DateFilter-Dc-gSGwk.js} +1 -1
  2. package/dist/{NumberFilter-CtiZ9uj8.js → NumberFilter-vP38Wp6j.js} +1 -1
  3. package/dist/{PeriodFilter-DzqiMb-b.js → PeriodFilter-Ba1uYUnT.js} +1 -1
  4. package/dist/{SelectFilter-BOYlF7rX.js → SelectFilter-BioGT6Nn.js} +1 -1
  5. package/dist/{TextFilter-BOFRNfcX.js → TextFilter-B84dpnoq.js} +1 -1
  6. package/dist/components/Accordion/Accordion.d.ts +13 -2
  7. package/dist/components/Accordion/composables/useAccordionState.d.ts +2 -1
  8. package/dist/components/Amelipro/AmeliproAutoCompleteField/AmeliproAutoCompleteField.d.ts +7 -7
  9. package/dist/components/Amelipro/AmeliproCheckbox/AmeliproCheckbox.d.ts +1 -1
  10. package/dist/components/Amelipro/AmeliproCustomSelector/AmeliproCustomSelector.d.ts +1 -1
  11. package/dist/components/Amelipro/AmeliproPostalAddressField/AmeliproPostalAddressField.d.ts +1 -1
  12. package/dist/components/Amelipro/AmeliproSelect/AmeliproSelect.d.ts +7 -7
  13. package/dist/components/Amelipro/AmeliproTabs/AmeliproTabs.d.ts +16 -16
  14. package/dist/components/Amelipro/AmeliproTextArea/AmeliproTextArea.d.ts +1 -1
  15. package/dist/components/Amelipro/AmeliproTextField/AmeliproTextField.d.ts +1 -1
  16. package/dist/components/Customs/Selects/SyAutocomplete/SyAutocomplete.d.ts +22 -1
  17. package/dist/components/Customs/Selects/SyAutocomplete/locales.d.ts +5 -0
  18. package/dist/components/Customs/Selects/SyInputSelect/SyInputSelect.d.ts +1 -1
  19. package/dist/components/Customs/Selects/SySelect/SySelect.d.ts +1 -1
  20. package/dist/components/Customs/Selects/SySelect/locales.d.ts +1 -0
  21. package/dist/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.d.ts +1 -1
  22. package/dist/components/Customs/SyCheckbox/SyCheckbox.d.ts +1 -1
  23. package/dist/components/Customs/SyRadioGroup/SyRadioGroup.d.ts +1 -1
  24. package/dist/components/Customs/SyTextField/SyTextField.d.ts +5 -2
  25. package/dist/components/DatePicker/CalendarMode/DatePicker.d.ts +13 -9
  26. package/dist/components/DatePicker/ComplexDatePicker/ComplexDatePicker.d.ts +7 -5
  27. package/dist/components/DatePicker/DateTextInput/DateTextInput.d.ts +2 -1
  28. package/dist/components/ErrorPage/ErrorPage.d.ts +3 -1
  29. package/dist/components/FileList/UploadItem/UploadItem.d.ts +6 -0
  30. package/dist/components/FileList/UploadItem/locales.d.ts +1 -4
  31. package/dist/components/FileUpload/FileUploadContent.d.ts +2 -0
  32. package/dist/components/FileUpload/validateFiles.d.ts +2 -1
  33. package/dist/components/HeaderBar/HeaderBar.d.ts +2 -1
  34. package/dist/components/HeaderBar/HeaderLogo/HeaderLogo.d.ts +2 -1
  35. package/dist/components/HeaderNavigationBar/HeaderNavigationBar.d.ts +2 -1
  36. package/dist/components/MonthPicker/MonthPicker.d.ts +1939 -0
  37. package/dist/components/MonthPicker/MonthPickerText/MonthPickerInput.d.ts +1899 -0
  38. package/dist/components/MonthPicker/MonthPickerText/useTextField.d.ts +21 -0
  39. package/dist/components/MonthPicker/MonthPickerVisual/MonthPickerVisual.d.ts +21 -0
  40. package/dist/components/MonthPicker/MonthPickerVisual/MonthPickerVisualProps.d.ts +12 -0
  41. package/dist/components/MonthPicker/MonthPickerVisual/MonthSelector.d.ts +11 -0
  42. package/dist/components/MonthPicker/MonthPickerVisual/VisualPickerFooter.d.ts +6 -0
  43. package/dist/components/MonthPicker/MonthPickerVisual/VisualPickerHeader.d.ts +14 -0
  44. package/dist/components/MonthPicker/MonthPickerVisual/YearSelector.d.ts +14 -0
  45. package/dist/components/MonthPicker/MonthPickerVisual/useMonthGrid.d.ts +9 -0
  46. package/dist/components/MonthPicker/MonthPickerVisual/useYearGrid.d.ts +8 -0
  47. package/dist/components/MonthPicker/MonthPickerVisual/utils.d.ts +8 -0
  48. package/dist/components/MonthPicker/locales.d.ts +12 -0
  49. package/dist/components/MonthPicker/useMonthPickerValidation.d.ts +25 -0
  50. package/dist/components/NirField/NirField.d.ts +3 -1
  51. package/dist/components/NotificationBar/Notification/Notification.d.ts +3 -0
  52. package/dist/components/PasswordField/PasswordField.d.ts +1 -1
  53. package/dist/components/PeriodField/PeriodField.d.ts +29 -21
  54. package/dist/components/PhoneField/PhoneField.d.ts +2 -1
  55. package/dist/components/SyBtnMenu/SyBtnMenu.d.ts +1 -1
  56. package/dist/components/SyHeading/SyHeading.a11y.test.d.ts +1 -0
  57. package/dist/components/SyHeading/SyHeading.d.ts +4 -2
  58. package/dist/components/SyHeading/SyHeading.test.d.ts +1 -0
  59. package/dist/components/SyTextArea/SyTextArea.d.ts +1 -1
  60. package/dist/components/Tables/SyServerTable/SyServerTable.d.ts +4 -4
  61. package/dist/components/Tables/SyTable/SyTable.d.ts +4 -4
  62. package/dist/components/Tables/common/SyTablePagination.d.ts +6 -6
  63. package/dist/components/index.d.ts +1 -0
  64. package/dist/design-system-v3.js +102 -99
  65. package/dist/design-system-v3.umd.cjs +126 -126
  66. package/dist/designTokens/tokens/cnam/cnamContextual.d.ts +5 -0
  67. package/dist/{main-CEl4J8_T.js → main-aLKwdMi1.js} +11167 -10522
  68. package/dist/main.d.ts +1 -0
  69. package/dist/style.css +1 -1
  70. package/package.json +10 -4
  71. package/src/assets/apTokens.scss +2 -2
  72. package/src/assets/overrides/_btns.scss +8 -0
  73. package/src/assets/overrides/_forms.scss +9 -0
  74. package/src/assets/overrides/_icons.scss +38 -9
  75. package/src/assets/overrides/_tables.scss +19 -0
  76. package/src/components/Accordion/Accordion.mdx +23 -9
  77. package/src/components/Accordion/Accordion.stories.ts +153 -3
  78. package/src/components/Accordion/Accordion.vue +7 -6
  79. package/src/components/Accordion/composables/__tests__/useAccordionState.spec.ts +40 -12
  80. package/src/components/Accordion/composables/useAccordionState.ts +3 -4
  81. package/src/components/Accordion/tests/accordion.spec.ts +131 -19
  82. package/src/components/Amelipro/AmeliproPagination/AmeliproPagination.mdx +3 -1
  83. package/src/components/Amelipro/AmeliproPagination/AmeliproPagination.stories.ts +8 -0
  84. package/src/components/BackBtn/accessibilite/Accessibility.mdx +62 -10
  85. package/src/components/BackToTopBtn/BackToTopBtn.stories.ts +9 -3
  86. package/src/components/BackToTopBtn/accessibilite/Accessibility.mdx +86 -6
  87. package/src/components/Captcha/tests/Captcha.spec.ts +0 -29
  88. package/src/components/Captcha/tests/__snapshots__/Captcha.spec.ts.snap +2 -110
  89. package/src/components/Customs/Selects/SelectBtnField/accessibilite/Accessibility.mdx +133 -10
  90. package/src/components/Customs/Selects/SyAutocomplete/SyAutocomplete.stories.ts +379 -93
  91. package/src/components/Customs/Selects/SyAutocomplete/SyAutocomplete.vue +144 -83
  92. package/src/components/Customs/Selects/SyAutocomplete/accessibilite/Accessibilite.stories.ts +40 -1
  93. package/src/components/Customs/Selects/SyAutocomplete/accessibilite/Accessibility.mdx +7 -1
  94. package/src/components/Customs/Selects/SyAutocomplete/locales.ts +5 -0
  95. package/src/components/Customs/Selects/SyAutocomplete/tests/SyAutocomplete.a11y.spec.ts +96 -0
  96. package/src/components/Customs/Selects/SyAutocomplete/tests/SyAutocomplete.spec.ts +234 -9
  97. package/src/components/Customs/Selects/SyAutocomplete/utils/ariaManager.ts +13 -3
  98. package/src/components/Customs/Selects/SyAutocomplete/utils/useSelectionLogic.ts +9 -10
  99. package/src/components/Customs/Selects/SySelect/SySelect.vue +46 -3
  100. package/src/components/Customs/Selects/SySelect/locales.ts +1 -0
  101. package/src/components/Customs/SyIcon/SyIcon.vue +1 -1
  102. package/src/components/Customs/SyIcon/tests/SyIcon.a11y.spec.ts +20 -0
  103. package/src/components/Customs/SyIconButton/SyIconButton.mdx +46 -0
  104. package/src/components/Customs/SyIconButton/SyIconButton.stories.ts +184 -0
  105. package/src/components/Customs/SyIconButton/SyIconButton.vue +38 -0
  106. package/src/components/Customs/SyIconButton/accessibilite/Accessibility.mdx +64 -0
  107. package/src/components/Customs/SyIconButton/tests/SyIconButton.a11y.spec.ts +87 -0
  108. package/src/components/Customs/SyIconButton/tests/SyIconButton.spec.ts +152 -0
  109. package/src/components/Customs/SyIconButton/tests/__snapshots__/SyIconButton.spec.ts.snap +61 -0
  110. package/src/components/Customs/SyPagination/SyPagination.vue +5 -5
  111. package/src/components/Customs/SyTextField/SyTextField.vue +20 -2
  112. package/src/components/Customs/SyTextField/accessibilite/Accessibility.mdx +67 -9
  113. package/src/components/Customs/SyTextField/tests/SyTextField.a11y.spec.ts +15 -0
  114. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +36 -0
  115. package/src/components/DataList/accessibilite/Accessibility.mdx +79 -11
  116. package/src/components/DataListGroup/accessibilite/Accessibility.mdx +80 -11
  117. package/src/components/DownloadBtn/tests/DownloadBtn.a11y.spec.ts +25 -0
  118. package/src/components/ErrorPage/ErrorPage.stories.ts +113 -19
  119. package/src/components/ErrorPage/ErrorPage.vue +17 -2
  120. package/src/components/ErrorPage/tests/ErrorPage.a11y.spec.ts +17 -0
  121. package/src/components/ErrorPage/tests/ErrorPage.spec.ts +21 -1
  122. package/src/components/ErrorPage/tests/__snapshots__/ErrorPage.spec.ts.snap +0 -1
  123. package/src/components/ExternalLinks/tests/ExternalLinks.a11y.spec.ts +23 -0
  124. package/src/components/FileList/FileList.stories.ts +51 -1
  125. package/src/components/FileList/UploadItem/UploadItem.vue +13 -6
  126. package/src/components/FileList/UploadItem/locales.ts +3 -12
  127. package/src/components/FileList/accessibilite/Accessibility.mdx +3 -0
  128. package/src/components/FileUpload/FileUpload.vue +2 -1
  129. package/src/components/FileUpload/FileUploadContent.vue +2 -1
  130. package/src/components/FileUpload/tests/FileUpload.spec.ts +47 -0
  131. package/src/components/FileUpload/validateFiles.ts +5 -2
  132. package/src/components/FranceConnectBtn/accessibilite/Accessibility.mdx +62 -9
  133. package/src/components/HeaderBar/HeaderBar.vue +2 -1
  134. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +2 -1
  135. package/src/components/HeaderNavigationBar/HeaderNavigationBar.vue +2 -1
  136. package/src/components/LunarCalendar/accessibilite/Accessibility.mdx +74 -8
  137. package/src/components/LunarCalendar/tests/LunarCalendar.a11y.spec.ts +163 -0
  138. package/src/components/MaintenancePage/MaintenancePage.vue +1 -1
  139. package/src/components/MaintenancePage/tests/MaintenancePage.spec.ts +4 -5
  140. package/src/components/MaintenancePage/tests/__snapshots__/MaintenancePage.spec.ts.snap +0 -1
  141. package/src/components/MonthPicker/MonthPicker.mdx +35 -0
  142. package/src/components/MonthPicker/MonthPicker.stories.ts +527 -0
  143. package/src/components/MonthPicker/MonthPicker.vue +79 -0
  144. package/src/components/MonthPicker/MonthPickerText/MonthPickerInput.vue +89 -0
  145. package/src/components/MonthPicker/MonthPickerText/useTextField.ts +27 -0
  146. package/src/components/MonthPicker/MonthPickerVisual/MonthPickerVisual.vue +154 -0
  147. package/src/components/MonthPicker/MonthPickerVisual/MonthPickerVisualProps.ts +13 -0
  148. package/src/components/MonthPicker/MonthPickerVisual/MonthSelector.vue +137 -0
  149. package/src/components/MonthPicker/MonthPickerVisual/VisualPickerFooter.vue +60 -0
  150. package/src/components/MonthPicker/MonthPickerVisual/VisualPickerHeader.vue +149 -0
  151. package/src/components/MonthPicker/MonthPickerVisual/YearSelector.vue +143 -0
  152. package/src/components/MonthPicker/MonthPickerVisual/useMonthGrid.ts +45 -0
  153. package/src/components/MonthPicker/MonthPickerVisual/useYearGrid.ts +45 -0
  154. package/src/components/MonthPicker/MonthPickerVisual/utils.ts +17 -0
  155. package/src/components/MonthPicker/accessibilite/Accessibility.mdx +59 -0
  156. package/src/components/MonthPicker/locales.ts +12 -0
  157. package/src/components/MonthPicker/tests/MonthPicker.a11y.spec.ts +71 -0
  158. package/src/components/MonthPicker/tests/MonthPicker.spec.ts +1248 -0
  159. package/src/components/MonthPicker/tests/__snapshots__/MonthPicker.spec.ts.snap +2545 -0
  160. package/src/components/MonthPicker/useMonthPickerValidation.ts +30 -0
  161. package/src/components/NirField/NirField.mdx +1 -2
  162. package/src/components/NirField/NirField.stories.ts +66 -6
  163. package/src/components/NotFoundPage/tests/NotFoundPage.spec.ts +2 -3
  164. package/src/components/NotFoundPage/tests/__snapshots__/NotFoundPage.spec.ts.snap +22 -14
  165. package/src/components/NotificationBar/Notification/Notification.vue +3 -1
  166. package/src/components/NotificationBar/NotificationBar.stories.ts +154 -0
  167. package/src/components/NotificationBar/tests/NotificationBar.a11y.spec.ts +26 -0
  168. package/src/components/NotificationBar/tests/NotificationBar.spec.ts +60 -0
  169. package/src/components/RangeField/accessibilite/Accessibility.mdx +79 -11
  170. package/src/components/SkipLink/tests/SkipLink.a11y.spec.ts +23 -0
  171. package/src/components/StatusPage/StatusPage.stories.ts +118 -0
  172. package/src/components/StatusPage/StatusPage.vue +5 -3
  173. package/src/components/StatusPage/tests/StatusPage.a11y.spec.ts +22 -0
  174. package/src/components/StatusPage/tests/StatusPage.spec.ts +22 -0
  175. package/src/components/StatusPage/tests/__snapshots__/StatusPage.spec.ts.snap +22 -14
  176. package/src/components/SubHeader/tests/SubHeader.a11y.spec.ts +20 -0
  177. package/src/components/SyAlert/SyAlert.vue +1 -0
  178. package/src/components/SyAlert/accessibilite/Accessibility.mdx +79 -9
  179. package/src/components/SyAlert/tests/SyAlert.a11y.spec.ts +23 -0
  180. package/src/components/SyHeading/SyHeading.a11y.test.ts +149 -0
  181. package/src/components/SyHeading/SyHeading.test.ts +115 -0
  182. package/src/components/SyHeading/SyHeading.vue +5 -3
  183. package/src/components/SyTextArea/accessibilite/Accessibility.mdx +80 -8
  184. package/src/components/SyTextArea/tests/SyTextArea.a11y.spec.ts +151 -0
  185. package/src/components/ToolbarContainer/tests/ToolbarContainer.a11y.spec.ts +126 -0
  186. package/src/components/UploadWorkflow/tests/__snapshots__/UploadWorkflow.spec.ts.snap +2 -2
  187. package/src/components/index.ts +1 -0
  188. package/src/composables/useFormFieldErrorHandling.ts +11 -2
  189. package/src/designTokens/tokens/cnam/cnamContextual.ts +6 -1
  190. package/src/main.ts +2 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { mount, flushPromises } from '@vue/test-utils'
3
- import { VMenu, VChip } from 'vuetify/components'
3
+ import { VMenu } from 'vuetify/components'
4
4
 
5
5
  import SyAutocomplete from '../SyAutocomplete.vue'
6
6
  import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue'
@@ -128,13 +128,13 @@ describe('SyAutocomplete', () => {
128
128
 
129
129
  // Some environments can emit a follow-up input event with the previous DOM value.
130
130
  // Ensure it doesn't re-populate the query after selection.
131
- textField.vm.$emit('update:modelValue', 'Option 1, Opt')
131
+ textField.vm.$emit('update:modelValue', 'Opt')
132
132
  await flushPromises()
133
133
  await wrapper.vm.$nextTick()
134
134
 
135
- // Query should be cleared, leaving only selected label prefix in input
135
+ // Search and input should be cleared; selected items render as inline labels, not in input value
136
136
  expect(wrapper.vm.search).toBe('')
137
- expect(getInputEl()!.value).toBe('Option 1, ')
137
+ expect(getInputEl()!.value).toBe('')
138
138
  })
139
139
 
140
140
  it('displays chips in multiple mode', async () => {
@@ -150,12 +150,13 @@ describe('SyAutocomplete', () => {
150
150
  textKey: 'text',
151
151
  valueKey: 'value',
152
152
  },
153
+ attachTo: document.body,
153
154
  })
154
155
 
155
156
  await wrapper.vm.$nextTick()
156
- const chips = wrapper.findAllComponents(VChip)
157
+ const chips = wrapper.findAll('.v-chip')
157
158
  expect(chips.length).toBe(1)
158
- expect(chips[0]!.text()).toBe('Option 1')
159
+ expect(chips[0]!.text()).toContain('Option 1')
159
160
  })
160
161
 
161
162
  it('removes chip when close button is clicked', async () => {
@@ -171,14 +172,15 @@ describe('SyAutocomplete', () => {
171
172
  textKey: 'text',
172
173
  valueKey: 'value',
173
174
  },
175
+ attachTo: document.body,
174
176
  })
175
177
 
176
178
  await wrapper.vm.$nextTick()
177
- const chip = wrapper.findComponent(VChip)
178
- await chip.vm.$emit('click:close')
179
+ const closeBtn = wrapper.find('.v-chip__close')
180
+ await closeBtn.trigger('click')
179
181
  await wrapper.vm.$nextTick()
180
182
 
181
- expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[]])
183
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([null])
182
184
  })
183
185
 
184
186
  it('shows clear button when clearable and has selection', async () => {
@@ -298,6 +300,181 @@ describe('SyAutocomplete', () => {
298
300
  expect(isMenuOverlayActive()).toBe(false)
299
301
  })
300
302
 
303
+ describe('selectionText', () => {
304
+ const selectionText = (selected: unknown[]) => `${selected.length} élément${selected.length > 1 ? 's' : ''} sélectionné${selected.length > 1 ? 's' : ''}`
305
+
306
+ it('displays custom text in prepend-inner when items are selected', async () => {
307
+ wrapper.unmount()
308
+ wrapper = mount(SyAutocomplete, {
309
+ props: {
310
+ modelValue: ['1', '2'],
311
+ items,
312
+ multiple: true,
313
+ selectionText,
314
+ label: 'Test selectionText',
315
+ textKey: 'text',
316
+ valueKey: 'value',
317
+ menuId,
318
+ },
319
+ attachTo: document.body,
320
+ })
321
+ await wrapper.vm.$nextTick()
322
+
323
+ const selectionTextEl = wrapper.find('.sy-autocomplete__selection-text')
324
+ expect(selectionTextEl.exists()).toBe(true)
325
+ expect(selectionTextEl.text()).toBe('2 éléments sélectionnés')
326
+ expect(getInputEl()?.value).toBe('')
327
+ })
328
+
329
+ it('does not display selection text element when no items are selected', async () => {
330
+ wrapper.unmount()
331
+ wrapper = mount(SyAutocomplete, {
332
+ props: {
333
+ modelValue: [],
334
+ items,
335
+ multiple: true,
336
+ selectionText,
337
+ label: 'Test selectionText vide',
338
+ textKey: 'text',
339
+ valueKey: 'value',
340
+ menuId,
341
+ },
342
+ attachTo: document.body,
343
+ })
344
+ await wrapper.vm.$nextTick()
345
+
346
+ expect(wrapper.find('.sy-autocomplete__selection-text').exists()).toBe(false)
347
+ })
348
+
349
+ it('keeps custom text visible when menu is open', async () => {
350
+ wrapper.unmount()
351
+ wrapper = mount(SyAutocomplete, {
352
+ props: {
353
+ modelValue: ['1', '2'],
354
+ items,
355
+ multiple: true,
356
+ selectionText,
357
+ label: 'Test selectionText ouvert',
358
+ textKey: 'text',
359
+ valueKey: 'value',
360
+ menuId,
361
+ },
362
+ attachTo: document.body,
363
+ })
364
+ await wrapper.vm.$nextTick()
365
+
366
+ const input = wrapper.find('input')
367
+ await input.trigger('click')
368
+ await flushPromises()
369
+ await wrapper.vm.$nextTick()
370
+
371
+ // Custom text still visible in prepend-inner, input empty for searching
372
+ expect(wrapper.find('.sy-autocomplete__selection-text').text()).toBe('2 éléments sélectionnés')
373
+ expect(getInputEl()?.value).toBe('')
374
+ })
375
+
376
+ it('updates custom text when selection changes', async () => {
377
+ wrapper.unmount()
378
+ wrapper = mount(SyAutocomplete, {
379
+ props: {
380
+ modelValue: ['1'],
381
+ items,
382
+ multiple: true,
383
+ selectionText,
384
+ label: 'Test selectionText update',
385
+ textKey: 'text',
386
+ valueKey: 'value',
387
+ menuId,
388
+ },
389
+ attachTo: document.body,
390
+ })
391
+ await wrapper.vm.$nextTick()
392
+ expect(wrapper.find('.sy-autocomplete__selection-text').text()).toBe('1 élément sélectionné')
393
+
394
+ await wrapper.setProps({ modelValue: ['1', '2', '3'] })
395
+ await wrapper.vm.$nextTick()
396
+ expect(wrapper.find('.sy-autocomplete__selection-text').text()).toBe('3 éléments sélectionnés')
397
+ })
398
+ })
399
+
400
+ describe('loading', () => {
401
+ it('shows progress bar when loading is true', async () => {
402
+ wrapper.unmount()
403
+ wrapper = mount(SyAutocomplete, {
404
+ props: {
405
+ modelValue: null,
406
+ items,
407
+ label: 'Test Loading',
408
+ textKey: 'text',
409
+ valueKey: 'value',
410
+ loading: true,
411
+ menuId,
412
+ },
413
+ attachTo: document.body,
414
+ })
415
+
416
+ await wrapper.vm.$nextTick()
417
+ const progressBar = wrapper.find('.v-progress-linear')
418
+ expect(progressBar.exists()).toBe(true)
419
+ })
420
+
421
+ it('does not show progress bar when loading is false', async () => {
422
+ await wrapper.vm.$nextTick()
423
+ const progressBar = wrapper.find('.v-progress-linear')
424
+ expect(progressBar.exists()).toBe(false)
425
+ })
426
+
427
+ it('hides no-data message while loading', async () => {
428
+ wrapper.unmount()
429
+ wrapper = mount(SyAutocomplete, {
430
+ props: {
431
+ modelValue: null,
432
+ items: [],
433
+ label: 'Test Loading No Data',
434
+ textKey: 'text',
435
+ valueKey: 'value',
436
+ loading: true,
437
+ menuId,
438
+ },
439
+ attachTo: document.body,
440
+ })
441
+
442
+ const input = wrapper.find('input')
443
+ await input.trigger('click')
444
+ await flushPromises()
445
+ await wrapper.vm.$nextTick()
446
+
447
+ // No-data item should not appear while loading
448
+ const noDataItem = document.body.querySelector(`#${menuId} .v-list-item`)
449
+ expect(noDataItem).toBeNull()
450
+ })
451
+
452
+ it('shows no-data message once loading is done and items is empty', async () => {
453
+ wrapper.unmount()
454
+ wrapper = mount(SyAutocomplete, {
455
+ props: {
456
+ modelValue: null,
457
+ items: [],
458
+ label: 'Test Loading Done',
459
+ textKey: 'text',
460
+ valueKey: 'value',
461
+ loading: false,
462
+ menuId,
463
+ },
464
+ attachTo: document.body,
465
+ })
466
+
467
+ const input = wrapper.find('input')
468
+ await input.trigger('click')
469
+ await flushPromises()
470
+ await wrapper.vm.$nextTick()
471
+
472
+ const listItems = document.body.querySelectorAll(`#${menuId} .v-list-item`)
473
+ const texts = Array.from(listItems).map(el => el.textContent?.trim())
474
+ expect(texts).toContain('Aucune option')
475
+ })
476
+ })
477
+
301
478
  it('selects and deselects items in multiple mode (mouse + keyboard)', async () => {
302
479
  wrapper.unmount()
303
480
  wrapper = mount(SyAutocomplete, {
@@ -342,4 +519,52 @@ describe('SyAutocomplete', () => {
342
519
  option0 = getOption(0)
343
520
  expect(option0?.getAttribute('aria-selected')).toBe('false')
344
521
  })
522
+
523
+ describe('hideDetails', () => {
524
+ it('hides the details zone when hideDetails is true', async () => {
525
+ wrapper.unmount()
526
+ wrapper = mount(SyAutocomplete, {
527
+ props: {
528
+ modelValue: null,
529
+ items,
530
+ label: 'Test hideDetails',
531
+ textKey: 'text',
532
+ valueKey: 'value',
533
+ hideDetails: true,
534
+ menuId,
535
+ },
536
+ attachTo: document.body,
537
+ })
538
+
539
+ await wrapper.vm.$nextTick()
540
+ expect(wrapper.find('.v-input__details').exists()).toBe(false)
541
+ })
542
+
543
+ it('shows the details zone when hideDetails is false', async () => {
544
+ await wrapper.vm.$nextTick()
545
+ expect(wrapper.find('.v-input__details').exists()).toBe(true)
546
+ })
547
+
548
+ it('does not show error messages when hideDetails is true even with validation errors', async () => {
549
+ wrapper.unmount()
550
+ wrapper = mount(SyAutocomplete, {
551
+ props: {
552
+ modelValue: null,
553
+ items,
554
+ label: 'Test hideDetails + erreur',
555
+ textKey: 'text',
556
+ valueKey: 'value',
557
+ hideDetails: true,
558
+ hasError: true,
559
+ errorMessages: ['Erreur de validation'],
560
+ menuId,
561
+ },
562
+ attachTo: document.body,
563
+ })
564
+
565
+ await wrapper.vm.$nextTick()
566
+ expect(wrapper.find('.v-input__details').exists()).toBe(false)
567
+ expect(wrapper.find('.v-messages').exists()).toBe(false)
568
+ })
569
+ })
345
570
  })
@@ -145,9 +145,19 @@ export const ariaManager = {
145
145
  popupRendered,
146
146
  },
147
147
  )
148
- const labelToApply = inputLabel || inputEl.getAttribute('aria-label') || inputEl.getAttribute('placeholder') || ''
149
- if (labelToApply) {
150
- inputEl.setAttribute('aria-label', labelToApply)
148
+ const labelledById = inputEl.getAttribute('aria-labelledby')
149
+ const labelElement = labelledById ? document.getElementById(labelledById) : null
150
+ const labelElementHasText = Boolean(labelElement?.textContent?.trim())
151
+ if (labelElementHasText) {
152
+ // aria-labelledby references a visible label — aria-label is redundant and causes validator errors
153
+ inputEl.removeAttribute('aria-label')
154
+ }
155
+ else {
156
+ // No visible label element (e.g. chips mode with label="") — use aria-label as fallback
157
+ const labelToApply = inputLabel || inputEl.getAttribute('aria-label') || inputEl.getAttribute('placeholder') || ''
158
+ if (labelToApply) {
159
+ inputEl.setAttribute('aria-label', labelToApply)
160
+ }
151
161
  }
152
162
  inputEl.setAttribute('aria-haspopup', 'listbox')
153
163
  if (!popupRendered) {
@@ -19,15 +19,9 @@ export function useSelectionLogic(
19
19
  emit: EmitFunction,
20
20
  ) {
21
21
  const resetValue = () => {
22
- if (props.multiple) {
23
- selected.value = []
24
- emit('update:modelValue', [])
25
- }
26
- else {
27
- selected.value = null
28
- search.value = ''
29
- emit('update:modelValue', null)
30
- }
22
+ selected.value = null
23
+ search.value = ''
24
+ emit('update:modelValue', null)
31
25
  }
32
26
 
33
27
  const updateMultipleSelection = (valueKeyValue: string | number, valueToStore: ItemType | string | number) => {
@@ -44,7 +38,12 @@ export function useSelectionLogic(
44
38
  arr.push(valueToStore)
45
39
  }
46
40
  selected.value = arr as SelectArray
47
- emit('update:modelValue', arr as SelectArray)
41
+ if (arr.length === 0) {
42
+ emit('update:modelValue', null)
43
+ }
44
+ else {
45
+ emit('update:modelValue', arr as SelectArray)
46
+ }
48
47
  }
49
48
 
50
49
  const updateSingleSelection = (valueToStore: SelectValue, item: ItemType) => {
@@ -414,6 +414,11 @@
414
414
  return ''
415
415
  }
416
416
 
417
+ // If inline labels are shown, return empty string to hide input text
418
+ if (hasMultipleSelections.value) {
419
+ return ''
420
+ }
421
+
417
422
  // For multiple mode, show default option text when nothing is selected
418
423
  if (props.multiple) {
419
424
  if (!selectedItem.value || (Array.isArray(selectedItem.value) && selectedItem.value.length === 0)) {
@@ -457,6 +462,10 @@
457
462
  return props.chips && props.multiple && Array.isArray(selectedItem.value) && selectedItem.value.length > 0
458
463
  })
459
464
 
465
+ const hasMultipleSelections = computed(() => {
466
+ return !props.chips && props.multiple && Array.isArray(selectedItem.value) && (selectedItem.value as unknown[]).length > 0
467
+ })
468
+
460
469
  const hasSelectionToClear = computed(() => {
461
470
  return props.multiple
462
471
  ? (((selectedItem.value as unknown[] | null | undefined)?.length) ?? 0) > 0
@@ -904,7 +913,10 @@
904
913
  <template #activator="{ props: activatorProps }">
905
914
  <div
906
915
  class="sy-select"
907
- :class="{ 'sy-select--clearable': props.clearable }"
916
+ :class="{
917
+ 'sy-select--clearable': props.clearable,
918
+ 'sy-select--with-chips': hasChips,
919
+ }"
908
920
  >
909
921
  <VTextField
910
922
  :id="inputId"
@@ -920,7 +932,7 @@
920
932
  :rules="isRequired && !props.disableErrorHandling ? ['Le champ est requis.'] : []"
921
933
  :bg-color="props.bgColor"
922
934
  :density="props.density"
923
- :active="hasChips || isOpen"
935
+ :active="hasChips || hasMultipleSelections || isOpen"
924
936
  readonly
925
937
  :hide-details="props.hideMessages && !showHelpTextAsMessage"
926
938
  :hint="showHelpTextAsMessage ? props.helpText : ''"
@@ -955,12 +967,21 @@
955
967
  size="small"
956
968
  class="ma-1"
957
969
  closable
958
- :close-label="`Supprimer ${getChipText(item)}`"
970
+ :close-label="locales.removeChip(getChipText(item))"
959
971
  @click:close="removeChip(item)"
960
972
  >
961
973
  {{ getChipText(item) }}
962
974
  </VChip>
963
975
  </div>
976
+ <template v-else-if="hasMultipleSelections">
977
+ <span
978
+ v-for="item in (selectedItem as unknown[])"
979
+ :key="props.returnObject && item ? String((item as ItemType)[props.valueKey]) : String(item)"
980
+ class="sy-select__label"
981
+ >
982
+ {{ getChipText(item) }}
983
+ </span>
984
+ </template>
964
985
  <!-- Prepend -->
965
986
  <template
966
987
  v-if="$slots.prepend || props.prependIcon || props.prependTooltip"
@@ -1227,6 +1248,11 @@
1227
1248
  opacity: var(--v-medium-emphasis-opacity) !important;
1228
1249
  }
1229
1250
 
1251
+ /* Style spécifique pour les chips */
1252
+ :deep(.v-chip .v-chip__close .v-icon__svg) {
1253
+ fill: inherit !important;
1254
+ }
1255
+
1230
1256
  .sy-select__clear-button {
1231
1257
  position: absolute;
1232
1258
  background: transparent;
@@ -1249,6 +1275,18 @@
1249
1275
  margin: 2px;
1250
1276
  }
1251
1277
 
1278
+ .sy-select__label {
1279
+ align-self: center;
1280
+ white-space: nowrap;
1281
+ flex-shrink: 0;
1282
+ font-size: inherit;
1283
+
1284
+ &:not(:last-of-type)::after {
1285
+ content: ',';
1286
+ margin-right: 4px;
1287
+ }
1288
+ }
1289
+
1252
1290
  .sy-select :deep(.v-field__input) {
1253
1291
  opacity: 1;
1254
1292
  color: tokens.$grey-darken-20 !important;
@@ -1274,4 +1312,9 @@
1274
1312
  position: absolute;
1275
1313
  white-space: nowrap;
1276
1314
  }
1315
+
1316
+ .sy-select--with-chips :deep(.v-field__input input) {
1317
+ position: absolute;
1318
+ z-index: -1;
1319
+ }
1277
1320
  </style>
@@ -1,3 +1,4 @@
1
1
  export const locales = {
2
2
  clear: 'Effacer la sélection',
3
+ removeChip: (label: string) => `Supprimer ${label}`,
3
4
  }
@@ -70,7 +70,7 @@
70
70
  :size="props.size"
71
71
  :role="props.role"
72
72
  :aria-label="resolvedDecorative ? undefined : props.label"
73
- :aria-hidden="resolvedDecorative ? 'true' : undefined"
73
+ :aria-hidden="resolvedDecorative ? true : undefined"
74
74
  >
75
75
  {{ icon }}
76
76
  </v-icon>
@@ -0,0 +1,20 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, it } from 'vitest'
4
+ import { mount } from '@vue/test-utils'
5
+ import { axe } from 'vitest-axe'
6
+ import { assertNoA11yViolations } from '@tests/unit/accessibility/axeUtils'
7
+ import SyIcon from '../SyIcon.vue'
8
+
9
+ describe('SyIcon – accessibility (axe)', () => {
10
+ it('has no obvious axe violations', async () => {
11
+ const wrapper = mount(SyIcon, {
12
+ props: {
13
+ icon: 'home',
14
+ },
15
+ })
16
+
17
+ const results = await axe(wrapper.element as HTMLElement)
18
+ assertNoA11yViolations(results, 'SyIcon – default state')
19
+ })
20
+ })
@@ -0,0 +1,46 @@
1
+ import { Meta, Primary, Controls, Canvas } from '@storybook/blocks';
2
+ import * as SyIconButtonStories from './SyIconButton.stories';
3
+ import '@/stories/styles/shared.css';
4
+
5
+ <Meta of={SyIconButtonStories} />
6
+
7
+ <div className="header">
8
+ <h1>SyIconButton</h1>
9
+ <p>Le composant `SyIconButton` est un bouton icône qui combine `v-btn` de Vuetify et `SyIcon` en garantissant l'accessibilité via une prop `label` obligatoire portée par `aria-label`.</p>
10
+ </div>
11
+
12
+ <Canvas of={SyIconButtonStories.Default} />
13
+
14
+ # API
15
+
16
+ <Controls of={SyIconButtonStories.Default} />
17
+
18
+
19
+ # Accessibilité
20
+
21
+ Le composant `SyIconButton` gère automatiquement les attributs ARIA :
22
+
23
+ - **Label obligatoire** : la prop `label` est portée par le bouton via `aria-label` — elle constitue le nom accessible du bouton
24
+ - **Icône décorative** : `SyIcon` interne reçoit `:decorative="true"` car le bouton porte déjà le nom accessible via `aria-label` — l'icône est donc ignorée par les lecteurs d'écran
25
+
26
+ > **Important :** Le `label` doit décrire l'**action** du bouton et non l'icône elle-même (ex. _"Fermer"_ plutôt que _"Croix"_). Ne jamais fournir un `label` vide.
27
+
28
+ Pour plus d'informations sur l'accessibilité, consultez la [page dédiée à l'accessibilité](?path=/docs/customs-syiconbutton--accessibility).
29
+
30
+ ## Exemples
31
+
32
+ ### Bouton icône par défaut
33
+
34
+ <Canvas of={SyIconButtonStories.Default} />
35
+
36
+ ### Bouton icône désactivé
37
+
38
+ <Canvas of={SyIconButtonStories.Disabled} />
39
+
40
+ ### Bouton icône avec couleur
41
+
42
+ <Canvas of={SyIconButtonStories.WithColor} />
43
+
44
+ ### Bouton icône avec taille personnalisée
45
+
46
+ <Canvas of={SyIconButtonStories.WithSize} />