@mozaic-ds/vue 2.15.0 → 2.17.0

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 (125) hide show
  1. package/dist/mozaic-vue.css +2 -1
  2. package/dist/mozaic-vue.d.ts +1040 -408
  3. package/dist/mozaic-vue.js +17183 -6742
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +26 -6
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +11 -5
  8. package/src/components/BrandPresets.mdx +2 -2
  9. package/src/components/ComponentsMapping.mdx +98 -0
  10. package/src/components/accordionlist/MAccordionList.figma.ts +43 -0
  11. package/src/components/accordionlistitem/MAccordionListItem.figma.ts +27 -0
  12. package/src/components/actionbottombar/MActionBottomBar.figma.ts +24 -0
  13. package/src/components/actionlistbox/MActionListbox.figma.ts +30 -0
  14. package/src/components/actionlistbox/MActionListbox.spec.ts +14 -0
  15. package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
  16. package/src/components/actionlistbox/MActionListbox.vue +13 -1
  17. package/src/components/actionlistbox/README.md +2 -1
  18. package/src/components/avatar/MAvatar.figma.ts +31 -0
  19. package/src/components/breadcrumb/MBreadcrumb.figma.ts +31 -0
  20. package/src/components/builtinmenu/MBuiltInMenu.figma.ts +23 -0
  21. package/src/components/button/MButton.figma.ts +41 -0
  22. package/src/components/button/README.md +2 -0
  23. package/src/components/callout/MCallout.figma.ts +29 -0
  24. package/src/components/carousel/MCarousel.figma.ts +32 -0
  25. package/src/components/checkbox/MCheckbox.figma.ts +45 -0
  26. package/src/components/checkboxgroup/MCheckboxGroup.figma.ts +30 -0
  27. package/src/components/checklistmenu/MCheckListMenu.figma.ts +29 -0
  28. package/src/components/circularprogressbar/MCircularProgressbar.figma.ts +31 -0
  29. package/src/components/combobox/MCombobox.figma.ts +48 -0
  30. package/src/components/combobox/MCombobox.spec.ts +246 -0
  31. package/src/components/combobox/MCombobox.stories.ts +190 -0
  32. package/src/components/combobox/MCombobox.vue +286 -0
  33. package/src/components/combobox/README.md +52 -0
  34. package/src/components/container/MContainer.figma.ts +30 -0
  35. package/src/components/datatable/DataTable.stories.ts +277 -0
  36. package/src/components/datatable/DataTableCells.stories.ts +251 -0
  37. package/src/components/datatable/DataTableEmpty.stories.ts +102 -0
  38. package/src/components/datatable/DataTableExpandable.stories.ts +95 -0
  39. package/src/components/datatable/DataTableNested.stories.ts +96 -0
  40. package/src/components/datatable/DataTableSelectable.stories.ts +124 -0
  41. package/src/components/datatable/DataTableSortable.stories.ts +164 -0
  42. package/src/components/datatable/MDataTable.types.ts +54 -0
  43. package/src/components/datatable/assets/styles.scss +10 -0
  44. package/src/components/datatable/datatable.mdx +62 -0
  45. package/src/components/datatable/tools/data.js +8 -0
  46. package/src/components/datatable/tools/data.json +2018 -0
  47. package/src/components/datatable/utils.js +19 -0
  48. package/src/components/datepicker/MDatepicker.figma.ts +20 -0
  49. package/src/components/divider/MDivider.figma.ts +30 -0
  50. package/src/components/drawer/MDrawer.figma.ts +37 -0
  51. package/src/components/drawer/README.md +1 -1
  52. package/src/components/field/MField.figma.ts +30 -0
  53. package/src/components/field/MField.stories.ts +105 -0
  54. package/src/components/fileuploader/MFileUploader.figma.ts +23 -0
  55. package/src/components/fileuploaderitem/MFileUploaderItem.figma.ts +27 -0
  56. package/src/components/flag/MFlag.figma.ts +26 -0
  57. package/src/components/iconbutton/MIconButton.figma.ts +54 -0
  58. package/src/components/kpiitem/MKpiItem.figma.ts +33 -0
  59. package/src/components/linearprogressbarbuffer/MLinearProgressbarBuffer.figma.ts +31 -0
  60. package/src/components/linearprogressbarpercentage/MLinearProgressbarPercentage.figma.ts +26 -0
  61. package/src/components/link/MLink.figma.ts +32 -0
  62. package/src/components/loader/MLoader.figma.ts +30 -0
  63. package/src/components/loadingoverlay/MLoadingOverlay.figma.ts +18 -0
  64. package/src/components/modal/MModal.figma.ts +27 -0
  65. package/src/components/navigationindicator/MNavigationIndicator.figma.ts +24 -0
  66. package/src/components/numberbadge/MNumberBadge.figma.ts +31 -0
  67. package/src/components/optionListbox/MOptionListbox.figma.ts +36 -0
  68. package/src/components/optionListbox/MOptionListbox.spec.ts +527 -0
  69. package/src/components/optionListbox/MOptionListbox.vue +470 -0
  70. package/src/components/optionListbox/README.md +63 -0
  71. package/src/components/overlay/MOverlay.figma.ts +20 -0
  72. package/src/components/pageheader/MPageHeader.figma.ts +21 -0
  73. package/src/components/pagination/MPagination.figma.ts +34 -0
  74. package/src/components/passwordinput/MPasswordInput.figma.ts +30 -0
  75. package/src/components/phonenumber/MPhoneNumber.figma.ts +47 -0
  76. package/src/components/pincode/MPincode.figma.ts +41 -0
  77. package/src/components/pincode/MPincode.spec.ts +1 -4
  78. package/src/components/pincode/MPincode.vue +11 -15
  79. package/src/components/popover/MPopover.figma.ts +42 -0
  80. package/src/components/quantityselector/MQuantitySelector.figma.ts +50 -0
  81. package/src/components/radio/MRadio.figma.ts +40 -0
  82. package/src/components/radiogroup/MRadioGroup.figma.ts +30 -0
  83. package/src/components/segmentedcontrol/MSegmentedControl.figma.ts +33 -0
  84. package/src/components/select/MSelect.figma.ts +49 -0
  85. package/src/components/sidebar/MSidebar.figma.ts +28 -0
  86. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.figma.ts +19 -0
  87. package/src/components/sidebarfooter/MSidebarFooter.figma.ts +21 -0
  88. package/src/components/sidebarheader/MSidebarHeader.figma.ts +18 -0
  89. package/src/components/sidebarnavitem/MSidebarNavItem.figma.ts +23 -0
  90. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.figma.ts +20 -0
  91. package/src/components/starrating/MStarRating.figma.ts +35 -0
  92. package/src/components/statusbadge/MStatusBadge.figma.ts +27 -0
  93. package/src/components/statusdot/MStatusDot.figma.ts +31 -0
  94. package/src/components/statusmessage/MStatusMessage.figma.ts +28 -0
  95. package/src/components/statusmessage/MStatusMessage.spec.ts +15 -0
  96. package/src/components/statusmessage/MStatusMessage.stories.ts +4 -0
  97. package/src/components/statusmessage/MStatusMessage.vue +7 -0
  98. package/src/components/statusmessage/README.md +2 -0
  99. package/src/components/statusnotification/MStatusNotification.figma.ts +29 -0
  100. package/src/components/stepperbottombar/MStepperBottomBar.figma.ts +20 -0
  101. package/src/components/steppercompact/MStepperCompact.figma.ts +21 -0
  102. package/src/components/stepperinline/MStepperInline.figma.ts +23 -0
  103. package/src/components/stepperstacked/MStepperStacked.figma.ts +23 -0
  104. package/src/components/stepperstacked/MStepperStacked.spec.ts +162 -0
  105. package/src/components/stepperstacked/MStepperStacked.stories.ts +57 -0
  106. package/src/components/stepperstacked/MStepperStacked.vue +106 -0
  107. package/src/components/stepperstacked/README.md +15 -0
  108. package/src/components/tabs/MTabs.figma.ts +33 -0
  109. package/src/components/tag/MTag.figma.ts +26 -0
  110. package/src/components/tag/MTag.stories.ts +13 -3
  111. package/src/components/tag/MTag.vue +11 -1
  112. package/src/components/tag/README.md +6 -0
  113. package/src/components/textarea/MTextArea.figma.ts +28 -0
  114. package/src/components/textinput/MTextInput.figma.ts +51 -0
  115. package/src/components/textinput/MTextInput.vue +13 -1
  116. package/src/components/textinput/README.md +15 -1
  117. package/src/components/tile/MTile.figma.ts +31 -0
  118. package/src/components/tileclickable/MTileClickable.figma.ts +31 -0
  119. package/src/components/tileexpandable/MTileExpandable.figma.ts +31 -0
  120. package/src/components/tileselectable/MTileSelectable.figma.ts +29 -0
  121. package/src/components/toaster/MToaster.figma.ts +25 -0
  122. package/src/components/toggle/MToggle.figma.ts +39 -0
  123. package/src/components/togglegroup/MToggleGroup.figma.ts +30 -0
  124. package/src/components/tooltip/MTooltip.figma.ts +29 -0
  125. package/src/main.ts +1 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Code Connect mapping for MCarousel
3
+ * Links Figma Carousel (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=5-6454',
9
+ {
10
+ props: {
11
+ hasScrollbar: figma.boolean('Has scrollbar'),
12
+ hasPagination: figma.boolean('Has pagination'),
13
+ breakpoint: figma.enum('Breakpoint', {
14
+ 'S to M (320 to 1023px)': 's',
15
+ 'L to XXL (1024 and up)': 'l',
16
+ }),
17
+ },
18
+ example: () =>
19
+ html`<script setup>
20
+ import { MCarousel } from '@mozaic-ds/vue';
21
+ </script>
22
+
23
+ <MCarousel>
24
+ <template #header>
25
+ <h2>Carousel title</h2>
26
+ </template>
27
+ <div>Slide 1</div>
28
+ <div>Slide 2</div>
29
+ <div>Slide 3</div>
30
+ </MCarousel>`,
31
+ },
32
+ );
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Code Connect mapping for MCheckbox
3
+ * Links Figma Checkbox (stand-alone) (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-29180',
9
+ {
10
+ props: {
11
+ modelValue: figma.enum('Is checked', {
12
+ True: true,
13
+ False: false,
14
+ }),
15
+ indeterminate: figma.enum('is Indeterminate', {
16
+ True: true,
17
+ False: false,
18
+ }),
19
+ disabled: figma.enum('State', {
20
+ Disabled: true,
21
+ Default: false,
22
+ Hovered: false,
23
+ Focused: false,
24
+ 'Read-only': false,
25
+ }),
26
+ isInvalid: figma.enum('is invalid', {
27
+ True: true,
28
+ False: false,
29
+ }),
30
+ },
31
+ example: ({ modelValue, indeterminate, disabled, isInvalid }) =>
32
+ html`<script setup>
33
+ import { MCheckbox } from '@mozaic-ds/vue';
34
+ </script>
35
+
36
+ <MCheckbox
37
+ id="checkbox-id"
38
+ label="Checkbox Label"
39
+ :model-value=${modelValue}
40
+ :indeterminate=${indeterminate}
41
+ disabled=${disabled}
42
+ :is-invalid=${isInvalid}
43
+ ></MCheckbox>`,
44
+ },
45
+ );
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Code Connect mapping for MCheckboxGroup
3
+ * Links Figma Checkbox group (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=6-29096',
9
+ {
10
+ props: {
11
+ inline: figma.enum('Layout', {
12
+ 'Stacked (default)': false,
13
+ Inline: true,
14
+ }),
15
+ },
16
+ example: ({ inline }) =>
17
+ html`<script setup>
18
+ import { MCheckboxGroup } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MCheckboxGroup
22
+ name="group"
23
+ :options="[
24
+ { id: '1', label: 'Option 1', value: '1' },
25
+ { id: '2', label: 'Option 2', value: '2' },
26
+ ]"
27
+ inline=${inline}
28
+ />`,
29
+ },
30
+ );
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Code Connect mapping for MCheckListMenu
3
+ * Links Figma Check-list menu (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=72-30681',
9
+ {
10
+ props: {
11
+ outlined: figma.enum('is outlined', {
12
+ 'true': true,
13
+ 'false': false,
14
+ }),
15
+ },
16
+ example: ({ outlined }) =>
17
+ html`<script setup>
18
+ import { MCheckListMenu } from '@mozaic-ds/vue';
19
+ </script>
20
+
21
+ <MCheckListMenu
22
+ :items="[
23
+ { label: 'Label', checked: true },
24
+ { label: 'Label', checked: false },
25
+ ]"
26
+ outlined=${outlined}
27
+ />`,
28
+ },
29
+ );
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Code Connect mapping for MCircularProgressbar
3
+ * Links Figma Circular progress bar (ADS2) to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=61-2204',
9
+ {
10
+ props: {
11
+ size: figma.enum('Size', {
12
+ S: 's',
13
+ M: 'm',
14
+ L: 'l',
15
+ }),
16
+ value: figma.enum('Completion', {
17
+ '0%': 0,
18
+ '25%': 25,
19
+ '50%': 50,
20
+ '75%': 75,
21
+ '100%': 100,
22
+ }),
23
+ },
24
+ example: ({ size, value }) =>
25
+ html`<script setup>
26
+ import { MCircularProgressbar } from '@mozaic-ds/vue';
27
+ </script>
28
+
29
+ <MCircularProgressbar size=${size} :value=${value} aria-label="Progress bar"></MCircularProgressbar>`,
30
+ },
31
+ );
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Code Connect mapping for MCombobox
3
+ * Links Figma _combobox / base to @mozaic-ds/vue
4
+ */
5
+ import figma, { html } from '@figma/code-connect/html';
6
+
7
+ figma.connect(
8
+ 'https://www.figma.com/design/Zyh9RyabNaqkjbuFWP9Aqj/%E2%9C%A8-Components--ADS2---Stable-version-?node-id=19683-17907',
9
+ {
10
+ props: {
11
+ size: figma.enum('Size', {
12
+ S: 's',
13
+ 'M (default)': 'm',
14
+ }),
15
+ disabled: figma.enum('State', {
16
+ Disabled: true,
17
+ Default: false,
18
+ Hovered: false,
19
+ Focused: false,
20
+ 'Read-only': false,
21
+ }),
22
+ readonly: figma.enum('State', {
23
+ 'Read-only': true,
24
+ Default: false,
25
+ Hovered: false,
26
+ Focused: false,
27
+ Disabled: false,
28
+ }),
29
+ isInvalid: figma.enum('Is invalid', {
30
+ True: true,
31
+ False: false,
32
+ }),
33
+ },
34
+ example: ({ size, disabled, readonly, isInvalid }) =>
35
+ html`<script setup>
36
+ import { MCombobox } from '@mozaic-ds/vue';
37
+ </script>
38
+
39
+ <MCombobox
40
+ id="combobox-id"
41
+ size=${size}
42
+ disabled=${disabled}
43
+ readonly=${readonly}
44
+ :is-invalid=${isInvalid}
45
+ :options="[{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }]"
46
+ ></MCombobox>`,
47
+ },
48
+ );
@@ -0,0 +1,246 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { defineComponent, ref, nextTick } from 'vue';
4
+ import MCombobox from './MCombobox.vue';
5
+
6
+ const MOptionListboxStub = defineComponent({
7
+ name: 'MOptionListbox',
8
+ props: [
9
+ 'modelValue',
10
+ 'open',
11
+ 'multiple',
12
+ 'search',
13
+ 'actions',
14
+ 'checkableSections',
15
+ 'searchPlaceholder',
16
+ 'selectLabel',
17
+ 'clearLabel',
18
+ 'options',
19
+ 'id',
20
+ ],
21
+ emits: ['update:modelValue', 'open', 'close'],
22
+ setup() {
23
+ const activeIndex = ref(-1);
24
+ const listboxEl = ref(document.createElement('div'));
25
+
26
+ // On crée une fonction mock que l’on expose
27
+ const toggleValue = vi.fn();
28
+
29
+ return {
30
+ activeIndex,
31
+ listboxEl,
32
+ handleKeydown: () => {},
33
+ toggleValue, // <- expose ici
34
+ };
35
+ },
36
+ template: `<div />`,
37
+ });
38
+
39
+ const MTagStub = defineComponent({
40
+ name: 'MTag',
41
+ props: ['id', 'label', 'type', 'size'],
42
+ emits: ['remove-tag'],
43
+ template: `<div class="m-tag-stub">{{ label }}</div>`,
44
+ });
45
+
46
+ const MButtonStub = defineComponent({
47
+ name: 'MButton',
48
+ props: ['outlined', 'size'],
49
+ emits: ['click'],
50
+ template: `<button @click="$emit('click')"><slot/></button>`,
51
+ });
52
+
53
+ const CrossCircleFilled24 = defineComponent({
54
+ name: 'CrossCircleFilled24',
55
+ template: `<svg/>`,
56
+ });
57
+ const ChevronDown24 = defineComponent({
58
+ name: 'ChevronDown24',
59
+ template: `<svg/>`,
60
+ });
61
+
62
+ describe('MCombobox', () => {
63
+ const options = [
64
+ { label: 'One', value: 1 },
65
+ { label: 'Two', value: 2 },
66
+ { label: 'Three', value: 3 },
67
+ ];
68
+
69
+ it('renders placeholder when no selection', () => {
70
+ const wrapper = mount(MCombobox, {
71
+ props: { modelValue: null, options },
72
+ global: {
73
+ components: {
74
+ MOptionListbox: MOptionListboxStub,
75
+ MTag: MTagStub,
76
+ MButton: MButtonStub,
77
+ CrossCircleFilled24,
78
+ ChevronDown24,
79
+ },
80
+ },
81
+ });
82
+
83
+ const control = wrapper.find('.mc-combobox__control');
84
+ expect(control.exists()).toBe(true);
85
+ expect(control.text()).toBe('Select an option');
86
+ });
87
+
88
+ it('renders selected label for single value', () => {
89
+ const wrapper = mount(MCombobox, {
90
+ props: { modelValue: 1, options },
91
+ global: {
92
+ components: {
93
+ MOptionListbox: MOptionListboxStub,
94
+ MTag: MTagStub,
95
+ MButton: MButtonStub,
96
+ CrossCircleFilled24,
97
+ ChevronDown24,
98
+ },
99
+ },
100
+ });
101
+
102
+ const control = wrapper.find('.mc-combobox__control');
103
+ expect(control.text()).toBe('One');
104
+ });
105
+
106
+ it('multiple selection shows joins values', async () => {
107
+ const wrapper = mount(MCombobox, {
108
+ props: { modelValue: [1, 2], multiple: true, options },
109
+ global: {
110
+ components: {
111
+ MOptionListbox: MOptionListboxStub,
112
+ MTag: MTagStub,
113
+ MButton: MButtonStub,
114
+ CrossCircleFilled24,
115
+ ChevronDown24,
116
+ },
117
+ },
118
+ });
119
+
120
+ expect(wrapper.find('.mc-combobox__control').text()).toBe('One, Two');
121
+ });
122
+
123
+ it('toggles listbox open/close on control click', async () => {
124
+ const wrapper = mount(MCombobox, {
125
+ props: { modelValue: null, options },
126
+ global: {
127
+ components: {
128
+ MOptionListbox: MOptionListboxStub,
129
+ MTag: MTagStub,
130
+ MButton: MButtonStub,
131
+ CrossCircleFilled24,
132
+ ChevronDown24,
133
+ },
134
+ },
135
+ });
136
+
137
+ const root = wrapper.find('.mc-combobox');
138
+ const control = wrapper.find('.mc-combobox__control');
139
+
140
+ await control.trigger('click');
141
+ expect(root.classes()).toContain('mc-combobox--open');
142
+
143
+ await control.trigger('click');
144
+ expect(root.classes()).not.toContain('mc-combobox--open');
145
+ });
146
+
147
+ it('clear button clears selection and emits update:modelValue', async () => {
148
+ const wrapperSingle = mount(MCombobox, {
149
+ props: { modelValue: 1, clearable: true, options },
150
+ global: {
151
+ components: {
152
+ MOptionListbox: MOptionListboxStub,
153
+ MTag: MTagStub,
154
+ MButton: MButtonStub,
155
+ CrossCircleFilled24,
156
+ ChevronDown24,
157
+ },
158
+ },
159
+ });
160
+
161
+ const clearBtnSingle = wrapperSingle.find('.mc-combobox__clear');
162
+ expect(clearBtnSingle.exists()).toBe(true);
163
+ await clearBtnSingle.trigger('click');
164
+ const emittedSingle = wrapperSingle.emitted('update:modelValue') || [];
165
+ expect(emittedSingle.length).toBeGreaterThan(0);
166
+ expect(emittedSingle[emittedSingle.length - 1][0]).toBeNull();
167
+
168
+ const wrapperMulti = mount(MCombobox, {
169
+ props: { modelValue: [1], multiple: true, clearable: true, options },
170
+ global: {
171
+ components: {
172
+ MOptionListbox: MOptionListboxStub,
173
+ MTag: MTagStub,
174
+ MButton: MButtonStub,
175
+ CrossCircleFilled24,
176
+ ChevronDown24,
177
+ },
178
+ },
179
+ });
180
+
181
+ const clearBtnMulti = wrapperMulti.find('.mc-combobox__clear');
182
+ expect(clearBtnMulti.exists()).toBe(true);
183
+ await clearBtnMulti.trigger('click');
184
+ const emittedMulti = wrapperMulti.emitted('update:modelValue') || [];
185
+ expect(emittedMulti.length).toBeGreaterThan(0);
186
+
187
+ const last = emittedMulti[emittedMulti.length - 1][0];
188
+ expect(Array.isArray(last)).toBe(true);
189
+ expect(last).toEqual([]);
190
+ });
191
+
192
+ it('activeDescendant reflects child listbox activeIndex', async () => {
193
+ const wrapper = mount(MCombobox, {
194
+ props: { modelValue: null, options },
195
+ global: {
196
+ components: {
197
+ MOptionListbox: MOptionListboxStub,
198
+ MTag: MTagStub,
199
+ MButton: MButtonStub,
200
+ CrossCircleFilled24,
201
+ ChevronDown24,
202
+ },
203
+ },
204
+ });
205
+
206
+ const listboxRef = (wrapper.vm as InstanceType<typeof MCombobox>).$refs
207
+ .listbox as { activeIndex: number };
208
+ expect(listboxRef).toBeTruthy();
209
+
210
+ listboxRef.activeIndex = 2;
211
+ await nextTick();
212
+
213
+ const control = wrapper.find('.mc-combobox__control');
214
+ const attr = control.attributes()['aria-activedescendant'];
215
+ expect(attr).toBeTruthy();
216
+
217
+ expect(attr.includes('-2')).toBe(true);
218
+ });
219
+
220
+ it('clicking outside closes the listbox', async () => {
221
+ const wrapper = mount(MCombobox, {
222
+ props: { modelValue: null, options },
223
+ global: {
224
+ components: {
225
+ MOptionListbox: MOptionListboxStub,
226
+ MTag: MTagStub,
227
+ MButton: MButtonStub,
228
+ CrossCircleFilled24,
229
+ ChevronDown24,
230
+ },
231
+ },
232
+ attachTo: document.body,
233
+ });
234
+
235
+ const root = wrapper.find('.mc-combobox');
236
+ const control = wrapper.find('.mc-combobox__control');
237
+
238
+ await control.trigger('click');
239
+ expect(root.classes()).toContain('mc-combobox--open');
240
+
241
+ document.dispatchEvent(new MouseEvent('click', { bubbles: true }));
242
+
243
+ await nextTick();
244
+ expect(root.classes()).not.toContain('mc-combobox--open');
245
+ });
246
+ });
@@ -0,0 +1,190 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MCombobox from './MCombobox.vue';
3
+ import { computed, ref } from 'vue';
4
+ import type { ListboxOption } from '../optionListbox/MOptionListbox.vue';
5
+ import MTag from '../tag/MTag.vue';
6
+ import MButton from '../button/MButton.vue';
7
+
8
+ const defaultOptions = Array.from({ length: 12 }).map((_el, index) => {
9
+ return {
10
+ label: `Option ${index + 1}`,
11
+ value: `option${index + 1}`,
12
+ };
13
+ });
14
+
15
+ let optionCount = 0;
16
+
17
+ const optionsWithSections: ListboxOption[] = Array.from({ length: 12 }).map(
18
+ (_el, index) => {
19
+ const isSection = index % 3 === 0;
20
+
21
+ if (!isSection) {
22
+ optionCount++;
23
+ }
24
+
25
+ return {
26
+ label: `${isSection ? 'Section' : 'Option'} ${isSection ? index / 3 + 1 : optionCount}`,
27
+ value: !isSection ? `option${optionCount}` : undefined,
28
+ type: isSection ? 'section' : 'option',
29
+ };
30
+ },
31
+ );
32
+
33
+ const meta: Meta<typeof MCombobox> = {
34
+ title: 'Form elements/Combobox',
35
+ component: MCombobox,
36
+ tags: ['v2'],
37
+ parameters: {
38
+ docs: {
39
+ description: {
40
+ component:
41
+ 'A combobox is an input field that allows users to select an option from a dropdown list or enter a custom value. It combines the functionality of a text input and a dropdown menu, providing flexibility and ease of use in forms and user interfaces.',
42
+ },
43
+ story: {
44
+ height: '300px',
45
+ },
46
+ },
47
+ },
48
+ args: {
49
+ modelValue: null,
50
+ checkableSections: true,
51
+ options: defaultOptions,
52
+ },
53
+ render: (args) => ({
54
+ components: { MCombobox },
55
+ setup() {
56
+ const counterLabel = computed(
57
+ () => `${(args.modelValue as Array<unknown>)?.length} selected`,
58
+ );
59
+ return { args, counterLabel };
60
+ },
61
+ template: `
62
+ <MCombobox v-bind="args" v-model="args.modelValue" :counter-label="counterLabel"></MCombobox>
63
+ `,
64
+ }),
65
+ };
66
+ export default meta;
67
+ type Story = StoryObj<typeof MCombobox>;
68
+
69
+ export const Default: Story = {};
70
+
71
+ export const Multiple: Story = {
72
+ args: {
73
+ multiple: true,
74
+ modelValue: [],
75
+ },
76
+ };
77
+
78
+ export const SearchInput: Story = {
79
+ args: {
80
+ search: true,
81
+ modelValue: [],
82
+ multiple: true,
83
+ },
84
+ };
85
+
86
+ export const ActionButtons: Story = {
87
+ args: {
88
+ modelValue: [],
89
+ multiple: true,
90
+ actions: true,
91
+ },
92
+ };
93
+
94
+ export const WithSections: Story = {
95
+ args: {
96
+ options: optionsWithSections,
97
+ },
98
+ };
99
+
100
+ export const SelectableSections: Story = {
101
+ args: {
102
+ modelValue: [],
103
+ multiple: true,
104
+ options: optionsWithSections,
105
+ checkableSections: true,
106
+ },
107
+ };
108
+
109
+ export const Clearable = {
110
+ args: {
111
+ clearable: true,
112
+ },
113
+ };
114
+
115
+ export const Disabled: Story = {
116
+ args: {
117
+ disabled: true,
118
+ },
119
+ };
120
+
121
+ export const Readonly: Story = {
122
+ args: {
123
+ readonly: true,
124
+ },
125
+ };
126
+
127
+ export const Invalid: Story = {
128
+ args: {
129
+ invalid: true,
130
+ },
131
+ };
132
+
133
+ export const AdditionalInformation: Story = {
134
+ args: {
135
+ options: defaultOptions.map((option) => ({
136
+ ...option,
137
+ content: 'Additional information',
138
+ })),
139
+ },
140
+ };
141
+
142
+ export const RemovableTags: Story = {
143
+ args: {
144
+ modelValue: [],
145
+ options: defaultOptions,
146
+ multiple: true,
147
+ },
148
+ render: (args) => ({
149
+ components: { MCombobox, MTag, MButton },
150
+ setup() {
151
+ function findByValue(value: string | number) {
152
+ return args.options.find((option) => option.value === value);
153
+ }
154
+
155
+ const open = ref(false);
156
+ const maxResultsDisplayed = ref(9);
157
+ const counterLabel = computed(
158
+ () => `${(args.modelValue as Array<unknown>)?.length} selected`,
159
+ );
160
+ return { args, counterLabel, maxResultsDisplayed, findByValue, open };
161
+ },
162
+ template: `
163
+ <MCombobox v-bind="args" v-model="args.modelValue" :counter-label="counterLabel" @update:open="open = $event"></MCombobox>
164
+
165
+ <template
166
+ v-if="!open"
167
+ >
168
+ <div style="width: 300px; display: flex;align-items: center;gap: 10px;flex-wrap: wrap;margin: 16px 0;">
169
+ <MTag
170
+ v-for="(item, index) in args.modelValue?.slice(0, maxResultsDisplayed)"
171
+ :key="index"
172
+ id="tag"
173
+ :label="findByValue(item)?.label || ''"
174
+ type="removable"
175
+ size="s"
176
+ />
177
+ </div>
178
+
179
+ <MButton
180
+ v-if="args.modelValue?.length > maxResultsDisplayed"
181
+ outlined
182
+ size="s"
183
+ @click="maxResultsDisplayed = args.modelValue?.length"
184
+ >
185
+ Show more
186
+ </MButton>
187
+ </template>
188
+ `,
189
+ }),
190
+ };