@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,205 @@
1
+ <script lang="ts" setup>
2
+ import { type PropType, computed, onMounted } from 'vue'
3
+ import { RatingEnum, useRating } from '../Rating'
4
+ import { locales } from './locales'
5
+ import { propValidator } from '@/utils/propValidator'
6
+ import {
7
+ mdiEmoticonHappyOutline,
8
+ mdiEmoticonSadOutline,
9
+ mdiEmoticonNeutralOutline,
10
+ } from '@mdi/js'
11
+
12
+ import { useDisplay } from 'vuetify'
13
+
14
+ const { smAndDown } = useDisplay()
15
+ const isMobile = computed(() => smAndDown.value)
16
+
17
+ const props = defineProps({
18
+ label: {
19
+ type: String as PropType<string | null>,
20
+ default: RatingEnum.EMOTION,
21
+ },
22
+ length: {
23
+ type: Number,
24
+ default: 3,
25
+ validator: (value: number) => propValidator('length', ['2', '3'], value.toString()),
26
+ },
27
+ itemLabels: {
28
+ type: Array as PropType<string[]>,
29
+ default: () => locales.defaultEmotionLabels,
30
+ },
31
+ readonly: {
32
+ type: Boolean,
33
+ default: false,
34
+ },
35
+ modelValue: {
36
+ type: Number,
37
+ default: -1,
38
+ },
39
+ })
40
+
41
+ const sadIcon = mdiEmoticonSadOutline
42
+ const neutralIcon = mdiEmoticonNeutralOutline
43
+ const happyIcon = mdiEmoticonHappyOutline
44
+
45
+ const btnSize = computed(() => {
46
+ return isMobile.value ? '70px' : '88px'
47
+ })
48
+
49
+ const emit = defineEmits(['update:modelValue'])
50
+ const { hasAnswered, emitInputEvent } = useRating(props, emit)
51
+
52
+ const isActive = (index: number) => {
53
+ return index === props.modelValue - 1
54
+ }
55
+
56
+ const getIcon = (index: number) => {
57
+ if (index === 0) {
58
+ return sadIcon
59
+ }
60
+ if (index === 1 && props.length === 3) {
61
+ return neutralIcon
62
+ }
63
+ return happyIcon
64
+ }
65
+
66
+ const getColor = (index: number) => {
67
+ const colors = ['sad', 'neutral', 'happy']
68
+ if (props.length === 2) {
69
+ const filteredColors = colors.filter(item => item !== 'neutral')
70
+ return filteredColors[index]
71
+ }
72
+ return colors[index]
73
+ }
74
+
75
+ const getEmotionLabel = (value: number) => {
76
+ if (props.length === 2) {
77
+ const filteredLabels = props.itemLabels.filter((_, index) => index !== 1)
78
+ return filteredLabels[value]
79
+ }
80
+ return props.itemLabels[value]
81
+ }
82
+
83
+ onMounted(() => {
84
+ const hiddenInputs = document.querySelectorAll('.v-rating__hidden')
85
+ hiddenInputs.forEach((input) => {
86
+ (input as HTMLElement).setAttribute('aria-hidden', 'true')
87
+ })
88
+ })
89
+ </script>
90
+
91
+ <template>
92
+ <fieldset class="vd-emotion-picker">
93
+ <legend class="text-h6 mb-6">
94
+ <slot name="label">
95
+ {{ props.label }}
96
+ </slot>
97
+ </legend>
98
+
99
+ <VRating
100
+ :model-value="props.modelValue"
101
+ :length="props.length"
102
+ :readonly="props.readonly"
103
+ class="max-width-none mx-n1 mx-sm-n2"
104
+ @update:model-value="(value) => emit('update:modelValue', value)"
105
+ >
106
+ <template #item="{ index }">
107
+ <VBtn
108
+ :disabled="props.readonly || hasAnswered"
109
+ :title="getEmotionLabel(index)"
110
+ :aria-pressed="isActive(index).toString()"
111
+ :class="[getColor(index), { 'v-btn--active': isActive(index) }]"
112
+ :min-height="btnSize"
113
+ :min-width="btnSize"
114
+ variant="text"
115
+ class="rounded-lg px-1 px-sm-4 mx-1 mx-sm-2"
116
+ @click="emitInputEvent(index + 1)"
117
+ >
118
+ <VIcon
119
+ size="40"
120
+ color="currentColor"
121
+ class="pa-0"
122
+ >
123
+ {{ getIcon(index) }}
124
+ </VIcon>
125
+
126
+ <span
127
+ v-if="getEmotionLabel(index)"
128
+ :class="{ 'text-secondary': !isActive(index) }"
129
+ class="text-subtitle-2 mt-1"
130
+ >
131
+ {{ getEmotionLabel(index) }}
132
+ </span>
133
+ </VBtn>
134
+ </template>
135
+ </VRating>
136
+ </fieldset>
137
+ </template>
138
+
139
+ <style lang="scss" scoped>
140
+ @use '@/assets/tokens';
141
+
142
+ .vd-emotion-picker {
143
+ border: 0;
144
+ }
145
+
146
+ .v-rating .v-btn {
147
+ transition: 0.2s;
148
+ border: 1px solid transparent;
149
+ opacity: 1;
150
+ background: transparent;
151
+
152
+ :deep(.v-btn__content) {
153
+ flex-direction: column;
154
+ }
155
+
156
+ .text-secondary {
157
+ color: tokens.$grey-darken-20 !important;
158
+ }
159
+
160
+ &.sad {
161
+ color: tokens.$orange-darken-40 !important;
162
+ }
163
+
164
+ &.neutral {
165
+ color: tokens.$yellow-darken-40 !important;
166
+ }
167
+
168
+ &.happy {
169
+ color: tokens.$turquoise-darken-40 !important;
170
+ }
171
+
172
+ &--active.v-btn--disabled .v-icon {
173
+ color: currentcolor !important;
174
+ }
175
+
176
+ &:focus,
177
+ &--active {
178
+ border-color: currentcolor !important;
179
+ }
180
+
181
+ &:hover,
182
+ &:focus,
183
+ &--active {
184
+ :deep(.v-btn__overlay, .v-btn__underlay) {
185
+ display: none;
186
+ }
187
+
188
+ &.sad {
189
+ background: tokens.$orange-lighten-90;
190
+ }
191
+
192
+ &.neutral {
193
+ background: tokens.$yellow-lighten-90;
194
+ }
195
+
196
+ &.happy {
197
+ background: tokens.$turquoise-lighten-90;
198
+ }
199
+ }
200
+ }
201
+
202
+ .v-theme--light.v-btn--disabled :deep(.v-icon) {
203
+ color: tokens.$grey-lighten-20 !important;
204
+ }
205
+ </style>
@@ -0,0 +1,3 @@
1
+ export const locales = {
2
+ defaultEmotionLabels: ['Pas du tout', 'Moyen', 'Parfait !'],
3
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { vuetify } from '@tests/unit/setup'
4
+
5
+ import EmotionPicker from '../EmotionPicker.vue'
6
+
7
+ describe('EmotionPicker', () => {
8
+ it('renders correctly', () => {
9
+ const wrapper = mount(EmotionPicker, {
10
+ global: {
11
+ plugins: [vuetify],
12
+ },
13
+ propsData: {
14
+ label: 'Pourriez-vous donner une note ?',
15
+ itemLabels: ['Pas du tout', 'Peut-être', 'Oui super'],
16
+ },
17
+ })
18
+
19
+ expect(wrapper.html()).toContain('Pourriez-vous donner une note ?')
20
+ expect(wrapper.html()).toContain('Pas du tout')
21
+ expect(wrapper.html()).toContain('Peut-être')
22
+ expect(wrapper.html()).toContain('Oui super')
23
+ expect(wrapper.html()).toMatchSnapshot()
24
+ })
25
+
26
+ it('renders correctly with only 2 items', () => {
27
+ const wrapper = mount(EmotionPicker, {
28
+ global: {
29
+ plugins: [vuetify],
30
+ },
31
+ propsData: {
32
+ label: 'Pourriez-vous donner une note ?',
33
+ itemLabels: ['Pas du tout', 'Not used', 'Oui super'],
34
+ length: 2,
35
+ },
36
+ })
37
+
38
+ expect(wrapper.html()).toContain('Pourriez-vous donner une note ?')
39
+ expect(wrapper.html()).toContain('Pas du tout')
40
+ expect(wrapper.html()).toContain('Oui super')
41
+ expect(wrapper.html()).not.toContain('Not used')
42
+ expect(wrapper.html()).toMatchSnapshot()
43
+ })
44
+
45
+ it('renders in mobile mode', async () => {
46
+ // @ts-expect-error - Property 'happyDOM' does not exist on type 'Window & typeof globalThis'.
47
+ window.happyDOM.setInnerWidth(600)
48
+
49
+ const wrapper = mount(EmotionPicker, {
50
+ global: {
51
+ plugins: [vuetify],
52
+ },
53
+ propsData: {
54
+ label: 'Pourriez-vous donner une note ?',
55
+ itemLabels: ['Pas du tout', 'Peut-être', 'Oui super'],
56
+ },
57
+ })
58
+
59
+ await wrapper.vm.$nextTick()
60
+
61
+ expect(wrapper.html()).toContain('70px')
62
+ })
63
+
64
+ it('emit the right value when an item is clicked', async () => {
65
+ const wrapper = mount(EmotionPicker, {
66
+ global: {
67
+ plugins: [vuetify],
68
+ },
69
+ })
70
+
71
+ const items = wrapper.findAll('button')
72
+ await items[0].trigger('click')
73
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
74
+ expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toBe(1)
75
+ })
76
+
77
+ it('have no labels when no labels are provided', () => {
78
+ const wrapper = mount(EmotionPicker, {
79
+ global: {
80
+ plugins: [vuetify],
81
+ },
82
+ propsData: {
83
+ itemLabels: [],
84
+ },
85
+ })
86
+
87
+ wrapper.findAll('button').forEach((button) => {
88
+ expect(button.text()).toBe('')
89
+ })
90
+ })
91
+
92
+ it('change the style of the buttons when an item is clicked', async () => {
93
+ const wrapper = mount(EmotionPicker, {
94
+ global: {
95
+ plugins: [vuetify],
96
+ },
97
+ })
98
+
99
+ const items = wrapper.findAll('button')
100
+
101
+ await wrapper.setProps({ modelValue: 1 })
102
+ expect(items[0].classes()).toContain('v-btn--active')
103
+ })
104
+ })
@@ -0,0 +1,66 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`EmotionPicker > renders correctly 1`] = `
4
+ "<fieldset data-v-9f5befe4="" class="vd-emotion-picker">
5
+ <legend data-v-9f5befe4="" class="text-h6 mb-6">Pourriez-vous donner une note ?</legend>
6
+ <div data-v-9f5befe4="" class="v-rating v-theme--light max-width-none mx-n1 mx-sm-n2"><label for="v-rating-0-0" class=""><span class="v-rating__hidden">Rating 0 of 3</span>
7
+ <!---->
8
+ </label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-0" type="radio" checked="" tabindex="-1" value="0">
9
+ <div class="v-rating__wrapper">
10
+ <!---->
11
+ <div class="v-rating__item"><label for="v-rating-0-1" class=""><span class="v-rating__hidden">Rating 1 of 3</span><button data-v-9f5befe4="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-text sad rounded-lg px-1 px-sm-4 mx-1 mx-sm-2" style="min-height: 88px; min-width: 88px;" title="Pas du tout" aria-pressed="false"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
12
+ <!----><span class="v-btn__content" data-no-activator=""><i data-v-9f5befe4="" class="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M12,14C13.75,14 15.29,14.72 16.19,15.81L14.77,17.23C14.32,16.5 13.25,16 12,16C10.75,16 9.68,16.5 9.23,17.23L7.81,15.81C8.71,14.72 10.25,14 12,14Z mdi v-icon notranslate v-theme--light text-currentColor pa-0" style="font-size: 40px; height: 40px; width: 40px;" aria-hidden="true"></i><span data-v-9f5befe4="" class="text-secondary text-subtitle-2 mt-1">Pas du tout</span></span>
13
+ <!---->
14
+ <!---->
15
+ </button></label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-1" type="radio" tabindex="-1" value="1"></div>
16
+ <!---->
17
+ </div>
18
+ <div class="v-rating__wrapper">
19
+ <!---->
20
+ <div class="v-rating__item"><label for="v-rating-0-2" class=""><span class="v-rating__hidden">Rating 2 of 3</span><button data-v-9f5befe4="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-text neutral rounded-lg px-1 px-sm-4 mx-1 mx-sm-2" style="min-height: 88px; min-width: 88px;" title="Peut-être" aria-pressed="false"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
21
+ <!----><span class="v-btn__content" data-no-activator=""><i data-v-9f5befe4="" class="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M8.5,11A1.5,1.5 0 0,1 7,9.5A1.5,1.5 0 0,1 8.5,8A1.5,1.5 0 0,1 10,9.5A1.5,1.5 0 0,1 8.5,11M17,9.5A1.5,1.5 0 0,1 15.5,11A1.5,1.5 0 0,1 14,9.5A1.5,1.5 0 0,1 15.5,8A1.5,1.5 0 0,1 17,9.5M16,14V16H8V14H16Z mdi v-icon notranslate v-theme--light text-currentColor pa-0" style="font-size: 40px; height: 40px; width: 40px;" aria-hidden="true"></i><span data-v-9f5befe4="" class="text-secondary text-subtitle-2 mt-1">Peut-être</span></span>
22
+ <!---->
23
+ <!---->
24
+ </button></label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-2" type="radio" tabindex="-1" value="2"></div>
25
+ <!---->
26
+ </div>
27
+ <div class="v-rating__wrapper">
28
+ <!---->
29
+ <div class="v-rating__item"><label for="v-rating-0-3" class=""><span class="v-rating__hidden">Rating 3 of 3</span><button data-v-9f5befe4="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-text happy rounded-lg px-1 px-sm-4 mx-1 mx-sm-2" style="min-height: 88px; min-width: 88px;" title="Oui super" aria-pressed="false"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
30
+ <!----><span class="v-btn__content" data-no-activator=""><i data-v-9f5befe4="" class="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z mdi v-icon notranslate v-theme--light text-currentColor pa-0" style="font-size: 40px; height: 40px; width: 40px;" aria-hidden="true"></i><span data-v-9f5befe4="" class="text-secondary text-subtitle-2 mt-1">Oui super</span></span>
31
+ <!---->
32
+ <!---->
33
+ </button></label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-3" type="radio" tabindex="-1" value="3"></div>
34
+ <!---->
35
+ </div>
36
+ </div>
37
+ </fieldset>"
38
+ `;
39
+
40
+ exports[`EmotionPicker > renders correctly with only 2 items 1`] = `
41
+ "<fieldset data-v-9f5befe4="" class="vd-emotion-picker">
42
+ <legend data-v-9f5befe4="" class="text-h6 mb-6">Pourriez-vous donner une note ?</legend>
43
+ <div data-v-9f5befe4="" class="v-rating v-theme--light max-width-none mx-n1 mx-sm-n2"><label for="v-rating-0-0" class=""><span class="v-rating__hidden">Rating 0 of 2</span>
44
+ <!---->
45
+ </label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-0" type="radio" checked="" tabindex="-1" value="0">
46
+ <div class="v-rating__wrapper">
47
+ <!---->
48
+ <div class="v-rating__item"><label for="v-rating-0-1" class=""><span class="v-rating__hidden">Rating 1 of 2</span><button data-v-9f5befe4="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-text sad rounded-lg px-1 px-sm-4 mx-1 mx-sm-2" style="min-height: 88px; min-width: 88px;" title="Pas du tout" aria-pressed="false"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
49
+ <!----><span class="v-btn__content" data-no-activator=""><i data-v-9f5befe4="" class="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M12,14C13.75,14 15.29,14.72 16.19,15.81L14.77,17.23C14.32,16.5 13.25,16 12,16C10.75,16 9.68,16.5 9.23,17.23L7.81,15.81C8.71,14.72 10.25,14 12,14Z mdi v-icon notranslate v-theme--light text-currentColor pa-0" style="font-size: 40px; height: 40px; width: 40px;" aria-hidden="true"></i><span data-v-9f5befe4="" class="text-secondary text-subtitle-2 mt-1">Pas du tout</span></span>
50
+ <!---->
51
+ <!---->
52
+ </button></label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-1" type="radio" tabindex="-1" value="1"></div>
53
+ <!---->
54
+ </div>
55
+ <div class="v-rating__wrapper">
56
+ <!---->
57
+ <div class="v-rating__item"><label for="v-rating-0-2" class=""><span class="v-rating__hidden">Rating 2 of 2</span><button data-v-9f5befe4="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-text happy rounded-lg px-1 px-sm-4 mx-1 mx-sm-2" style="min-height: 88px; min-width: 88px;" title="Oui super" aria-pressed="false"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
58
+ <!----><span class="v-btn__content" data-no-activator=""><i data-v-9f5befe4="" class="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z mdi v-icon notranslate v-theme--light text-currentColor pa-0" style="font-size: 40px; height: 40px; width: 40px;" aria-hidden="true"></i><span data-v-9f5befe4="" class="text-secondary text-subtitle-2 mt-1">Oui super</span></span>
59
+ <!---->
60
+ <!---->
61
+ </button></label><input class="v-rating__hidden" name="v-rating-0" id="v-rating-0-2" type="radio" tabindex="-1" value="2"></div>
62
+ <!---->
63
+ </div>
64
+ </div>
65
+ </fieldset>"
66
+ `;
@@ -0,0 +1,159 @@
1
+ <script lang="ts" setup>
2
+ import { computed, onMounted } from 'vue'
3
+ import { RatingEnum, useRating } from '../Rating'
4
+ import { locales } from './locales'
5
+ import type { PropType } from 'vue'
6
+
7
+ import SySelect from '@/components/Customs/SySelect/SySelect.vue'
8
+
9
+ import { useDisplay } from 'vuetify'
10
+
11
+ interface SelectItem {
12
+ text: string
13
+ value: number
14
+ }
15
+
16
+ const props = defineProps({
17
+ label: {
18
+ type: String as PropType<string | undefined>,
19
+ default: RatingEnum.NUMBER,
20
+ },
21
+ length: {
22
+ type: Number,
23
+ default: 10,
24
+ },
25
+ itemLabels: {
26
+ type: Array as PropType<string[]>,
27
+ default: () => [],
28
+ },
29
+ readonly: {
30
+ type: Boolean,
31
+ default: false,
32
+ },
33
+ modelValue: {
34
+ type: Number,
35
+ default: -1,
36
+ },
37
+ })
38
+
39
+ const { smAndDown } = useDisplay()
40
+ const isMobile = computed(() => smAndDown.value)
41
+
42
+ const emit = defineEmits(['update:modelValue'])
43
+ const { hasAnswered, emitInputEvent } = useRating(props, emit)
44
+
45
+ const selectItems = computed<SelectItem[]>(() => {
46
+ return [...Array(props.length)].map((_, index) => ({
47
+ text: `${index + 1}`,
48
+ value: index + 1,
49
+ }))
50
+ })
51
+
52
+ const shouldDisplayLabels = computed(() => props.itemLabels.length === 2)
53
+
54
+ onMounted(() => {
55
+ const hiddenInputs = document.querySelectorAll('.v-rating__hidden')
56
+ hiddenInputs.forEach((input) => {
57
+ (input as HTMLElement).setAttribute('aria-hidden', 'true')
58
+ })
59
+ })
60
+ </script>
61
+
62
+ <template>
63
+ <fieldset class="vd-number-picker">
64
+ <legend class="d-sr-only">
65
+ <slot name="label">
66
+ {{ props.label }}
67
+ </slot>
68
+ </legend>
69
+ <SySelect
70
+ v-if="isMobile"
71
+ :model-value="props.modelValue === -1 ? undefined : props.modelValue"
72
+ :label="props.label"
73
+ :disabled="props.readonly || hasAnswered"
74
+ :items="selectItems"
75
+ @update:model-value="(value) => emit('update:modelValue', value)"
76
+ />
77
+ <template v-else>
78
+ <legend class="text-h6 mb-6">
79
+ <slot name="label">
80
+ {{ props.label }}
81
+ </slot>
82
+ </legend>
83
+ <div
84
+ v-if="!hasAnswered"
85
+ class="d-inline-block"
86
+ >
87
+ <VRating
88
+ :model-value="props.modelValue"
89
+ :length="props.length"
90
+ :readonly="props.readonly || hasAnswered"
91
+ class="d-flex flex-wrap max-width-none"
92
+ @update:model-value="(value) => emit('update:modelValue', value)"
93
+ >
94
+ <template #item="{ index }">
95
+ <VBtn
96
+ :aria-label="locales.ariaLabel(index + 1, props.length)"
97
+ :disabled="props.readonly"
98
+ size="x-small"
99
+ variant="outlined"
100
+ color="primary"
101
+ height="36px"
102
+ class="text-body-2 pa-0 mr-2"
103
+ @click="emitInputEvent(index + 1)"
104
+ >
105
+ {{ index + 1 }}
106
+ </VBtn>
107
+ </template>
108
+ </VRating>
109
+ <div
110
+ v-if="shouldDisplayLabels"
111
+ class="d-flex justify-space-between mt-1"
112
+ >
113
+ <span
114
+ :aria-label="`${locales.ariaLabel(1, props.length)} ${
115
+ props.itemLabels[0]
116
+ }.`"
117
+ class="text-caption"
118
+ v-text="props.itemLabels[0]"
119
+ />
120
+ <span
121
+ :aria-label="`${locales.ariaLabel(props.length, props.length)} ${
122
+ props.itemLabels[1]
123
+ }.`"
124
+ class="text-caption mr-2"
125
+ v-text="props.itemLabels[1]"
126
+ />
127
+ </div>
128
+ </div>
129
+ <p
130
+ v-else
131
+ :aria-label="locales.ariaLabel(props.modelValue, props.length)"
132
+ class="mb-0 d-flex align-center"
133
+ >
134
+ <VBtn
135
+ aria-hidden="true"
136
+ :disabled="true"
137
+ size="x-small"
138
+ variant="outlined"
139
+ color="primary"
140
+ height="36px"
141
+ class="vd-btn-answer text-body-2 mr-1 pa-0"
142
+ >
143
+ {{ props.modelValue }}
144
+ </VBtn>
145
+ / {{ props.length }}
146
+ </p>
147
+ </template>
148
+ </fieldset>
149
+ </template>
150
+
151
+ <style lang="scss" scoped>
152
+ .vd-number-picker {
153
+ border: 0;
154
+ }
155
+
156
+ .vd-btn-answer.v-btn.v-btn--disabled {
157
+ opacity: 1;
158
+ }
159
+ </style>
@@ -0,0 +1,4 @@
1
+ export const locales = {
2
+ ariaLabel: (index: number, length: number): string =>
3
+ `Note de ${index} sur ${length}.`,
4
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { vuetify } from '@tests/unit/setup'
4
+
5
+ import NumberPicker from '../NumberPicker.vue'
6
+
7
+ describe('NumberPicker', () => {
8
+ it('renders correctly', () => {
9
+ const wrapper = mount(NumberPicker, {
10
+ global: {
11
+ plugins: [vuetify],
12
+ },
13
+ propsData: {
14
+ label: 'Pourriez-vous donner une note ?',
15
+ },
16
+ })
17
+
18
+ const btns = wrapper.findAll('button')
19
+
20
+ expect(btns).toHaveLength(10)
21
+ btns.forEach((btn, i) => {
22
+ expect(btn.text()).toBe((i + 1).toString())
23
+ })
24
+ expect(wrapper.html()).toMatchSnapshot()
25
+ })
26
+
27
+ it('emits an event when a number is selected', async () => {
28
+ const wrapper = mount(NumberPicker, {
29
+ global: {
30
+ plugins: [vuetify],
31
+ },
32
+ })
33
+
34
+ await wrapper.findAll('button')!.at(3)!.trigger('click')
35
+
36
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual([4])
37
+ })
38
+
39
+ it('change the displayed value when the modelValue is updated', async () => {
40
+ const wrapper = mount(NumberPicker, {
41
+ global: {
42
+ plugins: [vuetify],
43
+ },
44
+ props: {
45
+ modelValue: 3,
46
+ },
47
+ })
48
+
49
+ const btn = wrapper.findAll('button')[0]
50
+
51
+ await wrapper.setProps({ modelValue: 4 })
52
+ expect(btn.text()).toBe('4')
53
+
54
+ await wrapper.setProps({ modelValue: 5 })
55
+ expect(btn.text()).toBe('5')
56
+ })
57
+
58
+ it('renders correctly in xs window', async () => {
59
+ // @ts-expect-error - Property 'happyDOM' does not exist on type 'Window & typeof globalThis'.
60
+ window.happyDOM.setInnerWidth(600)
61
+
62
+ const wrapper = mount(NumberPicker, {
63
+ global: {
64
+ plugins: [vuetify],
65
+ },
66
+ })
67
+ await wrapper.vm.$nextTick()
68
+
69
+ const select = wrapper.find('.sy-select')
70
+
71
+ expect(select.exists()).toBe(true)
72
+ })
73
+ })