@mozaic-ds/vue 2.11.0 → 2.13.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 +1 -1
  2. package/dist/mozaic-vue.d.ts +791 -353
  3. package/dist/mozaic-vue.js +2945 -2404
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +5 -5
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +7 -6
  8. package/src/components/{usingPresets.mdx → BrandPresets.mdx} +2 -2
  9. package/src/components/Changelog.mdx +19 -0
  10. package/src/components/Color.mdx +226 -0
  11. package/src/components/Contributing.mdx +12 -6
  12. package/src/components/GettingStarted.mdx +1 -1
  13. package/src/components/Icon.stories.ts +134 -0
  14. package/src/components/Welcome.mdx +49 -0
  15. package/src/components/accordionlist/MAccordionList.spec.ts +136 -0
  16. package/src/components/accordionlist/MAccordionList.stories.ts +123 -0
  17. package/src/components/accordionlist/MAccordionList.vue +91 -0
  18. package/src/components/accordionlist/README.md +24 -0
  19. package/src/components/accordionlist/m-accordion-list.const.ts +9 -0
  20. package/src/components/accordionlistitem/MAccordionListItem.spec.ts +123 -0
  21. package/src/components/accordionlistitem/MAccordionListItem.vue +95 -0
  22. package/src/components/accordionlistitem/README.md +23 -0
  23. package/src/components/actionbottombar/MActionBottomBar.spec.ts +52 -0
  24. package/src/components/actionbottombar/MActionBottomBar.stories.ts +162 -0
  25. package/src/components/actionbottombar/MActionBottomBar.vue +45 -0
  26. package/src/components/actionbottombar/README.md +31 -0
  27. package/src/components/actionlistbox/MActionListbox.spec.ts +134 -0
  28. package/src/components/actionlistbox/MActionListbox.stories.ts +74 -0
  29. package/src/components/actionlistbox/MActionListbox.vue +89 -0
  30. package/src/components/actionlistbox/README.md +25 -0
  31. package/src/components/avatar/MAvatar.stories.ts +1 -1
  32. package/src/components/breadcrumb/README.md +14 -0
  33. package/src/components/builtinmenu/MBuiltInMenu.spec.ts +111 -0
  34. package/src/components/builtinmenu/MBuiltInMenu.stories.ts +59 -0
  35. package/src/components/builtinmenu/MBuiltInMenu.vue +110 -0
  36. package/src/components/builtinmenu/README.md +32 -0
  37. package/src/components/button/MButton.spec.ts +1 -1
  38. package/src/components/button/MButton.stories.ts +165 -5
  39. package/src/components/button/README.md +33 -1
  40. package/src/components/callout/MCallout.spec.ts +7 -6
  41. package/src/components/callout/MCallout.stories.ts +1 -2
  42. package/src/components/carousel/MCarousel.spec.ts +1 -2
  43. package/src/components/carousel/MCarousel.stories.ts +2 -1
  44. package/src/components/carousel/MCarousel.vue +1 -2
  45. package/src/components/carousel/README.md +14 -0
  46. package/src/components/checkbox/README.md +14 -0
  47. package/src/components/checkboxgroup/README.md +14 -0
  48. package/src/components/checklistmenu/MCheckListMenu.spec.ts +77 -0
  49. package/src/components/checklistmenu/MCheckListMenu.stories.ts +47 -0
  50. package/src/components/checklistmenu/MCheckListMenu.vue +61 -0
  51. package/src/components/checklistmenu/README.md +32 -0
  52. package/src/components/circularprogressbar/README.md +15 -1
  53. package/src/components/datepicker/MDatepicker.vue +1 -1
  54. package/src/components/divider/README.md +22 -0
  55. package/src/components/drawer/MDrawer.vue +1 -2
  56. package/src/components/drawer/README.md +16 -0
  57. package/src/components/field/README.md +14 -0
  58. package/src/components/fileuploader/MFileUploader.spec.ts +304 -0
  59. package/src/components/fileuploader/MFileUploader.stories.ts +123 -0
  60. package/src/components/fileuploader/MFileUploader.vue +314 -0
  61. package/src/components/fileuploader/README.md +58 -0
  62. package/src/components/fileuploaderitem/MFileUploaderItem.spec.ts +91 -0
  63. package/src/components/fileuploaderitem/MFileUploaderItem.vue +180 -0
  64. package/src/components/fileuploaderitem/README.md +58 -0
  65. package/src/components/flag/README.md +1 -1
  66. package/src/components/iconbutton/MIconButton.spec.ts +1 -1
  67. package/src/components/iconbutton/MIconButton.stories.ts +116 -7
  68. package/src/components/iconbutton/README.md +25 -1
  69. package/src/components/kpiitem/MKpiItem.vue +5 -3
  70. package/src/components/linearprogressbarbuffer/README.md +14 -0
  71. package/src/components/link/MLink.stories.ts +1 -2
  72. package/src/components/link/README.md +14 -0
  73. package/src/components/loader/README.md +20 -0
  74. package/src/components/loadingoverlay/README.md +14 -0
  75. package/src/components/modal/MModal.stories.ts +1 -2
  76. package/src/components/modal/MModal.vue +1 -1
  77. package/src/components/modal/README.md +16 -0
  78. package/src/components/numberbadge/README.md +17 -1
  79. package/src/components/overlay/README.md +16 -0
  80. package/src/components/pagination/MPagination.vue +1 -2
  81. package/src/components/pagination/README.md +18 -0
  82. package/src/components/passwordinput/MPasswordInput.vue +1 -1
  83. package/src/components/passwordinput/README.md +14 -0
  84. package/src/components/phonenumber/MPhoneNumber.spec.ts +7 -6
  85. package/src/components/phonenumber/MPhoneNumber.vue +1 -1
  86. package/src/components/quantityselector/MQuantitySelector.vue +1 -2
  87. package/src/components/radio/README.md +14 -0
  88. package/src/components/radiogroup/README.md +14 -0
  89. package/src/components/select/README.md +14 -0
  90. package/src/components/starrating/MStarRating.spec.ts +1 -2
  91. package/src/components/starrating/MStarRating.vue +1 -3
  92. package/src/components/statusbadge/README.md +14 -0
  93. package/src/components/statusdot/README.md +14 -0
  94. package/src/components/statusmessage/MStatusMessage.spec.ts +6 -4
  95. package/src/components/statusmessage/MStatusMessage.vue +6 -4
  96. package/src/components/statusmessage/README.md +14 -0
  97. package/src/components/statusnotification/MStatusNotification.spec.ts +6 -4
  98. package/src/components/statusnotification/MStatusNotification.stories.ts +1 -1
  99. package/src/components/statusnotification/MStatusNotification.vue +7 -5
  100. package/src/components/statusnotification/README.md +14 -0
  101. package/src/components/stepperbottombar/MStepperBottomBar.spec.ts +134 -0
  102. package/src/components/stepperbottombar/MStepperBottomBar.stories.ts +72 -0
  103. package/src/components/stepperbottombar/MStepperBottomBar.vue +131 -0
  104. package/src/components/stepperbottombar/README.md +40 -0
  105. package/src/components/steppercompact/README.md +14 -0
  106. package/src/components/stepperinline/MStepperInline.spec.ts +78 -0
  107. package/src/components/stepperinline/MStepperInline.stories.ts +49 -0
  108. package/src/components/stepperinline/MStepperInline.vue +93 -0
  109. package/src/components/stepperinline/README.md +11 -0
  110. package/src/components/tabs/MTabs.stories.ts +1 -1
  111. package/src/components/tabs/README.md +16 -0
  112. package/src/components/tag/MTag.vue +1 -1
  113. package/src/components/tag/README.md +14 -0
  114. package/src/components/textinput/MTextInput.spec.ts +1 -1
  115. package/src/components/textinput/MTextInput.stories.ts +1 -1
  116. package/src/components/textinput/MTextInput.vue +1 -1
  117. package/src/components/toaster/MToaster.spec.ts +6 -4
  118. package/src/components/toaster/MToaster.vue +7 -5
  119. package/src/components/toaster/README.md +16 -0
  120. package/src/components/toggle/README.md +14 -0
  121. package/src/components/togglegroup/README.md +14 -0
  122. package/src/main.ts +8 -0
  123. package/src/components/Introduction.mdx +0 -100
  124. package/src/components/Support.mdx +0 -18
  125. package/src/components/usingIcons.mdx +0 -35
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MCheckListMenu from './MCheckListMenu.vue';
3
+ import { action } from 'storybook/actions';
4
+
5
+ const meta: Meta<typeof MCheckListMenu> = {
6
+ title: 'Navigation/Check-list menu',
7
+ component: MCheckListMenu,
8
+ tags: ['v2'],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'A check-list menu is a structured vertical list where each item represents a distinct section of content. It enables users to navigate through and validate different parts of an interface in any order. Unlike linear steppers, this component offers flexibility by allowing non-sequential completion, making it ideal for user profile setup, application settings, or flexible onboarding processes where users can choose their own path through the experience.',
14
+ },
15
+ },
16
+ },
17
+ args: {
18
+ items: [
19
+ { label: 'Label', checked: true },
20
+ { label: 'Label', checked: true },
21
+ { label: 'Label', checked: false },
22
+ { label: 'Label', checked: true },
23
+ ],
24
+ modelValue: 0,
25
+ },
26
+ render: (args) => ({
27
+ components: { MCheckListMenu },
28
+ setup() {
29
+ const handleUpdate = action('update:modelValue');
30
+
31
+ return { args, handleUpdate };
32
+ },
33
+ template: `
34
+ <MCheckListMenu v-model="args.modelValue" v-bind="args" @update:modelValue="handleUpdate"></MCheckListMenu>
35
+ `,
36
+ }),
37
+ };
38
+ export default meta;
39
+ type Story = StoryObj<typeof MCheckListMenu>;
40
+
41
+ export const Default: Story = {};
42
+
43
+ export const Outlined: Story = {
44
+ args: {
45
+ outlined: true,
46
+ },
47
+ };
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <MBuiltInMenu
3
+ v-model="currentMenuItem"
4
+ :items="menuItems"
5
+ :outlined="props.outlined"
6
+ />
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue';
11
+ import MBuiltInMenu from '../builtinmenu/MBuiltInMenu.vue';
12
+ import type { MenuItem } from '../builtinmenu/MBuiltInMenu.vue';
13
+ import { CheckCircleFilled24 } from '@mozaic-ds/icons-vue';
14
+
15
+ /**
16
+ * A check-list menu is a structured vertical list where each item represents a distinct section of content. It enables users to navigate through and validate different parts of an interface in any order. Unlike linear steppers, this component offers flexibility by allowing non-sequential completion, making it ideal for user profile setup, application settings, or flexible onboarding processes where users can choose their own path through the experience.
17
+ */
18
+
19
+ export type CheckListMenuItem = Omit<MenuItem, 'icon'> & { checked: boolean };
20
+
21
+ const props = defineProps<{
22
+ /**
23
+ * Specifies the key of the currently selected menu item. It allows the component to highlight or style the corresponding item to indicate it is selected or currently in use.
24
+ */
25
+ modelValue?: number;
26
+ /**
27
+ * Defines the menu items, each of which sets a checked state and act as a button, link, or router-link.
28
+ */
29
+ items: CheckListMenuItem[];
30
+ /**
31
+ * When enabled, adds a visible border around the wrapper to highlight or separate its content.
32
+ */
33
+ outlined?: boolean;
34
+ }>();
35
+
36
+ const emit = defineEmits<{
37
+ /**
38
+ * Emitted when the selected item changes, providing the updated selected value.
39
+ */
40
+ (on: 'update:modelValue', value: number): void;
41
+ }>();
42
+
43
+ const currentMenuItem = computed({
44
+ get() {
45
+ return props.modelValue;
46
+ },
47
+ set(value: number) {
48
+ emit('update:modelValue', value);
49
+ },
50
+ });
51
+
52
+ const menuItems = computed<MenuItem[]>(() =>
53
+ props.items.map((item) => ({
54
+ label: item.label,
55
+ icon: item.checked ? CheckCircleFilled24 : undefined,
56
+ href: item.href,
57
+ to: item.to,
58
+ target: item.target,
59
+ })),
60
+ );
61
+ </script>
@@ -0,0 +1,32 @@
1
+ # MCheckListMenu
2
+
3
+ A check-list menu is a structured vertical list where each item represents a distinct section of content. It enables users to navigate through and validate different parts of an interface in any order. Unlike linear steppers, this component offers flexibility by allowing non-sequential completion, making it ideal for user profile setup, application settings, or flexible onboarding processes where users can choose their own path through the experience.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `modelValue` | Specifies the key of the currently selected menu item. It allows the component to highlight or style the corresponding item to indicate it is selected or currently in use. | `number` | - |
11
+ | `items*` | Defines the menu items, each of which sets a checked state and act as a button, link, or router-link. | `CheckListMenuItem[]` | - |
12
+ | `outlined` | When enabled, adds a visible border around the wrapper to highlight or separate its content. | `boolean` | - |
13
+
14
+ ## Events
15
+
16
+ | Name | Description | Type |
17
+ | --- | --- | --- |
18
+ | `update:modelValue` | Emitted when the selected item changes, providing the updated selected value. | [value: number] |
19
+
20
+ ## Dependencies
21
+
22
+ ### Depends on
23
+
24
+ - [MBuiltInMenu](../builtinmenu)
25
+
26
+ ### Graph
27
+
28
+ ```mermaid
29
+ graph TD;
30
+ MCheckListMenu --> MBuiltInMenu
31
+ style MCheckListMenu fill:#008240,stroke:#333,stroke-width:4px
32
+ ```
@@ -9,6 +9,20 @@ A circular progress bar visually represents progress toward a goal or completion
9
9
  | --- | --- | --- | --- |
10
10
  | `size` | Sets the size of the progress bar. | `"s"` `"m"` `"l"` | - |
11
11
  | `value` | The current value of the progress bar. | `number` | `0` |
12
- | `type` | Shows either a percentage or custom content. | `"percentage"` `"content"` | `"percentage"` |
12
+ | `type` | Shows either a percentage or custom content. | `"content"` `"percentage"` | `"percentage"` |
13
13
  | `contentValue` | Main content shown when `type` is `'content'`. | `string` | - |
14
14
  | `additionalInfo` | Additional text shown to define the `contentValue`. | `string` | - |
15
+
16
+ ## Dependencies
17
+
18
+ ### Used By
19
+
20
+ - [MStepperCompact](../steppercompact)
21
+
22
+ ### Graph
23
+
24
+ ```mermaid
25
+ graph TD;
26
+ MStepperCompact --> MCircularProgressbar
27
+ style MCircularProgressbar fill:#008240,stroke:#333,stroke-width:4px
28
+ ```
@@ -36,7 +36,7 @@
36
36
 
37
37
  <script setup lang="ts">
38
38
  import { computed, ref } from 'vue';
39
- import CrossCircleFilled24 from '@mozaic-ds/icons-vue/src/components/CrossCircleFilled24/CrossCircleFilled24.vue';
39
+ import { CrossCircleFilled24 } from '@mozaic-ds/icons-vue';
40
40
  /**
41
41
  * A date picker is an input component that allows users to select a date from a calendar interface or manually enter a date value. It enhances usability by providing structured date selection, reducing input errors, and ensuring format consistency. Date Pickers are commonly used in forms, booking systems, scheduling tools, and data filtering interfaces to facilitate accurate date entry.<br><br> To put a label, requierement text, help text or to apply a valid or invalid message, the examples are available in the [Field section](/docs/form-elements-field--docs#input).
42
42
  */
@@ -16,3 +16,25 @@ A divider is a visual element used to separate content or sections within an int
16
16
  | Name | Description |
17
17
  | --- | --- |
18
18
  | `default` | Use this slot to insert the content who need a vertical divider |
19
+
20
+ ## Dependencies
21
+
22
+ ### Used By
23
+
24
+ - [MActionBottomBar](../actionbottombar)
25
+ - [MActionListbox](../actionlistbox)
26
+ - [MFileUploaderItem](../fileuploaderitem)
27
+ - [MStepperBottomBar](../stepperbottombar)
28
+ - [MTabs](../tabs)
29
+
30
+ ### Graph
31
+
32
+ ```mermaid
33
+ graph TD;
34
+ MActionBottomBar --> MDivider
35
+ MActionListbox --> MDivider
36
+ MFileUploaderItem --> MDivider
37
+ MStepperBottomBar --> MDivider
38
+ MTabs --> MDivider
39
+ style MDivider fill:#008240,stroke:#333,stroke-width:4px
40
+ ```
@@ -66,8 +66,7 @@
66
66
 
67
67
  <script setup lang="ts">
68
68
  import { computed, watch, type VNode, ref, onMounted, onUnmounted } from 'vue';
69
- import ArrowBack24 from '@mozaic-ds/icons-vue/src/components/ArrowBack24/ArrowBack24.vue';
70
- import Cross24 from '@mozaic-ds/icons-vue/src/components/Cross24/Cross24.vue';
69
+ import { ArrowBack24, Cross24 } from '@mozaic-ds/icons-vue';
71
70
  import MIconButton from '../iconbutton/MIconButton.vue';
72
71
  import MOverlay from '../overlay/MOverlay.vue';
73
72
  /**
@@ -29,3 +29,19 @@ A drawer is a sliding panel that appears from the side of the screen, providing
29
29
  | --- | --- | --- |
30
30
  | `back` | Emits when click back button of the drawer. | [] |
31
31
  | `update:open` | Emits when the drawer open state changes, updating the modelValue prop. | [value: boolean] |
32
+
33
+ ## Dependencies
34
+
35
+ ### Depends on
36
+
37
+ - [MIconButton](../iconbutton)
38
+ - [MOverlay](../overlay)
39
+
40
+ ### Graph
41
+
42
+ ```mermaid
43
+ graph TD;
44
+ MDrawer --> MIconButton
45
+ MDrawer --> MOverlay
46
+ style MDrawer fill:#008240,stroke:#333,stroke-width:4px
47
+ ```
@@ -23,3 +23,17 @@ A field label is a text element that identifies the purpose of an input field, p
23
23
  | Name | Description |
24
24
  | --- | --- |
25
25
  | `default` | Use this slot to insert the form element of your choice |
26
+
27
+ ## Dependencies
28
+
29
+ ### Depends on
30
+
31
+ - [MLoader](../loader)
32
+
33
+ ### Graph
34
+
35
+ ```mermaid
36
+ graph TD;
37
+ MField --> MLoader
38
+ style MField fill:#008240,stroke:#333,stroke-width:4px
39
+ ```
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MFileUploader, {
4
+ type FilesValidationState,
5
+ type FileUploaderProps,
6
+ } from './MFileUploader.vue';
7
+ import { nextTick } from 'vue';
8
+
9
+ type WrapperVm = {
10
+ dragCounter: number;
11
+ };
12
+
13
+ const globalStubs = {
14
+ MFileUploaderItem: {
15
+ template: '<div class="file-item"><slot name="action" /></div>',
16
+ name: 'MFileUploaderItem',
17
+ props: ['file', 'valid'],
18
+ emits: ['delete'],
19
+ },
20
+ Upload24: true,
21
+ };
22
+
23
+ const createFile = (name: string, size: number, type: string) => {
24
+ const file = new File(['a'.repeat(size)], name, { type });
25
+ Object.defineProperty(file, 'size', { value: size });
26
+ return file;
27
+ };
28
+
29
+ const mountUploader = (props = {} as FileUploaderProps) =>
30
+ mount(MFileUploader, { props, global: { stubs: globalStubs } });
31
+
32
+ const triggerFileInputChange = async (
33
+ wrapper: ReturnType<typeof mount>,
34
+ files: File[],
35
+ ) => {
36
+ const input = wrapper.find('input[type="file"]');
37
+ Object.defineProperty(input.element, 'files', {
38
+ value: files,
39
+ configurable: true,
40
+ });
41
+ await input.trigger('change');
42
+ };
43
+
44
+ const triggerDrop = async (
45
+ wrapper: ReturnType<typeof mount>,
46
+ files: File[],
47
+ ) => {
48
+ const dropZone = wrapper.find('.mc-file-uploader__input');
49
+ await dropZone.trigger('drop', {
50
+ dataTransfer: {
51
+ files,
52
+ items: files.map((f) => ({
53
+ kind: 'file',
54
+ type: f.type,
55
+ getAsFile: () => f,
56
+ })),
57
+ },
58
+ preventDefault: vi.fn(),
59
+ stopPropagation: vi.fn(),
60
+ });
61
+ };
62
+
63
+ describe('MFileUploader.vue', () => {
64
+ it('allows file upload via input', async () => {
65
+ const wrapper = mountUploader({
66
+ modelValue: [],
67
+ });
68
+ const file = createFile('test.png', 1024, 'image/png');
69
+ await triggerFileInputChange(wrapper, [file]);
70
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
71
+ });
72
+
73
+ it('clears input and returns if no new valid files', async () => {
74
+ const existingFile = createFile('exist.png', 100, 'image/png');
75
+ const wrapper = mountUploader({
76
+ modelValue: [existingFile],
77
+ });
78
+
79
+ const input = wrapper.find('input[type="file"]');
80
+ Object.defineProperty(input.element, 'files', {
81
+ value: [],
82
+ configurable: true,
83
+ });
84
+
85
+ await input.trigger('change');
86
+
87
+ expect((input.element as HTMLInputElement).value).toBe('');
88
+ expect(wrapper.emitted('update:modelValue')).toBeFalsy();
89
+ });
90
+
91
+ it.each([
92
+ ['Enter', 'Enter'],
93
+ ['Space', ' '],
94
+ ])('clicks file input when %s is pressed', async (_, key) => {
95
+ const wrapper = mountUploader({
96
+ modelValue: [],
97
+ });
98
+ const inputEl = wrapper.find('input[type="file"]')
99
+ .element as HTMLInputElement;
100
+ const clickSpy = vi.spyOn(inputEl, 'click');
101
+ const label = wrapper.find('.mc-file-uploader__input');
102
+ await label.trigger('keydown', { key });
103
+ expect(clickSpy).toHaveBeenCalled();
104
+ });
105
+
106
+ it('allows drag and drop', async () => {
107
+ const wrapper = mountUploader({
108
+ modelValue: [],
109
+ hasDragDrop: true,
110
+ });
111
+ const file = createFile('dragged.pdf', 500, 'application/pdf');
112
+ await triggerDrop(wrapper, [file]);
113
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
114
+ });
115
+
116
+ it('handles dragenter/dragleave class correctly', async () => {
117
+ const wrapper = mountUploader({
118
+ modelValue: [],
119
+ hasDragDrop: true,
120
+ });
121
+ const labelEl = wrapper.get('.mc-file-uploader__input');
122
+
123
+ await labelEl.trigger('dragenter');
124
+ expect(labelEl.classes()).toContain('mc-file-uploader__input--dragged');
125
+
126
+ await labelEl.trigger('dragleave');
127
+ expect(labelEl.classes()).not.toContain('mc-file-uploader__input--dragged');
128
+ });
129
+
130
+ it('does not update on drop if disabled or hasDragDrop is false', async () => {
131
+ const wrapperFalse = mountUploader({
132
+ modelValue: [],
133
+ hasDragDrop: false,
134
+ });
135
+ await triggerDrop(wrapperFalse, [
136
+ createFile('a.pdf', 100, 'application/pdf'),
137
+ ]);
138
+ expect(wrapperFalse.emitted('update:modelValue')).toBeFalsy();
139
+
140
+ const wrapperDisabled = mountUploader({
141
+ modelValue: [],
142
+ hasDragDrop: true,
143
+ disabled: true,
144
+ });
145
+ await triggerDrop(wrapperDisabled, [
146
+ createFile('b.pdf', 100, 'application/pdf'),
147
+ ]);
148
+ expect(wrapperDisabled.emitted('update:modelValue')).toBeFalsy();
149
+ });
150
+
151
+ it('does not update dragCounter on dragEnter if hasDragDrop is false', async () => {
152
+ const buttonWrapper = mountUploader({
153
+ modelValue: [],
154
+ hasDragDrop: false,
155
+ });
156
+
157
+ const vm = buttonWrapper.vm as unknown as WrapperVm;
158
+
159
+ vm.dragCounter = 1;
160
+ const input = buttonWrapper.find('.mc-file-uploader__input');
161
+ await input.trigger('dragenter');
162
+ expect(vm.dragCounter).toBe(1);
163
+ });
164
+
165
+ it('does not update dragCounter on dragLeave if hasDragDrop is false', async () => {
166
+ const buttonWrapper = mountUploader({
167
+ modelValue: [],
168
+ hasDragDrop: false,
169
+ });
170
+
171
+ const vm = buttonWrapper.vm as unknown as WrapperVm;
172
+
173
+ vm.dragCounter = 1;
174
+ const input = buttonWrapper.find('.mc-file-uploader__input');
175
+ await input.trigger('dragleave');
176
+ expect(vm.dragCounter).toBe(1);
177
+ });
178
+
179
+ it('adds new files on change when multiple = true', async () => {
180
+ const file1 = createFile('a.txt', 1, 'text/plain');
181
+ const file2 = createFile('b.txt', 1, 'text/plain');
182
+
183
+ const wrapper = mountUploader({
184
+ modelValue: [file1],
185
+ multiple: true,
186
+ });
187
+
188
+ const input = wrapper.find('input[type="file"]');
189
+ Object.defineProperty(input.element, 'files', {
190
+ value: [file2],
191
+ configurable: true,
192
+ });
193
+
194
+ await input.trigger('change');
195
+
196
+ const updates = wrapper.emitted('update:modelValue')![0][0] as File[];
197
+ expect(updates.map((f) => f.name)).toEqual(['a.txt', 'b.txt']);
198
+ expect((input.element as HTMLInputElement).value).toBe('');
199
+ });
200
+
201
+ it('replaces existing file on change when multiple = false', async () => {
202
+ const file1 = createFile('a.txt', 1, 'text/plain');
203
+ const file2 = createFile('b.txt', 1, 'text/plain');
204
+
205
+ const wrapper = mountUploader({
206
+ modelValue: [file1],
207
+ multiple: false,
208
+ });
209
+
210
+ const input = wrapper.find('input[type="file"]');
211
+ Object.defineProperty(input.element, 'files', {
212
+ value: [file2],
213
+ configurable: true,
214
+ });
215
+
216
+ await input.trigger('change');
217
+
218
+ const updates = wrapper.emitted('update:modelValue')![0][0] as File[];
219
+ expect(updates.map((f) => f.name)).toEqual(['b.txt']);
220
+ expect((input.element as HTMLInputElement).value).toBe('');
221
+ });
222
+
223
+ it('merges or replaces files on drop based on multiple prop', async () => {
224
+ const file1 = createFile('a.txt', 1, 'text/plain');
225
+ const file2 = createFile('b.txt', 1, 'text/plain');
226
+
227
+ // multiple = true
228
+ const wrapperMulti = mountUploader({
229
+ modelValue: [file1],
230
+ multiple: true,
231
+ hasDragDrop: true,
232
+ });
233
+ await triggerDrop(wrapperMulti, [file2]);
234
+ const payloadMulti = wrapperMulti.emitted(
235
+ 'update:modelValue',
236
+ )![0][0] as File[];
237
+ expect(payloadMulti.map((f) => f.name)).toEqual(['a.txt', 'b.txt']);
238
+
239
+ // multiple = false
240
+ const wrapperSingle = mountUploader({
241
+ modelValue: [file1],
242
+ multiple: false,
243
+ });
244
+ await triggerFileInputChange(wrapperSingle, [file2]);
245
+ const payloadSingle = wrapperSingle.emitted(
246
+ 'update:modelValue',
247
+ )![0][0] as File[];
248
+ expect(payloadSingle.map((f) => f.name)).toEqual(['b.txt']);
249
+ });
250
+
251
+ describe('Validation', () => {
252
+ it('validates size, extension and custom rules', async () => {
253
+ const valid = createFile('good.png', 1000, 'image/png');
254
+ const heavy = createFile('heavy.png', 5000, 'image/png');
255
+ const wrong = createFile('wrong.txt', 1000, 'text/plain');
256
+ const customRule = vi.fn().mockReturnValue(false);
257
+
258
+ const wrapper = mountUploader({
259
+ modelValue: [
260
+ valid,
261
+ heavy,
262
+ wrong,
263
+ createFile('test.png', 100, 'image/png'),
264
+ ],
265
+ maxSize: 2000,
266
+ allowedExtensions: ['png', 'jpg'],
267
+ rules: [customRule],
268
+ });
269
+
270
+ const emitted = wrapper.emitted(
271
+ 'validation',
272
+ )![0][0] as FilesValidationState;
273
+ expect(emitted['good.png'].size).toBe(true);
274
+ expect(emitted['heavy.png'].size).toBe(false);
275
+ expect(emitted['test.png'].customValidation).toBe(false);
276
+ });
277
+ });
278
+
279
+ it('handles file deletion', async () => {
280
+ const f1 = createFile('1.png', 100, 'image/png');
281
+ const f2 = createFile('2.png', 100, 'image/png');
282
+ const wrapper = mountUploader({
283
+ modelValue: [f1, f2],
284
+ showFilesList: true,
285
+ });
286
+
287
+ const items = wrapper.findAllComponents({ name: 'MFileUploaderItem' });
288
+ await items[0].vm.$emit('delete');
289
+ await nextTick();
290
+
291
+ const payload = wrapper.emitted('update:modelValue')![0][0] as File[];
292
+ expect(payload.map((f) => f.name)).toEqual(['2.png']);
293
+ });
294
+
295
+ it('disables input when disabled=true', () => {
296
+ const wrapper = mountUploader({
297
+ disabled: true,
298
+ modelValue: [],
299
+ });
300
+ expect(
301
+ wrapper.find('input[type="file"]').attributes('disabled'),
302
+ ).toBeDefined();
303
+ });
304
+ });
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { action } from 'storybook/actions';
3
+ import MFileUploader from './MFileUploader.vue';
4
+ import MFileUploaderItem from '../fileuploaderitem/MFileUploaderItem.vue';
5
+ import { ref } from 'vue';
6
+
7
+ const meta: Meta<typeof MFileUploader> = {
8
+ title: 'Form Elements/File uploader',
9
+ component: MFileUploader,
10
+ subcomponents: { MFileUploaderItem },
11
+ tags: ['v2'],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component: `A file uploader allows users to upload one or multiple files by either dragging and dropping files into a dedicated area or selecting them manually through their local folders. It provides real-time feedback on upload progress and file status, including file name, size, and success or error indicators. File uploaders are commonly used in forms, content management systems, and document submission processes to facilitate seamless file handling.`,
16
+ },
17
+ },
18
+ },
19
+ args: {
20
+ hasDragDrop: true,
21
+ showFilesList: true,
22
+ disabled: false,
23
+ title: 'Drag & drop',
24
+ subtitle: 'or',
25
+ uploadButtonLabel: 'Upload file(s)',
26
+ multiple: true,
27
+ },
28
+ render: (args) => ({
29
+ components: { MFileUploader },
30
+ setup() {
31
+ const handleUpdate = action('update:modelValue');
32
+
33
+ const files = ref([]);
34
+ return { args, files, handleUpdate };
35
+ },
36
+ template: `
37
+ <MFileUploader v-model="files" v-bind="args" @update:modelValue="handleUpdate">
38
+ </MFileUploader>
39
+ `,
40
+ }),
41
+ };
42
+ export default meta;
43
+ type Story = StoryObj<typeof MFileUploader>;
44
+ type ItemStory = StoryObj<typeof MFileUploaderItem>;
45
+
46
+ export const Standard: Story = {};
47
+
48
+ export const WithoutDragAndDrop = {
49
+ args: {
50
+ hasDragDrop: false,
51
+ },
52
+ };
53
+
54
+ export const Disabled: Story = {
55
+ args: {
56
+ disabled: true,
57
+ },
58
+ };
59
+
60
+ export const InlineFileWithError: ItemStory = {
61
+ args: {
62
+ valid: false,
63
+ file: {
64
+ name: 'File.txt',
65
+ },
66
+ information: 'Additional information',
67
+ },
68
+ render: (args) => ({
69
+ components: { MFileUploaderItem },
70
+ setup() {
71
+ return { args };
72
+ },
73
+ template: `
74
+ <div style="width: 400px">
75
+ <MFileUploaderItem v-bind="args" />
76
+ </div>
77
+ `,
78
+ }),
79
+ };
80
+
81
+ export const StackedFileItem: ItemStory = {
82
+ args: {
83
+ valid: true,
84
+ file: {
85
+ name: 'File.txt',
86
+ },
87
+ format: 'stacked',
88
+ information: 'Additional information',
89
+ },
90
+ render: (args) => ({
91
+ components: { MFileUploaderItem },
92
+ setup() {
93
+ return { args };
94
+ },
95
+ template: `
96
+ <div style="width: 400px">
97
+ <MFileUploaderItem v-bind="args" />
98
+ </div>
99
+ `,
100
+ }),
101
+ };
102
+
103
+ export const StackedFileWithError: ItemStory = {
104
+ args: {
105
+ valid: false,
106
+ file: {
107
+ name: 'File.txt',
108
+ },
109
+ format: 'stacked',
110
+ information: 'Additional information',
111
+ },
112
+ render: (args) => ({
113
+ components: { MFileUploaderItem },
114
+ setup() {
115
+ return { args };
116
+ },
117
+ template: `
118
+ <div style="width: 400px">
119
+ <MFileUploaderItem v-bind="args" />
120
+ </div>
121
+ `,
122
+ }),
123
+ };