@cnamts/synapse 0.0.11-alpha → 0.0.12-alpha

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 (108) hide show
  1. package/dist/design-system-v3.js +3878 -3189
  2. package/dist/design-system-v3.umd.cjs +1 -1
  3. package/dist/src/components/Amelipro/types/languages.d.ts +6 -0
  4. package/dist/src/components/Amelipro/types/types.d.ts +65 -0
  5. package/dist/src/components/CookieBanner/CookieBanner.d.ts +1 -1
  6. package/dist/src/components/Customs/SyInputSelect/SyInputSelect.d.ts +2 -0
  7. package/dist/src/components/Customs/SyTextField/SyTextField.d.ts +29 -23
  8. package/dist/src/components/Customs/SyTextField/types.d.ts +1 -0
  9. package/dist/src/components/DatePicker/DatePicker.d.ts +70 -59
  10. package/dist/src/components/DatePicker/DateTextInput.d.ts +67 -56
  11. package/dist/src/components/ErrorPage/ErrorPage.d.ts +1 -1
  12. package/dist/src/components/FileList/FileList.d.ts +1 -0
  13. package/dist/src/components/FileList/UploadItem/UploadItem.d.ts +1 -1
  14. package/dist/src/components/FilterSideBar/FilterSideBar.d.ts +31 -0
  15. package/dist/src/components/FilterSideBar/locales.d.ts +7 -0
  16. package/dist/src/components/FilterSideBar/tests/FilterSideBar.spec.d.ts +1 -0
  17. package/dist/src/components/LangBtn/LangBtn.d.ts +2 -2
  18. package/dist/src/components/NirField/NirField.d.ts +940 -0
  19. package/dist/src/components/NotificationBar/NotificationBar.d.ts +1 -1
  20. package/dist/src/components/PasswordField/PasswordField.d.ts +40 -8
  21. package/dist/src/components/PeriodField/PeriodField.d.ts +142 -120
  22. package/dist/src/components/PhoneField/PhoneField.d.ts +11 -2
  23. package/dist/src/components/RatingPicker/EmotionPicker/EmotionPicker.d.ts +1 -1
  24. package/dist/src/components/RatingPicker/NumberPicker/NumberPicker.d.ts +1 -1
  25. package/dist/src/components/RatingPicker/StarsPicker/StarsPicker.d.ts +1 -1
  26. package/dist/src/components/UploadWorkflow/config.d.ts +29 -0
  27. package/dist/src/components/UploadWorkflow/locales.d.ts +7 -0
  28. package/dist/src/components/UploadWorkflow/tests/UploadWorkflow.spec.d.ts +1 -0
  29. package/dist/src/components/UploadWorkflow/types.d.ts +19 -0
  30. package/dist/src/components/UploadWorkflow/useFileList.d.ts +10 -0
  31. package/dist/src/components/UploadWorkflow/useFileUploadJourney.d.ts +9 -0
  32. package/dist/src/components/index.d.ts +2 -0
  33. package/dist/src/composables/rules/useFieldValidation.d.ts +1 -0
  34. package/dist/src/composables/validation/tests/useValidation.spec.d.ts +1 -0
  35. package/dist/src/composables/validation/useValidation.d.ts +39 -0
  36. package/dist/src/designTokens/index.d.ts +3 -1
  37. package/dist/src/vuetifyConfig.d.ts +81 -0
  38. package/dist/style.css +1 -1
  39. package/package.json +1 -1
  40. package/src/assets/_elevations.scss +89 -0
  41. package/src/assets/_fonts.scss +6 -0
  42. package/src/assets/_radius.scss +86 -0
  43. package/src/assets/_spacers.scss +149 -0
  44. package/src/assets/settings.scss +7 -3
  45. package/src/assets/tokens.scss +32 -29
  46. package/src/components/Amelipro/types/languages.d.ts +6 -0
  47. package/src/components/Amelipro/types/types.d.ts +65 -0
  48. package/src/components/Customs/SyInputSelect/SyInputSelect.stories.ts +65 -0
  49. package/src/components/Customs/SyInputSelect/SyInputSelect.vue +13 -3
  50. package/src/components/Customs/SySelect/SySelect.stories.ts +88 -5
  51. package/src/components/Customs/SySelect/SySelect.vue +36 -10
  52. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +135 -2
  53. package/src/components/Customs/SyTextField/SyTextField.stories.ts +576 -85
  54. package/src/components/Customs/SyTextField/SyTextField.vue +132 -104
  55. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +190 -38
  56. package/src/components/Customs/SyTextField/types.d.ts +1 -0
  57. package/src/components/DatePicker/DatePicker.vue +405 -137
  58. package/src/components/DatePicker/DateTextInput.vue +15 -0
  59. package/src/components/DatePicker/tests/DatePicker.spec.ts +8 -15
  60. package/src/components/FileList/FileList.vue +2 -1
  61. package/src/components/FileList/UploadItem/UploadItem.vue +10 -0
  62. package/src/components/FileUpload/FileUpload.stories.ts +84 -0
  63. package/src/components/FileUpload/FileUpload.vue +1 -0
  64. package/src/components/FileUpload/tests/FileUpload.spec.ts +4 -4
  65. package/src/components/FilterInline/FilterInline.mdx +180 -34
  66. package/src/components/FilterInline/FilterInline.stories.ts +363 -6
  67. package/src/components/FilterSideBar/FilterSideBar.mdx +237 -0
  68. package/src/components/FilterSideBar/FilterSideBar.stories.ts +798 -0
  69. package/src/components/FilterSideBar/FilterSideBar.vue +193 -0
  70. package/src/components/FilterSideBar/locales.ts +8 -0
  71. package/src/components/FilterSideBar/tests/FilterSideBar.spec.ts +305 -0
  72. package/src/components/FilterSideBar/tests/__snapshots__/FilterSideBar.spec.ts.snap +39 -0
  73. package/src/components/HeaderBar/Usages.mdx +1 -1
  74. package/src/components/NirField/NirField.stories.ts +573 -29
  75. package/src/components/NirField/NirField.vue +397 -359
  76. package/src/components/NirField/tests/NirField.spec.ts +88 -52
  77. package/src/components/NirField/tests//342/200/257dataset/342/200/257.md +12 -0
  78. package/src/components/NotificationBar/Accessibilite.stories.ts +4 -0
  79. package/src/components/NotificationBar/NotificationBar.stories.ts +18 -13
  80. package/src/components/PasswordField/PasswordField.mdx +129 -47
  81. package/src/components/PasswordField/PasswordField.stories.ts +924 -120
  82. package/src/components/PasswordField/PasswordField.vue +209 -99
  83. package/src/components/PasswordField/tests/PasswordField.spec.ts +138 -9
  84. package/src/components/PeriodField/PeriodField.vue +55 -54
  85. package/src/components/PhoneField/PhoneField.stories.ts +69 -0
  86. package/src/components/PhoneField/PhoneField.vue +3 -0
  87. package/src/components/PhoneField/indicatifs.ts +1 -1
  88. package/src/components/UploadWorkflow/UploadWorkflow.mdx +75 -0
  89. package/src/components/UploadWorkflow/UploadWorkflow.stories.ts +943 -0
  90. package/src/components/UploadWorkflow/UploadWorkflow.vue +230 -0
  91. package/src/components/UploadWorkflow/config.ts +29 -0
  92. package/src/components/UploadWorkflow/locales.ts +8 -0
  93. package/src/components/UploadWorkflow/tests/UploadWorkflow.spec.ts +257 -0
  94. package/src/components/UploadWorkflow/tests/__snapshots__/UploadWorkflow.spec.ts.snap +54 -0
  95. package/src/components/UploadWorkflow/types.ts +21 -0
  96. package/src/components/UploadWorkflow/useFileList.ts +84 -0
  97. package/src/components/UploadWorkflow/useFileUploadJourney.ts +18 -0
  98. package/src/components/index.ts +2 -0
  99. package/src/composables/rules/useFieldValidation.ts +5 -2
  100. package/src/composables/validation/tests/useValidation.spec.ts +154 -0
  101. package/src/composables/validation/useValidation.ts +165 -0
  102. package/src/designTokens/index.ts +4 -0
  103. package/src/stories/Demarrer/Accueil.mdx +1 -1
  104. package/src/stories/DesignTokens/ThemePA.mdx +4 -30
  105. package/src/stories/GuideDuDev/UtiliserLesRules.mdx +319 -76
  106. package/src/stories/GuideDuDev/moduleDeNotification.mdx +1 -1
  107. package/src/vuetifyConfig.ts +61 -0
  108. package/src/composables/useFilterable/__snapshots__/useFilterable.spec.ts.snap +0 -3
@@ -69,22 +69,35 @@
69
69
 
70
70
  if (parts.length !== dateParts.length) return null
71
71
 
72
- let day = '', month = '', year = ''
72
+ let day = 0, month = 0, year = 0
73
73
 
74
74
  // Extraire les valeurs selon leur position dans le format
75
75
  parts.forEach((part, index) => {
76
- const value = dateParts[index]
77
- if (part.includes('DD')) day = value
78
- else if (part.includes('MM')) month = value
76
+ const value = parseInt(dateParts[index], 10)
77
+ if (isNaN(value)) return
78
+
79
+ if (part.includes('DD') || part.includes('D')) day = value
80
+ else if (part.includes('MM') || part.includes('M')) month = value - 1 // Les mois en JS sont 0-indexés
79
81
  else if (part.includes('YYYY')) year = value
80
- else if (part.includes('YY')) year = '20' + value // Assumons que nous sommes au 21ème siècle
82
+ else if (part.includes('YY')) {
83
+ // Gestion intelligente des années à 2 chiffres
84
+ // Si l'année est < 50, on considère qu'elle est dans le 21ème siècle
85
+ // Sinon, elle est dans le 20ème siècle
86
+ year = value < 50 ? 2000 + value : 1900 + value
87
+ }
81
88
  })
82
89
 
83
- // Vérifier que nous avons toutes les parties nécessaires
84
- if (!day || !month || !year) return null
90
+ // Vérifier que nous avons toutes les parties nécessaires et qu'elles sont dans des plages valides
91
+ if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1000 || year > 9999) return null
92
+
93
+ // Créer la date à midi (12:00) pour éviter les problèmes de décalage de fuseau horaire
94
+ // Cela garantit que la date reste la même lors de la conversion en UTC
95
+ const date = new Date(year, month, day, 12, 0, 0)
85
96
 
86
- const date = new Date(`${year}-${month}-${day}`)
87
- return isNaN(date.getTime()) ? null : date
97
+ // Vérifier que la date est valide (par exemple, 31 février n'existe pas)
98
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null
99
+
100
+ return date
88
101
  }
89
102
 
90
103
  function initializeSelectedDates(
@@ -92,24 +105,45 @@
92
105
  ): Date | Date[] | null {
93
106
  if (!modelValue) return null
94
107
 
108
+ // Déterminer le format à utiliser pour l'analyse
109
+ const parseFormat = props.dateFormatReturn || props.format
110
+
95
111
  if (Array.isArray(modelValue)) {
96
112
  if (modelValue.length >= 2) {
97
- const dates = [parseDate(modelValue[0]), parseDate(modelValue[1])]
98
- // Vérifie si l'une des dates est invalide
113
+ // Essayer d'abord avec le format de retour, puis avec le format d'affichage
114
+ let dates = [parseDate(modelValue[0], parseFormat), parseDate(modelValue[1], parseFormat)]
115
+
116
+ // Si l'une des dates est invalide avec le format de retour, essayer avec le format d'affichage
117
+ if (dates.some(date => date === null) && props.dateFormatReturn) {
118
+ dates = [parseDate(modelValue[0], props.format), parseDate(modelValue[1], props.format)]
119
+ }
120
+
121
+ // Vérifie si l'une des dates est toujours invalide
99
122
  if (dates.some(date => date === null)) {
100
123
  return []
101
124
  }
125
+
102
126
  // Vérifie si la première date est après la seconde
103
127
  if (dates[0] && dates[1] && dates[0] > dates[1]) {
104
128
  return []
105
129
  }
130
+
106
131
  // Filtrer les dates nulles et convertir en tableau de Date
107
132
  return dates.filter((date): date is Date => date !== null)
108
133
  }
134
+
109
135
  if (modelValue.length === 1) {
110
- const date = parseDate(modelValue[0])
136
+ // Essayer d'abord avec le format de retour, puis avec le format d'affichage
137
+ let date = parseDate(modelValue[0], parseFormat)
138
+
139
+ // Si la date est invalide avec le format de retour, essayer avec le format d'affichage
140
+ if (date === null && props.dateFormatReturn) {
141
+ date = parseDate(modelValue[0], props.format)
142
+ }
143
+
111
144
  return date === null ? [] : [date]
112
145
  }
146
+
113
147
  return []
114
148
  }
115
149
 
@@ -118,7 +152,14 @@
118
152
  return null
119
153
  }
120
154
 
121
- const date = parseDate(modelValue)
155
+ // Essayer d'abord avec le format de retour, puis avec le format d'affichage
156
+ let date = parseDate(modelValue, parseFormat)
157
+
158
+ // Si la date est invalide avec le format de retour, essayer avec le format d'affichage
159
+ if (date === null && props.dateFormatReturn) {
160
+ date = parseDate(modelValue, props.format)
161
+ }
162
+
122
163
  return date === null ? null : date
123
164
  }
124
165
 
@@ -135,8 +176,125 @@
135
176
 
136
177
  const textInputValue = ref<string>('')
137
178
 
179
+ // Variable pour éviter les mises à jour récursives
180
+ const isUpdatingFromInternal = ref(false)
181
+
182
+ // Déclaration des règles de validation
183
+ type Rule = { type: string, options: RuleOptions }
184
+ const customRules = ref<Rule[]>(props.customRules || [])
185
+ const customWarningRules = ref<Rule[]>(props.customWarningRules || [])
186
+
187
+ const { generateRules } = useFieldValidation()
188
+ const validationRules = generateRules(customRules.value)
189
+ const warningValidationRules = generateRules(customWarningRules.value)
190
+
191
+ // Déclaration de la fonction validateDates avant son utilisation
192
+ const validateDates = (forceValidation = false) => {
193
+ // Réinitialiser tous les messages
194
+ errorMessages.value = []
195
+ successMessages.value = []
196
+ warningMessages.value = []
197
+
198
+ if (props.noCalendar) {
199
+ // En mode no-calendar, on délègue la validation au DateTextInput
200
+ return
201
+ }
202
+
203
+ // Vérifier si le champ est requis et vide
204
+ // Si forceValidation est true, on ignore les conditions de validation interactive
205
+ if ((forceValidation || !isUpdatingFromInternal.value) && props.required && (!selectedDates.value || (Array.isArray(selectedDates.value) && selectedDates.value.length === 0))) {
206
+ errorMessages.value.push('La date est requise.')
207
+ return
208
+ }
209
+
210
+ if (!selectedDates.value) return
211
+
212
+ // Préparer les dates à valider
213
+ const datesToValidate = Array.isArray(selectedDates.value)
214
+ ? selectedDates.value
215
+ : [selectedDates.value]
216
+
217
+ // Collecter tous les messages
218
+ const allErrors: string[] = []
219
+ const allWarnings: string[] = []
220
+ const allSuccess: string[] = []
221
+
222
+ // Appliquer les règles de validation
223
+ datesToValidate.forEach((date) => {
224
+ // Appliquer d'abord les règles de validation standard
225
+ validationRules.forEach((rule) => {
226
+ const result = rule(date)
227
+ if (result?.error) allErrors.push(result.error)
228
+ else if (result?.success) allSuccess.push(result.success)
229
+ })
230
+
231
+ // Ensuite appliquer les règles d'avertissement
232
+ warningValidationRules.forEach((rule) => {
233
+ const result = rule(date)
234
+ if (result?.warning) allWarnings.push(result.warning)
235
+ else if (result?.success && !allErrors.length) allSuccess.push(result.success)
236
+ })
237
+ })
238
+
239
+ // Dédoublonner et assigner les messages
240
+ errorMessages.value = [...new Set(allErrors)]
241
+ warningMessages.value = [...new Set(allWarnings)]
242
+ successMessages.value = [...new Set(allSuccess)]
243
+ }
244
+
245
+ // Fonction centralisée pour mettre à jour le modèle
246
+ const updateModel = (value: DateValue) => {
247
+ // Éviter les mises à jour inutiles
248
+ if (JSON.stringify(value) === JSON.stringify(props.modelValue)) return
249
+
250
+ try {
251
+ isUpdatingFromInternal.value = true
252
+ emit('update:modelValue', value)
253
+ }
254
+ finally {
255
+ // S'assurer que le flag est toujours réinitialisé
256
+ setTimeout(() => {
257
+ isUpdatingFromInternal.value = false
258
+ }, 0)
259
+ }
260
+ }
261
+
262
+ // Watcher pour mettre à jour le modèle lorsque les dates sélectionnées changent
138
263
  watch(selectedDates, (newValue) => {
264
+ // Valider les dates
139
265
  validateDates()
266
+
267
+ // Mettre à jour le modèle si nécessaire
268
+ if (newValue !== null) {
269
+ updateModel(formattedDate.value)
270
+
271
+ // Mettre à jour textInputValue pour le DateTextInput
272
+ try {
273
+ isUpdatingFromInternal.value = true
274
+ if (Array.isArray(newValue)) {
275
+ // Pour les plages de dates, utiliser la première date
276
+ if (newValue.length > 0) {
277
+ textInputValue.value = formatDate(newValue[0], props.format)
278
+ }
279
+ }
280
+ else {
281
+ // Pour une date unique
282
+ textInputValue.value = formatDate(newValue, props.format)
283
+ }
284
+ }
285
+ finally {
286
+ setTimeout(() => {
287
+ isUpdatingFromInternal.value = false
288
+ }, 0)
289
+ }
290
+ }
291
+ else {
292
+ updateModel(null)
293
+ // Réinitialiser textInputValue
294
+ textInputValue.value = ''
295
+ }
296
+
297
+ // Gérer la visibilité du date picker
140
298
  if (props.displayRange) {
141
299
  if (Array.isArray(newValue) && newValue.length >= 2) {
142
300
  isDatePickerVisible.value = false
@@ -163,11 +321,27 @@
163
321
  // Formate une date unique au format spécifié
164
322
  const formatDate = (date: Date, format: string): string => {
165
323
  if (!date) return ''
324
+
325
+ // Formats de base
166
326
  const day = date.getDate().toString().padStart(2, '0')
167
327
  const month = (date.getMonth() + 1).toString().padStart(2, '0')
168
328
  const year = date.getFullYear().toString()
169
329
  const shortYear = year.slice(-2)
170
- return format.replace('YYYY', year).replace('YY', shortYear).replace('MM', month).replace('DD', day)
330
+
331
+ // Formats sans padding
332
+ const dayNoPad = date.getDate().toString()
333
+ const monthNoPad = (date.getMonth() + 1).toString()
334
+
335
+ // Remplacer les tokens dans l'ordre correct (du plus spécifique au moins spécifique)
336
+ let result = format
337
+ .replace(/YYYY/g, year)
338
+ .replace(/YY/g, shortYear)
339
+ .replace(/MM/g, month)
340
+ .replace(/M/g, monthNoPad)
341
+ .replace(/DD/g, day)
342
+ .replace(/D/g, dayNoPad)
343
+
344
+ return result
171
345
  }
172
346
 
173
347
  // Date(s) formatée(s) en chaîne de caractères pour la valeur de retour
@@ -208,6 +382,9 @@
208
382
  }, { immediate: true })
209
383
 
210
384
  watch(textInputValue, (newValue) => {
385
+ // Éviter les mises à jour récursives
386
+ if (isUpdatingFromInternal.value) return
387
+
211
388
  // Parse la date avec le format d'affichage
212
389
  const date = parseDate(newValue, props.format)
213
390
  if (date) {
@@ -215,12 +392,50 @@
215
392
  const formattedValue = props.dateFormatReturn
216
393
  ? formatDate(date, props.dateFormatReturn)
217
394
  : formatDate(date, props.format)
218
- emit('update:modelValue', formattedValue)
395
+ updateModel(formattedValue)
396
+
397
+ // Mettre à jour selectedDates sans déclencher de watchers supplémentaires
398
+ try {
399
+ isUpdatingFromInternal.value = true
400
+ selectedDates.value = date
401
+ // Mettre à jour l'affichage formaté
402
+ displayFormattedDate.value = formatDate(date, props.format)
403
+ }
404
+ finally {
405
+ setTimeout(() => {
406
+ isUpdatingFromInternal.value = false
407
+ }, 0)
408
+ }
409
+ }
410
+ else if (newValue) {
411
+ // Même si la date n'est pas valide, conserver la valeur saisie
412
+ // pour éviter que la date ne disparaisse
413
+ updateModel(newValue)
414
+ // Mettre à jour l'affichage formaté pour qu'il corresponde à ce qui est saisi
415
+ try {
416
+ isUpdatingFromInternal.value = true
417
+ displayFormattedDate.value = newValue
418
+ }
419
+ finally {
420
+ setTimeout(() => {
421
+ isUpdatingFromInternal.value = false
422
+ }, 0)
423
+ }
219
424
  }
220
425
  else {
221
- emit('update:modelValue', newValue || null)
426
+ updateModel(null)
427
+ // Réinitialiser l'affichage formaté
428
+ try {
429
+ isUpdatingFromInternal.value = true
430
+ displayFormattedDate.value = ''
431
+ selectedDates.value = null
432
+ }
433
+ finally {
434
+ setTimeout(() => {
435
+ isUpdatingFromInternal.value = false
436
+ }, 0)
437
+ }
222
438
  }
223
- updateSelectedDates(newValue)
224
439
  })
225
440
 
226
441
  // Date(s) formatée(s) en chaîne de caractères pour l'affichage
@@ -284,8 +499,6 @@
284
499
 
285
500
  // Si on clique dans le conteneur du DatePicker, on ne fait rien
286
501
  if (container) return
287
-
288
- isDatePickerVisible.value = false
289
502
  emit('closed')
290
503
  // Déclencher la validation à la fermeture
291
504
  validateDates()
@@ -299,19 +512,50 @@
299
512
  })).replace(/\b\w/g, l => l.toUpperCase())
300
513
  })
301
514
 
302
- onMounted(() => {
303
- document.addEventListener('click', handleClickOutside)
304
- if (props.modelValue) {
515
+ // Watcher pour le modelValue pour synchroniser les dates sélectionnées
516
+ watch(() => props.modelValue, (newValue) => {
517
+ // Éviter les mises à jour récursives
518
+ if (isUpdatingFromInternal.value) return
519
+
520
+ try {
521
+ isUpdatingFromInternal.value = true
522
+
523
+ if (!newValue || newValue === '') {
524
+ // Réinitialiser les valeurs
525
+ selectedDates.value = null
526
+ textInputValue.value = ''
527
+ displayFormattedDate.value = ''
528
+ }
529
+ else {
530
+ // Initialiser les dates sélectionnées
531
+ selectedDates.value = initializeSelectedDates(newValue)
532
+
533
+ // Mettre à jour l'affichage
534
+ if (selectedDates.value) {
535
+ displayFormattedDate.value = displayFormattedDateComputed.value || ''
536
+ }
537
+ }
538
+
539
+ // Valider les dates
305
540
  validateDates()
306
541
  }
307
- if (selectedDates.value !== null) {
308
- validateDates()
309
- // Force format application on mount
310
- emit('update:modelValue', formattedDate.value)
542
+ finally {
543
+ setTimeout(() => {
544
+ isUpdatingFromInternal.value = false
545
+ }, 0)
311
546
  }
547
+ }, { immediate: true })
548
+
549
+ onMounted(() => {
550
+ document.addEventListener('click', handleClickOutside)
551
+
552
+ // Initialiser l'affichage formaté
312
553
  if (displayFormattedDateComputed.value) {
313
554
  displayFormattedDate.value = displayFormattedDateComputed.value
314
555
  }
556
+
557
+ // Valider les dates au montage
558
+ validateDates()
315
559
  })
316
560
 
317
561
  onBeforeUnmount(() => {
@@ -324,7 +568,8 @@
324
568
  if (props.noCalendar) {
325
569
  return dateTextInputRef.value?.validateOnSubmit()
326
570
  }
327
- validateDates()
571
+ // Forcer la validation pour ignorer les conditions de validation interactive
572
+ validateDates(true)
328
573
  return errorMessages.value.length === 0
329
574
  }
330
575
 
@@ -337,25 +582,69 @@
337
582
  initializeSelectedDates,
338
583
  })
339
584
 
340
- // les btns du date picker ne sont pas accessibles, on les rend accessibles
341
- watch(isDatePickerVisible, async (newValue) => {
342
- if (newValue) {
343
- await nextTick()
344
- const arrowDown = document.querySelector('.v-btn.v-btn--icon.v-theme--light.v-btn--density-comfortable.v-btn--size-default.v-btn--variant-text.v-date-picker-controls__mode-btn')
345
- const arrowLeftButtons = document.querySelectorAll('.v-btn.v-btn--icon.v-theme--light.v-btn--density-default.v-btn--size-default.v-btn--variant-text')
585
+ // Fonction pour améliorer l'accessibilité du DatePicker
586
+ const updateAccessibility = async () => {
587
+ await nextTick()
588
+
589
+ // Utiliser des attributs data pour sélectionner les éléments, ce qui est plus stable que les classes CSS
590
+ const datePickerEl = document.querySelector('.v-date-picker')
591
+ if (!datePickerEl) return
592
+
593
+ // Ajouter un attribut role="application" au conteneur principal
594
+ datePickerEl.setAttribute('role', 'application')
595
+ datePickerEl.setAttribute('aria-label', 'Sélecteur de date')
596
+
597
+ // Sélectionner tous les boutons de navigation
598
+ const navigationButtons = datePickerEl.querySelectorAll('button')
346
599
 
347
- if (arrowDown) {
348
- arrowDown.setAttribute('aria-label', 'Fleche vers le bas')
600
+ // Attribuer des labels significatifs basés sur la position ou l'icône
601
+ navigationButtons.forEach((button) => {
602
+ const iconEl = button.querySelector('.v-icon')
603
+ if (!iconEl) return
604
+
605
+ // Utiliser le contenu de l'icône pour déterminer sa fonction
606
+ const iconContent = iconEl.textContent || ''
607
+ const iconClasses = iconEl.className || ''
608
+
609
+ if (iconClasses.includes('mdi-chevron-left') || iconContent.includes('chevron-left')) {
610
+ button.setAttribute('aria-label', 'Mois précédent')
611
+ }
612
+ else if (iconClasses.includes('mdi-chevron-right') || iconContent.includes('chevron-right')) {
613
+ button.setAttribute('aria-label', 'Mois suivant')
349
614
  }
615
+ else if (iconClasses.includes('mdi-chevron-down') || iconContent.includes('chevron-down')
616
+ || iconClasses.includes('mdi-menu-down') || iconContent.includes('menu-down')) {
617
+ button.setAttribute('aria-label', 'Changer de vue')
618
+ }
619
+ })
350
620
 
351
- arrowLeftButtons.forEach((button, index) => {
352
- if (index === 0) {
353
- button.setAttribute('aria-label', 'Fleche vers la gauche')
354
- }
355
- else if (index === 1) {
356
- button.setAttribute('aria-label', 'Fleche vers la droite')
357
- }
358
- })
621
+ // Ajouter des instructions pour les lecteurs d'écran
622
+ let srOnlyEl = datePickerEl.querySelector('.sr-only-instructions')
623
+ if (!srOnlyEl) {
624
+ srOnlyEl = document.createElement('span')
625
+ srOnlyEl.className = 'sr-only-instructions'
626
+ srOnlyEl.setAttribute('aria-live', 'polite')
627
+ // Utiliser HTMLElement pour accéder aux propriétés de style
628
+ const srOnlyHtmlEl = srOnlyEl as HTMLElement
629
+ srOnlyHtmlEl.style.position = 'absolute'
630
+ srOnlyHtmlEl.style.width = '1px'
631
+ srOnlyHtmlEl.style.height = '1px'
632
+ srOnlyHtmlEl.style.padding = '0'
633
+ srOnlyHtmlEl.style.margin = '-1px'
634
+ srOnlyHtmlEl.style.overflow = 'hidden'
635
+ srOnlyHtmlEl.style.clip = 'rect(0, 0, 0, 0)'
636
+ srOnlyHtmlEl.style.whiteSpace = 'nowrap'
637
+ srOnlyHtmlEl.style.border = '0'
638
+ srOnlyEl.textContent = 'Utilisez les flèches pour naviguer entre les dates et Entrée pour sélectionner une date'
639
+
640
+ datePickerEl.prepend(srOnlyEl)
641
+ }
642
+ }
643
+
644
+ // Appliquer les améliorations d'accessibilité quand le DatePicker devient visible
645
+ watch(isDatePickerVisible, async (newValue) => {
646
+ if (newValue) {
647
+ await updateAccessibility()
359
648
  }
360
649
  })
361
650
 
@@ -367,71 +656,7 @@
367
656
  isDatePickerVisible.value = true
368
657
  }
369
658
 
370
- type Rule = { type: string, options: RuleOptions }
371
-
372
- const customRules = ref<Rule[]>(props.customRules || [])
373
- const customWarningRules = ref<Rule[]>(props.customWarningRules || [])
374
-
375
- const { generateRules } = useFieldValidation()
376
- const validationRules = generateRules(customRules.value)
377
- const warningValidationRules = generateRules(customWarningRules.value)
378
-
379
- const validateDates = () => {
380
- errorMessages.value = []
381
- successMessages.value = []
382
- warningMessages.value = []
383
-
384
- if (props.noCalendar) {
385
- // En mode no-calendar, on délègue la validation au DateTextInput
386
- return
387
- }
388
-
389
- const addMessages = (dates, rules) => {
390
- dates.forEach((date) => {
391
- rules.forEach((rule) => {
392
- const result = rule(date)
393
- if (result?.error) {
394
- errorMessages.value.push(result.error)
395
- errorMessages.value = [...new Set(errorMessages.value)]
396
- }
397
- else if (result?.warning) {
398
- warningMessages.value.push(result.warning)
399
- warningMessages.value = [...new Set(warningMessages.value)]
400
- }
401
- else if (result?.success) {
402
- successMessages.value.push(result.success)
403
- successMessages.value = [...new Set(successMessages.value)]
404
- }
405
- })
406
- })
407
- }
408
-
409
- const handleValidation = (dates) => {
410
- if (Array.isArray(dates) && dates.length > 1) {
411
- // Pour une plage, on ne vérifie que le premier et le dernier jour
412
- const [firstDate, ...rest] = dates
413
- const lastDate = rest[rest.length - 1]
414
- const datesToValidate = [firstDate, lastDate]
415
-
416
- // Validation des règles
417
- addMessages(datesToValidate, validationRules)
418
- addMessages(datesToValidate, warningValidationRules)
419
- }
420
- else {
421
- // Pour une date unique, on valide normalement
422
- const datesToValidate = Array.isArray(dates) ? dates : [dates]
423
- addMessages(datesToValidate, validationRules)
424
- addMessages(datesToValidate, warningValidationRules)
425
- }
426
- }
427
-
428
- if (props.required && (!selectedDates.value || (Array.isArray(selectedDates.value) && selectedDates.value.length === 0))) {
429
- errorMessages.value.push('La date est requise.')
430
- }
431
- else if (selectedDates.value) {
432
- handleValidation(Array.isArray(selectedDates.value) ? selectedDates.value : [selectedDates.value])
433
- }
434
- }
659
+ // Fonctions et constantes déjà déclarées plus haut dans le code
435
660
 
436
661
  const getIcon = () => {
437
662
  if (props.noCalendar) {
@@ -451,13 +676,43 @@
451
676
 
452
677
  // Watch sur modelValue pour gérer les changements externes
453
678
  watch(() => props.modelValue, (newValue) => {
454
- if (!newValue || newValue === '') {
455
- selectedDates.value = null
456
- textInputValue.value = ''
457
- displayFormattedDate.value = ''
679
+ // Éviter les mises à jour récursives
680
+ if (isUpdatingFromInternal.value) return
681
+
682
+ try {
683
+ isUpdatingFromInternal.value = true
684
+
685
+ if (!newValue || newValue === '') {
686
+ selectedDates.value = null
687
+ textInputValue.value = ''
688
+ displayFormattedDate.value = ''
689
+ }
690
+ else {
691
+ // Initialiser les dates sélectionnées
692
+ selectedDates.value = initializeSelectedDates(newValue)
693
+
694
+ // Mettre à jour l'affichage et le textInputValue
695
+ if (selectedDates.value) {
696
+ if (Array.isArray(selectedDates.value)) {
697
+ if (selectedDates.value.length > 0) {
698
+ textInputValue.value = formatDate(selectedDates.value[0], props.format)
699
+ displayFormattedDate.value = displayFormattedDateComputed.value || ''
700
+ }
701
+ }
702
+ else {
703
+ textInputValue.value = formatDate(selectedDates.value, props.format)
704
+ displayFormattedDate.value = displayFormattedDateComputed.value || ''
705
+ }
706
+ }
707
+ }
708
+
709
+ // Valider les dates
710
+ validateDates()
458
711
  }
459
- else {
460
- selectedDates.value = initializeSelectedDates(newValue)
712
+ finally {
713
+ setTimeout(() => {
714
+ isUpdatingFromInternal.value = false
715
+ }, 0)
461
716
  }
462
717
  }, { immediate: true })
463
718
  </script>
@@ -515,29 +770,42 @@
515
770
  @append-icon-click="handleAppendIconClick"
516
771
  />
517
772
  </template>
518
- <transition name="fade">
519
- <v-locale-provider locale="fr">
520
- <VDatePicker
521
- v-if="isDatePickerVisible && !props.noCalendar"
522
- v-model="selectedDates"
523
- :first-day-of-week="1"
524
- :multiple="props.displayRange ? 'range' : false"
525
- :show-adjacent-months="true"
526
- :show-week="props.showWeekNumber"
527
- :view-mode="props.isBirthDate ? 'year' : 'month'"
528
- color="primary"
529
- >
530
- <template #title>
531
- Sélectionnez une date
532
- </template>
533
- <template #header>
534
- <h3 class="mx-auto my-auto ml-5 mb-4">
535
- {{ todayInString }}
536
- </h3>
537
- </template>
538
- </VDatePicker>
539
- </v-locale-provider>
540
- </transition>
773
+ <div>
774
+ <VMenu
775
+ v-if="!props.noCalendar"
776
+ v-model="isDatePickerVisible"
777
+ activator="parent"
778
+ :min-width="0"
779
+ location="bottom"
780
+ :close-on-content-click="false"
781
+ :open-on-click="false"
782
+ transition="fade-transition"
783
+ attach="body"
784
+ :offset="[-20, 5]"
785
+ >
786
+ <transition name="fade">
787
+ <VDatePicker
788
+ v-if="isDatePickerVisible && !props.noCalendar"
789
+ v-model="selectedDates"
790
+ :first-day-of-week="1"
791
+ :multiple="props.displayRange ? 'range' : false"
792
+ :show-adjacent-months="true"
793
+ :show-week="props.showWeekNumber"
794
+ :view-mode="props.isBirthDate ? 'year' : 'month'"
795
+ color="primary"
796
+ >
797
+ <template #title>
798
+ Sélectionnez une date
799
+ </template>
800
+ <template #header>
801
+ <h3 class="mx-auto my-auto ml-5 mb-4">
802
+ {{ todayInString }}
803
+ </h3>
804
+ </template>
805
+ </VDatePicker>
806
+ </transition>
807
+ </VMenu>
808
+ </div>
541
809
  </div>
542
810
  </template>
543
811