diff --git a/docs/app/components/content/examples/file-upload/FileUploadDefaultSlotExample.vue b/docs/app/components/content/examples/file-upload/FileUploadDefaultSlotExample.vue new file mode 100644 index 0000000000..b73c18704e --- /dev/null +++ b/docs/app/components/content/examples/file-upload/FileUploadDefaultSlotExample.vue @@ -0,0 +1,96 @@ + + + diff --git a/docs/app/components/content/examples/file-upload/FileUploadFilesBottomSlotExample.vue b/docs/app/components/content/examples/file-upload/FileUploadFilesBottomSlotExample.vue new file mode 100644 index 0000000000..52ac9f5c3c --- /dev/null +++ b/docs/app/components/content/examples/file-upload/FileUploadFilesBottomSlotExample.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/app/components/content/examples/file-upload/FileUploadFilesTopSlotExample.vue b/docs/app/components/content/examples/file-upload/FileUploadFilesTopSlotExample.vue new file mode 100644 index 0000000000..c16bbe4091 --- /dev/null +++ b/docs/app/components/content/examples/file-upload/FileUploadFilesTopSlotExample.vue @@ -0,0 +1,43 @@ + + + diff --git a/docs/app/components/content/examples/file-upload/FileUploadFormValidationExample.vue b/docs/app/components/content/examples/file-upload/FileUploadFormValidationExample.vue new file mode 100644 index 0000000000..314e51ca54 --- /dev/null +++ b/docs/app/components/content/examples/file-upload/FileUploadFormValidationExample.vue @@ -0,0 +1,73 @@ + + + diff --git a/docs/content/3.components/file-upload.md b/docs/content/3.components/file-upload.md new file mode 100644 index 0000000000..f40a852cb4 --- /dev/null +++ b/docs/content/3.components/file-upload.md @@ -0,0 +1,338 @@ +--- +title: FileUpload +description: 'An input element to upload files.' +category: form +links: + - label: GitHub + icon: i-simple-icons-github + to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/FileUpload.vue +navigation.badge: Soon +--- + +## Usage + +Use the `v-model` directive to control the value of the FileUpload. + +::component-code +--- +ignore: + - modelValue + - class +external: + - modelValue +props: + modelValue: null + class: 'w-96 min-h-48' +--- +:: + +### Multiple + +Use the `multiple` prop to allow multiple files to be selected. + +::component-code +--- +ignore: + - class +props: + multiple: true + class: 'w-96 min-h-48' +--- +:: + +### Dropzone + +Use the `dropzone` prop to enable/disable the droppable area. Defaults to `true`. + +::component-code +--- +ignore: + - class +props: + dropzone: false + class: 'w-96 min-h-48' +--- +:: + +### Interactive + +Use the `interactive` prop to enable/disable the clickable area. Defaults to `true`. + +::tip{to="#with-files-bottom-slot"} +This can be useful when adding a [`Button`](/components/button) component in the `#actions` slot. +:: + +::component-code +--- +ignore: + - class +props: + interactive: false + class: 'w-96 min-h-48' +--- +:: + +### Accept + +Use the `accept` prop to specify the allowed file types for the input. Provide a comma-separated list of [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types) or file extensions (e.g., `image/png,application/pdf,.jpg`). Defaults to `*` (all file types). + +::component-code +--- +ignore: + - accept + - class +props: + accept: 'image/*' + class: 'w-96 min-h-48' +--- +:: + +### Label + +Use the `label` prop to set the label of the FileUpload. + +::component-code +--- +prettier: true +ignore: + - class +props: + label: 'Drop your image here' + class: 'w-96 min-h-48' +--- +:: + +### Description + +Use the `description` prop to set the description of the FileUpload. + +::component-code +--- +prettier: true +ignore: + - label + - class +props: + label: 'Drop your image here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' + class: 'w-96 min-h-48' +--- +:: + +### Icon + +Use the `icon` prop to set the icon of the FileUpload. Defaults to `i-lucide-upload`. + +::component-code +--- +prettier: true +ignore: + - label + - description + - class +props: + icon: 'i-lucide-image' + label: 'Drop your image here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' + class: 'w-96 min-h-48' +--- +:: + +::framework-only +#nuxt +:::tip{to="/getting-started/icons/nuxt#theme"} +You can customize this icon globally in your `app.config.ts` under `ui.icons.upload` key. +::: + +#vue +:::tip{to="/getting-started/icons/vue#theme"} +You can customize this icon globally in your `vite.config.ts` under `ui.icons.upload` key. +::: +:: + +### Color + +Use the `color` prop to change the color of the FileUpload. + +::component-code +--- +prettier: true +ignore: + - label + - description + - class +props: + color: neutral + highlight: true + label: 'Drop your image here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' + class: 'w-96 min-h-48' +--- +:: + +::note +The `highlight` prop is used here to show the focus state. It's used internally when a validation error occurs. +:: + +### Variant + +Use the `variant` prop to change the variant of the FileUpload. + +::component-code +--- +ignore: + - class +props: + variant: button +--- +:: + +### Size + +Use the `size` prop to change the size of the FileUpload. + +::component-code +--- +prettier: true +ignore: + - label + - description + - class +props: + size: xl + variant: area + label: 'Drop your image here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' +--- +:: + +### Layout + +Use the `layout` prop to change how the files are displayed in the FileUpload. Defaults to `grid`. + +::warning +This prop only works when `variant` is `area`. +:: + +::component-code +--- +prettier: true +ignore: + - label + - description + - multiple + - class + - ui.base +props: + layout: list + multiple: true + label: 'Drop your images here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' + class: 'w-96' + ui: + base: 'min-h-48' +--- +:: + +### Position + +Use the `position` prop to change the position of the files in the FileUpload. Defaults to `outside`. + +::warning +This prop only works when `variant` is `area` and when `layout` is `list`. +:: + +::component-code +--- +prettier: true +ignore: + - label + - description + - multiple + - layout + - class + - ui.base +props: + position: inside + layout: list + multiple: true + label: 'Drop your images here' + description: 'SVG, PNG, JPG or GIF (max. 2MB)' + class: 'w-96' + ui: + base: 'min-h-48' +--- +:: + +## Examples + +### With Form validation + +You can use the FileUpload within a [Form](/components/form) and [FormField](/components/form-field) components to handle validation and error handling. + +::component-example +--- +name: 'file-upload-form-validation-example' +--- +:: + +### With default slot + +You can use the default slot to make your own FileUpload component. + +::component-example +--- +name: 'file-upload-default-slot-example' +--- +:: + +### With files-bottom slot + +You can use the `files-bottom` slot to add a [Button](/components/button) under the files list to remove all files for example. + +::component-example +--- +name: 'file-upload-files-bottom-slot-example' +--- +:: + +::note{to="#interactive"} +The `interactive` prop is set to `false` in this example to prevent the default clickable area. +:: + +### With files-top slot + +You can use the `files-top` slot to add a [Button](/components/button) above the files list to add new files for example. + +::component-example +--- +name: 'file-upload-files-top-slot-example' +--- +:: + +## API + +### Props + +:component-props + +### Slots + +:component-slots + +### Emits + +:component-emits + +### Expose + +When accessing the component via a template ref, you can use the following: + +| Name | Type | +| ---- | ---- | +| `inputRef`{lang="ts-type"} | `Ref`{lang="ts-type"} | +| `dropzoneRef`{lang="ts-type"} | `Ref`{lang="ts-type"} | + +## Theme + +:component-theme diff --git a/docs/public/components/dark/file-upload.png b/docs/public/components/dark/file-upload.png new file mode 100644 index 0000000000..6349f7ab85 Binary files /dev/null and b/docs/public/components/dark/file-upload.png differ diff --git a/docs/public/components/light/file-upload.png b/docs/public/components/light/file-upload.png new file mode 100644 index 0000000000..dbc78e7973 Binary files /dev/null and b/docs/public/components/light/file-upload.png differ diff --git a/playground-vue/src/app.vue b/playground-vue/src/app.vue index cc2e69d572..64285334fa 100644 --- a/playground-vue/src/app.vue +++ b/playground-vue/src/app.vue @@ -35,6 +35,7 @@ const components = [ 'command-palette', 'drawer', 'dropdown-menu', + 'file-upload', 'form', 'form-field', 'input', diff --git a/playground/app/app.vue b/playground/app/app.vue index 7348fbd70f..61662537c6 100644 --- a/playground/app/app.vue +++ b/playground/app/app.vue @@ -35,6 +35,7 @@ const components = [ 'command-palette', 'drawer', 'dropdown-menu', + 'file-upload', 'form', 'form-field', 'input', diff --git a/playground/app/pages/components/file-upload.vue b/playground/app/pages/components/file-upload.vue new file mode 100644 index 0000000000..b109ba0c77 --- /dev/null +++ b/playground/app/pages/components/file-upload.vue @@ -0,0 +1,194 @@ + + + diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 8eebc437f9..04c54f003a 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -12,6 +12,10 @@ export default defineNuxtConfig({ compatibilityDate: '2024-07-09', + hub: { + blob: true + }, + vite: { optimizeDeps: { // prevents reloading page when navigating between components diff --git a/playground/server/api/blob.put.ts b/playground/server/api/blob.put.ts new file mode 100644 index 0000000000..cf1a2500a5 --- /dev/null +++ b/playground/server/api/blob.put.ts @@ -0,0 +1,12 @@ +export default eventHandler(async (event) => { + return hubBlob().handleUpload(event, { + formKey: 'files', // read file or files form the `formKey` field of request body (body should be a `FormData` object) + multiple: true, // when `true`, the `formKey` field will be an array of `Blob` objects + ensure: { + types: ['image/jpeg', 'image/png'] // allowed types of the file + }, + put: { + addRandomSuffix: true + } + }) +}) diff --git a/src/runtime/components/FileUpload.vue b/src/runtime/components/FileUpload.vue new file mode 100644 index 0000000000..992b5bbffc --- /dev/null +++ b/src/runtime/components/FileUpload.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/src/runtime/composables/useFileUpload.ts b/src/runtime/composables/useFileUpload.ts new file mode 100644 index 0000000000..9f7c15612b --- /dev/null +++ b/src/runtime/composables/useFileUpload.ts @@ -0,0 +1,96 @@ +import { ref, computed, unref, onMounted, watch, reactive } from 'vue' +import { useFileDialog, useDropZone } from '@vueuse/core' +import type { MaybeRef } from '@vueuse/core' + +export interface UseFileUploadOptions { + /** + * Specifies the allowed file types. Provide a comma-separated list of MIME types or file extensions. + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept + * @defaultValue '*' + */ + accept?: MaybeRef + reset?: boolean + multiple?: boolean + dropzone?: boolean + onUpdate: (files: File[]) => void +} + +function parseAcceptToDataTypes(accept: string): string[] | undefined { + if (!accept || accept === '*') { + return undefined + } + + const types = accept + .split(',') + .map(type => type.trim()) + .filter((type) => { + return !type.startsWith('.') + }) + + return types.length > 0 ? types : undefined +} + +export function useFileUpload(options: UseFileUploadOptions) { + const { + accept = '*', + reset = false, + multiple = false, + dropzone = true, + onUpdate + } = options + const inputRef = ref() + const dropzoneRef = ref() + + const dataTypes = computed(() => parseAcceptToDataTypes(unref(accept))) + + const onDrop = (files: FileList | File[] | null) => { + if (!files || files.length === 0) { + return + } + if (files instanceof FileList) { + files = Array.from(files) + } + if (files.length > 1 && !multiple) { + files = [files[0]!] + } + onUpdate(files) + } + + const isDragging = ref(false) + const fileDialog = reactive({ + open: () => { + } + }) + + function open() { + fileDialog.open() + } + + onMounted(() => { + const { isOverDropZone } = dropzone + ? useDropZone(dropzoneRef, { dataTypes: dataTypes.value, onDrop }) + : { isOverDropZone: ref(false) } + + watch(isOverDropZone, (value) => { + isDragging.value = value + }) + + const { onChange, open } = useFileDialog({ + accept: unref(accept), + multiple, + input: unref(inputRef), + reset + }) + + fileDialog.open = open + + onChange(fileList => onDrop(fileList)) + }) + + return { + isDragging, + open, + inputRef, + dropzoneRef + } +} diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index f413b71caa..bb9f09513c 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -20,6 +20,7 @@ export * from '../components/Container.vue' export * from '../components/ContextMenu.vue' export * from '../components/Drawer.vue' export * from '../components/DropdownMenu.vue' +export * from '../components/FileUpload.vue' export * from '../components/Form.vue' export * from '../components/FormField.vue' export * from '../components/Icon.vue' diff --git a/src/theme/file-upload.ts b/src/theme/file-upload.ts new file mode 100644 index 0000000000..94fdb5077a --- /dev/null +++ b/src/theme/file-upload.ts @@ -0,0 +1,194 @@ +import type { ModuleOptions } from '../module' + +export default (options: Required) => ({ + slots: { + root: 'relative flex flex-col', + base: ['w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-2', options.theme.transitions && 'transition-[background]'], + wrapper: 'flex flex-col items-center justify-center text-center', + icon: 'shrink-0', + avatar: 'shrink-0', + label: 'font-medium text-default mt-2', + description: 'text-muted mt-1', + actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4', + files: '', + file: 'relative', + fileLeadingAvatar: 'shrink-0', + fileWrapper: 'flex flex-col min-w-0', + fileName: 'text-default truncate', + fileSize: 'text-muted truncate', + fileTrailingButton: '' + }, + variants: { + color: { + ...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])), + neutral: '' + }, + variant: { + area: { + wrapper: 'px-4 py-3', + base: 'p-4' + }, + button: { + } + }, + size: { + xs: { + base: 'text-xs', + icon: 'size-4', + file: 'text-xs px-2 py-1 gap-1', + fileWrapper: 'flex-row gap-1' + }, + sm: { + base: 'text-xs', + icon: 'size-4', + file: 'text-xs px-2.5 py-1.5 gap-1.5', + fileWrapper: 'flex-row gap-1' + }, + md: { + base: 'text-sm', + icon: 'size-5', + file: 'text-xs px-2.5 py-1.5 gap-1.5' + }, + lg: { + base: 'text-sm', + icon: 'size-5', + file: 'text-sm px-3 py-2 gap-2', + fileSize: 'text-xs' + }, + xl: { + base: 'text-base', + icon: 'size-6', + file: 'text-sm px-3 py-2 gap-2' + } + }, + layout: { + list: { + root: 'gap-2 items-start', + files: 'flex flex-col w-full gap-2', + file: 'min-w-0 flex items-center border border-default rounded-md w-full', + fileTrailingButton: 'ms-auto' + }, + grid: { + fileWrapper: 'hidden', + fileLeadingAvatar: 'size-full rounded-lg', + fileTrailingButton: 'absolute -top-1.5 -right-1.5 p-0 rounded-full border-2 border-bg' + } + }, + position: { + inside: '', + outside: '' + }, + dropzone: { + true: 'border-dashed data-[dragging=true]:bg-elevated/25' + }, + interactive: { + true: '' + }, + highlight: { + true: '' + }, + multiple: { + true: '' + }, + disabled: { + true: 'cursor-not-allowed opacity-75' + } + }, + compoundVariants: [...(options.theme.colors || []).map((color: string) => ({ + color, + class: `focus-visible:outline-${color}` + })), ...(options.theme.colors || []).map((color: string) => ({ + color, + highlight: true, + class: `border-${color}` + })), { + color: 'neutral', + class: 'focus-visible:outline-inverted' + }, { + color: 'neutral', + highlight: true, + class: 'border-inverted' + }, { + size: 'xs', + layout: 'list', + class: { + fileTrailingButton: '-mr-1' + } + }, { + size: 'sm', + layout: 'list', + class: { + fileTrailingButton: '-mr-1.5' + } + }, { + size: 'md', + layout: 'list', + class: { + fileTrailingButton: '-mr-1.5' + } + }, { + size: 'lg', + layout: 'list', + class: { + fileTrailingButton: '-mr-2' + } + }, { + size: 'xl', + layout: 'list', + class: { + fileTrailingButton: '-mr-2' + } + }, { + variant: 'button', + size: 'xs', + class: { + base: 'p-1' + } + }, { + variant: 'button', + size: 'sm', + class: { + base: 'p-1.5' + } + }, { + variant: 'button', + size: 'md', + class: { + base: 'p-1.5' + } + }, { + variant: 'button', + size: 'lg', + class: { + base: 'p-2' + } + }, { + variant: 'button', + size: 'xl', + class: { + base: 'p-2' + } + }, { + layout: 'grid', + multiple: true, + class: { + files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full', + file: 'p-0 aspect-square' + } + }, { + layout: 'grid', + multiple: false, + class: { + file: 'absolute inset-0 p-0' + } + }, { + interactive: true, + disabled: false, + class: 'hover:bg-elevated/25' + }], + defaultVariants: { + color: 'primary', + variant: 'area', + size: 'md' + } +}) diff --git a/src/theme/icons.ts b/src/theme/icons.ts index b0d6357b65..777a85b2c5 100644 --- a/src/theme/icons.ts +++ b/src/theme/icons.ts @@ -11,10 +11,12 @@ export default { close: 'i-lucide-x', ellipsis: 'i-lucide-ellipsis', external: 'i-lucide-arrow-up-right', + file: 'i-lucide-file', folder: 'i-lucide-folder', folderOpen: 'i-lucide-folder-open', loading: 'i-lucide-loader-circle', minus: 'i-lucide-minus', plus: 'i-lucide-plus', - search: 'i-lucide-search' + search: 'i-lucide-search', + upload: 'i-lucide-upload' } diff --git a/src/theme/index.ts b/src/theme/index.ts index e03aecd3b5..3958378cd5 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -19,6 +19,7 @@ export { default as container } from './container' export { default as contextMenu } from './context-menu' export { default as drawer } from './drawer' export { default as dropdownMenu } from './dropdown-menu' +export { default as fileUpload } from './file-upload' export { default as form } from './form' export { default as formField } from './form-field' export { default as input } from './input' diff --git a/test/components/Checkbox.spec.ts b/test/components/Checkbox.spec.ts index a02df04497..1ca750448f 100644 --- a/test/components/Checkbox.spec.ts +++ b/test/components/Checkbox.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/checkbox' import { renderForm } from '../utils/form' import { mount, flushPromises } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('Checkbox', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/CheckboxGroup.spec.ts b/test/components/CheckboxGroup.spec.ts index a393b9cff4..61987e7dfe 100644 --- a/test/components/CheckboxGroup.spec.ts +++ b/test/components/CheckboxGroup.spec.ts @@ -6,7 +6,7 @@ import theme from '#build/ui/checkbox-group' import themeCheckbox from '#build/ui/checkbox' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('CheckboxGroup', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/FileUpload.spec.ts b/test/components/FileUpload.spec.ts new file mode 100644 index 0000000000..14a3ff9df9 --- /dev/null +++ b/test/components/FileUpload.spec.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, vi, test } from 'vitest' +import { mount } from '@vue/test-utils' +import FileUpload from '../../src/runtime/components/FileUpload.vue' +import type { FileUploadProps, FileUploadSlots } from '../../src/runtime/components/FileUpload.vue' +import ComponentRender from '../component-render' +import { renderForm } from '../utils/form' +import type { FormInputEvents } from '../../src/module' +import theme from '#build/ui/file-upload' + +// Mock URL.createObjectURL to return deterministic blob URLs +URL.createObjectURL = vi.fn((file: File | Blob) => { + if (file instanceof File) { + return `blob:mock-url-${file.name}` + } + return 'blob:mock-url-blob' +}) + +async function setFilesOnInput(input: any, files: File[]) { + // Create a DataTransfer and add files + const data = new DataTransfer() + files.forEach(file => data.items.add(file)) + // Set files property via Object.defineProperty + Object.defineProperty(input.element, 'files', { + value: data.files, + writable: false, + configurable: true + }) + // Trigger change event + await input.trigger('change') +} + +describe('FileUpload', () => { + const sizes = Object.keys(theme.variants.size) as any + const variants = Object.keys(theme.variants.variant) as any + const layouts = Object.keys(theme.variants.layout) as any + const positions = Object.keys(theme.variants.position) as any + + const modelValue = [new File([], 'file.txt', { type: 'text/plain' })] + + const props = { modelValue } + + it.each([ + // Props + ['with modelValue', { props }], + ['with id', { props: { id: 'id' } }], + ['with name', { props: { name: 'name' } }], + ['with icon', { props: { icon: 'i-lucide-image' } }], + ['with label', { props: { label: 'Drop your image here' } }], + ['with description', { props: { description: 'SVG, PNG, JPG or GIF (max. 2MB)' } }], + ['with neutral color', { props: { color: 'neutral' } }], + ...variants.map((variant: string) => [`with variant ${variant}`, { props: { ...props, variant } }]), + ...layouts.map((layout: string) => [`with layout ${layout}`, { props: { ...props, layout } }]), + ...layouts.map((layout: string) => [`with layout ${layout} multiple`, { props: { ...props, layout, multiple: true } }]), + ...positions.map((position: string) => [`with position ${position}`, { props: { ...props, position } }]), + ...positions.map((position: string) => [`with position ${position} multiple`, { props: { ...props, position, multiple: true } }]), + ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), + ...sizes.map((size: string) => [`with size ${size} variant button`, { props: { ...props, size, variant: 'button' } }]), + ['with required', { props: { required: true } }], + ['with disabled', { props: { disabled: true } }], + ['with accept', { props: { accept: 'image/*' } }], + ['with multiple', { props: { ...props, multiple: true } }], + ['without dropzone', { props: { dropzone: false } }], + ['without interactive', { props: { interactive: false } }], + ['with required', { props: { required: true } }], + ['with disabled', { props: { disabled: true } }], + ['with fileIcon', { props: { ...props, fileIcon: 'i-lucide-house' } }], + ['with fileDelete', { props: { ...props, fileDelete: { color: 'primary' } } }], + ['with fileDeleteIcon', { props: { ...props, fileDeleteIcon: 'i-lucide-trash' } }], + ['with ariaLabel', { attrs: { 'aria-label': 'Aria label' } }], + ['with as', { props: { as: 'section' } }], + ['with class', { props: { class: 'w-full gap-4' } }], + ['with ui', { props: { ui: { base: 'rounded-xl' } } }], + // Slots + ['with default slot', { props, slots: { default: () => 'Default slot' } }], + ['with leading slot', { props, slots: { leading: () => 'Leading slot' } }], + ['with label slot', { props, slots: { label: () => 'Label slot' } }], + ['with description slot', { props, slots: { description: () => 'Description slot' } }], + ['with actions slot', { props, slots: { actions: () => 'Actions slot' } }], + ['with files slot', { props, slots: { files: () => 'Files slot' } }], + ['with files-top slot', { props, slots: { 'files-top': () => 'Files top slot' } }], + ['with files-bottom slot', { props, slots: { 'files-bottom': () => 'Files bottom slot' } }], + ['with file slot', { props, slots: { file: () => 'File slot' } }], + ['with file-leading slot', { props, slots: { 'file-leading': () => 'File leading slot' } }], + ['with file-name slot', { props, slots: { 'file-name': () => 'File name slot' } }], + ['with file-size slot', { props, slots: { 'file-size': () => 'File size slot' } }], + ['with file-trailing slot', { props, slots: { 'file-trailing': () => 'File trailing slot' } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: FileUploadProps, slots?: Partial }) => { + const html = await ComponentRender(nameOrHtml, options, FileUpload) + expect(html).toMatchSnapshot() + }) + + describe('emits', () => { + test('update:modelValue event', async () => { + const wrapper = mount(FileUpload) + const input = wrapper.find('input') + const file1 = new File(['foo'], 'file1.txt', { type: 'text/plain' }) + const file2 = new File(['bar'], 'file2.txt', { type: 'text/plain' }) + await setFilesOnInput(input, [file1, file2]) + expect(wrapper.emitted('update:modelValue')).toBeTruthy() + }) + + test('change event', async () => { + const wrapper = mount(FileUpload) + const input = wrapper.find('input') + const file1 = new File(['foo'], 'file1.txt', { type: 'text/plain' }) + const file2 = new File(['bar'], 'file2.txt', { type: 'text/plain' }) + await setFilesOnInput(input, [file1, file2]) + expect(wrapper.emitted('change')).toBeTruthy() + }) + }) + + describe.skip('form integration', async () => { + async function createForm(validateOn?: FormInputEvents[]) { + const wrapper = await renderForm({ + props: { + validateOn, + validateOnInputDelay: 0, + async validate(state: any) { + const files = Array.isArray(state.value) ? state.value : [] + if (!files.length || files.some((f: any) => f.file.name !== 'valid')) { + return [{ name: 'value', message: 'Error message' }] + } + return [] + } + }, + slotTemplate: ` + + + + ` + }) + const input = wrapper.find('#input') + return { + wrapper, + input + } + } + + test('validate on change works', async () => { + const { input, wrapper } = await createForm(['change']) + await setFilesOnInput(input, [new File(['foo'], 'invalid.txt', { type: 'text/plain' })]) + await input.trigger('change') + expect(wrapper.text()).toContain('Error message') + + await setFilesOnInput(input, [new File(['foo'], 'valid', { type: 'text/plain' })]) + await input.trigger('change') + expect(wrapper.text()).not.toContain('Error message') + }) + + test('validate on input works', async () => { + const { input, wrapper } = await createForm(['input']) + await setFilesOnInput(input, [new File(['foo'], 'invalid.txt', { type: 'text/plain' })]) + expect(wrapper.text()).toContain('Error message') + + await setFilesOnInput(input, [new File(['foo'], 'valid', { type: 'text/plain' })]) + expect(wrapper.text()).not.toContain('Error message') + }) + }) +}) diff --git a/test/components/Input.spec.ts b/test/components/Input.spec.ts index e7eb2590c9..0824767977 100644 --- a/test/components/Input.spec.ts +++ b/test/components/Input.spec.ts @@ -6,7 +6,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('Input', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/InputMenu.spec.ts b/test/components/InputMenu.spec.ts index 0fdf637da2..904bcc8d91 100644 --- a/test/components/InputMenu.spec.ts +++ b/test/components/InputMenu.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' import { flushPromises, mount } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' import { expectEmitPayloadType } from '../utils/types' describe('InputMenu', () => { diff --git a/test/components/InputNumber.spec.ts b/test/components/InputNumber.spec.ts index 3c6382a9c2..f1287910a1 100644 --- a/test/components/InputNumber.spec.ts +++ b/test/components/InputNumber.spec.ts @@ -6,7 +6,7 @@ import InputNumber from '../../src/runtime/components/InputNumber.vue' import type { InputNumberProps, InputNumberSlots } from '../../src/runtime/components/InputNumber.vue' import ComponentRender from '../component-render' import theme from '#build/ui/input-number' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' import { renderForm } from '../utils/form' describe('InputNumber', () => { diff --git a/test/components/PinInput.spec.ts b/test/components/PinInput.spec.ts index 6ff74edafc..93ea34f27c 100644 --- a/test/components/PinInput.spec.ts +++ b/test/components/PinInput.spec.ts @@ -6,7 +6,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/pin-input' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('PinInput', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/RadioGroup.spec.ts b/test/components/RadioGroup.spec.ts index 519d8e2f6c..18a9877836 100644 --- a/test/components/RadioGroup.spec.ts +++ b/test/components/RadioGroup.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/radio-group' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('RadioGroup', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/Select.spec.ts b/test/components/Select.spec.ts index 564c160e4c..50325fddf4 100644 --- a/test/components/Select.spec.ts +++ b/test/components/Select.spec.ts @@ -5,7 +5,7 @@ import type { SelectProps, SelectSlots } from '../../src/runtime/components/Sele import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' import { expectEmitPayloadType } from '../utils/types' describe('Select', () => { diff --git a/test/components/SelectMenu.spec.ts b/test/components/SelectMenu.spec.ts index 9ac2b34c37..5ab2601b2a 100644 --- a/test/components/SelectMenu.spec.ts +++ b/test/components/SelectMenu.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' import { flushPromises, mount } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' import { expectEmitPayloadType } from '../utils/types' describe('SelectMenu', () => { diff --git a/test/components/Slider.spec.ts b/test/components/Slider.spec.ts index 71cd2e1dc0..ffb288a748 100644 --- a/test/components/Slider.spec.ts +++ b/test/components/Slider.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/slider' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('Slider', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/Switch.spec.ts b/test/components/Switch.spec.ts index 2442bdc119..8d4e02d72b 100644 --- a/test/components/Switch.spec.ts +++ b/test/components/Switch.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/switch' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('Switch', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/Textarea.spec.ts b/test/components/Textarea.spec.ts index 445c78cfce..a57a7470e6 100644 --- a/test/components/Textarea.spec.ts +++ b/test/components/Textarea.spec.ts @@ -5,7 +5,7 @@ import type { TextareaProps, TextareaSlots } from '../../src/runtime/components/ import ComponentRender from '../component-render' import theme from '#build/ui/textarea' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormInputEvents } from '../../src/module' describe('Textarea', () => { const sizes = Object.keys(theme.variants.size) as any diff --git a/test/components/__snapshots__/FileUpload-vue.spec.ts.snap b/test/components/__snapshots__/FileUpload-vue.spec.ts.snap new file mode 100644 index 0000000000..e3f172e2fc --- /dev/null +++ b/test/components/__snapshots__/FileUpload-vue.spec.ts.snap @@ -0,0 +1,962 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FileUpload > renders with accept correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with actions slot correctly 1`] = ` +"
+
+ +
+ + +
Actions slot
+
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with ariaLabel correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with as correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with class correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with default slot correctly 1`] = `"
Default slot
"`; + +exports[`FileUpload > renders with description correctly 1`] = ` +"
+
+ +
+ +
SVG, PNG, JPG or GIF (max. 2MB)
+ +
+
+ +
" +`; + +exports[`FileUpload > renders with description slot correctly 1`] = ` +"
+
+ +
+ +
Description slot
+ +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with disabled correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with disabled correctly 2`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with file slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
File slot
+
+
" +`; + +exports[`FileUpload > renders with file-leading slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
File leading slot
+
+
" +`; + +exports[`FileUpload > renders with file-name slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with file-size slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with file-trailing slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ File trailing slot +
+
+
" +`; + +exports[`FileUpload > renders with fileDelete correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with fileDeleteIcon correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with fileIcon correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with files slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
Files slot
+
" +`; + +exports[`FileUpload > renders with files-bottom slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
Files bottom slot +
" +`; + +exports[`FileUpload > renders with files-top slot correctly 1`] = ` +"
+
+ +
+ + + +
+
Files top slot
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with icon correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with id correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with label correctly 1`] = ` +"
+
+ +
+
Drop your image here
+ + +
+
+ +
" +`; + +exports[`FileUpload > renders with label slot correctly 1`] = ` +"
+
+ +
+
Label slot
+ + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with layout grid correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with layout grid multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with layout list correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+
file.txt0B
+
+
+
" +`; + +exports[`FileUpload > renders with layout list multiple correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+
file.txt0B
+
+
+
" +`; + +exports[`FileUpload > renders with leading slot correctly 1`] = ` +"
+
+ +
Leading slot + + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with modelValue correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with name correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with neutral color correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with position inside correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with position inside multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with position outside correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with position outside multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with required correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with required correctly 2`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with size lg correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size lg variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size md correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size md variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size sm correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size sm variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xl correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xl variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xs correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xs variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with ui correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with variant area correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders without dropzone correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders without interactive correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; diff --git a/test/components/__snapshots__/FileUpload.spec.ts.snap b/test/components/__snapshots__/FileUpload.spec.ts.snap new file mode 100644 index 0000000000..d5a2ea87a7 --- /dev/null +++ b/test/components/__snapshots__/FileUpload.spec.ts.snap @@ -0,0 +1,962 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FileUpload > renders with accept correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with actions slot correctly 1`] = ` +"
+
+ +
+ + +
Actions slot
+
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with ariaLabel correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with as correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with class correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with default slot correctly 1`] = `"
Default slot
"`; + +exports[`FileUpload > renders with description correctly 1`] = ` +"
+
+ +
+ +
SVG, PNG, JPG or GIF (max. 2MB)
+ +
+
+ +
" +`; + +exports[`FileUpload > renders with description slot correctly 1`] = ` +"
+
+ +
+ +
Description slot
+ +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with disabled correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with disabled correctly 2`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with file slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
File slot
+
+
" +`; + +exports[`FileUpload > renders with file-leading slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
File leading slot
+
+
" +`; + +exports[`FileUpload > renders with file-name slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with file-size slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with file-trailing slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ File trailing slot +
+
+
" +`; + +exports[`FileUpload > renders with fileDelete correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with fileDeleteIcon correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with fileIcon correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with files slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
Files slot
+
" +`; + +exports[`FileUpload > renders with files-bottom slot correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
Files bottom slot +
" +`; + +exports[`FileUpload > renders with files-top slot correctly 1`] = ` +"
+
+ +
+ + + +
+
Files top slot
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with icon correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with id correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with label correctly 1`] = ` +"
+
+ +
+
Drop your image here
+ + +
+
+ +
" +`; + +exports[`FileUpload > renders with label slot correctly 1`] = ` +"
+
+ +
+
Label slot
+ + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with layout grid correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with layout grid multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with layout list correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+
file.txt0B
+
+
+
" +`; + +exports[`FileUpload > renders with layout list multiple correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+
file.txt0B
+
+
+
" +`; + +exports[`FileUpload > renders with leading slot correctly 1`] = ` +"
+
+ +
Leading slot + + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with modelValue correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with name correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with neutral color correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with position inside correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with position inside multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with position outside correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with position outside multiple correctly 1`] = ` +"
+
+
+
+ +
+
+ +
+ +
" +`; + +exports[`FileUpload > renders with required correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with required correctly 2`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with size lg correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size lg variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size md correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size md variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size sm correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size sm variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xl correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xl variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xs correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with size xs variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with ui correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders with variant area correctly 1`] = ` +"
+
+ +
+ + + +
+
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders with variant button correctly 1`] = ` +"
+
+
+ +
+
+
" +`; + +exports[`FileUpload > renders without dropzone correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`; + +exports[`FileUpload > renders without interactive correctly 1`] = ` +"
+
+ +
+ + + +
+
+ +
" +`;