@cnamts/synapse 0.0.8-alpha → 0.0.9-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 (111) hide show
  1. package/dist/design-system-v3.d.ts +584 -128
  2. package/dist/design-system-v3.js +4176 -2694
  3. package/dist/design-system-v3.umd.cjs +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/assets/settings.scss +1 -1
  7. package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
  8. package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
  9. package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
  10. package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
  11. package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
  12. package/src/components/Customs/SySelect/SySelect.vue +9 -4
  13. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
  14. package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
  15. package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
  16. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
  17. package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
  18. package/src/components/Customs/SyTextField/types.d.ts +2 -2
  19. package/src/components/DatePicker/DatePicker.mdx +191 -0
  20. package/src/components/DatePicker/DatePicker.stories.ts +787 -0
  21. package/src/components/DatePicker/DatePicker.vue +560 -0
  22. package/src/components/DatePicker/DateTextInput.vue +409 -0
  23. package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
  24. package/src/components/DialogBox/DialogBox.stories.ts +1 -1
  25. package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
  26. package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
  27. package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
  28. package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
  29. package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +9 -9
  30. package/src/components/FileUpload/FileUpload.mdx +165 -0
  31. package/src/components/FileUpload/FileUpload.stories.ts +429 -0
  32. package/src/components/FileUpload/FileUpload.vue +195 -0
  33. package/src/components/FileUpload/FileUploadContent.vue +109 -0
  34. package/src/components/FileUpload/locales.ts +10 -0
  35. package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
  36. package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
  37. package/src/components/FileUpload/useFileDrop.ts +23 -0
  38. package/src/components/FileUpload/validateFiles.ts +39 -0
  39. package/src/components/NirField/NirField.stories.ts +1 -1
  40. package/src/components/NirField/NirField.vue +2 -1
  41. package/src/components/PasswordField/Accessibilite.mdx +14 -0
  42. package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
  43. package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
  44. package/src/components/PasswordField/PasswordField.vue +3 -3
  45. package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
  46. package/src/components/PhoneField/PhoneField.vue +44 -60
  47. package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
  48. package/src/components/RangeField/RangeField.mdx +54 -0
  49. package/src/components/RangeField/RangeField.stories.ts +189 -0
  50. package/src/components/RangeField/RangeField.vue +157 -0
  51. package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
  52. package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
  53. package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
  54. package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
  55. package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
  56. package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
  57. package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
  58. package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
  59. package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
  60. package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
  61. package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
  62. package/src/components/RangeField/RangeSlider/types.ts +15 -0
  63. package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
  64. package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
  65. package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
  66. package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
  67. package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
  68. package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
  69. package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
  70. package/src/components/RangeField/config.ts +7 -0
  71. package/src/components/RangeField/locales.ts +4 -0
  72. package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
  73. package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
  74. package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
  75. package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
  76. package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
  77. package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
  78. package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
  79. package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
  80. package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
  81. package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
  82. package/src/components/RatingPicker/Rating.ts +45 -0
  83. package/src/components/RatingPicker/RatingPicker.mdx +56 -0
  84. package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
  85. package/src/components/RatingPicker/RatingPicker.vue +122 -0
  86. package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
  87. package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
  88. package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
  89. package/src/components/RatingPicker/locales.ts +3 -0
  90. package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
  91. package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
  92. package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
  93. package/src/components/SearchListField/SearchListField.mdx +74 -0
  94. package/src/components/SearchListField/SearchListField.stories.ts +126 -0
  95. package/src/components/SearchListField/SearchListField.vue +194 -0
  96. package/src/components/SearchListField/locales.ts +5 -0
  97. package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
  98. package/src/components/SearchListField/types.d.ts +4 -0
  99. package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
  100. package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
  101. package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
  102. package/src/components/SelectBtnField/config.ts +11 -0
  103. package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
  104. package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
  105. package/src/components/SelectBtnField/types.d.ts +11 -0
  106. package/src/components/index.ts +8 -1
  107. package/src/composables/rules/useFieldValidation.ts +172 -44
  108. package/src/designTokens/index.ts +3 -3
  109. package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
  110. package/src/utils/calcHumanFileSize/index.ts +12 -0
  111. package/src/utils/calcHumanFileSize/tests/calcHumanFileSize.spec.ts +21 -0
@@ -0,0 +1,409 @@
1
+ <script lang="ts" setup>
2
+ import { computed, ref, watch, onMounted } from 'vue'
3
+ import SyTextField from '../Customs/SyTextField/SyTextField.vue'
4
+ import { useFieldValidation } from '@/composables/rules/useFieldValidation'
5
+ import type { RuleOptions } from '@/composables/rules/useFieldValidation'
6
+
7
+ // Type pour les valeurs de date
8
+ type DateValue = string | [string, string]
9
+
10
+ const props = withDefaults(defineProps<{
11
+ modelValue?: DateValue
12
+ displayFormat?: string
13
+ returnFormat?: string
14
+ range?: boolean
15
+ placeholder?: string
16
+ rules?: { type: string, options: RuleOptions }[]
17
+ warningRules?: { type: string, options: RuleOptions }[]
18
+ required?: boolean
19
+ }>(), {
20
+ modelValue: undefined,
21
+ displayFormat: 'DD/MM/YYYY',
22
+ returnFormat: '',
23
+ range: false,
24
+ placeholder: '',
25
+ rules: () => [],
26
+ warningRules: () => [],
27
+ required: false,
28
+ })
29
+
30
+ const emit = defineEmits<{
31
+ (e: 'update:modelValue', value: DateValue): void
32
+ }>()
33
+
34
+ const inputValue = ref('')
35
+
36
+ const { generateRules } = useFieldValidation()
37
+
38
+ // Règles de validation
39
+ const validationRules = computed(() => generateRules(props.rules || []))
40
+ const warningValidationRules = computed(() => generateRules(props.warningRules || []))
41
+
42
+ // États de validation
43
+ const hasError = ref(false)
44
+ const hasWarning = ref(false)
45
+ const hasSuccess = ref(false)
46
+ const errorMessages = ref<string[]>([])
47
+ const warningMessages = ref<string[]>([])
48
+ const successMessages = ref<string[]>([])
49
+
50
+ // Valider le champ
51
+ const validateField = () => {
52
+ // Réinitialiser les états
53
+ errorMessages.value = []
54
+ warningMessages.value = []
55
+ successMessages.value = []
56
+
57
+ if (props.required && !inputValue.value) {
58
+ errorMessages.value.push('La date est requise.')
59
+ hasError.value = true
60
+ return
61
+ }
62
+
63
+ if (inputValue.value) {
64
+ if (props.range) {
65
+ // Pour une plage, valider chaque date séparément
66
+ const [start, end] = inputValue.value.split('-').map(d => d.trim())
67
+ if (start && isValidDate(start)) {
68
+ // Validation des erreurs et succès pour la date de début
69
+ addMessages(start, validationRules.value, 'error')
70
+ addMessages(start, validationRules.value, 'success')
71
+ // Validation des warnings pour la date de début
72
+ addMessages(start, warningValidationRules.value, 'warning')
73
+ }
74
+
75
+ if (end && isValidDate(end)) {
76
+ // Validation des erreurs et succès pour la date de fin
77
+ addMessages(end, validationRules.value, 'error')
78
+ addMessages(end, validationRules.value, 'success')
79
+ // Validation des warnings pour la date de fin
80
+ addMessages(end, warningValidationRules.value, 'warning')
81
+
82
+ // Valider que la date de fin est après la date de début
83
+ if (start && isValidDate(start)) {
84
+ const startDate = parseDate(start, props.displayFormat)
85
+ const endDate = parseDate(end, props.displayFormat)
86
+ if (startDate && endDate && endDate < startDate) {
87
+ errorMessages.value.push('La date de fin doit être après la date de début')
88
+ }
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ // Validation pour une date simple
94
+ addMessages(inputValue.value, validationRules.value, 'error')
95
+ addMessages(inputValue.value, validationRules.value, 'success')
96
+ addMessages(inputValue.value, warningValidationRules.value, 'warning')
97
+ }
98
+ }
99
+
100
+ // Mettre à jour les états
101
+ hasError.value = errorMessages.value.length > 0
102
+ hasWarning.value = warningMessages.value.length > 0
103
+ hasSuccess.value = successMessages.value.length > 0 && !hasError.value && !hasWarning.value
104
+ }
105
+
106
+ // Fonction pour ajouter les messages
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
108
+ const addMessages = (value: string, rules: any[], messageType: 'error' | 'warning' | 'success') => {
109
+ if (!value) return
110
+
111
+ // Convertir la date au format YYYY-MM-DD pour la validation
112
+ const validationValue = toValidationFormat(value)
113
+ if (!validationValue) return
114
+
115
+ rules.forEach((rule) => {
116
+ const result = rule(validationValue)
117
+ if (result) {
118
+ const targetMessages = messageType === 'error'
119
+ ? errorMessages
120
+ : messageType === 'warning'
121
+ ? warningMessages
122
+ : successMessages
123
+ if (result[messageType]) {
124
+ targetMessages.value.push(result[messageType])
125
+ targetMessages.value = [...new Set(targetMessages.value)]
126
+ }
127
+ }
128
+ })
129
+ }
130
+
131
+ // Convert a date string to YYYY-MM-DD format for validation
132
+ const toValidationFormat = (dateStr: string): string => {
133
+ if (!dateStr) return ''
134
+ const date = parseDate(dateStr, 'DD/MM/YYYY')
135
+ if (!date) return dateStr
136
+
137
+ // Formatter en YYYY-MM-DD
138
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
139
+ }
140
+
141
+ // Fonction pour parser les dates selon le format spécifié
142
+ const parseDate = (dateStr: string, format: string): Date | null => {
143
+ const formatParts = format.split(/[^A-Z]/)
144
+ const dateParts = dateStr.split(/[^0-9]/)
145
+
146
+ if (dateParts.length !== formatParts.length) return null
147
+
148
+ let day = 1, month = 0, year = 2000
149
+
150
+ for (let i = 0; i < formatParts.length; i++) {
151
+ const value = parseInt(dateParts[i])
152
+ if (isNaN(value)) return null
153
+
154
+ switch (formatParts[i]) {
155
+ case 'DD':
156
+ day = value
157
+ break
158
+ case 'MM':
159
+ month = value - 1 // JavaScript months are 0-based
160
+ break
161
+ case 'YYYY':
162
+ case 'YY':
163
+ year = value
164
+ if (formatParts[i] === 'YY') {
165
+ const currentYear = new Date().getFullYear()
166
+ const currentCentury = Math.floor(currentYear / 100) * 100
167
+ year = currentCentury + value
168
+ }
169
+ break
170
+ }
171
+ }
172
+
173
+ const date = new Date(year, month, day)
174
+ return date.getFullYear() === year && date.getMonth() === month && date.getDate() === day ? date : null
175
+ }
176
+
177
+ // Convert between different date formats
178
+ const convertDateFormat = (dateStr: string, fromFormat: string, toFormat: string): string => {
179
+ if (!dateStr) return ''
180
+
181
+ const date = parseDate(dateStr, fromFormat)
182
+ if (!date) return dateStr
183
+
184
+ // Format the date according to the target format
185
+ let result = toFormat
186
+ result = result.replace('YYYY', String(date.getFullYear()))
187
+ result = result.replace('YY', String(date.getFullYear()).slice(-2))
188
+ result = result.replace('MM', String(date.getMonth() + 1).padStart(2, '0'))
189
+ result = result.replace('DD', String(date.getDate()).padStart(2, '0'))
190
+ return result
191
+ }
192
+
193
+ // Format a date input string (add slashes)
194
+ const formatDateInput = (value: string): string => {
195
+ const numbers = value.replace(/\D/g, '')
196
+ const chars = numbers.split('')
197
+
198
+ if (chars.length <= 2) return numbers
199
+ if (chars.length <= 4) return `${chars.slice(0, 2).join('')}/${chars.slice(2).join('')}`
200
+
201
+ // Permettre les années à 4 chiffres
202
+ const yearPart = chars.slice(4).join('')
203
+ return `${chars.slice(0, 2).join('')}/${chars.slice(2, 4).join('')}/${yearPart}`
204
+ }
205
+
206
+ // Initialisation au montage
207
+ onMounted(() => {
208
+ if (props.modelValue) {
209
+ if (Array.isArray(props.modelValue)) {
210
+ // Si on reçoit un tableau de dates, on les formate pour l'affichage
211
+ const [start, end] = props.modelValue
212
+ const formattedStart = convertDateFormat(start, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
213
+ const formattedEnd = convertDateFormat(end, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
214
+ inputValue.value = `${formattedStart} - ${formattedEnd}`
215
+ }
216
+ else if (typeof props.modelValue === 'string') {
217
+ // Si on reçoit une chaîne, on la convertit dans le format d'affichage
218
+ inputValue.value = convertDateFormat(props.modelValue, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
219
+ }
220
+ validateField()
221
+ }
222
+ })
223
+
224
+ // Mise à jour quand la valeur change
225
+ watch(() => props.modelValue, (newValue) => {
226
+ if (newValue) {
227
+ if (Array.isArray(newValue)) {
228
+ // Si on reçoit un tableau de dates, on les formate pour l'affichage
229
+ const [start, end] = newValue
230
+ const formattedStart = convertDateFormat(start, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
231
+ const formattedEnd = convertDateFormat(end, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
232
+ inputValue.value = `${formattedStart} - ${formattedEnd}`
233
+ }
234
+ else if (typeof newValue === 'string') {
235
+ // Si on reçoit une chaîne, on la convertit dans le format d'affichage
236
+ inputValue.value = convertDateFormat(newValue, props.returnFormat || 'YYYY-MM-DD', props.displayFormat)
237
+ }
238
+ validateField()
239
+ }
240
+ else {
241
+ inputValue.value = ''
242
+ }
243
+ })
244
+
245
+ // Émission de la valeur
246
+ const emitValue = (value: string) => {
247
+ if (!value) {
248
+ emit('update:modelValue', '')
249
+ return
250
+ }
251
+
252
+ const returnFormat = props.returnFormat || 'YYYY-MM-DD'
253
+
254
+ if (props.range) {
255
+ const [start, end] = value.split('-').map(d => d.trim())
256
+ if (start && end) {
257
+ // Pour les ranges, convertir les deux dates au format demandé
258
+ const formattedStart = convertDateFormat(start, props.displayFormat, returnFormat)
259
+ const formattedEnd = convertDateFormat(end, props.displayFormat, returnFormat)
260
+ emit('update:modelValue', [formattedStart, formattedEnd])
261
+ }
262
+ }
263
+ else {
264
+ // Pour une date simple, convertir au format demandé
265
+ const formattedValue = convertDateFormat(value, props.displayFormat, returnFormat)
266
+ emit('update:modelValue', formattedValue)
267
+ }
268
+ }
269
+
270
+ // Handle the input value formatting
271
+ const maxLength = computed(() => props.range ? 23 : 10) // 10 pour "DD/MM/YYYY", 23 pour "DD/MM/YYYY - DD/MM/YYYY"
272
+ const handleInput = (value: string) => {
273
+ // Limiter la longueur du texte
274
+ if (value.length > maxLength.value) {
275
+ value = value.slice(0, maxLength.value)
276
+ }
277
+
278
+ if (!value) {
279
+ inputValue.value = ''
280
+ emitValue('')
281
+ validateField()
282
+ return
283
+ }
284
+
285
+ // Garder uniquement les chiffres et le séparateur de plage
286
+ let formattedValue = value.replace(/[^\d-]/g, '')
287
+
288
+ if (props.range) {
289
+ const [start, end] = formattedValue.split('-').map(d => d.trim())
290
+ let result = ''
291
+
292
+ // Formater la première date
293
+ if (start) {
294
+ result = formatDateInput(start)
295
+
296
+ // Si la première date est complète, ajouter automatiquement le séparateur
297
+ const yearLength = props.displayFormat.includes('YYYY') ? 8 : 6
298
+ if (start.length === yearLength && !formattedValue.includes('-')) {
299
+ result += ' -'
300
+ }
301
+ }
302
+
303
+ // Ajouter le séparateur et la deuxième date si présente
304
+ if (formattedValue.includes('-')) {
305
+ if (!result.includes(' -')) {
306
+ result += ' -'
307
+ }
308
+ if (end) {
309
+ result += ' ' + formatDateInput(end)
310
+ }
311
+ }
312
+
313
+ formattedValue = result
314
+ }
315
+ else {
316
+ formattedValue = formatDateInput(formattedValue)
317
+ }
318
+
319
+ inputValue.value = formattedValue
320
+ emitValue(formattedValue)
321
+ }
322
+
323
+ // Handle blur event to format the final value
324
+ const handleBlur = () => {
325
+ if (!inputValue.value) {
326
+ emitValue('')
327
+ return
328
+ }
329
+
330
+ if (props.range) {
331
+ const [start, end] = inputValue.value.split('-').map(d => d.trim())
332
+ if (start && end) {
333
+ // Nettoyer et formater les dates
334
+ const cleanStart = start.replace(/\D/g, '')
335
+ const cleanEnd = end.replace(/\D/g, '')
336
+
337
+ // Extraire les parties des dates
338
+ const startDay = cleanStart.slice(0, 2)
339
+ const startMonth = cleanStart.slice(2, 4)
340
+ const startYear = cleanStart.slice(4)
341
+
342
+ const endDay = cleanEnd.slice(0, 2)
343
+ const endMonth = cleanEnd.slice(2, 4)
344
+ const endYear = cleanEnd.slice(4)
345
+
346
+ // Formater pour l'affichage
347
+ const formattedStart = `${startDay}/${startMonth}/${startYear}`
348
+ const formattedEnd = `${endDay}/${endMonth}/${endYear}`
349
+ inputValue.value = `${formattedStart} - ${formattedEnd}`
350
+
351
+ // Émettre les dates si elles sont valides
352
+ if (isValidDate(formattedStart) && isValidDate(formattedEnd)) {
353
+ emitValue(`${formattedStart} - ${formattedEnd}`)
354
+ }
355
+ }
356
+ }
357
+ else {
358
+ const cleanValue = inputValue.value.replace(/\D/g, '')
359
+
360
+ // Extraire les parties de la date
361
+ const day = cleanValue.slice(0, 2)
362
+ const month = cleanValue.slice(2, 4)
363
+ const year = cleanValue.slice(4)
364
+
365
+ // Formater pour l'affichage
366
+ const formattedValue = `${day}/${month}/${year}`
367
+ inputValue.value = formattedValue
368
+
369
+ if (isValidDate(formattedValue)) {
370
+ emitValue(formattedValue)
371
+ }
372
+ }
373
+
374
+ validateField()
375
+ }
376
+
377
+ // Validate single date
378
+ const isValidDate = (dateString: string): boolean => {
379
+ return parseDate(dateString, props.displayFormat) !== null
380
+ }
381
+
382
+ // Exposer la méthode de validation
383
+ const validateOnSubmit = () => {
384
+ validateField()
385
+ return errorMessages.value.length === 0
386
+ }
387
+
388
+ defineExpose({
389
+ validate: validateField,
390
+ validateOnSubmit,
391
+ })
392
+ </script>
393
+
394
+ <template>
395
+ <SyTextField
396
+ v-model="inputValue"
397
+ :error="hasError"
398
+ :error-messages="errorMessages"
399
+ :messages="successMessages"
400
+ :placeholder="placeholder"
401
+ :success="hasSuccess"
402
+ :warning="hasWarning"
403
+ :warning-messages="warningMessages"
404
+ :maxlength="maxLength"
405
+ v-bind="$attrs"
406
+ @blur="handleBlur"
407
+ @update:model-value="handleInput"
408
+ />
409
+ </template>
@@ -0,0 +1,266 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
3
+ import { vuetify } from '@tests/unit/setup'
4
+ import { nextTick } from 'vue'
5
+ import DatePicker from '../DatePicker.vue'
6
+
7
+ describe('DatePicker.vue', () => {
8
+ let wrapper
9
+
10
+ beforeEach(() => {
11
+ wrapper = mount(DatePicker, {
12
+ global: {
13
+ plugins: [vuetify],
14
+ },
15
+ props: {
16
+ modelValue: '',
17
+ required: true,
18
+ },
19
+ })
20
+ })
21
+
22
+ it('displays the placeholder text', () => {
23
+ const placeholder = 'Sélectionner une date'
24
+ const wrapper = mount(DatePicker, {
25
+ global: {
26
+ plugins: [vuetify],
27
+ },
28
+ props: { placeholder },
29
+ })
30
+ expect(wrapper.find('input').attributes('placeholder')).toBe(undefined)
31
+ })
32
+
33
+ it('emits update:model-value event on date selection', async () => {
34
+ const wrapper = mount(DatePicker, {
35
+ global: {
36
+ plugins: [vuetify],
37
+ },
38
+ })
39
+ const input = wrapper.find('input')
40
+ await input.setValue('01/01/2023')
41
+ expect(wrapper.emitted('update:model-value')).toBeTruthy()
42
+ })
43
+
44
+ it('renders the component', () => {
45
+ const wrapper = mount(DatePicker, {
46
+ global: {
47
+ plugins: [vuetify],
48
+ },
49
+ })
50
+ expect(wrapper.exists()).toBe(true)
51
+ })
52
+
53
+ it('emits the correct formatted date on date selection', async () => {
54
+ const input = wrapper.find('input')
55
+ await input.setValue('01/01/2023')
56
+ expect(wrapper.emitted('update:model-value')).toBeTruthy()
57
+ expect(wrapper.emitted('update:model-value')[0]).toEqual(['01/01/2023'])
58
+ })
59
+
60
+ it('toggles the date picker visibility on focus', async () => {
61
+ const input = wrapper.find('input')
62
+ await input.trigger('focus')
63
+ expect(wrapper.vm.isDatePickerVisible).toBe(true)
64
+ })
65
+
66
+ it('hides the date picker on outside click', async () => {
67
+ const input = wrapper.find('input')
68
+ await input.trigger('focus')
69
+ expect(wrapper.vm.isDatePickerVisible).toBe(true)
70
+
71
+ document.body.click()
72
+ await nextTick()
73
+ expect(wrapper.vm.isDatePickerVisible).toBe(false)
74
+ })
75
+
76
+ it('updates aria-labels for date picker navigation buttons', async () => {
77
+ wrapper.vm.isDatePickerVisible = true
78
+ await nextTick()
79
+
80
+ const arrowDown = document.querySelector(
81
+ '.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',
82
+ )
83
+ const arrowLeftButtons = document.querySelectorAll(
84
+ '.v-btn.v-btn--icon.v-theme--light.v-btn--density-default.v-btn--size-default.v-btn--variant-text',
85
+ )
86
+
87
+ if (arrowDown) {
88
+ expect(arrowDown.getAttribute('aria-label')).toBe('Fleche vers le bas')
89
+ }
90
+ arrowLeftButtons.forEach((button, index) => {
91
+ if (index === 0) {
92
+ expect(button.getAttribute('aria-label')).toBe('Fleche vers la gauche')
93
+ }
94
+ else if (index === 1) {
95
+ expect(button.getAttribute('aria-label')).toBe('Fleche vers la droite')
96
+ }
97
+ })
98
+ })
99
+
100
+ it('handles invalid date input gracefully', async () => {
101
+ const input = wrapper.find('input')
102
+ await input.setValue('invalid date')
103
+ expect(wrapper.vm.selectedDates).toBeNull()
104
+ expect(wrapper.emitted('update:model-value')).toBeFalsy()
105
+ })
106
+
107
+ it('hides the date picker when at least two dates are selected in range mode', async () => {
108
+ const wrapper = mount(DatePicker, {
109
+ global: {
110
+ plugins: [vuetify],
111
+ },
112
+ props: {
113
+ displayRange: true, // Activer le mode plage
114
+ },
115
+ })
116
+
117
+ // Simule la visibilité initiale du date picker
118
+ wrapper.vm.isDatePickerVisible = true
119
+
120
+ // Simule la sélection de deux dates via la propriété `selectedDates`
121
+ wrapper.vm.selectedDates = [new Date('2023-01-01'), new Date('2023-01-05')]
122
+ await nextTick()
123
+
124
+ // Vérifie que le date picker est masqué
125
+ expect(wrapper.vm.isDatePickerVisible).toBe(false)
126
+ })
127
+
128
+ it('removes the click event listener on unmount', async () => {
129
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener')
130
+
131
+ // Monte le composant
132
+ const wrapper = mount(DatePicker, {
133
+ global: {
134
+ plugins: [vuetify],
135
+ },
136
+ })
137
+
138
+ // Démonte le composant
139
+ wrapper.unmount()
140
+
141
+ // Vérifie que removeEventListener a été appelé avec les bons arguments
142
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('click', wrapper.vm.handleClickOutside)
143
+
144
+ // Nettoie le spy
145
+ removeEventListenerSpy.mockRestore()
146
+ })
147
+
148
+ it('returns an array of all dates between two valid dates', () => {
149
+ const datesArray = ['01/01/2023', '05/01/2023']
150
+ const result = wrapper.vm.initializeSelectedDates(datesArray)
151
+
152
+ expect(Array.isArray(result)).toBe(true)
153
+ expect(result.length).toBe(2)
154
+ expect(result[0]).toBeInstanceOf(Date)
155
+ expect(result[1]).toBeInstanceOf(Date)
156
+ expect(result[0].toISOString().split('T')[0]).toBe('2023-01-01')
157
+ expect(result[1].toISOString().split('T')[0]).toBe('2023-01-05')
158
+ })
159
+
160
+ it('handles invalid date inputs gracefully', () => {
161
+ const datesArray = ['invalid date', '05/01/2023']
162
+ const result = wrapper.vm.initializeSelectedDates(datesArray)
163
+
164
+ expect(result).toEqual([])
165
+ })
166
+
167
+ it('handles a single date correctly', () => {
168
+ const singleDate = '01/01/2023'
169
+ const result = wrapper.vm.initializeSelectedDates(singleDate)
170
+
171
+ expect(result).toBeInstanceOf(Date)
172
+ expect(result.toISOString().split('T')[0]).toBe('2023-01-01')
173
+ })
174
+
175
+ it('returns an empty array if start date is after end date', () => {
176
+ const datesArray = ['05/01/2023', '01/01/2023']
177
+ const result = wrapper.vm.initializeSelectedDates(datesArray)
178
+
179
+ expect(result).toEqual([])
180
+ })
181
+
182
+ it('returns true when there are no validation errors', async () => {
183
+ const wrapper = mount(DatePicker, {
184
+ global: {
185
+ plugins: [vuetify],
186
+ },
187
+ props: {
188
+ required: true, // Le champ est requis
189
+ },
190
+ })
191
+
192
+ // Simule une date valide
193
+ wrapper.vm.selectedDates = [new Date('2023-01-01')]
194
+ await nextTick()
195
+
196
+ const result = wrapper.vm.validateOnSubmit()
197
+
198
+ // Vérifie que validateOnSubmit retourne true et qu'il n'y a pas d'erreurs
199
+ expect(result).toBe(true)
200
+ expect(wrapper.vm.errorMessages).toEqual([])
201
+ })
202
+
203
+ it('returns false when there are validation errors', async () => {
204
+ const wrapper = mount(DatePicker, {
205
+ global: {
206
+ plugins: [vuetify],
207
+ },
208
+ props: {
209
+ required: true, // Le champ est requis
210
+ },
211
+ })
212
+
213
+ // Simule l'absence de date sélectionnée
214
+ wrapper.vm.selectedDates = null
215
+ await nextTick()
216
+
217
+ const result = wrapper.vm.validateOnSubmit()
218
+
219
+ // Vérifie que validateOnSubmit retourne false et qu'il y a des erreurs
220
+ expect(result).toBe(false)
221
+ expect(wrapper.vm.errorMessages).toContain('La date est requise.')
222
+ })
223
+
224
+ it('parses a valid date string into a Date instance', () => {
225
+ const modelValue = '15/01/2023' // Chaîne valide
226
+ const result = wrapper.vm.initializeSelectedDates(modelValue)
227
+
228
+ expect(result).toBeInstanceOf(Date) // Doit retourner une instance de Date
229
+ expect(result.toISOString().split('T')[0]).toBe('2023-01-15') // Correspond à la date attendue
230
+ })
231
+
232
+ it('returns null if modelValue is null or undefined', () => {
233
+ const modelValue = null
234
+ const result = wrapper.vm.initializeSelectedDates(modelValue)
235
+
236
+ expect(result).toBeNull() // Doit retourner null
237
+ })
238
+
239
+ it('handles an invalid date string gracefully', () => {
240
+ const modelValue = 'invalid date'
241
+ const result = wrapper.vm.initializeSelectedDates(modelValue)
242
+
243
+ expect(result).toBeNull()
244
+ })
245
+
246
+ it('sets selectedDates to null when input is empty', () => {
247
+ wrapper.vm.updateSelectedDates('') // Simule un input vide
248
+
249
+ expect(wrapper.vm.selectedDates).toBeNull() // Vérifie que selectedDates est null
250
+ })
251
+
252
+ it('parses a valid date string and updates selectedDates', () => {
253
+ const validInput = '15/01/2023'
254
+ wrapper.vm.updateSelectedDates(validInput) // Simule un input valide
255
+
256
+ expect(wrapper.vm.selectedDates).toBeInstanceOf(Date) // Vérifie que selectedDates est une instance de Date
257
+ expect(wrapper.vm.selectedDates.toISOString().split('T')[0]).toBe('2023-01-15') // Vérifie la date exacte
258
+ })
259
+
260
+ it('does not update selectedDates for invalid date string', () => {
261
+ const invalidInput = 'invalid-date'
262
+ wrapper.vm.updateSelectedDates(invalidInput) // Simule un input invalide
263
+
264
+ expect(wrapper.vm.selectedDates).toBeNull() // Vérifie que selectedDates reste null
265
+ })
266
+ })
@@ -3,7 +3,7 @@ import { VBtn } from 'vuetify/components'
3
3
  import DialogBox from './DialogBox.vue'
4
4
  import { fn } from '@storybook/test'
5
5
 
6
- const meta = {
6
+ const meta: Meta<typeof DialogBox> = {
7
7
  title: 'Composants/Feedback/DialogBox',
8
8
  component: DialogBox,
9
9
  parameters: {
@@ -0,0 +1,14 @@
1
+ import { Meta, Story } from '@storybook/addon-docs';
2
+ import * as AccessStories from './Accessibilite.stories.ts';
3
+
4
+ <Meta of={AccessStories} />
5
+
6
+ Accessibilité
7
+ =============
8
+ <Story of={AccessStories.Legende} />
9
+ <br />
10
+
11
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
12
+
13
+ <Story of={AccessStories.AccessibilitePanel} />
14
+ <br />