@cnamts/synapse 0.0.7-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 (198) hide show
  1. package/dist/design-system-v3.d.ts +785 -372
  2. package/dist/design-system-v3.js +4993 -3357
  3. package/dist/design-system-v3.umd.cjs +1 -10
  4. package/dist/style.css +1 -1
  5. package/package.json +10 -2
  6. package/src/assets/settings.scss +2 -2
  7. package/src/assets/tokens.scss +107 -112
  8. package/src/components/BackBtn/BackBtn.vue +4 -4
  9. package/src/components/BackToTopBtn/BackToTopBtn.vue +1 -0
  10. package/src/components/CollapsibleList/CollapsibleList.mdx +1 -1
  11. package/src/components/CollapsibleList/CollapsibleList.vue +43 -44
  12. package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
  13. package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
  14. package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
  15. package/src/components/ContextualMenu/ContextualMenu.mdx +118 -0
  16. package/src/components/ContextualMenu/ContextualMenu.stories.ts +430 -0
  17. package/src/components/ContextualMenu/ContextualMenu.vue +101 -0
  18. package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
  19. package/src/components/ContextualMenu/tests/ContextualMenu.spec.ts +115 -0
  20. package/src/components/ContextualMenu/tests/__snapshots__/ContextualMenu.spec.ts.snap +10 -0
  21. package/src/components/ContextualMenu/types.ts +5 -0
  22. package/src/components/CookieBanner/CookieBanner.stories.ts +1 -2
  23. package/src/components/CookieBanner/CookieBanner.vue +13 -10
  24. package/src/components/CookieBanner/tests/__snapshots__/CookieBanner.spec.ts.snap +17 -15
  25. package/src/components/CookiesSelection/CookiesInformation/CookiesInformation.vue +6 -1
  26. package/src/components/CookiesSelection/CookiesInformation/locales.ts +1 -0
  27. package/src/components/CookiesSelection/CookiesTable/CookiesTable.vue +1 -0
  28. package/src/components/CookiesSelection/tests/__snapshots__/CookiesSelection.spec.ts.snap +17 -15
  29. package/src/components/CopyBtn/CopyBtn.vue +7 -7
  30. package/src/components/Customs/SyBtnSelect/SyBtnSelect.vue +26 -26
  31. package/src/components/Customs/SyInputSelect/SyInputSelect.vue +24 -24
  32. package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
  33. package/src/components/Customs/SySelect/SySelect.vue +36 -30
  34. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
  35. package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
  36. package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
  37. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
  38. package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
  39. package/src/components/Customs/SyTextField/types.d.ts +2 -2
  40. package/src/components/DataList/DataList.stories.ts +3 -2
  41. package/src/components/DataList/DataList.vue +1 -1
  42. package/src/components/DataListGroup/DataListGroup.stories.ts +3 -2
  43. package/src/components/DataListItem/DataListItem.vue +12 -12
  44. package/src/components/DatePicker/DatePicker.mdx +191 -0
  45. package/src/components/DatePicker/DatePicker.stories.ts +787 -0
  46. package/src/components/DatePicker/DatePicker.vue +560 -0
  47. package/src/components/DatePicker/DateTextInput.vue +409 -0
  48. package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
  49. package/src/components/DialogBox/DialogBox.mdx +28 -2
  50. package/src/components/DialogBox/DialogBox.stories.ts +2 -2
  51. package/src/components/DialogBox/DialogBox.vue +3 -2
  52. package/src/components/DownloadBtn/DownloadBtn.vue +2 -1
  53. package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
  54. package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
  55. package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
  56. package/src/components/ExternalLinks/ExternalLinks.mdx +86 -0
  57. package/src/components/ExternalLinks/ExternalLinks.stories.ts +553 -0
  58. package/src/components/ExternalLinks/ExternalLinks.vue +200 -0
  59. package/src/components/ExternalLinks/config.ts +34 -0
  60. package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
  61. package/src/components/ExternalLinks/locales.ts +4 -0
  62. package/src/components/ExternalLinks/tests/ExternalLinks.spec.ts +154 -0
  63. package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +159 -0
  64. package/src/components/FileUpload/FileUpload.mdx +165 -0
  65. package/src/components/FileUpload/FileUpload.stories.ts +429 -0
  66. package/src/components/FileUpload/FileUpload.vue +195 -0
  67. package/src/components/FileUpload/FileUploadContent.vue +109 -0
  68. package/src/components/FileUpload/locales.ts +10 -0
  69. package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
  70. package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
  71. package/src/components/FileUpload/useFileDrop.ts +23 -0
  72. package/src/components/FileUpload/validateFiles.ts +39 -0
  73. package/src/components/FooterBar/FooterBar.vue +105 -80
  74. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +14 -13
  75. package/src/components/HeaderBar/HeaderBar.vue +3 -3
  76. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderBurgerMenu.vue +11 -7
  77. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuItem/HeaderMenuItem.vue +5 -5
  78. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuSection/HeaderMenuSection.vue +2 -2
  79. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderSubMenu/HeaderSubMenu.vue +10 -8
  80. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +2 -2
  81. package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +2 -1
  82. package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +2 -1
  83. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +10 -10
  84. package/src/components/HeaderBar/consts.scss +1 -1
  85. package/src/components/HeaderLoading/HeaderLoading.vue +12 -11
  86. package/src/components/HeaderNavigationBar/HeaderNavigationBar.vue +2 -1
  87. package/src/components/HeaderNavigationBar/HorizontalNavbar/HorizontalNavbar.vue +9 -9
  88. package/src/components/HeaderToolbar/HeaderToolbar.vue +215 -202
  89. package/src/components/LangBtn/LangBtn.vue +8 -6
  90. package/src/components/LogoBrandSection/LogoBrandSection.stories.ts +2 -2
  91. package/src/components/NirField/NirField.stories.ts +8 -8
  92. package/src/components/NirField/NirField.vue +46 -48
  93. package/src/components/NotFoundPage/NotFoundPage.stories.ts +33 -2
  94. package/src/components/NotFoundPage/NotFoundPage.vue +17 -0
  95. package/src/components/NotificationBar/NotificationBar.mdx +5 -5
  96. package/src/components/NotificationBar/NotificationBar.stories.ts +410 -314
  97. package/src/components/NotificationBar/NotificationBar.vue +43 -41
  98. package/src/components/PageContainer/PageContainer.vue +4 -4
  99. package/src/components/PasswordField/Accessibilite.mdx +14 -0
  100. package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
  101. package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
  102. package/src/components/PasswordField/PasswordField.mdx +70 -0
  103. package/src/components/PasswordField/PasswordField.stories.ts +213 -0
  104. package/src/components/PasswordField/PasswordField.vue +189 -0
  105. package/src/components/PasswordField/config.ts +11 -0
  106. package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
  107. package/src/components/PasswordField/locales.ts +4 -0
  108. package/src/components/PasswordField/tests/PasswordField.spec.ts +96 -0
  109. package/src/components/PhoneField/PhoneField.mdx +0 -2
  110. package/src/components/PhoneField/PhoneField.stories.ts +10 -50
  111. package/src/components/PhoneField/PhoneField.vue +77 -93
  112. package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
  113. package/src/components/RangeField/RangeField.mdx +54 -0
  114. package/src/components/RangeField/RangeField.stories.ts +189 -0
  115. package/src/components/RangeField/RangeField.vue +157 -0
  116. package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
  117. package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
  118. package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
  119. package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
  120. package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
  121. package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
  122. package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
  123. package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
  124. package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
  125. package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
  126. package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
  127. package/src/components/RangeField/RangeSlider/types.ts +15 -0
  128. package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
  129. package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
  130. package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
  131. package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
  132. package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
  133. package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
  134. package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
  135. package/src/components/RangeField/config.ts +7 -0
  136. package/src/components/RangeField/locales.ts +4 -0
  137. package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
  138. package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
  139. package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
  140. package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
  141. package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
  142. package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
  143. package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
  144. package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
  145. package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
  146. package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
  147. package/src/components/RatingPicker/Rating.ts +45 -0
  148. package/src/components/RatingPicker/RatingPicker.mdx +56 -0
  149. package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
  150. package/src/components/RatingPicker/RatingPicker.vue +122 -0
  151. package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
  152. package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
  153. package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
  154. package/src/components/RatingPicker/locales.ts +3 -0
  155. package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
  156. package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
  157. package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
  158. package/src/components/SearchListField/SearchListField.mdx +74 -0
  159. package/src/components/SearchListField/SearchListField.stories.ts +126 -0
  160. package/src/components/SearchListField/SearchListField.vue +194 -0
  161. package/src/components/SearchListField/locales.ts +5 -0
  162. package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
  163. package/src/components/SearchListField/types.d.ts +4 -0
  164. package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
  165. package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
  166. package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
  167. package/src/components/SelectBtnField/config.ts +11 -0
  168. package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
  169. package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
  170. package/src/components/SelectBtnField/types.d.ts +11 -0
  171. package/src/components/SkipLink/SkipLink.vue +10 -10
  172. package/src/components/SocialMediaLinks/SocialMediaLinks.vue +28 -26
  173. package/src/components/SubHeader/SubHeader.vue +32 -31
  174. package/src/components/SyAlert/SyAlert.vue +12 -12
  175. package/src/components/UserMenuBtn/UserMenuBtn.vue +1 -1
  176. package/src/components/UserMenuBtn/config.ts +1 -1
  177. package/src/components/index.ts +17 -7
  178. package/src/composables/rules/useFieldValidation.ts +172 -44
  179. package/src/designTokens/index.ts +6 -4
  180. package/src/designTokens/{bootstrapColors.md → paColors.md} +1 -1
  181. package/src/designTokens/tokens/cnam/cnamLightTheme.ts +2 -0
  182. package/src/designTokens/tokens/pa/paColors.ts +171 -0
  183. package/src/designTokens/tokens/pa/paContextual.ts +58 -0
  184. package/src/designTokens/tokens/pa/paDarkTheme.ts +5 -0
  185. package/src/designTokens/tokens/pa/paLightTheme.ts +123 -0
  186. package/src/designTokens/tokens/pa/paSemantic.ts +87 -0
  187. package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
  188. package/src/stories/GuideDuDev/CreerUneIssue.mdx +64 -0
  189. package/src/stories/GuideDuDev/{CommentUtiliserLesRules.mdx → UtiliserLesRules.mdx} +2 -2
  190. package/src/stories/GuideDuDev/components.stories.ts +9 -7
  191. package/src/stories/Guidelines/Vuetify/Vuetify.stories.ts +163 -88
  192. package/src/stories/Guidelines/Vuetify/VuetifyItems.ts +250 -23
  193. package/src/temp/TestDTComponent.vue +5 -6
  194. package/src/utils/calcHumanFileSize/index.ts +12 -0
  195. package/src/utils/calcHumanFileSize/tests/calcHumanFileSize.spec.ts +21 -0
  196. package/src/designTokens/tokens/bootstrap/bootstrapColors.ts +0 -158
  197. package/src/designTokens/tokens/bootstrap/bootstrapLightTheme.ts +0 -22
  198. package/src/stories/GuideDuDev/CommentContribuer.mdx +0 -22
@@ -0,0 +1,74 @@
1
+ import {Canvas, Meta, Controls, Source} from '@storybook/blocks';
2
+ import * as SearchListFieldStories from './SearchListField.stories';
3
+ import SearchListField from './SearchListField.vue';
4
+
5
+ <Meta title="Composants/Formulaires/SearchListField" component={SearchListField}/>
6
+
7
+ # SearchListField
8
+
9
+ Le composant `SearchListField` est utilisé pour afficher un champ de saisie de mot de passe et gérer sa validation.
10
+ Il permet également d’afficher ou de masquer le contenu du champ à l’aide d’une icône.
11
+
12
+ <Canvas story={{height: '150px'}} of={SearchListFieldStories.Default}/>
13
+
14
+ # API
15
+
16
+ <Controls of={SearchListFieldStories.Default}/>
17
+
18
+ ## Utilisation de base
19
+
20
+ <Source
21
+ dark
22
+ code={`
23
+ <script setup lang="ts">
24
+ import { ref } from 'vue'
25
+ import { SearchListField } from '@cnamts/synapse'
26
+
27
+ import type { SearchListItem } from '@cnamts/synapse/src/components/SearchListField/types'
28
+
29
+ const items: SearchListItem[] = [
30
+ {
31
+ label: 'Chirurgien-dentiste',
32
+ value: 'chirurgien-dentiste',
33
+ },
34
+ {
35
+ label: 'Infirmier',
36
+ value: 'infirmier',
37
+ },
38
+ {
39
+ label: 'Orthophoniste',
40
+ value: 'orthophoniste',
41
+ },
42
+ {
43
+ label: 'Orthoptiste',
44
+ value: 'orthoptiste',
45
+ },
46
+ {
47
+ label: 'Pédicure-podologue',
48
+ value: 'pedicure-podologue',
49
+ },
50
+ {
51
+ label: 'Pharmacien',
52
+ value: 'pharmacien',
53
+ },
54
+ ]
55
+
56
+ const value = ref<string | null>(null)
57
+
58
+ const selectChoice = (choice: string) => {
59
+ value.value = choice
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <SearchListField
65
+ :items="items"
66
+ @update:model-value="($event) => selectChoice($event)"
67
+ />
68
+ <br>
69
+ <pre>Selected: {{ value }}</pre>
70
+ </template>
71
+ `}
72
+ />
73
+
74
+
@@ -0,0 +1,126 @@
1
+ import type { StoryObj, Meta } from '@storybook/vue3'
2
+ import SearchListField from './SearchListField.vue'
3
+
4
+ const meta = {
5
+ title: 'Composants/Formulaires/SearchListField',
6
+ component: SearchListField,
7
+ parameters: {
8
+ layout: 'fullscreen',
9
+ controls: { exclude: ['filteredItems', 'search', 'emitChangeEvent'] },
10
+ },
11
+ decorators: [
12
+ () => ({
13
+ template: '<div style="padding: 20px;"><story/></div>',
14
+ }),
15
+ ],
16
+ argTypes: {
17
+ modelValue: {
18
+ default: '[]',
19
+ },
20
+ items: { control: { type: 'object' } },
21
+ },
22
+ } satisfies Meta<typeof SearchListField>
23
+
24
+ export default meta
25
+
26
+ type Story = StoryObj<typeof meta>
27
+
28
+ /**
29
+ * Story par défaut
30
+ */
31
+ export const Default: Story = {
32
+ args: {
33
+ modelValue: [],
34
+ items: [
35
+ {
36
+ label: 'Chirurgien-dentiste',
37
+ value: 'chirurgien-dentiste',
38
+ },
39
+ {
40
+ label: 'Infirmier',
41
+ value: 'infirmier',
42
+ },
43
+ {
44
+ label: 'Orthophoniste',
45
+ value: 'orthophoniste',
46
+ },
47
+ {
48
+ label: 'Orthoptiste',
49
+ value: 'orthoptiste',
50
+ },
51
+ {
52
+ label: 'Pédicure-podologue',
53
+ value: 'pedicure-podologue',
54
+ },
55
+ {
56
+ label: 'Pharmacien',
57
+ value: 'pharmacien',
58
+ },
59
+ ],
60
+ },
61
+ render: (args) => {
62
+ return {
63
+ components: { SearchListField },
64
+ setup() {
65
+ return { args }
66
+ },
67
+ template: `
68
+ <SearchListField v-bind="args" v-model="args.modelValue"/>
69
+ `,
70
+ }
71
+ },
72
+ parameters: {
73
+ sourceCode: [
74
+ {
75
+ name: 'Template',
76
+ code: `
77
+ <template>
78
+ <SearchListField
79
+ v-model="modelValue"
80
+ :items="items"
81
+ />
82
+ {{ modelValue }}
83
+ </template>
84
+ `,
85
+ },
86
+ {
87
+ name: 'Script',
88
+ code: `
89
+ <script setup lang="ts">
90
+ import { ref } from 'vue'
91
+ import SearchListField from '@cnamts/synapse'
92
+
93
+ const modelValue = ref('')
94
+
95
+ const items = [
96
+ {
97
+ label: 'Chirurgien-dentiste',
98
+ value: 'chirurgien-dentiste',
99
+ },
100
+ {
101
+ label: 'Infirmier',
102
+ value: 'infirmier',
103
+ },
104
+ {
105
+ label: 'Orthophoniste',
106
+ value: 'orthophoniste',
107
+ },
108
+ {
109
+ label: 'Orthoptiste',
110
+ value: 'orthoptiste',
111
+ },
112
+ {
113
+ label: 'Pédicure-podologue',
114
+ value: 'pedicure-podologue',
115
+ },
116
+ {
117
+ label: 'Pharmacien',
118
+ value: 'pharmacien',
119
+ },
120
+ ]
121
+ </script>
122
+ `,
123
+ },
124
+ ],
125
+ },
126
+ }
@@ -0,0 +1,194 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, watch } from 'vue'
3
+ import { mdiMagnify } from '@mdi/js'
4
+ import type { PropType } from 'vue'
5
+ import type { SearchListItem } from './types'
6
+ import { locales } from './locales'
7
+
8
+ import { SyTextField } from '@/components'
9
+
10
+ const props = defineProps({
11
+ modelValue: {
12
+ type: Array as PropType<unknown[]>,
13
+ default: () => [],
14
+ },
15
+ items: {
16
+ type: Array as PropType<SearchListItem[]>,
17
+ default: () => [],
18
+ },
19
+ outlined: {
20
+ type: Boolean,
21
+ default: true,
22
+ },
23
+ })
24
+
25
+ const emit = defineEmits(['update:modelValue'])
26
+
27
+ const search = ref<string | null>(null)
28
+ const internalValue = ref<unknown[]>(props.modelValue)
29
+ const searchIcon = mdiMagnify
30
+
31
+ watch(
32
+ () => props.modelValue,
33
+ (newValue) => {
34
+ internalValue.value = newValue
35
+ },
36
+ )
37
+
38
+ const filteredItems = computed(() => {
39
+ if (search.value === null) {
40
+ return props.items
41
+ }
42
+ const searchLower = search.value.toLowerCase()
43
+ return props.items.filter(item =>
44
+ item.label.toLowerCase().includes(searchLower),
45
+ )
46
+ })
47
+
48
+ const emitChangeEvent = (value: unknown[]) => {
49
+ emit('update:modelValue', value)
50
+ }
51
+
52
+ defineExpose({
53
+ filteredItems,
54
+ search,
55
+ emitChangeEvent,
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <div class="vd-search-list">
61
+ <SyTextField
62
+ v-model="search"
63
+ :label="locales.search"
64
+ aria-describedby="search-description"
65
+ aria-labelledby="search-label"
66
+ hide-details
67
+ color="primary"
68
+ :variant="outlined ? 'outlined' : 'underlined'"
69
+ clearable
70
+ tabindex="0"
71
+ >
72
+ <template #prepend-inner>
73
+ <VIcon class="mr-1">
74
+ {{ searchIcon }}
75
+ </VIcon>
76
+ </template>
77
+ </SyTextField>
78
+
79
+ <!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
80
+ <label
81
+ id="search-label"
82
+ class="d-sr-only"
83
+ >
84
+ {{ locales.search }}
85
+ </label>
86
+ <span
87
+ id="search-description"
88
+ class="d-sr-only"
89
+ >
90
+ {{ locales.search }}
91
+ </span>
92
+
93
+ <VList
94
+ id="search-list"
95
+ v-model:selected="internalValue"
96
+ title="search-list"
97
+ select-strategy="classic"
98
+ class="pb-0"
99
+ role="listbox"
100
+ aria-labelledby="search-list-title"
101
+ @update:selected="emitChangeEvent"
102
+ >
103
+ <h2
104
+ id="search-list-title"
105
+ class="d-sr-only"
106
+ >
107
+ {{ locales.searchListTitle }}
108
+ </h2>
109
+ <VListItem
110
+ v-for="(item, index) in filteredItems"
111
+ :id="`search-list-item-${index}`"
112
+ :key="index"
113
+ :value="item.value"
114
+ role="option"
115
+ :aria-selected="internalValue.includes(item.value)"
116
+ :aria-labelledby="`search-list-item-${index}`"
117
+ :tabindex="0"
118
+ active-class="text-primary"
119
+ class="d-flex align-center justify-start mx-4"
120
+ >
121
+ <span
122
+ :id="`search-list-item-label-${index}`"
123
+ class="d-sr-only"
124
+ >
125
+ {{ item.label }}
126
+ </span>
127
+ <template #prepend="{ isActive }">
128
+ <VListItemAction start>
129
+ <input
130
+ :id="`checkbox-${index}`"
131
+ type="checkbox"
132
+ role="option"
133
+ :checked="isActive"
134
+ :aria-selected="isActive"
135
+ :aria-label="`${locales.checkboxLabel} ${item.label}`"
136
+ :aria-labelledby="`checkbox-${index}`"
137
+ :title="`${locales.checkboxLabel} ${item.label}`"
138
+ class="custom-checkbox ml-2"
139
+ >
140
+ <!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
141
+ <label
142
+ :for="`checkbox-${index}`"
143
+ class="d-sr-only"
144
+ >
145
+ {{ locales.checkboxLabel }}
146
+ </label>
147
+ </VListItemAction>
148
+
149
+ <VListItemTitle>{{ item.label }}</VListItemTitle>
150
+ </template>
151
+ </VListItem>
152
+ </VList>
153
+ </div>
154
+ </template>
155
+
156
+ <style lang="scss" scoped>
157
+ @use '@/assets/tokens';
158
+
159
+ .v-list {
160
+ background: transparent;
161
+ }
162
+
163
+ .vd-search-list .v-list-item--active::before {
164
+ opacity: 0;
165
+ }
166
+
167
+ .custom-checkbox {
168
+ appearance: none;
169
+ width: 20px;
170
+ height: 20px;
171
+ border: 2px solid rgb(0 0 0 / 50%);
172
+ border-radius: 2px;
173
+ outline: none;
174
+ cursor: pointer;
175
+ transition: all 0.3s ease;
176
+ }
177
+
178
+ .custom-checkbox:checked {
179
+ background-color: tokens.$primary-base !important;
180
+ border-color: tokens.$primary-base !important;
181
+
182
+ &::before {
183
+ content: '\2713';
184
+ display: block;
185
+ text-align: center;
186
+ line-height: 15px;
187
+ color: #fff;
188
+ }
189
+ }
190
+
191
+ .custom-checkbox:hover {
192
+ border-color: tokens.$primary-darker !important;
193
+ }
194
+ </style>
@@ -0,0 +1,5 @@
1
+ export const locales = {
2
+ search: 'Rechercher',
3
+ searchListTitle: 'Liste des éléments',
4
+ checkboxLabel: 'Sélectionner la valeur',
5
+ }
@@ -0,0 +1,323 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import SearchListField from '../SearchListField.vue'
3
+ import { describe, it, expect, beforeEach } from 'vitest'
4
+ import { createVuetify } from 'vuetify'
5
+
6
+ describe('SearchListField.vue', () => {
7
+ let vuetify
8
+
9
+ beforeEach(() => {
10
+ vuetify = createVuetify()
11
+ })
12
+
13
+ it('renders the password field', () => {
14
+ const wrapper = mount(SearchListField, {
15
+ global: {
16
+ plugins: [vuetify],
17
+ },
18
+ propsData: {
19
+ items: [
20
+ {
21
+ label: 'Item 1',
22
+ value: 1,
23
+ },
24
+ {
25
+ label: 'Item 2',
26
+ value: 2,
27
+ },
28
+ ],
29
+ modelValue: [1],
30
+ },
31
+ })
32
+ expect(wrapper.exists()).toBe(true)
33
+ })
34
+
35
+ it('initial state', () => {
36
+ const wrapper = mount(SearchListField, {
37
+ global: {
38
+ plugins: [vuetify],
39
+ },
40
+ propsData: {
41
+ items: [
42
+ {
43
+ label: 'Item 1',
44
+ value: 1,
45
+ },
46
+ {
47
+ label: 'Item 2',
48
+ value: 2,
49
+ },
50
+ ],
51
+ },
52
+ })
53
+
54
+ expect(wrapper.vm.filteredItems).toEqual(wrapper.props().items)
55
+ expect(
56
+ wrapper.find('.vd-search-list .v-list-item--active').exists(),
57
+ ).toBe(false)
58
+ })
59
+
60
+ it('initial state with empty value prop', () => {
61
+ const wrapper = mount(SearchListField, {
62
+ global: {
63
+ plugins: [vuetify],
64
+ },
65
+ propsData: {
66
+ items: [
67
+ {
68
+ label: 'Item 1',
69
+ value: 1,
70
+ },
71
+ {
72
+ label: 'Item 2',
73
+ value: 2,
74
+ },
75
+ ],
76
+ modelValue: [],
77
+ },
78
+ })
79
+
80
+ expect(
81
+ wrapper.find('.vd-search-list .v-list-item--active').exists(),
82
+ ).toBe(false)
83
+ })
84
+
85
+ it('selects an item', async () => {
86
+ const wrapper = mount(SearchListField, {
87
+ global: {
88
+ plugins: [vuetify],
89
+ },
90
+ propsData: {
91
+ items: [
92
+ {
93
+ label: 'Item 1',
94
+ value: 1,
95
+ },
96
+ {
97
+ label: 'Item 2',
98
+ value: 2,
99
+ },
100
+ ],
101
+ },
102
+ })
103
+
104
+ const listItem = wrapper.find('.vd-search-list .v-list-item')
105
+ await listItem.trigger('click')
106
+ await wrapper.vm.$nextTick()
107
+
108
+ expect(wrapper.emitted('update:modelValue')).toEqual([[[1]]])
109
+ expect(listItem.classes()).toContain('v-list-item--active')
110
+ })
111
+
112
+ it('filters items based on search input', async () => {
113
+ const wrapper = mount(SearchListField, {
114
+ global: {
115
+ plugins: [vuetify],
116
+ },
117
+ propsData: {
118
+ items: [
119
+ {
120
+ label: 'Apple',
121
+ value: 'apple',
122
+ },
123
+ {
124
+ label: 'Banana',
125
+ value: 'banana',
126
+ },
127
+ {
128
+ label: 'Orange',
129
+ value: 'orange',
130
+ },
131
+ ],
132
+ },
133
+ })
134
+
135
+ wrapper.vm.search = 'Banana'
136
+ await wrapper.vm.$nextTick()
137
+
138
+ const filteredItems = wrapper.vm.filteredItems
139
+ expect(filteredItems).toHaveLength(1)
140
+ expect(filteredItems[0].label).toBe('Banana')
141
+ })
142
+
143
+ it('clears the search field', async () => {
144
+ const wrapper = mount(SearchListField, {
145
+ global: {
146
+ plugins: [vuetify],
147
+ },
148
+ propsData: {
149
+ items: [
150
+ {
151
+ label: 'Item 1',
152
+ value: 1,
153
+ },
154
+ {
155
+ label: 'Item 2',
156
+ value: 2,
157
+ },
158
+ ],
159
+ },
160
+ })
161
+
162
+ wrapper.vm.search = 'Item 1'
163
+ await wrapper.vm.$nextTick()
164
+
165
+ await wrapper.find('.v-icon--clickable').trigger('click')
166
+
167
+ expect(wrapper.vm.search).toBeNull()
168
+ })
169
+
170
+ it('filteredItems computed property', async () => {
171
+ const wrapper = mount(SearchListField, {
172
+ global: {
173
+ plugins: [vuetify],
174
+ },
175
+ propsData: {
176
+ items: [
177
+ {
178
+ label: 'Item 1',
179
+ value: 1,
180
+ },
181
+ {
182
+ label: 'Item 2',
183
+ value: 2,
184
+ },
185
+ ],
186
+ },
187
+ })
188
+
189
+ expect(wrapper.vm.filteredItems).toHaveLength(2)
190
+
191
+ wrapper.vm.search = 'Item 1'
192
+ await wrapper.vm.$nextTick()
193
+ expect(wrapper.vm.filteredItems).toHaveLength(1)
194
+ })
195
+
196
+ it('filteredItems computed property with null search', async () => {
197
+ const wrapper = mount(SearchListField, {
198
+ global: {
199
+ plugins: [vuetify],
200
+ },
201
+ propsData: {
202
+ items: [
203
+ {
204
+ label: 'Item 1',
205
+ value: 1,
206
+ },
207
+ {
208
+ label: 'Item 2',
209
+ value: 2,
210
+ },
211
+ ],
212
+ },
213
+ })
214
+
215
+ expect(wrapper.vm.filteredItems).toHaveLength(2)
216
+
217
+ wrapper.vm.search = null
218
+ await wrapper.vm.$nextTick()
219
+ expect(wrapper.vm.filteredItems).toHaveLength(2)
220
+ })
221
+
222
+ it('filteredItems computed property with multiple matching items', async () => {
223
+ const wrapper = mount(SearchListField, {
224
+ global: {
225
+ plugins: [vuetify],
226
+ },
227
+ propsData: {
228
+ items: [
229
+ {
230
+ label: 'Item 1',
231
+ value: 1,
232
+ },
233
+ {
234
+ label: 'Item 2',
235
+ value: 2,
236
+ },
237
+ {
238
+ label: 'Another Item 1',
239
+ value: 3,
240
+ },
241
+ ],
242
+ },
243
+ })
244
+
245
+ wrapper.vm.search = 'Item 1'
246
+ await wrapper.vm.$nextTick()
247
+ expect(wrapper.vm.filteredItems).toHaveLength(2)
248
+ })
249
+
250
+ it('emitChangeEvent method', async () => {
251
+ const wrapper = mount(SearchListField, {
252
+ global: {
253
+ plugins: [vuetify],
254
+ },
255
+ propsData: {
256
+ items: [
257
+ {
258
+ label: 'Item 1',
259
+ value: 1,
260
+ },
261
+ {
262
+ label: 'Item 2',
263
+ value: 2,
264
+ },
265
+ ],
266
+ },
267
+ })
268
+
269
+ wrapper.vm.emitChangeEvent([''])
270
+ await wrapper.vm.$nextTick()
271
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
272
+ })
273
+
274
+ it('emits the update:modelValue event when an item is selected', async () => {
275
+ const wrapper = mount(SearchListField, {
276
+ global: {
277
+ plugins: [vuetify],
278
+ },
279
+ propsData: {
280
+ items: [
281
+ {
282
+ label: 'Item 1',
283
+ value: 1,
284
+ },
285
+ {
286
+ label: 'Item 2',
287
+ value: 2,
288
+ },
289
+ ],
290
+ },
291
+ })
292
+
293
+ const listItem = wrapper.find('.vd-search-list .v-list-item')
294
+ listItem.trigger('click')
295
+ await wrapper.vm.$nextTick()
296
+
297
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
298
+ expect(wrapper.emitted('update:modelValue')).toEqual([[[1]]])
299
+ })
300
+
301
+ it('renders the password field without outlined prop', () => {
302
+ const wrapper = mount(SearchListField, {
303
+ global: {
304
+ plugins: [vuetify],
305
+ },
306
+ propsData: {
307
+ items: [
308
+ {
309
+ label: 'Item 1',
310
+ value: 1,
311
+ },
312
+ {
313
+ label: 'Item 2',
314
+ value: 2,
315
+ },
316
+ ],
317
+ outlined: false,
318
+ },
319
+ })
320
+
321
+ expect(wrapper.find('.v-field--variant-underlined')).toBeTruthy()
322
+ })
323
+ })
@@ -0,0 +1,4 @@
1
+ export interface SearchListItem {
2
+ label: string
3
+ value: unknown
4
+ }