@cnamts/synapse 1.1.0 → 1.1.1

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 (202) hide show
  1. package/dist/{AutocompleteFilter-DXd4szWO.js → AutocompleteFilter-CGF33skz.js} +1 -1
  2. package/dist/{DateFilter-BD59Kgwf.js → DateFilter-D7-MsKtx.js} +1 -1
  3. package/dist/{NumberFilter-BSMZE7uw.js → NumberFilter-bjQPPfsj.js} +1 -1
  4. package/dist/{PeriodFilter-keUdSSk0.js → PeriodFilter-B3wJpK8-.js} +1 -1
  5. package/dist/{SelectFilter-Dhvvwazl.js → SelectFilter-BN6DbKAV.js} +1 -1
  6. package/dist/{TextFilter-CU8FpXz0.js → TextFilter-BffP0J2f.js} +1 -1
  7. package/dist/{apLightTheme2026-DbS7BPUf.js → apLightTheme2026-C4ygwMHC.js} +11 -11
  8. package/dist/components/Amelipro/AmeliproAutoCompleteField/AmeliproAutoCompleteField.d.ts +6 -6
  9. package/dist/components/Amelipro/AmeliproSelect/AmeliproSelect.d.ts +6 -6
  10. package/dist/components/Amelipro/AmeliproTabs/AmeliproTabs.d.ts +6 -6
  11. package/dist/components/Captcha/Captcha.d.ts +27 -16
  12. package/dist/components/Captcha/CaptchaForm.d.ts +29 -3
  13. package/dist/components/Captcha/types.d.ts +14 -0
  14. package/dist/components/Captcha/useCaptchaValidation.d.ts +37 -0
  15. package/dist/components/Customs/Selects/SelectBtnField/SelectBtnField.d.ts +33 -13
  16. package/dist/components/Customs/Selects/SelectBtnField/composables/useSelectBtnFieldValidation.d.ts +23 -0
  17. package/dist/components/Customs/Selects/SyAutocomplete/composables/useSyAutocompleteValidation.d.ts +2 -2
  18. package/dist/components/Customs/Selects/SySelect/composables/useSySelectValidation.d.ts +2 -2
  19. package/dist/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.d.ts +17 -48
  20. package/dist/components/Customs/SyCheckBoxGroup/composables/useSyCheckBoxGroupValidation.d.ts +29 -0
  21. package/dist/components/Customs/SyCheckBoxGroup/types.d.ts +46 -0
  22. package/dist/components/Customs/SyCheckbox/SyCheckbox.d.ts +16 -51
  23. package/dist/components/Customs/SyCheckbox/composables/useSyCheckboxValidation.d.ts +27 -0
  24. package/dist/components/Customs/SyCheckbox/types.d.ts +49 -0
  25. package/dist/components/Customs/SyTextField/FieldState.d.ts +5 -0
  26. package/dist/components/Customs/SyTextField/useSyTextFieldValidation.d.ts +3 -3
  27. package/dist/components/DialogBox/DialogBox.d.ts +2 -0
  28. package/dist/components/DialogBox/locales.d.ts +1 -0
  29. package/dist/components/FilterSideBar/FilterSideBar.d.ts +4 -0
  30. package/dist/components/LunarCalendar/LunarCalendar.d.ts +43 -14
  31. package/dist/components/LunarCalendar/types.d.ts +35 -0
  32. package/dist/components/LunarCalendar/useLunarCalendarValidation.d.ts +11 -12
  33. package/dist/components/MonthPicker/MonthPicker.d.ts +72 -1747
  34. package/dist/components/MonthPicker/MonthPickerText/MonthPickerInput.d.ts +21 -1733
  35. package/dist/components/MonthPicker/MonthPickerText/useTextField.d.ts +5 -0
  36. package/dist/components/MonthPicker/locales.d.ts +1 -0
  37. package/dist/components/MonthPicker/types.d.ts +11 -0
  38. package/dist/components/MonthPicker/useMonthPickerValidation.d.ts +37 -24
  39. package/dist/components/NirField/NirField.d.ts +6 -4
  40. package/dist/components/NirField/useNirValidation.d.ts +7 -5
  41. package/dist/components/PageContainer/PageContainer.d.ts +8 -0
  42. package/dist/components/PasswordField/PasswordField.d.ts +2 -2
  43. package/dist/components/PasswordField/usePasswordFieldValidation.d.ts +2 -2
  44. package/dist/components/PhoneField/PhoneField.d.ts +960 -1938
  45. package/dist/components/PhoneField/indicatifs.d.ts +715 -8
  46. package/dist/components/PhoneField/locales.d.ts +7 -0
  47. package/dist/components/PhoneField/types.d.ts +29 -0
  48. package/dist/components/PhoneField/usePhoneFieldValidation.d.ts +45 -0
  49. package/dist/components/PhoneField/usePhoneIndicatifs.d.ts +947 -0
  50. package/dist/components/SyTextArea/composables/useSyTextAreaValidation.d.ts +2 -2
  51. package/dist/composables/unifyValidation/documentationValidationProps.d.ts +1 -1
  52. package/dist/composables/unifyValidation/useValidation.d.ts +4 -5
  53. package/dist/design-system-v3.js +2 -2
  54. package/dist/designTokens/tokens/amelipro/apLightTheme.d.ts +10 -10
  55. package/dist/designTokens/tokens/baseTokens.d.ts +18 -18
  56. package/dist/designTokens/tokens/cnam/cnamLightTheme.d.ts +10 -10
  57. package/dist/designTokens/tokens/pa/paLightTheme.d.ts +10 -10
  58. package/dist/designTokens/tokens/semanticTokens.d.ts +14 -14
  59. package/dist/{main-D8ryUoS5.js → main-C4wAktOs.js} +13718 -12991
  60. package/dist/synapse.css +1 -1
  61. package/dist/vuetifyConfig.js +1 -1
  62. package/package.json +7 -7
  63. package/src/assets/compat/_legacy-tokens.scss +91 -0
  64. package/src/assets/overrides/_utilities.scss +23 -0
  65. package/src/components/Accordion/Accordion.stories.ts +121 -1
  66. package/src/components/BackBtn/BackBtn.mdx +1 -1
  67. package/src/components/BackToTopBtn/BackToTopBtn.mdx +0 -1
  68. package/src/components/Captcha/Captcha.stories.ts +134 -31
  69. package/src/components/Captcha/Captcha.vue +95 -28
  70. package/src/components/Captcha/CaptchaForm.vue +51 -22
  71. package/src/components/Captcha/tests/Captcha.focus.spec.ts +214 -0
  72. package/src/components/Captcha/tests/Captcha.spec.ts +233 -24
  73. package/src/components/Captcha/tests/CaptchaForm.spec.ts +82 -0
  74. package/src/components/Captcha/tests/__snapshots__/Captcha.spec.ts.snap +16 -42
  75. package/src/components/Captcha/types.ts +15 -0
  76. package/src/components/Captcha/useCaptchaValidation.ts +87 -0
  77. package/src/components/Captcha/validation/validation.stories.ts +1194 -0
  78. package/src/components/ChipList/ChipList.mdx +0 -1
  79. package/src/components/CollapsibleList/CollapsibleList.mdx +0 -1
  80. package/src/components/CookieBanner/CookieBanner.mdx +0 -1
  81. package/src/components/CopyBtn/CopyBtn.mdx +0 -1
  82. package/src/components/Customs/Selects/SelectBtnField/SelectBtnField.stories.ts +123 -439
  83. package/src/components/Customs/Selects/SelectBtnField/SelectBtnField.vue +147 -41
  84. package/src/components/Customs/Selects/SelectBtnField/Validation/Validation.stories.ts +600 -0
  85. package/src/components/Customs/Selects/SelectBtnField/composables/useSelectBtnFieldValidation.ts +87 -0
  86. package/src/components/Customs/Selects/SelectBtnField/tests/SelectBtnField.spec.ts +402 -33
  87. package/src/components/Customs/Selects/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +52 -38
  88. package/src/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.stories.ts +342 -162
  89. package/src/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.vue +77 -129
  90. package/src/components/Customs/SyCheckBoxGroup/Validation/Validation.stories.ts +1008 -0
  91. package/src/components/Customs/SyCheckBoxGroup/composables/useSyCheckBoxGroupValidation.ts +107 -0
  92. package/src/components/Customs/SyCheckBoxGroup/tests/SyCheckBoxGroup.spec.ts +180 -7
  93. package/src/components/Customs/SyCheckBoxGroup/types.ts +49 -0
  94. package/src/components/Customs/SyCheckbox/SyCheckbox.stories.ts +41 -161
  95. package/src/components/Customs/SyCheckbox/SyCheckbox.vue +71 -148
  96. package/src/components/Customs/SyCheckbox/Validation/Validation.stories.ts +654 -0
  97. package/src/components/Customs/SyCheckbox/composables/useSyCheckboxValidation.ts +105 -0
  98. package/src/components/Customs/SyCheckbox/tests/SyCheckbox.spec.ts +106 -0
  99. package/src/components/Customs/SyCheckbox/tests/useSyCheckboxValidation.spec.ts +98 -0
  100. package/src/components/Customs/SyCheckbox/types.ts +51 -0
  101. package/src/components/Customs/SyTextField/FieldState.vue +50 -0
  102. package/src/components/Customs/SyTextField/SyTextField.vue +12 -9
  103. package/src/components/Customs/SyTextField/useSyTextFieldValidation.ts +2 -11
  104. package/src/components/DataList/DataList.mdx +0 -1
  105. package/src/components/DataListGroup/DataListGroup.mdx +0 -1
  106. package/src/components/DiacriticPicker/DiacriticPicker.mdx +0 -1
  107. package/src/components/DialogBox/DialogBox.mdx +0 -1
  108. package/src/components/DialogBox/DialogBox.stories.ts +399 -4
  109. package/src/components/DialogBox/DialogBox.vue +20 -0
  110. package/src/components/DialogBox/locales.ts +1 -0
  111. package/src/components/DialogBox/tests/DialogBox.spec.ts +73 -0
  112. package/src/components/DialogBox/tests/DialogBox.visual.cy.ts +24 -0
  113. package/src/components/ErrorPage/ErrorPage.mdx +1 -1
  114. package/src/components/ExternalLinks/ExternalLinks.mdx +0 -1
  115. package/src/components/FileList/FileList.mdx +0 -1
  116. package/src/components/FilterInline/FilterInline.mdx +0 -1
  117. package/src/components/FilterSideBar/FilterSideBar.mdx +8 -1
  118. package/src/components/FilterSideBar/FilterSideBar.stories.ts +133 -1
  119. package/src/components/FilterSideBar/FilterSideBar.vue +19 -2
  120. package/src/components/FilterSideBar/tests/FilterSideBar.spec.ts +55 -0
  121. package/src/components/FooterBar/FooterBar.mdx +0 -1
  122. package/src/components/FranceConnectBtn/FranceConnectBtn.mdx +0 -1
  123. package/src/components/HeaderBar/HeaderBar.mdx +0 -1
  124. package/src/components/HeaderLoading/HeaderLoading.mdx +0 -1
  125. package/src/components/LangBtn/LangBtn.mdx +0 -1
  126. package/src/components/Logo/Logo.mdx +1 -1
  127. package/src/components/LunarCalendar/LunarCalendar.mdx +6 -9
  128. package/src/components/LunarCalendar/LunarCalendar.stories.ts +243 -46
  129. package/src/components/LunarCalendar/LunarCalendar.vue +61 -26
  130. package/src/components/LunarCalendar/Validation/Validation.stories.ts +717 -0
  131. package/src/components/LunarCalendar/tests/LunarCalendar.a11y.spec.ts +1 -1
  132. package/src/components/LunarCalendar/tests/LunarCalendar.spec.ts +197 -6
  133. package/src/components/LunarCalendar/tests/useLunarCalendarValidation.spec.ts +287 -0
  134. package/src/components/LunarCalendar/types.ts +39 -0
  135. package/src/components/LunarCalendar/useLunarCalendarValidation.ts +115 -39
  136. package/src/components/MonthPicker/MonthPicker.stories.ts +38 -281
  137. package/src/components/MonthPicker/MonthPicker.vue +66 -17
  138. package/src/components/MonthPicker/MonthPickerText/MonthPickerInput.vue +44 -20
  139. package/src/components/MonthPicker/MonthPickerText/useTextField.ts +5 -0
  140. package/src/components/MonthPicker/Validation/Validation.stories.ts +1117 -0
  141. package/src/components/MonthPicker/locales.ts +1 -0
  142. package/src/components/MonthPicker/tests/MonthPicker.spec.ts +353 -2
  143. package/src/components/MonthPicker/tests/__snapshots__/MonthPicker.spec.ts.snap +12 -8
  144. package/src/components/MonthPicker/types.ts +16 -0
  145. package/src/components/MonthPicker/useMonthPickerValidation.ts +64 -27
  146. package/src/components/NirField/NirField.mdx +120 -66
  147. package/src/components/NirField/NirField.stories.ts +216 -0
  148. package/src/components/NirField/useNirValidation.ts +16 -17
  149. package/src/components/NotFoundPage/tests/__snapshots__/NotFoundPage.spec.ts.snap +263 -245
  150. package/src/components/NotificationBar/NotificationBar.mdx +0 -1
  151. package/src/components/PageContainer/PageContainer.mdx +0 -1
  152. package/src/components/PageContainer/PageContainer.stories.ts +170 -2
  153. package/src/components/PageContainer/PageContainer.vue +63 -8
  154. package/src/components/PageContainer/tests/__snapshots__/PageContainer.spec.ts.snap +19 -11
  155. package/src/components/PaginatedTable/PaginatedTable.mdx +0 -1
  156. package/src/components/PeriodField/PeriodField.mdx +0 -1
  157. package/src/components/PhoneField/PhoneField.mdx +2 -3
  158. package/src/components/PhoneField/PhoneField.stories.ts +227 -410
  159. package/src/components/PhoneField/PhoneField.vue +204 -438
  160. package/src/components/PhoneField/indicatifs.ts +1 -1
  161. package/src/components/PhoneField/locales.ts +7 -0
  162. package/src/components/PhoneField/tests/PhoneField.a11y.spec.ts +0 -1
  163. package/src/components/PhoneField/tests/PhoneField.spec.ts +517 -220
  164. package/src/components/PhoneField/types.ts +30 -0
  165. package/src/components/PhoneField/usePhoneFieldValidation.ts +119 -0
  166. package/src/components/PhoneField/usePhoneIndicatifs.ts +89 -0
  167. package/src/components/PhoneField/validation/validation.stories.ts +717 -0
  168. package/src/components/RangeField/RangeField.mdx +0 -1
  169. package/src/components/RatingPicker/RatingPicker.mdx +0 -1
  170. package/src/components/SocialMediaLinks/SocialMediaLinks.mdx +0 -1
  171. package/src/components/StatusPage/StatusPage.vue +1 -0
  172. package/src/components/StatusPage/tests/__snapshots__/StatusPage.spec.ts.snap +248 -230
  173. package/src/components/SubHeader/SubHeader.mdx +5 -6
  174. package/src/components/Tables/common/tests/SyTableFilter.spec.ts +11 -12
  175. package/src/components/UploadWorkflow/UploadWorkflow.mdx +0 -1
  176. package/src/components/UserMenuBtn/UserMenuBtn.mdx +0 -1
  177. package/src/components/UserMenuBtn/UserMenuBtn.stories.ts +177 -0
  178. package/src/composables/unifyValidation/documentationValidationProps.ts +1 -1
  179. package/src/composables/unifyValidation/tests/useValidation.spec.ts +13 -1
  180. package/src/composables/unifyValidation/useValidation.ts +37 -33
  181. package/src/composantsVuetify/VCard/VCard.mdx +4 -0
  182. package/src/composantsVuetify/VCard/v-card.stories.ts +93 -1
  183. package/src/composantsVuetify/VCarousel/VCarousel.mdx +74 -0
  184. package/src/composantsVuetify/VCarousel/v-carousel.stories.ts +531 -0
  185. package/src/composantsVuetify/VNavigationDrawer/VNavgationDrawer.mdx +53 -0
  186. package/src/composantsVuetify/VNavigationDrawer/v-navigation-drawer.stories.ts +310 -0
  187. package/src/composantsVuetify/VSlideGroup/VSlideGroup.mdx +105 -0
  188. package/src/composantsVuetify/VSlideGroup/v-slide-group.stories.ts +463 -0
  189. package/src/designTokens/tokens/baseColors.ts +1 -1
  190. package/src/designTokens/tokens/baseTokens.ts +18 -18
  191. package/src/stories/Components/Components.stories.ts +34 -1
  192. package/src/stories/Demarrer/Releases.stories.ts +16 -2
  193. package/src/stories/DesignTokens/Arrondis.mdx +1 -1
  194. package/src/stories/DesignTokens/Correspondances.mdx +219 -0
  195. package/src/stories/DesignTokens/UtiliserLesTokens.mdx +235 -0
  196. package/src/stories/DesignTokens/colors.stories.ts +569 -569
  197. package/src/stories/GuideDuDev/Amelipro.stories.ts +335 -267
  198. package/dist/components/LunarCalendar/useLunarCalendarRules.d.ts +0 -5
  199. package/dist/components/PhoneField/tests/types.d.ts +0 -18
  200. package/src/components/LunarCalendar/tests/useLunarCalendarRules.spec.ts +0 -184
  201. package/src/components/LunarCalendar/useLunarCalendarRules.ts +0 -96
  202. package/src/components/PhoneField/tests/types.d.ts +0 -19
@@ -1,426 +1,161 @@
1
1
  <script lang="ts" setup>
2
- import { computed, ref, watch, nextTick } from 'vue'
3
- import type { PropType } from 'vue'
4
- import { mdiAlertOutline, mdiCheck, mdiInformation, mdiPhone } from '@mdi/js'
5
- import { indicatifs } from './indicatifs'
6
- import { Mask } from 'maska'
7
- import { locales } from './locales'
2
+ import { computed, ref, toRef, watch } from 'vue'
3
+ import { mdiPhone, mdiCloseCircle } from '@mdi/js'
4
+ import { locales as defaultLocales } from './locales'
8
5
  import SySelect from '@/components/Customs/Selects/SySelect/SySelect.vue'
9
6
  import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue'
10
7
  import SyIcon from '@/components/Customs/SyIcon/SyIcon.vue'
11
- import { useValidation, type ValidationRule } from '@/composables/validation/useValidation'
12
- import { useValidatable } from '@/composables/validation/useValidatable'
13
-
14
- type DisplayFormat = 'code' | 'code-abbreviation' | 'code-country' | 'country' | 'abbreviation'
15
- type Indicatif = {
16
- code: string
17
- abbreviation: string
18
- country: string
19
- countryFr?: string
20
- mask?: string
21
- phoneLength: number
22
- }
23
-
24
- const props = defineProps({
25
- modelValue: { type: String, default: '' },
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
27
- dialCodeModel: { type: [String, Object] as PropType<string | Record<string, any>>, default: '' },
28
- required: { type: Boolean, default: false },
29
- outlined: { type: Boolean, default: true },
30
- outlinedIndicatif: { type: Boolean, default: true },
31
- withCountryCode: { type: Boolean, default: false },
32
- countryCodeRequired: { type: Boolean, default: false },
33
- displayFormat: { type: String as PropType<DisplayFormat>, default: 'code' },
34
- customIndicatifs: { type: Array as PropType<Indicatif[]>, default: () => [] },
35
- useCustomIndicatifsOnly: { type: Boolean, default: false },
36
- isValidatedOnBlur: { type: Boolean, default: true },
37
- displayAsterisk: { type: Boolean, default: false },
38
- disableErrorHandling: { type: Boolean, default: false },
39
- showSuccessMessages: { type: Boolean, default: false },
40
- bgColor: { type: String, default: 'white' },
41
- readonly: { type: Boolean, default: false },
42
- disabled: { type: Boolean, default: false },
43
- helpText: { type: String, default: '' },
44
- autocompleteCountryCode: { type: String, default: 'tel-country-code' },
45
- autocompletePhone: { type: String, default: 'tel-national' },
46
- withoutFieldset: { type: Boolean, default: false },
47
- })
48
-
49
- const emit = defineEmits(['update:modelValue', 'update:selectedDialCode', 'change'])
50
-
51
- const phoneNumber = ref(props.modelValue || '')
52
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
53
- const dialCode = ref<string | Record<string, any>>(props.dialCodeModel || '')
54
- // Force re-render of SySelect when needed (e.g., after reset)
55
- const dialSelectKey = ref(0)
56
- const counter = ref(10)
57
- const phoneMask = ref('## ## ## ## ##')
58
- const onBlur = ref(false)
59
-
60
- const buildDefaultMask = (length: number): string =>
61
- '#'.repeat(length || 10).replace(/(.{2})/g, '$1 ').trim()
62
-
63
- const toTrimmedDigits = (value: string, maxDigits: number): string => {
64
- return value.replace(/\D/g, '').slice(0, maxDigits)
65
- }
66
-
67
- // Cache the Mask instance — recreated only when phoneMask changes, not on every keystroke
68
- const maskInstance = computed(() => new Mask({ mask: phoneMask.value }))
69
- const applyMask = (digits: string): string => maskInstance.value.masked(digits)
70
-
71
- // phoneNumber is always masked, so this is just a stable public alias
72
- const computedValue = computed(() => phoneNumber.value)
73
-
74
- watch(() => props.modelValue, (newVal) => {
75
- if (newVal) {
76
- // Apply mask to incoming value to ensure consistent formatting
77
- const digits = toTrimmedDigits(newVal, counter.value)
78
- phoneNumber.value = applyMask(digits)
79
- }
80
- else {
81
- phoneNumber.value = ''
82
- }
83
- }, { immediate: true })
84
-
85
- const isIndicatifLike = (value: unknown): value is Indicatif => {
86
- return typeof value === 'object'
87
- && value !== null
88
- && 'code' in value
89
- && 'phoneLength' in value
90
- }
91
-
92
- watch(dialCode, async (newVal) => {
93
- // Storybook / composants parents peuvent fournir un objet indicatif "stale" (mask/phoneLength obsolètes).
94
- // On normalise donc TOUJOURS l'indicatif à partir de la liste dialCodeOptions
95
- const dialCodeValue = typeof newVal === 'object' && newVal !== null
96
- ? newVal.code
97
- : newVal
98
- const resolvedDialCode = dialCodeOptions.value.find(opt => opt.code === dialCodeValue)
99
- ?? (isIndicatifLike(newVal) ? newVal : null)
100
- const placeholdersCount = (resolvedDialCode?.mask?.match(/#/g) || []).length
101
- const normalizedDialCode = resolvedDialCode
102
- ? {
103
- ...resolvedDialCode,
104
- // Si le mask n'est pas cohérent avec phoneLength (ex: 10 placeholders mais phoneLength=9),
105
- // on reconstruit un mask de secours à partir de phoneLength.
106
- mask: resolvedDialCode.mask && placeholdersCount === resolvedDialCode.phoneLength
107
- ? resolvedDialCode.mask
108
- : buildDefaultMask(resolvedDialCode.phoneLength),
109
- }
110
- : null
111
-
112
- emit('update:selectedDialCode', normalizedDialCode ?? newVal)
113
-
114
- if (normalizedDialCode) {
115
- counter.value = normalizedDialCode.phoneLength || 10
116
- phoneMask.value = normalizedDialCode.mask || buildDefaultMask(normalizedDialCode.phoneLength)
117
- const digits = toTrimmedDigits(phoneNumber.value, counter.value)
118
- const maskedValue = applyMask(digits)
119
- phoneNumber.value = maskedValue
120
- emit('update:modelValue', maskedValue)
121
-
122
- await nextTick()
123
- await nextTick()
124
-
125
- // Le changement d'indicatif modifie les règles (longueur attendue), donc on revalide immédiatement
126
- // si une valeur est déjà saisie. Objectif: messages à jour sans nécessiter un nouveau blur.
127
- if (!shouldDisableErrorHandling.value && phoneNumber.value) {
128
- onBlur.value = true
129
- runValidation()
130
- }
131
- }
8
+ import { validationPropsDefaults } from '@/composables/unifyValidation/useValidation'
9
+ import type { PhoneFieldProps } from './types'
10
+ import { usePhoneIndicatifs } from './usePhoneIndicatifs'
11
+ import { vMaska } from 'maska/vue'
12
+ import { Mask } from 'maska'
13
+ import type { Indicatif } from './types'
14
+ import { usePhoneFieldValidation } from './usePhoneFieldValidation'
15
+ import FieldState from '@/components/Customs/SyTextField/FieldState.vue'
16
+
17
+ const props = withDefaults(defineProps<PhoneFieldProps>(), {
18
+ ...validationPropsDefaults,
19
+ modelValue: '',
20
+ dialCodeModel: '',
21
+ outlined: true,
22
+ withCountryCode: false,
23
+ displayFormat: 'code',
24
+ customIndicatifs: () => [],
25
+ useCustomIndicatifsOnly: false,
26
+ displayAsterisk: true,
27
+ bgColor: 'white',
28
+ helpText: '',
29
+ autocompleteCountryCode: 'tel-country-code',
30
+ autocompletePhone: 'tel-national',
31
+ withoutFieldset: false,
32
+ isClearable: false,
33
+ locales: () => defaultLocales,
132
34
  })
133
35
 
134
- /**
135
- * Calcule la position ajustée du curseur en tenant compte des espaces ajoutés par le masque
136
- * @param cursorPosition - Position originale du curseur
137
- * @param originalValue - Valeur avant application du masque
138
- * @param maskedValue - Valeur après application du masque
139
- * @returns Position ajustée du curseur
140
- */
141
- const calculateAdjustedPosition = (cursorPosition: number, originalValue: string, maskedValue: string): number => {
142
- // Compte combien de caractères non-espace se trouvent avant la position du curseur dans la valeur originale
143
- const digitsBeforeCursor = originalValue.substring(0, cursorPosition).replace(/\s/g, '').length
144
-
145
- // Parcours la valeur masquée pour trouver la position qui contient le même nombre de caractères non-espace
146
- let newPosition = 0
147
- let digitCount = 0
148
-
149
- for (let i = 0; i < maskedValue.length; i++) {
150
- if (maskedValue[i] !== ' ') {
151
- digitCount++
152
- }
153
-
154
- if (digitCount > digitsBeforeCursor) {
155
- break
156
- }
157
-
158
- newPosition = i + 1
159
- }
160
-
161
- return newPosition
162
- }
163
-
164
- const handlePhoneInput = (event: Event) => {
165
- const inputElement = event.target as HTMLInputElement
166
- const input = inputElement.value
167
-
168
- // Sauvegarder la position du curseur
169
- const cursorPosition = inputElement.selectionStart || 0
170
-
171
- // Appliquer le masque (en tronquant au nombre de chiffres attendu)
172
- const digits = toTrimmedDigits(input, counter.value)
173
- const maskedValue = applyMask(digits)
174
-
175
- // Mettre à jour la valeur
176
- phoneNumber.value = maskedValue
177
- emit('update:modelValue', maskedValue)
178
-
179
- // Restaurer la position du curseur sur le prochain cycle de rendu
180
- nextTick(() => {
181
- const adjustedPosition = calculateAdjustedPosition(cursorPosition, input, maskedValue)
182
- inputElement.setSelectionRange(adjustedPosition, adjustedPosition)
183
- })
184
- }
185
-
186
- const handlePhoneModelUpdate = (value: string | number | null) => {
187
- const digits = toTrimmedDigits(String(value ?? ''), counter.value)
188
- const maskedValue = applyMask(digits)
189
- phoneNumber.value = maskedValue
190
- emit('update:modelValue', maskedValue)
191
- }
192
-
193
- const handlePhoneKeydown = (event: KeyboardEvent) => {
194
- if (counter.value <= 0) return
195
- if (!event.key || !/\d/.test(event.key)) return
196
-
197
- const inputElement = event.target as HTMLInputElement | null
198
- const selectionStart = inputElement?.selectionStart ?? null
199
- const selectionEnd = inputElement?.selectionEnd ?? null
200
- const hasSelection
201
- = selectionStart !== null
202
- && selectionEnd !== null
203
- && selectionEnd > selectionStart
204
-
205
- const currentDigitsCount = phoneNumber.value.replace(/\D/g, '').length
206
- if (currentDigitsCount >= counter.value && !hasSelection) {
207
- event.preventDefault()
208
- }
209
- }
210
-
211
- const mergedDialCodes = computed(() =>
212
- props.useCustomIndicatifsOnly ? props.customIndicatifs : [...indicatifs, ...props.customIndicatifs],
213
- )
214
-
215
- const dialCodeOptions = computed(() =>
216
- mergedDialCodes.value.map(ind => ({
217
- ...ind,
218
- displayText: generateDisplayText(ind),
219
- plainDisplayText: generateDisplayText(ind, true),
220
- })),
36
+ const emits = defineEmits<{
37
+ 'update:modelValue': [value: string]
38
+ 'update:dialCodeModel': [value: Indicatif | string | undefined]
39
+ }>()
40
+
41
+ const phoneNumber = ref<string>(props.modelValue)
42
+ const { internalDialCode, dialCodeList } = usePhoneIndicatifs(
43
+ toRef(props, 'dialCodeModel'),
44
+ toRef(props, 'displayFormat'),
45
+ toRef(props, 'customIndicatifs'),
46
+ toRef(props, 'useCustomIndicatifsOnly'),
221
47
  )
222
48
 
223
- watch(() => props.readonly, () => {
224
- if (onBlur.value && !shouldDisableErrorHandling.value) {
225
- runValidation()
226
- }
49
+ watch (phoneNumber, (newVal) => {
50
+ emits('update:modelValue', newVal)
227
51
  })
228
52
 
229
- const getFranceDefault = () =>
230
- dialCodeOptions.value.find(opt => opt.code === '+33') ?? ''
231
-
232
- // Watcher pour initialiser dialCode à partir de props.dialCodeModel
233
- watch(() => props.dialCodeModel, (newVal) => {
234
- if (!newVal) {
235
- // Par défaut, pré-sélectionner la France (+33) quand l'indicatif est activé
236
- dialCode.value = props.withCountryCode ? getFranceDefault() : ''
237
- return
53
+ watch (() => props.modelValue, (newVal) => {
54
+ if (newVal !== phoneNumber.value) {
55
+ phoneNumber.value = newVal || ''
238
56
  }
239
-
240
- if (typeof newVal === 'object') {
241
- const matchingOption = dialCodeOptions.value.find(opt => opt.code === newVal.code)
242
- dialCode.value = matchingOption ?? newVal
243
- }
244
- else {
245
- dialCode.value = newVal
246
- }
247
- }, { immediate: true })
248
-
249
- function generateDisplayText(ind: Indicatif, plain = false): string {
250
- const countryName = ind.countryFr || ind.country
251
- const abbr = plain ? ind.abbreviation : `<abbr title="${countryName}">${ind.abbreviation}</abbr>`
252
- const format: Record<DisplayFormat, string> = {
253
- 'code': ind.code,
254
- 'code-abbreviation': `${ind.code} (${abbr})`,
255
- 'code-country': `${ind.code} ${countryName}`,
256
- 'country': countryName,
257
- 'abbreviation': abbr,
258
- }
259
- return format[props.displayFormat] ?? ind.code
260
- }
261
-
262
- const phoneFieldIdentifier = computed(() => props.withCountryCode
263
- ? locales.phoneNumberWithoutCountryLabel
264
- : locales.label,
265
- )
266
-
267
- const validationRules = computed<ValidationRule[]>(() => {
268
- const rules = [{
269
- type: 'exactLength',
270
- options: {
271
- length: counter.value,
272
- ignoreSpace: true,
273
- message: `Le numéro de téléphone doit contenir ${counter.value} chiffres.`,
274
- successMessage: `Le champ ${phoneFieldIdentifier.value} est valide.`,
275
- fieldIdentifier: phoneFieldIdentifier.value,
276
- },
277
- }] as ValidationRule[]
278
-
279
- if (props.required) {
280
- rules.unshift({
281
- type: 'required',
282
- options: {
283
- length: counter.value,
284
- ignoreSpace: true,
285
- message: `Le champ ${phoneFieldIdentifier.value} est requis.`,
286
- fieldIdentifier: phoneFieldIdentifier.value,
287
- },
288
- })
289
- }
290
-
291
- return rules
292
57
  })
293
58
 
294
- const shouldDisableErrorHandling = computed(() => props.disableErrorHandling || props.readonly)
295
-
296
- // When disabling error handling, immediately clear any existing validation state
297
- watch(shouldDisableErrorHandling, (disabled) => {
298
- if (disabled) {
299
- validation.clearValidation()
300
- }
59
+ watch (internalDialCode, (newVal) => {
60
+ emits('update:dialCodeModel', newVal)
301
61
  })
302
62
 
303
- const validation = useValidation({
304
- showSuccessMessages: props.showSuccessMessages,
305
- disableErrorHandling: shouldDisableErrorHandling.value,
306
- })
307
-
308
- const hasError = computed(() => !shouldDisableErrorHandling.value && validation.hasError.value)
309
- const hasWarning = computed(() => !shouldDisableErrorHandling.value && validation.hasWarning.value)
310
- const hasSuccess = computed(() =>
311
- !shouldDisableErrorHandling.value
312
- && !hasError.value
313
- && !hasWarning.value
314
- && validation.hasSuccess.value,
315
- )
316
-
317
- const iconColor = computed(() => {
318
- if (shouldDisableErrorHandling.value) return '#222324'
319
- if (hasError.value) return 'error'
320
- if (hasWarning.value) return 'warning'
321
- if (hasSuccess.value) return 'success'
322
- return '#222324'
323
- })
324
-
325
- const errors = computed(() => shouldDisableErrorHandling.value ? [] : validation.errors.value)
326
- const warnings = computed(() => shouldDisableErrorHandling.value ? [] : validation.warnings.value)
327
- const successes = computed(() =>
328
- shouldDisableErrorHandling.value || hasError.value || hasWarning.value
329
- ? []
330
- : validation.displaySuccesses.value,
331
- )
332
-
333
63
  const showHelpTextBelow = computed(() => !!props.helpText?.trim())
64
+ const focused = ref(false)
334
65
 
335
- const runValidation = async (): Promise<void> => {
336
- const cleanedValue = phoneNumber.value.replace(/\s/g, '')
337
- await validation.validateField(cleanedValue, validationRules.value)
338
- }
339
-
340
- function validateInputOnBlur() {
341
- emit('change', phoneNumber.value)
342
-
343
- if (!props.isValidatedOnBlur || shouldDisableErrorHandling.value) return
66
+ const {
67
+ errors,
68
+ warnings,
69
+ successes,
70
+ hasError,
71
+ hasWarning,
72
+ hasSuccess,
73
+ state,
74
+ iconColor,
75
+ validate,
76
+ clearValidation,
77
+ } = usePhoneFieldValidation({
78
+ modelValue: phoneNumber,
79
+ readonly: toRef(props, 'readonly'),
80
+ disabled: toRef(props, 'disabled'),
81
+ required: toRef(props, 'required'),
82
+ counter: computed(() => internalDialCode.value.phoneLength || 10),
83
+ phoneFieldIdentifier: computed(() => props.withCountryCode ? props.locales?.phoneNumberWithoutCountryLabel || 'Numéro de téléphone' : props.locales?.label || 'Téléphone'),
84
+ shouldDisableErrorHandling: computed(() => props.disableErrorHandling || props.readonly),
85
+ hasError: toRef(props, 'hasError'),
86
+ hasWarning: toRef(props, 'hasWarning'),
87
+ hasSuccess: toRef(props, 'hasSuccess'),
88
+ showSuccessMessages: toRef(props, 'showSuccessMessages'),
89
+ disableErrorHandling: toRef(props, 'disableErrorHandling'),
90
+ isValidateOnBlur: toRef(props, 'isValidateOnBlur'),
91
+ focused,
92
+ customRules: toRef(props, 'customRules'),
93
+ warningRules: toRef(props, 'customWarningRules'),
94
+ successRules: toRef(props, 'customSuccessRules'),
95
+ rules: toRef(props, 'rules'),
96
+ errorMessages: toRef(props, 'errorMessages'),
97
+ warningMessages: toRef(props, 'warningMessages'),
98
+ successMessages: toRef(props, 'successMessages'),
99
+ locales: toRef(props, 'locales'),
100
+ dialCode: internalDialCode,
101
+ withCountryCode: toRef(props, 'withCountryCode'),
102
+ })
344
103
 
345
- onBlur.value = true
346
- runValidation()
104
+ const validation = {
105
+ clearValidation,
106
+ errors,
107
+ warnings,
108
+ successes,
109
+ hasError,
110
+ hasWarning,
111
+ hasSuccess,
347
112
  }
348
113
 
349
- watch(phoneNumber, async (newValue) => {
350
- if (shouldDisableErrorHandling.value) return
114
+ const phoneMask = computed(() => internalDialCode.value.mask)
351
115
 
352
- if (!props.isValidatedOnBlur) {
353
- // Validation en temps réel (isValidatedOnBlur=false)
354
- const cleanedValue = newValue.replace(/\s/g, '')
355
- await validation.validateField(cleanedValue, validationRules.value)
356
- }
357
- else if (onBlur.value) {
358
- // Après un premier blur, effacer les erreurs pendant la frappe —
359
- // la revalidation se fera au prochain blur (comme SyTextField)
360
- validation.clearValidation()
361
- }
362
- })
116
+ // Rattrape l'autofill du navigateur : avec un champ indicatif séparé, le navigateur
117
+ // remplit `tel-national` sans le préfixe national « 0 » (ex. « 612345678 »). On le détecte
118
+ // via un saut du nombre de chiffres ( frappe au clavier) et on repréfixe « 0 ».
119
+ const maskInstance = computed(() => new Mask({ mask: phoneMask.value }))
120
+ const previousDigitCount = ref(0)
363
121
 
364
- watch(validationRules, () => {
365
- if (onBlur.value && !shouldDisableErrorHandling.value) {
366
- runValidation()
367
- }
368
- })
122
+ function handleNumberInput(event: Event) {
123
+ const target = event.target as HTMLInputElement | null
124
+ if (!target) return
369
125
 
370
- /**
371
- * Valide le champ lors de la soumission d'un formulaire
372
- * @returns Promise<boolean> - true si le champ est valide, false sinon
373
- */
374
- const validateOnSubmit = async (): Promise<boolean> => {
375
- if (shouldDisableErrorHandling.value) {
376
- return true
377
- }
126
+ const digits = target.value.replace(/\D/g, '')
127
+ const jumped = digits.length - previousDigitCount.value > 1
128
+ previousDigitCount.value = digits.length
378
129
 
379
- onBlur.value = true
380
- await runValidation()
381
-
382
- if (props.withCountryCode && props.countryCodeRequired && !dialCode.value) {
383
- validation.errors.value.push(`Le champ ${locales.indicatifLabel} est requis.`)
130
+ const expectedLength = internalDialCode.value.phoneLength || 10
131
+ if (props.withCountryCode && jumped && digits.length === expectedLength - 1 && !digits.startsWith('0')) {
132
+ const normalized = maskInstance.value.masked(`0${digits}`)
133
+ phoneNumber.value = normalized
134
+ previousDigitCount.value = expectedLength
384
135
  }
385
-
386
- return !validation.hasError.value
387
136
  }
388
137
 
389
- // Reset hook used by SyForm.reset() via useValidatable
390
- const reset = () => {
391
- // Reset interaction state and validation FIRST to avoid triggering watchers with errors
392
- onBlur.value = false
393
- validation.clearValidation()
138
+ const showClear = computed(() => {
139
+ if (!props.isClearable) return false
140
+ if (props.disabled) return false
141
+ return phoneNumber.value !== undefined && phoneNumber.value !== null && String(phoneNumber.value) !== ''
142
+ })
394
143
 
395
- // Clear content
144
+ function clearField() {
396
145
  phoneNumber.value = ''
397
- emit('update:modelValue', '')
398
-
399
- // Reset dial code : France par défaut si indicatif activé, sinon vide
400
- const defaultDialCode = props.withCountryCode ? getFranceDefault() : ''
401
- dialCode.value = defaultDialCode
402
- emit('update:selectedDialCode', defaultDialCode)
403
- counter.value = 10
404
- phoneMask.value = '## ## ## ## ##'
405
-
406
- // Force SySelect to be recreated to ensure internal classes are reset
407
- dialSelectKey.value++
146
+ clearValidation()
408
147
  }
409
148
 
410
- // Intégration avec le système de validation du formulaire
411
- useValidatable(validateOnSubmit, validation.clearValidation, reset)
412
-
413
149
  defineExpose({
414
- computedValue,
415
- dialCode,
416
- phoneMask,
417
- counter,
150
+ dialCodeList,
418
151
  hasError,
419
- phoneNumber,
420
- mergedDialCodes,
152
+ errors,
421
153
  validation,
422
- validateOnSubmit,
154
+ validateOnSubmit: validate,
155
+ phoneMask,
156
+ clearValidation,
423
157
  })
158
+
424
159
  </script>
425
160
 
426
161
  <template>
@@ -440,24 +175,20 @@
440
175
  class="phone-field-country"
441
176
  >
442
177
  <SySelect
443
- :key="dialSelectKey"
444
- v-model="dialCode"
445
- :items="dialCodeOptions"
178
+ v-model="internalDialCode"
179
+ :items="dialCodeList"
446
180
  :label="locales.indicatifLabel"
447
- :outlined="outlinedIndicatif"
448
- :required="countryCodeRequired"
449
- :aria-required="countryCodeRequired"
450
- :error="!!errors[1]"
451
- :error-messages="errors[1] ? [errors[1]] : []"
452
- :display-asterisk="displayAsterisk"
453
- :disable-error-handling="shouldDisableErrorHandling"
181
+ :outlined="props.outlined"
182
+ :aria-required="true"
183
+ :display-asterisk="props.displayAsterisk"
184
+ :disable-error-handling="props.disableErrorHandling || props.readonly"
185
+ :bg-color="props.bgColor"
186
+ :readonly="props.readonly"
187
+ :disabled="props.disabled"
188
+ :autocomplete="props.autocompleteCountryCode"
454
189
  :return-object="true"
455
- :bg-color="bgColor"
456
- :readonly="readonly"
457
- :disabled="disabled"
458
- :allow-html="displayFormat === 'code-abbreviation' || displayFormat === 'abbreviation'"
459
- :autocomplete="autocompleteCountryCode"
460
- class="custom-select mr-0 mr-sm-4"
190
+ :allow-html="true"
191
+ class="dial-code-select mr-0 mr-sm-4"
461
192
  text-key="displayText"
462
193
  plain-text-key="plainDisplayText"
463
194
  value-key="code"
@@ -465,25 +196,26 @@
465
196
  </div>
466
197
  <div class="phone-field-number">
467
198
  <SyTextField
468
- ref="phoneField"
469
- :model-value="phoneNumber"
470
- :counter="counter"
199
+ v-model="phoneNumber"
200
+ v-maska="internalDialCode.mask"
201
+ :counter="internalDialCode.phoneLength"
471
202
  :counter-value="(value: string) => value.replace(/\D/g, '').length"
472
203
  :label="withCountryCode ? locales.phoneNumberWithoutCountryLabel : locales.label"
473
- :required="required"
474
- :aria-required="required"
204
+ :required="props.required"
205
+ :aria-required="props.required"
475
206
  :error="hasError"
476
207
  :error-messages="errors"
477
208
  :warning-messages="warnings"
478
209
  :success-messages="successes"
479
210
  :show-success-messages="props.showSuccessMessages"
480
- :disable-error-handling="shouldDisableErrorHandling"
481
- :variant="outlined ? 'outlined' : 'underlined'"
482
- :display-asterisk="displayAsterisk"
483
- :readonly="readonly"
484
- :bg-color="bgColor"
485
- :disabled="disabled"
486
- :autocomplete="autocompletePhone"
211
+ :disable-error-handling="props.disableErrorHandling || props.readonly"
212
+ :variant="props.outlined ? 'outlined' : 'underlined'"
213
+ :display-asterisk="props.displayAsterisk"
214
+ :readonly="props.readonly"
215
+ :bg-color="props.bgColor"
216
+ :disabled="props.disabled"
217
+ :hide-details="props.hideDetails"
218
+ :autocomplete="props.autocompletePhone"
487
219
  :class="{
488
220
  'phone-field': true,
489
221
  'error-field': hasError,
@@ -492,30 +224,31 @@
492
224
  }"
493
225
  color="primary"
494
226
  type="tel"
495
- @blur="validateInputOnBlur"
496
- @update:model-value="handlePhoneModelUpdate"
497
- @input="handlePhoneInput"
498
- @keydown="handlePhoneKeydown"
227
+ @input="handleNumberInput"
228
+ @focus="focused = true"
229
+ @blur="focused = false"
499
230
  >
500
231
  <template #append-inner>
501
232
  <div class="d-flex align-center">
502
- <SyIcon
503
- v-if="hasError"
504
- color="error"
505
- :icon="mdiInformation"
506
- decorative
507
- />
508
- <SyIcon
509
- v-else-if="hasWarning"
510
- color="warning"
511
- :icon="mdiAlertOutline"
512
- decorative
513
- />
514
- <SyIcon
515
- v-else-if="hasSuccess"
516
- color="success"
517
- :icon="mdiCheck"
518
- decorative
233
+ <button
234
+ v-if="showClear"
235
+ type="button"
236
+ class="phone-field__clear-button mr-1"
237
+ :aria-label="props.label ? locales.clearButtonAriaLabelWithField(props.label) : locales.clearButtonAriaLabel"
238
+ :title="props.label ? locales.clearButtonTitleWithField(props.label) : locales.clearButtonTitle"
239
+ @click.stop="clearField"
240
+ @keydown.enter.stop
241
+ @keydown.space.stop
242
+ >
243
+ <SyIcon
244
+ class="phone-field__clear-icon"
245
+ :icon="mdiCloseCircle"
246
+ :decorative="true"
247
+ width="24"
248
+ />
249
+ </button>
250
+ <FieldState
251
+ :state="state"
519
252
  />
520
253
  <SyIcon
521
254
  class="ml-2"
@@ -598,7 +331,7 @@
598
331
  flex: 1 1 auto;
599
332
  }
600
333
 
601
- .custom-select {
334
+ .dial-code-select {
602
335
  margin-bottom: 0;
603
336
  min-width: 144px;
604
337
  }
@@ -630,4 +363,37 @@
630
363
  opacity: 0.38;
631
364
  }
632
365
  }
366
+
367
+ .phone-field__clear-button {
368
+ background: transparent;
369
+ border: none;
370
+ padding: 0;
371
+ cursor: pointer;
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+
376
+ .v-icon {
377
+ position: static;
378
+ }
379
+ }
380
+
381
+ .phone-field__clear-icon {
382
+ color: rgb(var(--v-theme-onSurface)) !important;
383
+ opacity: var(--v-medium-emphasis-opacity) !important;
384
+ }
385
+
386
+ /* Icône de validation (état) : atténuée à ~0.6 sur PhoneField */
387
+ .phone-field-number :deep(.field-state-icon .v-icon__svg) {
388
+ opacity: 0.6 !important;
389
+ }
390
+
391
+ /* …sauf en erreur, où l'icône reste à pleine opacité comme sur les autres composants */
392
+ .phone-field-number :deep(.error-icon .v-icon__svg) {
393
+ opacity: 1 !important;
394
+ }
395
+
396
+ :deep(.phone-field__clear-icon .v-icon__svg) {
397
+ fill: rgb(var(--v-theme-onSurface)) !important;
398
+ }
633
399
  </style>