@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.
- package/dist/design-system-v3.d.ts +584 -128
- package/dist/design-system-v3.js +4176 -2694
- package/dist/design-system-v3.umd.cjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/assets/settings.scss +1 -1
- package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
- package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
- package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
- package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
- package/src/components/Customs/SySelect/SySelect.vue +9 -4
- package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
- package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
- package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
- package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
- package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
- package/src/components/Customs/SyTextField/types.d.ts +2 -2
- package/src/components/DatePicker/DatePicker.mdx +191 -0
- package/src/components/DatePicker/DatePicker.stories.ts +787 -0
- package/src/components/DatePicker/DatePicker.vue +560 -0
- package/src/components/DatePicker/DateTextInput.vue +409 -0
- package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
- package/src/components/DialogBox/DialogBox.stories.ts +1 -1
- package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
- package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
- package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
- package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +9 -9
- package/src/components/FileUpload/FileUpload.mdx +165 -0
- package/src/components/FileUpload/FileUpload.stories.ts +429 -0
- package/src/components/FileUpload/FileUpload.vue +195 -0
- package/src/components/FileUpload/FileUploadContent.vue +109 -0
- package/src/components/FileUpload/locales.ts +10 -0
- package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
- package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
- package/src/components/FileUpload/useFileDrop.ts +23 -0
- package/src/components/FileUpload/validateFiles.ts +39 -0
- package/src/components/NirField/NirField.stories.ts +1 -1
- package/src/components/NirField/NirField.vue +2 -1
- package/src/components/PasswordField/Accessibilite.mdx +14 -0
- package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
- package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
- package/src/components/PasswordField/PasswordField.vue +3 -3
- package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/PhoneField/PhoneField.vue +44 -60
- package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
- package/src/components/RangeField/RangeField.mdx +54 -0
- package/src/components/RangeField/RangeField.stories.ts +189 -0
- package/src/components/RangeField/RangeField.vue +157 -0
- package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
- package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
- package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
- package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
- package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
- package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
- package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
- package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
- package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
- package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
- package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
- package/src/components/RangeField/RangeSlider/types.ts +15 -0
- package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
- package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
- package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
- package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
- package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
- package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
- package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
- package/src/components/RangeField/config.ts +7 -0
- package/src/components/RangeField/locales.ts +4 -0
- package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
- package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
- package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
- package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
- package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
- package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
- package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
- package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
- package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
- package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
- package/src/components/RatingPicker/Rating.ts +45 -0
- package/src/components/RatingPicker/RatingPicker.mdx +56 -0
- package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
- package/src/components/RatingPicker/RatingPicker.vue +122 -0
- package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
- package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
- package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
- package/src/components/RatingPicker/locales.ts +3 -0
- package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
- package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
- package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
- package/src/components/SearchListField/SearchListField.mdx +74 -0
- package/src/components/SearchListField/SearchListField.stories.ts +126 -0
- package/src/components/SearchListField/SearchListField.vue +194 -0
- package/src/components/SearchListField/locales.ts +5 -0
- package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
- package/src/components/SearchListField/types.d.ts +4 -0
- package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
- package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
- package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
- package/src/components/SelectBtnField/config.ts +11 -0
- package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
- package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
- package/src/components/SelectBtnField/types.d.ts +11 -0
- package/src/components/index.ts +8 -1
- package/src/composables/rules/useFieldValidation.ts +172 -44
- package/src/designTokens/index.ts +3 -3
- package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
- package/src/utils/calcHumanFileSize/index.ts +12 -0
- 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
|
+
})
|
|
@@ -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 />
|