@cnamts/synapse 0.0.8-alpha → 0.0.9-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/design-system-v3.d.ts +584 -128
  2. package/dist/design-system-v3.js +4176 -2694
  3. package/dist/design-system-v3.umd.cjs +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/assets/settings.scss +1 -1
  7. package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
  8. package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
  9. package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
  10. package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
  11. package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
  12. package/src/components/Customs/SySelect/SySelect.vue +9 -4
  13. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
  14. package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
  15. package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
  16. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
  17. package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
  18. package/src/components/Customs/SyTextField/types.d.ts +2 -2
  19. package/src/components/DatePicker/DatePicker.mdx +191 -0
  20. package/src/components/DatePicker/DatePicker.stories.ts +787 -0
  21. package/src/components/DatePicker/DatePicker.vue +560 -0
  22. package/src/components/DatePicker/DateTextInput.vue +409 -0
  23. package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
  24. package/src/components/DialogBox/DialogBox.stories.ts +1 -1
  25. package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
  26. package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
  27. package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
  28. package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
  29. package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +9 -9
  30. package/src/components/FileUpload/FileUpload.mdx +165 -0
  31. package/src/components/FileUpload/FileUpload.stories.ts +429 -0
  32. package/src/components/FileUpload/FileUpload.vue +195 -0
  33. package/src/components/FileUpload/FileUploadContent.vue +109 -0
  34. package/src/components/FileUpload/locales.ts +10 -0
  35. package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
  36. package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
  37. package/src/components/FileUpload/useFileDrop.ts +23 -0
  38. package/src/components/FileUpload/validateFiles.ts +39 -0
  39. package/src/components/NirField/NirField.stories.ts +1 -1
  40. package/src/components/NirField/NirField.vue +2 -1
  41. package/src/components/PasswordField/Accessibilite.mdx +14 -0
  42. package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
  43. package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
  44. package/src/components/PasswordField/PasswordField.vue +3 -3
  45. package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
  46. package/src/components/PhoneField/PhoneField.vue +44 -60
  47. package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
  48. package/src/components/RangeField/RangeField.mdx +54 -0
  49. package/src/components/RangeField/RangeField.stories.ts +189 -0
  50. package/src/components/RangeField/RangeField.vue +157 -0
  51. package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
  52. package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
  53. package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
  54. package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
  55. package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
  56. package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
  57. package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
  58. package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
  59. package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
  60. package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
  61. package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
  62. package/src/components/RangeField/RangeSlider/types.ts +15 -0
  63. package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
  64. package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
  65. package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
  66. package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
  67. package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
  68. package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
  69. package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
  70. package/src/components/RangeField/config.ts +7 -0
  71. package/src/components/RangeField/locales.ts +4 -0
  72. package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
  73. package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
  74. package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
  75. package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
  76. package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
  77. package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
  78. package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
  79. package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
  80. package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
  81. package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
  82. package/src/components/RatingPicker/Rating.ts +45 -0
  83. package/src/components/RatingPicker/RatingPicker.mdx +56 -0
  84. package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
  85. package/src/components/RatingPicker/RatingPicker.vue +122 -0
  86. package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
  87. package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
  88. package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
  89. package/src/components/RatingPicker/locales.ts +3 -0
  90. package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
  91. package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
  92. package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
  93. package/src/components/SearchListField/SearchListField.mdx +74 -0
  94. package/src/components/SearchListField/SearchListField.stories.ts +126 -0
  95. package/src/components/SearchListField/SearchListField.vue +194 -0
  96. package/src/components/SearchListField/locales.ts +5 -0
  97. package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
  98. package/src/components/SearchListField/types.d.ts +4 -0
  99. package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
  100. package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
  101. package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
  102. package/src/components/SelectBtnField/config.ts +11 -0
  103. package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
  104. package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
  105. package/src/components/SelectBtnField/types.d.ts +11 -0
  106. package/src/components/index.ts +8 -1
  107. package/src/composables/rules/useFieldValidation.ts +172 -44
  108. package/src/designTokens/index.ts +3 -3
  109. package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
  110. package/src/utils/calcHumanFileSize/index.ts +12 -0
  111. package/src/utils/calcHumanFileSize/tests/calcHumanFileSize.spec.ts +21 -0
@@ -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
+ }