@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.
- package/dist/mozaic-vue.css +1 -1
- package/dist/mozaic-vue.d.ts +702 -361
- package/dist/mozaic-vue.js +2791 -2428
- package/dist/mozaic-vue.js.map +1 -1
- package/dist/mozaic-vue.umd.cjs +5 -5
- package/dist/mozaic-vue.umd.cjs.map +1 -1
- package/package.json +7 -6
- package/src/components/{usingPresets.mdx → BrandPresets.mdx} +2 -2
- package/src/components/Changelog.mdx +19 -0
- package/src/components/Color.mdx +226 -0
- package/src/components/Contributing.mdx +12 -6
- package/src/components/GettingStarted.mdx +1 -1
- package/src/components/Icon.stories.ts +134 -0
- package/src/components/Welcome.mdx +49 -0
- package/src/components/accordionlist/MAccordionList.spec.ts +136 -0
- package/src/components/accordionlist/MAccordionList.stories.ts +123 -0
- package/src/components/accordionlist/MAccordionList.vue +91 -0
- package/src/components/accordionlist/README.md +24 -0
- package/src/components/accordionlist/m-accordion-list.const.ts +9 -0
- package/src/components/accordionlistitem/MAccordionListItem.spec.ts +123 -0
- package/src/components/accordionlistitem/MAccordionListItem.vue +95 -0
- package/src/components/accordionlistitem/README.md +23 -0
- package/src/components/actionbottombar/MActionBottomBar.spec.ts +52 -0
- package/src/components/actionbottombar/MActionBottomBar.stories.ts +162 -0
- package/src/components/actionbottombar/MActionBottomBar.vue +45 -0
- package/src/components/actionbottombar/README.md +31 -0
- package/src/components/actionlistbox/MActionListbox.spec.ts +134 -0
- package/src/components/actionlistbox/MActionListbox.stories.ts +74 -0
- package/src/components/actionlistbox/MActionListbox.vue +89 -0
- package/src/components/actionlistbox/README.md +25 -0
- package/src/components/avatar/MAvatar.stories.ts +1 -1
- package/src/components/breadcrumb/README.md +14 -0
- package/src/components/builtinmenu/MBuiltInMenu.stories.ts +2 -1
- package/src/components/builtinmenu/MBuiltInMenu.vue +1 -1
- package/src/components/builtinmenu/README.md +14 -0
- package/src/components/button/MButton.spec.ts +1 -1
- package/src/components/button/MButton.stories.ts +165 -5
- package/src/components/button/README.md +33 -1
- package/src/components/callout/MCallout.spec.ts +7 -6
- package/src/components/callout/MCallout.stories.ts +1 -2
- package/src/components/carousel/MCarousel.spec.ts +1 -2
- package/src/components/carousel/MCarousel.stories.ts +2 -1
- package/src/components/carousel/MCarousel.vue +1 -2
- package/src/components/carousel/README.md +14 -0
- package/src/components/checkbox/README.md +14 -0
- package/src/components/checkboxgroup/README.md +14 -0
- package/src/components/checklistmenu/MCheckListMenu.spec.ts +1 -1
- package/src/components/checklistmenu/MCheckListMenu.stories.ts +1 -0
- package/src/components/checklistmenu/MCheckListMenu.vue +1 -1
- package/src/components/checklistmenu/README.md +14 -0
- package/src/components/circularprogressbar/README.md +15 -1
- package/src/components/datepicker/MDatepicker.vue +1 -1
- package/src/components/divider/README.md +22 -0
- package/src/components/drawer/MDrawer.vue +1 -2
- package/src/components/drawer/README.md +16 -0
- package/src/components/field/README.md +14 -0
- package/src/components/fileuploader/MFileUploader.spec.ts +304 -0
- package/src/components/fileuploader/MFileUploader.stories.ts +123 -0
- package/src/components/fileuploader/MFileUploader.vue +314 -0
- package/src/components/fileuploader/README.md +58 -0
- package/src/components/fileuploaderitem/MFileUploaderItem.spec.ts +91 -0
- package/src/components/fileuploaderitem/MFileUploaderItem.vue +180 -0
- package/src/components/fileuploaderitem/README.md +58 -0
- package/src/components/flag/README.md +1 -1
- package/src/components/iconbutton/MIconButton.spec.ts +1 -1
- package/src/components/iconbutton/MIconButton.stories.ts +116 -7
- package/src/components/iconbutton/README.md +25 -1
- package/src/components/kpiitem/MKpiItem.vue +5 -3
- package/src/components/linearprogressbarbuffer/README.md +14 -0
- package/src/components/link/MLink.stories.ts +1 -2
- package/src/components/link/README.md +14 -0
- package/src/components/loader/README.md +20 -0
- package/src/components/loadingoverlay/README.md +14 -0
- package/src/components/modal/MModal.stories.ts +1 -2
- package/src/components/modal/MModal.vue +1 -1
- package/src/components/modal/README.md +16 -0
- package/src/components/numberbadge/README.md +17 -1
- package/src/components/overlay/README.md +16 -0
- package/src/components/pagination/MPagination.vue +1 -2
- package/src/components/pagination/README.md +18 -0
- package/src/components/passwordinput/MPasswordInput.vue +1 -1
- package/src/components/passwordinput/README.md +14 -0
- package/src/components/phonenumber/MPhoneNumber.spec.ts +7 -6
- package/src/components/phonenumber/MPhoneNumber.vue +1 -1
- package/src/components/quantityselector/MQuantitySelector.vue +1 -2
- package/src/components/radio/README.md +14 -0
- package/src/components/radiogroup/README.md +14 -0
- package/src/components/select/README.md +14 -0
- package/src/components/starrating/MStarRating.spec.ts +1 -2
- package/src/components/starrating/MStarRating.vue +1 -3
- package/src/components/statusbadge/README.md +14 -0
- package/src/components/statusdot/README.md +14 -0
- package/src/components/statusmessage/MStatusMessage.spec.ts +6 -4
- package/src/components/statusmessage/MStatusMessage.vue +6 -4
- package/src/components/statusmessage/README.md +14 -0
- package/src/components/statusnotification/MStatusNotification.spec.ts +6 -4
- package/src/components/statusnotification/MStatusNotification.stories.ts +1 -1
- package/src/components/statusnotification/MStatusNotification.vue +7 -5
- package/src/components/statusnotification/README.md +14 -0
- package/src/components/stepperbottombar/MStepperBottomBar.spec.ts +134 -0
- package/src/components/stepperbottombar/MStepperBottomBar.stories.ts +72 -0
- package/src/components/stepperbottombar/MStepperBottomBar.vue +131 -0
- package/src/components/stepperbottombar/README.md +40 -0
- package/src/components/steppercompact/README.md +14 -0
- package/src/components/stepperinline/MStepperInline.vue +1 -2
- package/src/components/tabs/MTabs.stories.ts +1 -1
- package/src/components/tabs/README.md +16 -0
- package/src/components/tag/MTag.vue +1 -1
- package/src/components/tag/README.md +14 -0
- package/src/components/textinput/MTextInput.spec.ts +1 -1
- package/src/components/textinput/MTextInput.stories.ts +1 -1
- package/src/components/textinput/MTextInput.vue +1 -1
- package/src/components/toaster/MToaster.spec.ts +6 -4
- package/src/components/toaster/MToaster.vue +7 -5
- package/src/components/toaster/README.md +16 -0
- package/src/components/toggle/README.md +14 -0
- package/src/components/togglegroup/README.md +14 -0
- package/src/main.ts +5 -0
- package/src/components/Introduction.mdx +0 -100
- package/src/components/Support.mdx +0 -18
- 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
|
+
```
|