@mozaic-ds/vue 2.12.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 (121) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +702 -361
  3. package/dist/mozaic-vue.js +2791 -2428
  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.stories.ts +2 -1
  34. package/src/components/builtinmenu/MBuiltInMenu.vue +1 -1
  35. package/src/components/builtinmenu/README.md +14 -0
  36. package/src/components/button/MButton.spec.ts +1 -1
  37. package/src/components/button/MButton.stories.ts +165 -5
  38. package/src/components/button/README.md +33 -1
  39. package/src/components/callout/MCallout.spec.ts +7 -6
  40. package/src/components/callout/MCallout.stories.ts +1 -2
  41. package/src/components/carousel/MCarousel.spec.ts +1 -2
  42. package/src/components/carousel/MCarousel.stories.ts +2 -1
  43. package/src/components/carousel/MCarousel.vue +1 -2
  44. package/src/components/carousel/README.md +14 -0
  45. package/src/components/checkbox/README.md +14 -0
  46. package/src/components/checkboxgroup/README.md +14 -0
  47. package/src/components/checklistmenu/MCheckListMenu.spec.ts +1 -1
  48. package/src/components/checklistmenu/MCheckListMenu.stories.ts +1 -0
  49. package/src/components/checklistmenu/MCheckListMenu.vue +1 -1
  50. package/src/components/checklistmenu/README.md +14 -0
  51. package/src/components/circularprogressbar/README.md +15 -1
  52. package/src/components/datepicker/MDatepicker.vue +1 -1
  53. package/src/components/divider/README.md +22 -0
  54. package/src/components/drawer/MDrawer.vue +1 -2
  55. package/src/components/drawer/README.md +16 -0
  56. package/src/components/field/README.md +14 -0
  57. package/src/components/fileuploader/MFileUploader.spec.ts +304 -0
  58. package/src/components/fileuploader/MFileUploader.stories.ts +123 -0
  59. package/src/components/fileuploader/MFileUploader.vue +314 -0
  60. package/src/components/fileuploader/README.md +58 -0
  61. package/src/components/fileuploaderitem/MFileUploaderItem.spec.ts +91 -0
  62. package/src/components/fileuploaderitem/MFileUploaderItem.vue +180 -0
  63. package/src/components/fileuploaderitem/README.md +58 -0
  64. package/src/components/flag/README.md +1 -1
  65. package/src/components/iconbutton/MIconButton.spec.ts +1 -1
  66. package/src/components/iconbutton/MIconButton.stories.ts +116 -7
  67. package/src/components/iconbutton/README.md +25 -1
  68. package/src/components/kpiitem/MKpiItem.vue +5 -3
  69. package/src/components/linearprogressbarbuffer/README.md +14 -0
  70. package/src/components/link/MLink.stories.ts +1 -2
  71. package/src/components/link/README.md +14 -0
  72. package/src/components/loader/README.md +20 -0
  73. package/src/components/loadingoverlay/README.md +14 -0
  74. package/src/components/modal/MModal.stories.ts +1 -2
  75. package/src/components/modal/MModal.vue +1 -1
  76. package/src/components/modal/README.md +16 -0
  77. package/src/components/numberbadge/README.md +17 -1
  78. package/src/components/overlay/README.md +16 -0
  79. package/src/components/pagination/MPagination.vue +1 -2
  80. package/src/components/pagination/README.md +18 -0
  81. package/src/components/passwordinput/MPasswordInput.vue +1 -1
  82. package/src/components/passwordinput/README.md +14 -0
  83. package/src/components/phonenumber/MPhoneNumber.spec.ts +7 -6
  84. package/src/components/phonenumber/MPhoneNumber.vue +1 -1
  85. package/src/components/quantityselector/MQuantitySelector.vue +1 -2
  86. package/src/components/radio/README.md +14 -0
  87. package/src/components/radiogroup/README.md +14 -0
  88. package/src/components/select/README.md +14 -0
  89. package/src/components/starrating/MStarRating.spec.ts +1 -2
  90. package/src/components/starrating/MStarRating.vue +1 -3
  91. package/src/components/statusbadge/README.md +14 -0
  92. package/src/components/statusdot/README.md +14 -0
  93. package/src/components/statusmessage/MStatusMessage.spec.ts +6 -4
  94. package/src/components/statusmessage/MStatusMessage.vue +6 -4
  95. package/src/components/statusmessage/README.md +14 -0
  96. package/src/components/statusnotification/MStatusNotification.spec.ts +6 -4
  97. package/src/components/statusnotification/MStatusNotification.stories.ts +1 -1
  98. package/src/components/statusnotification/MStatusNotification.vue +7 -5
  99. package/src/components/statusnotification/README.md +14 -0
  100. package/src/components/stepperbottombar/MStepperBottomBar.spec.ts +134 -0
  101. package/src/components/stepperbottombar/MStepperBottomBar.stories.ts +72 -0
  102. package/src/components/stepperbottombar/MStepperBottomBar.vue +131 -0
  103. package/src/components/stepperbottombar/README.md +40 -0
  104. package/src/components/steppercompact/README.md +14 -0
  105. package/src/components/stepperinline/MStepperInline.vue +1 -2
  106. package/src/components/tabs/MTabs.stories.ts +1 -1
  107. package/src/components/tabs/README.md +16 -0
  108. package/src/components/tag/MTag.vue +1 -1
  109. package/src/components/tag/README.md +14 -0
  110. package/src/components/textinput/MTextInput.spec.ts +1 -1
  111. package/src/components/textinput/MTextInput.stories.ts +1 -1
  112. package/src/components/textinput/MTextInput.vue +1 -1
  113. package/src/components/toaster/MToaster.spec.ts +6 -4
  114. package/src/components/toaster/MToaster.vue +7 -5
  115. package/src/components/toaster/README.md +16 -0
  116. package/src/components/toggle/README.md +14 -0
  117. package/src/components/togglegroup/README.md +14 -0
  118. package/src/main.ts +5 -0
  119. package/src/components/Introduction.mdx +0 -100
  120. package/src/components/Support.mdx +0 -18
  121. package/src/components/usingIcons.mdx +0 -35
@@ -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
+ };