@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,314 @@
1
+ <template>
2
+ <div
3
+ :class="{
4
+ 'mc-file-uploader': true,
5
+ 'mc-file-uploader--draggable': props.hasDragDrop,
6
+ }"
7
+ >
8
+ <div class="mc-file-uploader__container">
9
+ <input
10
+ ref="fileInput"
11
+ type="file"
12
+ aria-label="File input"
13
+ :accept="props.accept"
14
+ :multiple="props.multiple"
15
+ class="mc-file-uploader__hidden-input"
16
+ :disabled="props.disabled"
17
+ :aria-disabled="props.disabled"
18
+ @change="onChange"
19
+ />
20
+
21
+ <div
22
+ :class="{
23
+ 'mc-file-uploader__input': true,
24
+ 'mc-file-uploader__input--dragged': isDraggedOver,
25
+ 'mc-file-uploader__input--disabled': props.disabled,
26
+ }"
27
+ tabindex="0"
28
+ role="button"
29
+ :aria-disabled="props.disabled"
30
+ @drop.prevent.stop="onDrop"
31
+ @dragenter.prevent="onDragEnter"
32
+ @dragleave.prevent="onDragLeave"
33
+ @dragover.prevent
34
+ @click="fileInput!.click()"
35
+ @keydown.enter="fileInput!.click()"
36
+ @keydown.space.prevent="fileInput!.click()"
37
+ >
38
+ <template v-if="props.hasDragDrop">
39
+ <span class="mc-file-uploader__input-title">
40
+ {{ props.title }}
41
+ </span>
42
+
43
+ <span class="mc-file-uploader__input-subtitle">
44
+ {{ props.subtitle }}
45
+ </span>
46
+ </template>
47
+
48
+ <div class="mc-button mc-button--outlined mc-file-uploader__button">
49
+ <Upload24 class="mc-button__icon" />
50
+ <span class="mc-button__label">
51
+ {{ props.uploadButtonLabel }}
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div v-if="props.showFilesList" class="mc-file-uploader__files-list">
58
+ <FileUploaderItem
59
+ v-for="(file, index) in files"
60
+ :key="`${file.name}-${index}`"
61
+ :file="file"
62
+ :format="props.format"
63
+ :valid="isFileValid(file)"
64
+ :information="props.information"
65
+ :error-message="props.errorMessage"
66
+ :delete-button-label="props.deleteButtonLabel"
67
+ @delete="onDelete(index)"
68
+ >
69
+ <template #name="{ file }">
70
+ <slot name="name" v-bind="{ file }" />
71
+ </template>
72
+
73
+ <template #information="{ file }">
74
+ <slot name="information" v-bind="{ file }" />
75
+ </template>
76
+
77
+ <template #errorMessage>
78
+ <slot
79
+ name="errorMessage"
80
+ v-bind="{ validationState: filesValidationState[file.name] }"
81
+ />
82
+ </template>
83
+ </FileUploaderItem>
84
+ </div>
85
+ </div>
86
+ </template>
87
+
88
+ <script setup lang="ts">
89
+ import { computed, ref, useTemplateRef, watch, type VNode } from 'vue';
90
+ import { Upload24 } from '@mozaic-ds/icons-vue';
91
+ import FileUploaderItem, {
92
+ type NormalizedFile,
93
+ } from '../fileuploaderitem/MFileUploaderItem.vue';
94
+
95
+ export type FilesValidationState = Record<
96
+ string,
97
+ { size: boolean; extension: boolean; customValidation: boolean }
98
+ >;
99
+
100
+ /**
101
+ * 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.
102
+ */
103
+
104
+ export type FileUploaderProps = {
105
+ /**
106
+ * Model binding for the selected files (`NormalizedFile`)
107
+ */
108
+ modelValue: NormalizedFile[];
109
+ /**
110
+ * File types allowed by the uploader.
111
+ */
112
+ accept?: HTMLInputElement['accept'];
113
+ /**
114
+ * Enables selecting multiple files at once.
115
+ */
116
+ multiple?: boolean;
117
+ /**
118
+ * File extensions to validate.
119
+ */
120
+ allowedExtensions?: string[];
121
+ /**
122
+ * Maximum file size allowed (in bytes).
123
+ */
124
+ maxSize?: number;
125
+ /**
126
+ * Custom validation rules applied to each file.
127
+ */
128
+ rules?: ((file: NormalizedFile) => boolean)[];
129
+ /**
130
+ * Layout format of the item.
131
+ */
132
+ format?: 'inline' | 'stacked';
133
+ /**
134
+ * Custom error message for the file.
135
+ */
136
+ errorMessage?: string;
137
+ /**
138
+ * Label for the delete button.
139
+ */
140
+ deleteButtonLabel?: string;
141
+ /**
142
+ * Optional informational text displayed under the file name.
143
+ */
144
+ information?: string;
145
+ /**
146
+ * Enables drag & drop functionality.
147
+ */
148
+ hasDragDrop?: boolean;
149
+ /**
150
+ * Controls the display of the uploaded files list.
151
+ */
152
+ showFilesList?: boolean;
153
+ /**
154
+ * Disables the whole component.
155
+ */
156
+ disabled?: boolean;
157
+ /**
158
+ * Main drag & drop title.
159
+ */
160
+ title?: string;
161
+ /**
162
+ * Subtitle used in the drag & drop area.
163
+ */
164
+ subtitle?: string;
165
+ /**
166
+ * Label of the upload button.
167
+ */
168
+ uploadButtonLabel?: string;
169
+ };
170
+
171
+ const props = withDefaults(defineProps<FileUploaderProps>(), {
172
+ hasDragDrop: true,
173
+ showFilesList: true,
174
+ title: 'Drag & drop',
175
+ subtitle: 'or',
176
+ uploadButtonLabel: 'Upload file(s)',
177
+ });
178
+
179
+ const emit = defineEmits<{
180
+ /**
181
+ * Emitted when the file list changes to update the bound model.
182
+ */
183
+ 'update:modelValue': [v: NormalizedFile[]];
184
+ /**
185
+ * Emitted with an object describing the validation results for each file (size, extension, and custom validation).
186
+ */
187
+ validation: [validationState: FilesValidationState];
188
+ }>();
189
+
190
+ defineSlots<{
191
+ /**
192
+ * Slot for customizing the file name display.
193
+ * Receives `NormalizedFile` as slot props.
194
+ */
195
+ name: VNode;
196
+ /**
197
+ * Slot for displaying additional file information.
198
+ * Receives `NormalizedFile` as slot props.
199
+ */
200
+ information: VNode;
201
+ /**
202
+ * Slot for providing a custom error message display.
203
+ * Receives:
204
+ * `{ validationState: { size: boolean; extension: boolean; customValidation: boolean } }`
205
+ */
206
+ errorMessage: VNode;
207
+ }>();
208
+
209
+ const fileInput = useTemplateRef('fileInput');
210
+
211
+ const files = computed({
212
+ get: () => props.modelValue,
213
+ set: (v: NormalizedFile[]) => emit('update:modelValue', v),
214
+ });
215
+
216
+ const isDraggedOver = ref(false);
217
+ const dragCounter = ref(0);
218
+
219
+ const filesValidationState = ref<FilesValidationState>({});
220
+
221
+ function validateFiles(newFiles: NormalizedFile[] = []) {
222
+ const state: FilesValidationState = {};
223
+ newFiles.forEach((file) => {
224
+ const extension = file.name.split('.').pop()?.toLowerCase() || '';
225
+ state[file.name] = {
226
+ size: props.maxSize && file.size ? file.size <= props.maxSize : true,
227
+ extension: props.allowedExtensions
228
+ ? props.allowedExtensions.includes(extension)
229
+ : true,
230
+ customValidation: props.rules?.every((rule) => rule(file)) ?? true,
231
+ };
232
+ });
233
+ return state;
234
+ }
235
+
236
+ watch(
237
+ files,
238
+ (newFiles) => {
239
+ filesValidationState.value = validateFiles(newFiles);
240
+ emit('validation', filesValidationState.value);
241
+ },
242
+ { immediate: true },
243
+ );
244
+
245
+ function onDragEnter(e: Event) {
246
+ if (!props.hasDragDrop) return;
247
+
248
+ e.preventDefault();
249
+ dragCounter.value++;
250
+ isDraggedOver.value = true;
251
+ }
252
+
253
+ function onDragLeave(e: Event) {
254
+ if (!props.hasDragDrop) return;
255
+
256
+ e.preventDefault();
257
+ dragCounter.value--;
258
+ if (dragCounter.value === 0) isDraggedOver.value = false;
259
+ }
260
+
261
+ function filterExistingFile(file: NormalizedFile) {
262
+ return files.value.every((item) => item.name !== file.name);
263
+ }
264
+
265
+ function onChange(e: Event) {
266
+ const input = e.target as HTMLInputElement;
267
+ const newFiles = Array.from(input.files || []).filter(filterExistingFile);
268
+
269
+ if (!newFiles.length) {
270
+ input.value = '';
271
+ return;
272
+ }
273
+
274
+ files.value = props.multiple
275
+ ? [...files.value, ...newFiles]
276
+ : newFiles.slice(0, 1);
277
+
278
+ input.value = '';
279
+ }
280
+
281
+ function onDrop(e: DragEvent) {
282
+ if (!props.hasDragDrop) return;
283
+
284
+ e.preventDefault();
285
+ e.stopPropagation();
286
+ dragCounter.value = 0;
287
+ isDraggedOver.value = false;
288
+ if (props.disabled) return;
289
+
290
+ const droppedFiles = Array.from(e.dataTransfer?.files || []).filter(
291
+ filterExistingFile,
292
+ );
293
+ if (!droppedFiles.length) return;
294
+
295
+ files.value = props.multiple
296
+ ? [...files.value, ...droppedFiles]
297
+ : [droppedFiles[0]];
298
+ }
299
+
300
+ function onDelete(index: number) {
301
+ files.value = files.value.filter((_, i) => i !== index);
302
+ }
303
+
304
+ function isFileValid(file: NormalizedFile) {
305
+ return Object.values(filesValidationState.value[file.name] ?? {}).every(
306
+ Boolean,
307
+ );
308
+ }
309
+ </script>
310
+
311
+ <style lang="scss" scoped>
312
+ @use '@mozaic-ds/styles/components/file-uploader';
313
+ @use '@mozaic-ds/styles/components/button';
314
+ </style>
@@ -0,0 +1,58 @@
1
+ # MFileUploader
2
+
3
+ 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.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `modelValue*` | Model binding for the selected files (`NormalizedFile`) | `NormalizedFile[]` | - |
11
+ | `accept` | File types allowed by the uploader. | `string` | - |
12
+ | `multiple` | Enables selecting multiple files at once. | `boolean` | - |
13
+ | `allowedExtensions` | File extensions to validate. | `string[]` | - |
14
+ | `maxSize` | Maximum file size allowed (in bytes). | `number` | - |
15
+ | `rules` | Custom validation rules applied to each file. | `((file: NormalizedFile) => boolean)[]` | - |
16
+ | `format` | Layout format of the item. | `"inline"` `"stacked"` | - |
17
+ | `errorMessage` | Custom error message for the file. | `string` | - |
18
+ | `deleteButtonLabel` | Label for the delete button. | `string` | - |
19
+ | `information` | Optional informational text displayed under the file name. | `string` | - |
20
+ | `hasDragDrop` | Enables drag & drop functionality. | `boolean` | `true` |
21
+ | `showFilesList` | Controls the display of the uploaded files list. | `boolean` | `true` |
22
+ | `disabled` | Disables the whole component. | `boolean` | - |
23
+ | `title` | Main drag & drop title. | `string` | `"Drag & drop"` |
24
+ | `subtitle` | Subtitle used in the drag & drop area. | `string` | `"or"` |
25
+ | `uploadButtonLabel` | Label of the upload button. | `string` | `"Upload file(s)"` |
26
+
27
+ ## Slots
28
+
29
+ | Name | Description |
30
+ | --- | --- |
31
+ | `name` | Slot for customizing the file name display.
32
+ Receives `NormalizedFile` as slot props. |
33
+ | `information` | Slot for displaying additional file information.
34
+ Receives `NormalizedFile` as slot props. |
35
+ | `errorMessage` | Slot for providing a custom error message display.
36
+ Receives:
37
+ `{ validationState: { size: boolean; extension: boolean; customValidation: boolean } }` |
38
+
39
+ ## Events
40
+
41
+ | Name | Description | Type |
42
+ | --- | --- | --- |
43
+ | `update:modelValue` | - | `[v: NormalizedFile[]]` |
44
+ | `validation` | - | `[validationState: FilesValidationState]` |
45
+
46
+ ## Dependencies
47
+
48
+ ### Depends on
49
+
50
+ - [MFileUploaderItem](../fileuploaderitem)
51
+
52
+ ### Graph
53
+
54
+ ```mermaid
55
+ graph TD;
56
+ MFileUploader --> MFileUploaderItem
57
+ style MFileUploader fill:#008240,stroke:#333,stroke-width:4px
58
+ ```
@@ -0,0 +1,91 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { describe, it, expect } from 'vitest';
3
+ import MFileUploaderItem from './MFileUploaderItem.vue';
4
+
5
+ const globalStubs = {
6
+ MButton: {
7
+ props: ['ariaLabel'],
8
+ inheritAttrs: false,
9
+ emits: ['click'],
10
+ template:
11
+ '<button v-bind="$attrs" aria-label="Delete file" @click="$emit(\'click\')"><slot /></button>',
12
+ },
13
+ MLinearProgressbarPercentage: {
14
+ inheritAttrs: false,
15
+ template: '<div class="progress" :data-value="$attrs.value"></div>',
16
+ },
17
+ CheckCircle32: { template: '<i class="icon-check"></i>' },
18
+ WarningCircle32: { template: '<i class="icon-warning"></i>' },
19
+ Cross20: { template: '<i class="icon-cross"></i>' },
20
+ CrossCircleFilled20: { template: '<i class="icon-cross-filled"></i>' },
21
+ };
22
+
23
+ describe('MFileUploaderItem', () => {
24
+ it('shows information details when information prop is set', () => {
25
+ const wrapper = mount(MFileUploaderItem, {
26
+ props: { file: { name: 'file.txt' }, information: 'Some info' },
27
+ global: { stubs: globalStubs },
28
+ });
29
+ const details = wrapper.find('.mc-file-uploader-item__details');
30
+ expect(details.exists()).toBe(true);
31
+ expect(details.text()).toBe('Some info');
32
+ });
33
+
34
+ it('emits delete when inline delete button is clicked', async () => {
35
+ const wrapper = mount(MFileUploaderItem, {
36
+ props: { file: { name: 'file.txt' }, format: 'inline' },
37
+ global: { stubs: globalStubs },
38
+ });
39
+ const btn = wrapper.find('button[aria-label="Delete file"]');
40
+ expect(btn.exists()).toBe(true);
41
+ await btn.trigger('click');
42
+ expect(wrapper.emitted()).toHaveProperty('delete');
43
+ });
44
+
45
+ it('emits delete when stacked delete button is clicked', async () => {
46
+ const wrapper = mount(MFileUploaderItem, {
47
+ props: { file: { name: 'file.txt' }, format: 'stacked' },
48
+ global: { stubs: globalStubs },
49
+ });
50
+ const btn = wrapper.find('button[aria-label="Delete file"]');
51
+ expect(btn.exists()).toBe(true);
52
+ await btn.trigger('click');
53
+ expect(wrapper.emitted()).toHaveProperty('delete');
54
+ });
55
+
56
+ it('applies correct state modifier classes', async () => {
57
+ // valid => default
58
+ const wrapper = mount(MFileUploaderItem, {
59
+ props: { file: { name: 'file.txt' }, valid: true },
60
+ global: { stubs: globalStubs },
61
+ });
62
+ expect(wrapper.classes()).not.toContain('mc-file-uploader-item--default');
63
+
64
+ // invalid => error
65
+ await wrapper.setProps({ valid: false });
66
+ expect(wrapper.classes()).toContain('mc-file-uploader-item--error');
67
+ });
68
+
69
+ it('renders errorMessage prop and allows slot override', () => {
70
+ const wrapperProp = mount(MFileUploaderItem, {
71
+ props: { file: { name: 'file.txt' }, errorMessage: 'Prop error' },
72
+ global: { stubs: globalStubs },
73
+ });
74
+ expect(
75
+ wrapperProp.find('.mc-file-uploader-item__error-message').text(),
76
+ ).toContain('Prop error');
77
+
78
+ const wrapperSlot = mount(MFileUploaderItem, {
79
+ props: { file: { name: 'file.txt' }, errorMessage: 'Prop error' },
80
+ slots: {
81
+ errorMessage: '<span>Slot error</span>',
82
+ name: '<span>Slot name</span>',
83
+ information: '<span>Slot information</span>',
84
+ },
85
+ global: { stubs: globalStubs },
86
+ });
87
+ expect(
88
+ wrapperSlot.find('.mc-file-uploader-item__error-message').text(),
89
+ ).toContain('Slot error');
90
+ });
91
+ });
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <div
3
+ :class="{
4
+ 'mc-file-uploader-item': true,
5
+ [`mc-file-uploader-item--${stateModifier}`]: stateModifier !== 'default',
6
+ }"
7
+ >
8
+ <div
9
+ :class="{
10
+ 'mc-file-uploader-item__container': true,
11
+ [`mc-file-uploader-item__container--${props.format}`]: true,
12
+ }"
13
+ >
14
+ <div
15
+ :class="{
16
+ 'mc-file-uploader-item__meta-row': true,
17
+ 'mc-file-uploader-item__meta-row--with-info': props.information,
18
+ }"
19
+ >
20
+ <component :is="stateIcon" class="mc-file-uploader-item__status-icon" />
21
+
22
+ <div class="mc-file-uploader-item__info-content">
23
+ <span class="mc-file-uploader-item__label">
24
+ <slot name="name" v-bind="{ file: props.file }">
25
+ {{ props.file.name }}
26
+ </slot>
27
+ </span>
28
+
29
+ <span
30
+ v-if="props.information || $slots.information"
31
+ class="mc-file-uploader-item__details"
32
+ >
33
+ <slot name="information" v-bind="{ file: props.file }">
34
+ {{ props.information }}
35
+ </slot>
36
+ </span>
37
+ </div>
38
+ </div>
39
+
40
+ <template v-if="isStacked">
41
+ <MDivider />
42
+ <div class="mc-file-uploader-item__actions-container">
43
+ <MButton
44
+ ghost
45
+ size="s"
46
+ icon-position="left"
47
+ aria-label="Delete file"
48
+ @click="emit('delete')"
49
+ >
50
+ {{ props.deleteButtonLabel }}
51
+
52
+ <template #icon>
53
+ <Cross20 />
54
+ </template>
55
+ </MButton>
56
+ </div>
57
+ </template>
58
+
59
+ <template v-else>
60
+ <div class="mc-file-uploader-item__delete-button-container">
61
+ <MButton
62
+ ghost
63
+ size="s"
64
+ icon-position="only"
65
+ aria-label="Delete file"
66
+ @click="emit('delete')"
67
+ >
68
+ {{ props.deleteButtonLabel }}
69
+
70
+ <template #icon>
71
+ <CrossCircleFilled20 />
72
+ </template>
73
+ </MButton>
74
+ </div>
75
+ </template>
76
+ </div>
77
+
78
+ <span
79
+ v-if="!valid && (props.errorMessage || slots.errorMessage)"
80
+ class="mc-file-uploader-item__error-message"
81
+ >
82
+ <slot name="errorMessage">
83
+ {{ props.errorMessage }}
84
+ </slot>
85
+ </span>
86
+ </div>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import { computed, type VNode } from 'vue';
91
+ import {
92
+ CheckCircle32,
93
+ Cross20,
94
+ CrossCircleFilled20,
95
+ WarningCircle32,
96
+ } from '@mozaic-ds/icons-vue';
97
+ import MButton from '../button/MButton.vue';
98
+ import MDivider from '../divider/MDivider.vue';
99
+
100
+ export type NormalizedFile = {
101
+ name: string;
102
+ size?: number;
103
+ type?: HTMLInputElement['accept'];
104
+ };
105
+
106
+ /**
107
+ * 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.
108
+ */
109
+
110
+ const props = withDefaults(
111
+ defineProps<{
112
+ /**
113
+ * Represents the file to be displayed in the file uploader item.
114
+ */
115
+ file: NormalizedFile;
116
+ /**
117
+ * Layout format of the item.
118
+ */
119
+ format?: 'inline' | 'stacked';
120
+ /**
121
+ * Indicates if the file is valid according to the uploader's validation rules.
122
+ */
123
+ valid?: boolean;
124
+ /**
125
+ * Custom error message for the file.
126
+ */
127
+ errorMessage?: string;
128
+ /**
129
+ * Label for the delete button.
130
+ */
131
+ deleteButtonLabel?: string;
132
+ /**
133
+ * Optional informational text displayed under the file name.
134
+ */
135
+ information?: string;
136
+ }>(),
137
+ {
138
+ format: 'inline',
139
+ deleteButtonLabel: 'Delete',
140
+ },
141
+ );
142
+
143
+ const emit = defineEmits<{
144
+ /**
145
+ * Emitted when the user clicks the delete button for the file.
146
+ */
147
+ delete: [];
148
+ }>();
149
+
150
+ const slots = defineSlots<{
151
+ /**
152
+ * Slot for customizing the file name display.
153
+ * Receives `{ file: { name: string; size?: number;} }` as slot props.
154
+ */
155
+ name: VNode;
156
+ /**
157
+ * Slot for displaying additional file information.
158
+ */
159
+ information: VNode;
160
+ /**
161
+ * Slot for providing a custom error message display.
162
+ */
163
+ errorMessage: VNode;
164
+ }>();
165
+
166
+ const isStacked = computed(() => props.format === 'stacked');
167
+
168
+ const stateModifier = computed(() => (props.valid ? 'default' : 'error'));
169
+
170
+ const stateIconMap = {
171
+ default: CheckCircle32,
172
+ error: WarningCircle32,
173
+ };
174
+
175
+ const stateIcon = computed(() => stateIconMap[stateModifier.value]);
176
+ </script>
177
+
178
+ <style lang="scss" scoped>
179
+ @use '@mozaic-ds/styles/components/file-uploader';
180
+ </style>
@@ -0,0 +1,58 @@
1
+ # MFileUploaderItem
2
+
3
+ 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.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `file*` | Represents the file to be displayed in the file uploader item. | `NormalizedFile` | - |
11
+ | `format` | Layout format of the item. | `"inline"` `"stacked"` | `"inline"` |
12
+ | `valid` | Indicates if the file is valid according to the uploader's validation rules. | `boolean` | - |
13
+ | `errorMessage` | Custom error message for the file. | `string` | - |
14
+ | `deleteButtonLabel` | Label for the delete button. | `string` | `"Delete"` |
15
+ | `information` | Optional informational text displayed under the file name. | `string` | - |
16
+
17
+ ## Slots
18
+
19
+ | Name | Description |
20
+ | --- | --- |
21
+ | `name` | Slot for customizing the file name display.
22
+ Receives `{ file: { name: string; size?: number;} }` as slot props. |
23
+ | `information` | Slot for displaying additional file information. |
24
+ | `errorMessage` | Slot for providing a custom error message display. |
25
+
26
+ ## Events
27
+
28
+ | Name | Description | Type |
29
+ | --- | --- | --- |
30
+ | `delete` | - | `[]` |
31
+
32
+ ## Dependencies
33
+
34
+ ### Depends on
35
+
36
+ - [MButton](../button)
37
+ - [MDivider](../divider)
38
+
39
+ ### Graph
40
+
41
+ ```mermaid
42
+ graph TD;
43
+ MFileUploaderItem --> MButton
44
+ MFileUploaderItem --> MDivider
45
+ style MFileUploaderItem fill:#008240,stroke:#333,stroke-width:4px
46
+ ```
47
+
48
+ ### Used By
49
+
50
+ - [MFileUploader](../fileuploader)
51
+
52
+ ### Graph
53
+
54
+ ```mermaid
55
+ graph TD;
56
+ MFileUploader --> MFileUploaderItem
57
+ style MFileUploaderItem fill:#008240,stroke:#333,stroke-width:4px
58
+ ```