@cnamts/synapse 0.0.0-alpha.0 → 0.0.3-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 (96) hide show
  1. package/README.md +72 -2
  2. package/dist/design-system-v3.d.ts +234 -2
  3. package/dist/design-system-v3.js +1413 -4404
  4. package/dist/design-system-v3.umd.cjs +1 -2
  5. package/dist/style.css +1 -1
  6. package/package.json +37 -36
  7. package/src/components/Alert/Alert.vue +8 -8
  8. package/src/components/CollapsibleList/CollapsibleList.mdx +47 -0
  9. package/src/components/CollapsibleList/CollapsibleList.stories.ts +52 -0
  10. package/src/components/CollapsibleList/CollapsibleList.vue +157 -0
  11. package/src/components/CollapsibleList/tests/CollapsibleList.spec.ts +60 -0
  12. package/src/components/CollapsibleList/types.d.ts +5 -0
  13. package/src/components/Customs/CustomInputSelect/CustomInputSelect.mdx +42 -0
  14. package/src/components/Customs/CustomInputSelect/CustomInputSelect.stories.ts +154 -0
  15. package/src/components/Customs/CustomInputSelect/CustomInputSelect.vue +185 -0
  16. package/src/components/Customs/CustomInputSelect/tests/CustomInputSelect.spec.ts +216 -0
  17. package/src/components/Customs/CustomSelect/CustomSelect.mdx +47 -0
  18. package/src/components/Customs/CustomSelect/CustomSelect.stories.ts +182 -0
  19. package/src/components/Customs/CustomSelect/CustomSelect.vue +188 -0
  20. package/src/components/Customs/CustomSelect/tests/CustomSelect.spec.ts +236 -0
  21. package/src/components/FooterBar/A11yCompliance.ts +9 -0
  22. package/src/components/FooterBar/FooterBar.mdx +115 -0
  23. package/src/components/FooterBar/FooterBar.stories.ts +632 -0
  24. package/src/components/FooterBar/FooterBar.vue +330 -0
  25. package/src/components/FooterBar/config.ts +20 -0
  26. package/src/components/FooterBar/defaultSocialMediaLinks.ts +21 -0
  27. package/src/components/FooterBar/locales.ts +16 -0
  28. package/src/components/FooterBar/tests/FooterBar.spec.ts +167 -0
  29. package/src/components/FooterBar/tests/FooterBarConfig.spec.ts +36 -0
  30. package/src/components/FooterBar/tests/__snapshots__/FooterBar.spec.ts.snap +27 -0
  31. package/src/components/FooterBar/types.d.ts +10 -0
  32. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +2 -2
  33. package/src/components/HeaderBar/HeaderBar.mdx +137 -0
  34. package/src/components/HeaderBar/HeaderBar.stories.ts +159 -0
  35. package/src/components/HeaderBar/HeaderBar.vue +238 -0
  36. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.stories.ts +272 -0
  37. package/src/components/HeaderBar/HeaderComplexMenu/HeaderComplexMenu.vue +205 -0
  38. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.stories.ts +49 -0
  39. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/HeaderMenuItem.vue +51 -0
  40. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/HeaderMenuItem.spec.ts +16 -0
  41. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuItem/tests/__snapshots__/HeaderMenuItem.spec.ts.snap +3 -0
  42. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.stories.ts +56 -0
  43. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/HeaderMenuSection.vue +51 -0
  44. package/src/components/HeaderBar/HeaderComplexMenu/HeaderMenuSection/tests/HeaderMenuSection.spec.ts +33 -0
  45. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.stories.ts +137 -0
  46. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/HeaderSubMenu.vue +180 -0
  47. package/src/components/HeaderBar/HeaderComplexMenu/HeaderSubMenu/tests/HeaderSubMenu.spec.ts +63 -0
  48. package/src/components/HeaderBar/HeaderComplexMenu/conts.ts +1 -0
  49. package/src/components/HeaderBar/HeaderComplexMenu/locals.ts +4 -0
  50. package/src/components/HeaderBar/HeaderComplexMenu/tests/HeaderComplexMenu.spec.ts +129 -0
  51. package/src/components/HeaderBar/HeaderComplexMenu/tests/__snapshots__/HeaderComplexMenu.spec.ts.snap +18 -0
  52. package/src/components/HeaderBar/HeaderComplexMenu/tests/useHandleSubMenus.spec.ts +158 -0
  53. package/src/components/HeaderBar/HeaderComplexMenu/useHandleSubMenus.ts +49 -0
  54. package/src/components/HeaderBar/HeaderLogo/HeaderLogo.vue +106 -0
  55. package/src/components/HeaderBar/HeaderLogo/locales.ts +3 -0
  56. package/src/components/HeaderBar/HeaderLogo/logos/Logo-mobile.vue +117 -0
  57. package/src/components/HeaderBar/HeaderLogo/logos/Logo.vue +279 -0
  58. package/src/components/HeaderBar/HeaderLogo/tests/HeaderLogo.spec.ts +71 -0
  59. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +88 -0
  60. package/src/components/HeaderBar/HeaderMenuBtn/locals.ts +4 -0
  61. package/src/components/HeaderBar/consts.scss +7 -0
  62. package/src/components/HeaderBar/consts.ts +2 -0
  63. package/src/components/HeaderBar/locales.ts +3 -0
  64. package/src/components/HeaderBar/tests/HeaderBar.spec.ts +210 -0
  65. package/src/components/HeaderBar/tests/__snapshots__/HeaderBar.spec.ts.snap +50 -0
  66. package/src/components/HeaderBar/tests/useHeaderResponsiveMode.spec.ts +26 -0
  67. package/src/components/HeaderBar/tests/useScrollDirection.spec.ts +34 -0
  68. package/src/components/HeaderBar/useHeaderResponsiveMode.ts +25 -0
  69. package/src/components/HeaderBar/useScrollDirection.ts +26 -0
  70. package/src/components/LangBtn/LangBtn.mdx +2 -1
  71. package/src/components/LangBtn/LangBtn.vue +3 -3
  72. package/src/components/Logo/Logo.mdx +26 -0
  73. package/src/components/Logo/Logo.stories.ts +217 -0
  74. package/src/components/Logo/Logo.vue +397 -0
  75. package/src/components/Logo/LogoSize.ts +7 -0
  76. package/src/components/Logo/locales.ts +6 -0
  77. package/src/components/Logo/logoDimensionsMapping.ts +16 -0
  78. package/src/components/Logo/tests/Logo.spec.ts +75 -0
  79. package/src/components/Logo/types.d.ts +8 -0
  80. package/src/components/NotificationBar/NotificationBar.vue +5 -7
  81. package/src/components/PageContainer/PageContainer.vue +0 -1
  82. package/src/components/SocialMediaLinks/DefaultSocialMediaLinks.ts +21 -0
  83. package/src/components/SocialMediaLinks/SocialMediaLinks.mdx +15 -0
  84. package/src/components/SocialMediaLinks/SocialMediaLinks.stories.ts +72 -0
  85. package/src/components/SocialMediaLinks/SocialMediaLinks.vue +92 -0
  86. package/src/components/SocialMediaLinks/locales.ts +3 -0
  87. package/src/components/SocialMediaLinks/tests/DefaultSocialMediaLinks.spec.ts +21 -0
  88. package/src/components/SocialMediaLinks/tests/SocialMediaLinks.spec.ts +89 -0
  89. package/src/components/SocialMediaLinks/tests/__snapshots__/SocialMediaLinks.spec.ts.snap +24 -0
  90. package/src/components/SocialMediaLinks/types.d.ts +5 -0
  91. package/src/components/index.ts +6 -0
  92. package/src/directives/clickOutside.ts +24 -0
  93. package/src/temp/TestDTComponent.vue +6 -10
  94. package/src/temp/gridsTests.vue +0 -4
  95. package/src/utils/propValidator/index.ts +20 -0
  96. package/src/utils/propValidator/tests/propValidator.spec.ts +40 -0
@@ -0,0 +1,60 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { expect, describe, it } from 'vitest'
3
+
4
+ import CollapsibleList from '../CollapsibleList.vue'
5
+
6
+ import { vuetify } from '@tests/unit/setup'
7
+
8
+ describe('CollapsibleList', () => {
9
+ it('renders correctly', async () => {
10
+ const wrapper = mount(CollapsibleList, {
11
+ global: {
12
+ plugins: [vuetify],
13
+ },
14
+ propsData: {
15
+ listTitle: 'Santé',
16
+ items: [
17
+ {
18
+ text: 'Mon espace santé',
19
+ href: 'https://www.ameli.fr/assure/sante/mon-espace-sante',
20
+ },
21
+ {
22
+ text: 'Accomplir les bons gestes',
23
+ href: 'https://www.ameli.fr/assure/sante/bons-gestes',
24
+ },
25
+ ],
26
+ },
27
+ })
28
+
29
+ expect(wrapper.find('h4').text()).toBe('Santé')
30
+ expect(wrapper.findAll('a')).toHaveLength(2)
31
+ expect(wrapper.findAll('a').at(0)?.text()).toBe('Mon espace santé')
32
+ expect(wrapper.findAll('a').at(1)?.text()).toBe('Accomplir les bons gestes')
33
+ })
34
+
35
+ it('renders correctly with in mobile mode', () => {
36
+ const wrapper = mount(CollapsibleList, {
37
+ global: {
38
+ plugins: [vuetify],
39
+ },
40
+ propsData: {
41
+ listTitle: 'Santé',
42
+ items: [
43
+ {
44
+ text: 'Mon espace santé',
45
+ href: 'https://www.ameli.fr/assure/sante/mon-espace-sante',
46
+ },
47
+ {
48
+ text: 'Accomplir les bons gestes',
49
+ href: 'https://www.ameli.fr/assure/sante/bons-gestes',
50
+ },
51
+ ],
52
+ },
53
+ })
54
+
55
+ wrapper.vm.$vuetify.display.name = 'xs'
56
+ wrapper.vm.$vuetify.display.smAndDown = true
57
+
58
+ expect(wrapper.find('.vd-collapse-list-mobile')).toBeTruthy()
59
+ })
60
+ })
@@ -0,0 +1,5 @@
1
+ export interface ListItem {
2
+ text: string
3
+ href: string
4
+ ariaLabel?: string
5
+ }
@@ -0,0 +1,42 @@
1
+ import { Canvas, Meta, Controls, Story, Source } from '@storybook/blocks';
2
+ import * as CustomInputSelectStories from "./CustomInputSelect.stories.ts";
3
+
4
+ <Meta of={CustomInputSelectStories} />
5
+
6
+ # CustomSelect
7
+
8
+ Le composant `CustomInputSelect` est utilisé pour proposer une alternative au `v-select` de Vuetify qui ne respecte pas les règles RGAA.
9
+
10
+ <Canvas of={CustomInputSelectStories.Default} />
11
+
12
+ <Story of={CustomInputSelectStories.Info} />
13
+
14
+ # API
15
+
16
+ <Controls of={CustomInputSelectStories.Default} />
17
+
18
+ # Exemple d'utilisation
19
+
20
+ <Source dark code={`
21
+ <script setup lang="ts">
22
+ import CustomInputSelect from '@/components/Customs/CustomInputSelect/CustomInputSelect.vue'
23
+ import { ref } from 'vue'
24
+
25
+ const value = ref('fr')
26
+ const items = ['fr', 'en', 'co']
27
+ const ariaLabel = 'Choix de la langue'
28
+ </script>
29
+
30
+ <template>
31
+ <main>
32
+ <div class="mt-12 ml-12">
33
+ <CustomInputSelect
34
+ v-model="value"
35
+ :items="items"
36
+ :label="ariaLabel"
37
+ aria-label="Choix de la langue"
38
+ />
39
+ </div>
40
+ </main>
41
+ </template>
42
+ `} />
@@ -0,0 +1,154 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import CustomInputSelect from './CustomInputSelect.vue'
3
+ import { VBtn, VMenu, VList, VListItem, VListItemTitle } from 'vuetify/components'
4
+ import { ref } from 'vue'
5
+ import Alert from '../../Alert/Alert.vue'
6
+
7
+ const meta = {
8
+ title: 'Components/CustomInputSelect',
9
+ component: CustomInputSelect,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ controls: { exclude: ['selectedValue'] },
13
+ },
14
+ argTypes: {
15
+ selectedValue: { control: 'text' },
16
+ items: { control: 'object' },
17
+ errorMessages: { control: 'object' },
18
+ },
19
+ } as Meta<typeof CustomInputSelect>
20
+
21
+ export default meta
22
+
23
+ type Story = StoryObj<typeof meta>
24
+ export const Default: Story = {
25
+ args: {
26
+ items: [
27
+ { text: 'Option 1', value: '1' },
28
+ { text: 'Option 2', value: '2' },
29
+ ],
30
+ },
31
+ render: (args) => {
32
+ return {
33
+ components: { CustomInputSelect, VBtn, VMenu, VList, VListItem, VListItemTitle },
34
+ setup() {
35
+ return { args }
36
+ },
37
+ template: `
38
+ <div class="d-flex flex-wrap align-center pa-4">
39
+ <CustomInputSelect
40
+ v-bind="args"
41
+ />
42
+ </div>
43
+ <br/><br/><br/><br/>
44
+ `,
45
+ }
46
+ },
47
+ }
48
+
49
+ export const Outlined: Story = {
50
+ args: {
51
+ items: [
52
+ { text: 'Option 1', value: '1' },
53
+ { text: 'Option 2', value: '2' },
54
+ ],
55
+ },
56
+ render: (args) => {
57
+ return {
58
+ components: { CustomInputSelect, VBtn, VMenu, VList, VListItem, VListItemTitle },
59
+ setup() {
60
+ return { args }
61
+ },
62
+ template: `
63
+ <div class="d-flex flex-wrap align-center pa-4">
64
+ <CustomInputSelect
65
+ v-bind="args"
66
+ outlined
67
+ />
68
+ </div>
69
+ `,
70
+ }
71
+ },
72
+ }
73
+
74
+ export const withError: Story = {
75
+ args: {
76
+ items: [
77
+ { text: 'Option 1', value: '1' },
78
+ { text: 'Option 2', value: '2' },
79
+ ],
80
+ },
81
+ render: (args) => {
82
+ return {
83
+ components: { CustomInputSelect, VBtn, VMenu, VList, VListItem, VListItemTitle },
84
+ setup() {
85
+ const errorMessages = ref([])
86
+ const triggerError = () => {
87
+ // @ts-expect-error test error message
88
+ errorMessages.value = ['This is a test error message']
89
+ }
90
+ return { args, errorMessages, triggerError }
91
+ },
92
+ template: `
93
+ <div class="d-flex flex-wrap align-center pa-4">
94
+ <CustomInputSelect
95
+ v-bind="args"
96
+ :error-messages="errorMessages"
97
+ />
98
+ <VBtn @click="triggerError">
99
+ Trigger Error
100
+ </VBtn>
101
+ </div>
102
+ `,
103
+ }
104
+ },
105
+ }
106
+
107
+ export const withCustomKey: Story = {
108
+ args: {
109
+ items: [
110
+ { customKey: 'Option 1', value: '1' },
111
+ { customKey: 'Option 2', value: '2' },
112
+ ],
113
+ },
114
+ render: (args) => {
115
+ return {
116
+ components: { CustomInputSelect, VBtn, VMenu, VList, VListItem, VListItemTitle },
117
+ setup() {
118
+ return { args }
119
+ },
120
+ template: `
121
+ <div class="d-flex flex-wrap align-center pa-4">
122
+ <CustomInputSelect
123
+ v-bind="args"
124
+ outlined
125
+ text-key="customKey"
126
+ />
127
+ </div>
128
+ `,
129
+ }
130
+ },
131
+ }
132
+
133
+ export const Info: Story = {
134
+ render: (args) => {
135
+ return {
136
+ components: { Alert },
137
+ setup() {
138
+ return { args }
139
+ },
140
+ template: `
141
+ <Alert v-model="args.modelValue" :type="args.type" :variant="tonal" :closable="false">
142
+ <template #default>
143
+ <b>Format des items :</b>
144
+ <ul>
145
+ <li>- Si les items passés en props sont des objets, le composant les utilisera directement.</li>
146
+ <li>- Si les items sont un tableau de string, le composant les utilisera directement.</li>
147
+ </ul>
148
+ </template>
149
+ </Alert>
150
+ `,
151
+ }
152
+ },
153
+ tags: ['!dev'],
154
+ }
@@ -0,0 +1,185 @@
1
+ <script setup lang="ts">
2
+ import { mdiMenuDown } from '@mdi/js'
3
+ import { ref, watch, computed, type PropType } from 'vue'
4
+ import { VIcon, VList, VListItem, VListItemTitle } from 'vuetify/components'
5
+
6
+ const props = defineProps({
7
+ modelValue: {
8
+ type: [Object, String],
9
+ default: null,
10
+ },
11
+ items: {
12
+ type: Array,
13
+ default: () => [],
14
+ },
15
+ label: {
16
+ type: String,
17
+ default: 'Sélectionnez une option',
18
+ },
19
+ errorMessages: {
20
+ type: [String, Array] as PropType<string | readonly string[]>,
21
+ default: () => [],
22
+ },
23
+ required: {
24
+ type: Boolean,
25
+ default: false,
26
+ },
27
+ menuId: {
28
+ type: String,
29
+ default: 'custom-select-menu',
30
+ },
31
+ outlined: {
32
+ type: Boolean,
33
+ default: false,
34
+ },
35
+ textKey: {
36
+ type: String,
37
+ default: 'text',
38
+ },
39
+ valueKey: {
40
+ type: String,
41
+ default: 'value',
42
+ },
43
+ })
44
+
45
+ const emit = defineEmits(['update:modelValue'])
46
+
47
+ const isOpen = ref(false)
48
+ const selectedItem = ref<Record<string, unknown > | string | null>(props.modelValue)
49
+
50
+ const toggleMenu = () => {
51
+ isOpen.value = !isOpen.value
52
+ }
53
+
54
+ const closeList = () => {
55
+ isOpen.value = false
56
+ }
57
+
58
+ const inputId = ref(`custom-input-select-${Math.random().toString(36).substring(7)}`)
59
+
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
61
+ const selectItem = (item: any) => {
62
+ selectedItem.value = item
63
+ emit('update:modelValue', item)
64
+ isOpen.value = false
65
+ }
66
+
67
+ const getItemText = (item: unknown) => {
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
69
+ return (item as Record<string, any>)[props.textKey]
70
+ }
71
+
72
+ const selectedItemText = computed(() => {
73
+ if (selectedItem.value && typeof selectedItem.value === 'object') {
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
75
+ return (selectedItem.value as Record<string, any>)[props.textKey]
76
+ }
77
+ return props.label
78
+ })
79
+
80
+ watch(() => props.modelValue, (newValue) => {
81
+ selectedItem.value = newValue
82
+ })
83
+
84
+ const buttonClass = computed(() => {
85
+ return props.outlined ? 'v-btn v-btn--density-default v-btn--size-default v-btn--variant-outlined' : 'text-color'
86
+ })
87
+
88
+ const formattedItems = computed(() => {
89
+ return props.items.map((item) => {
90
+ if (typeof item === 'string') {
91
+ return { [props.textKey]: item, [props.valueKey]: item }
92
+ }
93
+ return item
94
+ })
95
+ })
96
+ </script>
97
+
98
+ <template>
99
+ <v-input
100
+ :id="inputId"
101
+ v-model="selectedItem"
102
+ :label="props.label"
103
+ :title="props.label"
104
+ role="menu"
105
+ :error-messages="errorMessages"
106
+ :required="required"
107
+ >
108
+ <div
109
+ ref="menu"
110
+ v-click-outside="closeList"
111
+ :class="['custom-select', buttonClass, 'primary']"
112
+ role="menu"
113
+ tabindex="0"
114
+ @click="toggleMenu"
115
+ @keydown.enter.prevent="toggleMenu"
116
+ @keydown.space.prevent="toggleMenu"
117
+ >
118
+ <span>{{ selectedItemText }}</span>
119
+ <VIcon> {{ mdiMenuDown }}</VIcon>
120
+ </div>
121
+ <VList
122
+ v-if="isOpen"
123
+ class="v-list"
124
+ :style="`max-width: ${$refs.menu ? $refs.menu.getBoundingClientRect().width : 0}px;`"
125
+ :aria-label="props.label"
126
+ :title="props.label"
127
+ @keydown.esc.prevent="isOpen = false"
128
+ >
129
+ <VListItem
130
+ v-for="(item, index) in formattedItems"
131
+ :key="index"
132
+ :ref="'options-' + index"
133
+ role="option"
134
+ class="v-list-item"
135
+ :aria-selected="selectedItem === item"
136
+ :tabindex="index + 1"
137
+ @click="selectItem(item)"
138
+ >
139
+ <VListItemTitle>
140
+ {{ getItemText(item) }}
141
+ </VListItemTitle>
142
+ </VListItem>
143
+ </VList>
144
+ </v-input>
145
+ </template>
146
+
147
+ <style scoped lang="scss">
148
+ @use '@/assets/tokens.scss';
149
+
150
+ .v-input {
151
+ cursor: pointer;
152
+ position: relative;
153
+ }
154
+
155
+ .v-list {
156
+ position: absolute;
157
+ top: 36px;
158
+ width: 100%;
159
+ z-index: 1;
160
+ background-color: white;
161
+ min-width: fit-content;
162
+ max-width: 150px;
163
+ padding: 0;
164
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12), 0 2px 10px rgba(0, 0, 0, 0.08);
165
+ border-radius: 4px;
166
+ overflow-y: auto;
167
+ max-height: 300px;
168
+ }
169
+
170
+ .v-list-item:hover {
171
+ background-color: rgba(0, 0, 0, 0.04);
172
+ }
173
+
174
+ .v-list-item[aria-selected='true'] {
175
+ background-color: rgba(0, 0, 0, 0.08);
176
+ }
177
+
178
+ .v-btn {
179
+ color: tokens.$blue-base;
180
+ }
181
+
182
+ .text-color {
183
+ color: tokens.$blue-base;
184
+ }
185
+ </style>
@@ -0,0 +1,216 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { expect, describe, it } from 'vitest'
3
+ import CustomSelect from '../CustomInputSelect.vue'
4
+ import { vuetify } from '@tests/unit/setup'
5
+
6
+ describe('CustomInputSelect.vue', () => {
7
+ it('renders the component with default props', () => {
8
+ const wrapper = mount(CustomSelect, {
9
+ global: {
10
+ plugins: [vuetify],
11
+ },
12
+ })
13
+ expect(wrapper.exists()).toBe(true)
14
+ expect(wrapper.find('.custom-select').text()).toBe('Sélectionnez une option')
15
+ })
16
+
17
+ it('toggles the menu when clicked', async () => {
18
+ const wrapper = mount(CustomSelect, {
19
+ global: {
20
+ plugins: [vuetify],
21
+ },
22
+ })
23
+ await wrapper.find('.custom-select').trigger('click')
24
+ expect(wrapper.find('.v-list').exists()).toBe(true)
25
+ await wrapper.find('.custom-select').trigger('click')
26
+ expect(wrapper.find('.v-list').exists()).toBe(false)
27
+ })
28
+
29
+ it('selects an item when clicked', async () => {
30
+ const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
31
+ const wrapper = mount(CustomSelect, {
32
+ props: { items },
33
+ global: {
34
+ plugins: [vuetify],
35
+ },
36
+ })
37
+ await wrapper.find('.custom-select').trigger('click')
38
+ const firstItem = wrapper.findAll('.v-list-item').at(0)
39
+ if (firstItem) {
40
+ await firstItem.trigger('click')
41
+ }
42
+ expect(wrapper.emitted()['update:modelValue'][0]).toEqual([{ text: 'Option 1', value: '1' }])
43
+ })
44
+
45
+ it('closes the menu when an item is selected', async () => {
46
+ const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
47
+ const wrapper = mount(CustomSelect, {
48
+ props: { items },
49
+ global: {
50
+ plugins: [vuetify],
51
+ },
52
+ })
53
+ await wrapper.find('.custom-select').trigger('click')
54
+ const firstItem = wrapper.findAll('.v-list-item').at(0)
55
+ if (firstItem) {
56
+ await firstItem.trigger('click')
57
+ }
58
+ expect(wrapper.find('.v-list').exists()).toBe(false)
59
+ })
60
+
61
+ it('displays the selected item text', async () => {
62
+ const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
63
+ const wrapper = mount(CustomSelect, {
64
+ props: { items, modelValue: { text: 'Option 1', value: '1' } },
65
+ global: {
66
+ plugins: [vuetify],
67
+ },
68
+ })
69
+ expect(wrapper.find('.custom-select').text()).toContain('Option 1')
70
+ })
71
+
72
+ it('closes the menu on escape key press', async () => {
73
+ const items = [{ text: 'Option 1', value: '1' }, { text: 'Option 2', value: '2' }]
74
+ const wrapper = mount(CustomSelect, {
75
+ props: { items },
76
+ global: {
77
+ plugins: [vuetify],
78
+ },
79
+ })
80
+ await wrapper.find('.custom-select').trigger('click')
81
+ await wrapper.find('.v-list').trigger('keydown.esc')
82
+ expect(wrapper.find('.v-list').exists()).toBe(false)
83
+ })
84
+
85
+ it('renders error messages when provided', () => {
86
+ const errorMessages = ['Error 1']
87
+ const wrapper = mount(CustomSelect, {
88
+ props: { errorMessages },
89
+ global: {
90
+ plugins: [vuetify],
91
+ },
92
+ })
93
+ expect(wrapper.find('.v-messages__message').text()).toContain('Error 1')
94
+ })
95
+
96
+ it('does not render error messages when not provided', () => {
97
+ const wrapper = mount(CustomSelect, {
98
+ global: {
99
+ plugins: [vuetify],
100
+ },
101
+ })
102
+ expect(wrapper.find('.v-messages__message').exists()).toBe(false)
103
+ })
104
+
105
+ it('does not render the label when not provided', () => {
106
+ const wrapper = mount(CustomSelect, {
107
+ global: {
108
+ plugins: [vuetify],
109
+ },
110
+ })
111
+ expect(wrapper.find('label').exists()).toBe(false)
112
+ })
113
+
114
+ it('returns the correct item text using getItemText', () => {
115
+ const wrapper = mount(CustomSelect, {
116
+ props: { textKey: 'text' },
117
+ global: {
118
+ plugins: [vuetify],
119
+ },
120
+ })
121
+ const item = { text: 'Option 1', value: '1' }
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
123
+ const instance = wrapper.vm as any
124
+ expect(instance.getItemText(item)).toBe('Option 1')
125
+ })
126
+
127
+ it('returns default text when selectedItem is null', () => {
128
+ const wrapper = mount(CustomSelect, {
129
+ global: {
130
+ plugins: [vuetify],
131
+ },
132
+ })
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
134
+ const instance = wrapper.vm as any
135
+ expect(instance.selectedItemText).toBe('Sélectionnez une option')
136
+ })
137
+
138
+ it('returns the correct text when selectedItem is an object', async () => {
139
+ const wrapper = mount(CustomSelect, {
140
+ props: { modelValue: { text: 'Option 1', value: '1' }, textKey: 'text' },
141
+ global: {
142
+ plugins: [vuetify],
143
+ },
144
+ })
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
146
+ const instance = wrapper.vm as any
147
+ await wrapper.setProps({ modelValue: { text: 'Option 1', value: '1' } })
148
+ expect(instance.selectedItemText).toBe('Option 1')
149
+ })
150
+
151
+ it('formats items correctly', () => {
152
+ const items = ['Option 1', 'Option 2']
153
+ const wrapper = mount(CustomSelect, {
154
+ props: { items, textKey: 'text', valueKey: 'value' },
155
+ global: {
156
+ plugins: [vuetify],
157
+ },
158
+ })
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
160
+ const formattedItems = (wrapper.vm as any).formattedItems
161
+ expect(formattedItems).toEqual([
162
+ { text: 'Option 1', value: 'Option 1' },
163
+ { text: 'Option 2', value: 'Option 2' },
164
+ ])
165
+ })
166
+
167
+ it('applies the correct button class when outlined is true', () => {
168
+ const wrapper = mount(CustomSelect, {
169
+ props: { outlined: true },
170
+ global: {
171
+ plugins: [vuetify],
172
+ },
173
+ })
174
+ expect(wrapper.find('.custom-select').classes()).toContain('v-btn--variant-outlined')
175
+ })
176
+
177
+ it('does not apply the outlined button class when outlined is false', () => {
178
+ const wrapper = mount(CustomSelect, {
179
+ props: { outlined: false },
180
+ global: {
181
+ plugins: [vuetify],
182
+ },
183
+ })
184
+ expect(wrapper.find('.custom-select').classes()).not.toContain('v-btn--variant-outlined')
185
+ })
186
+
187
+ it('updates selectedItem when v-model changes', async () => {
188
+ const wrapper = mount(CustomSelect, {
189
+ props: { modelValue: { text: 'Option 1', value: '1' }, textKey: 'text' },
190
+ global: {
191
+ plugins: [vuetify],
192
+ },
193
+ })
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
195
+ const instance = wrapper.vm as any
196
+ expect(instance.selectedItem).toEqual({ text: 'Option 1', value: '1' })
197
+
198
+ await wrapper.setProps({ modelValue: { text: 'Option 2', value: '2' } })
199
+ expect(instance.selectedItem).toEqual({ text: 'Option 2', value: '2' })
200
+ })
201
+
202
+ it('emits update:modelValue when selectedItem changes', async () => {
203
+ const wrapper = mount(CustomSelect, {
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
205
+ props: { modelValue: null as any, textKey: 'text' },
206
+ global: {
207
+ plugins: [vuetify],
208
+ },
209
+ })
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
211
+ const instance = wrapper.vm as any
212
+ instance.selectItem({ text: 'Option 1', value: '1' })
213
+ await wrapper.vm.$nextTick()
214
+ expect(wrapper.emitted()['update:modelValue'][0]).toEqual([{ text: 'Option 1', value: '1' }])
215
+ })
216
+ })
@@ -0,0 +1,47 @@
1
+ import { Canvas, Meta, Controls, Story, Source } from '@storybook/blocks';
2
+ import * as CustomSelectStories from "./CustomSelect.stories";
3
+
4
+ <Meta of={CustomSelectStories} />
5
+
6
+ # CustomSelect
7
+
8
+ Le composant `CustomSelect` est utilisé pour proposer une alternative au `v-select` de Vuetify qui ne respecte pas les règles d'accessibilité RGAA.
9
+
10
+ <Canvas of={CustomSelectStories.Default} />
11
+
12
+ <Story of={CustomSelectStories.Info} />
13
+
14
+ # API
15
+
16
+ <Controls of={CustomSelectStories.Default} />
17
+
18
+ # Exemple d'utilisation
19
+
20
+ <Source dark code={`
21
+ <script setup lang="ts">
22
+ import { ref } from 'vue'
23
+ import CustomSelect from '@/components/Customs/CustomSelect/CustomSelect.vue'
24
+
25
+ const selectedValue = ref(undefined)
26
+
27
+ const items = ref([
28
+ { text: 'Option 1', value: '1' },
29
+ { text: 'Option 2', value: '2' },
30
+ ])
31
+ </script>
32
+
33
+ <template>
34
+ <main class="ma-12">
35
+ SelectedValue: {{ selectedValue }}
36
+ <div class="d-flex flex-wrap align-center">
37
+ <custom-select
38
+ v-model="selectedValue"
39
+ :items="items"
40
+ text-key="text"
41
+ outlined
42
+ required
43
+ />
44
+ </div>
45
+ </main>
46
+ </template>
47
+ `} />