diff --git a/src/components.d.ts b/src/components.d.ts index 564ab0aa2..8dbfc41ab 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -8,6 +8,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AutocompleteTypes, DaisySize, Density, IAutocompleteItem, IAutocompleteNoResults, IInputFeedbackProp, ModusSize, Orientation, PopoverPlacement, SelectionMode, TextFieldTypes, TypographyHierarchy, TypographySize, TypographyWeight, WeekStartDay } from "./components/types"; import { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcrumbs"; import { ICollapseOptions } from "./components/modus-wc-collapse/modus-wc-collapse"; +import { IFileDropzoneErrorMessages } from "./components/modus-wc-file-dropzone/modus-wc-file-dropzone"; import { IInputFeedbackLevel } from "./components/modus-wc-input-feedback/modus-wc-input-feedback"; import { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-wc-loader"; import { LogoName } from "./components/modus-wc-logo/logo-constants"; @@ -25,6 +26,7 @@ import { ToastPosition } from "./components/modus-wc-toast/modus-wc-toast"; export { AutocompleteTypes, DaisySize, Density, IAutocompleteItem, IAutocompleteNoResults, IInputFeedbackProp, ModusSize, Orientation, PopoverPlacement, SelectionMode, TextFieldTypes, TypographyHierarchy, TypographySize, TypographyWeight, WeekStartDay } from "./components/types"; export { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcrumbs"; export { ICollapseOptions } from "./components/modus-wc-collapse/modus-wc-collapse"; +export { IFileDropzoneErrorMessages } from "./components/modus-wc-file-dropzone/modus-wc-file-dropzone"; export { IInputFeedbackLevel } from "./components/modus-wc-input-feedback/modus-wc-input-feedback"; export { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-wc-loader"; export { LogoName } from "./components/modus-wc-logo/logo-constants"; @@ -711,6 +713,10 @@ export namespace Components { * Disable the file input */ "disabled"?: boolean; + /** + * Custom error messages displayed when file validation fails + */ + "errorMessages"?: IFileDropzoneErrorMessages; /** * Custom instructions shown when files are dragged over the dropzone */ @@ -3934,6 +3940,10 @@ declare namespace LocalJSX { * Disable the file input */ "disabled"?: boolean; + /** + * Custom error messages displayed when file validation fails + */ + "errorMessages"?: IFileDropzoneErrorMessages; /** * Custom instructions shown when files are dragged over the dropzone */ diff --git a/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.spec.ts b/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.spec.ts index c64caee37..f2bc02310 100644 --- a/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.spec.ts +++ b/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.spec.ts @@ -408,6 +408,45 @@ describe('modus-wc-file-dropzone', () => { expect(component.uploadSuccess).toBe(true); }); + it('should use custom error message when file count validation fails', async () => { + const page = await newSpecPage({ + components: [ModusWcFileDropzone], + html: '', + }); + + const component = page.rootInstance; + const fileSelectSpy = jest.spyOn(component.fileSelect, 'emit'); + component.errorMessages = { + invalidCount: 'Custom file count error', + }; + + const file1 = new File(['test content 1'], 'file1.txt', { + type: 'text/plain', + }); + const file2 = new File(['test content 2'], 'file2.txt', { + type: 'text/plain', + }); + const tooManyFiles = { + 0: file1, + 1: file2, + length: 2, + item: (idx: number) => [file1, file2][idx], + } as unknown as FileList; + + const invalidEvent = { + target: { + files: tooManyFiles, + value: '', + }, + } as unknown as Event; + + component.handleFileChange(invalidEvent); + + expect(fileSelectSpy).not.toHaveBeenCalled(); + expect(component.invalidFile).toBe('count'); + expect(component.errorMessage).toBe('Custom file count error'); + }); + it('should validate total file size', async () => { const page = await newSpecPage({ components: [ModusWcFileDropzone], @@ -1337,6 +1376,54 @@ describe('modus-wc-file-dropzone', () => { expect(errorMessage).toBe('Validation error'); }); + it('should return custom error messages from errorMessages', async () => { + const page = await newSpecPage({ + components: [ModusWcFileDropzone], + html: '', + }); + + const component = page.rootInstance; + component.errorMessages = { + invalidCount: 'Custom count message', + invalidName: 'Custom name message', + invalidSize: 'Custom size message', + invalidType: 'Custom type message', + }; + + type PrivateMethods = { + getErrorMessage: (type: string) => string; + }; + + const getErrorMessage = (component as unknown as PrivateMethods)[ + 'getErrorMessage' + ].bind(component); + + expect(getErrorMessage('count')).toBe('Custom count message'); + expect(getErrorMessage('name')).toBe('Custom name message'); + expect(getErrorMessage('size')).toBe('Custom size message'); + expect(getErrorMessage('type')).toBe('Custom type message'); + }); + + it('should fallback to invalidFileTypeMessage when errorMessages type is not provided', async () => { + const page = await newSpecPage({ + components: [ModusWcFileDropzone], + html: '', + }); + + const component = page.rootInstance; + component.errorMessages = {}; + + type PrivateMethods = { + getErrorMessage: (type: string) => string; + }; + + const errorMessage = (component as unknown as PrivateMethods)[ + 'getErrorMessage' + ]('type'); + + expect(errorMessage).toBe('Legacy type message'); + }); + it('should return "0 Bytes" for zero or undefined bytes in formatFileSize', async () => { const page = await newSpecPage({ components: [ModusWcFileDropzone], diff --git a/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.stories.ts b/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.stories.ts index e6eee782a..64e1537cb 100644 --- a/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.stories.ts +++ b/src/components/modus-wc-file-dropzone/modus-wc-file-dropzone.stories.ts @@ -2,6 +2,7 @@ import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; +import type { IFileDropzoneErrorMessages } from './modus-wc-file-dropzone'; import { createShadowHostClass } from '../../providers/shadow-dom/shadow-host-helper'; interface FileDropzoneArgs { @@ -12,6 +13,7 @@ interface FileDropzoneArgs { 'include-state-icon'?: boolean; instructions?: string; 'invalid-file-type-message'?: string; + 'error-messages'?: IFileDropzoneErrorMessages; 'max-file-name-length'?: number; 'max-file-count'?: number; 'max-total-file-size-bytes'?: number; @@ -54,6 +56,10 @@ const meta: Meta = { description: 'Custom error message displayed when an invalid file type is selected', }, + 'error-messages': { + control: 'object', + description: 'Custom error messages displayed when file validation fails', + }, 'max-file-name-length': { control: 'number', description: @@ -107,11 +113,12 @@ export const Default: Story = { )} ?include-state-icon=${args['include-state-icon']} instructions=${ifDefined(args['instructions'])} + .errorMessages=${args['error-messages']} invalid-file-type-message=${ifDefined(args['invalid-file-type-message'])} max-file-name-length=${ifDefined(args['max-file-name-length'])} max-file-count=${ifDefined(args['max-file-count'])} max-total-file-size-bytes=${ifDefined(args['max-total-file-size-bytes'])} - ?multiple=${args.multiple} + ?multiple=${args['multiple']} success-message=${ifDefined(args['success-message'])} > `, @@ -197,12 +204,43 @@ export const fileValidations: Story = { 'max-file-name-length': 20, 'max-file-count': 3, 'max-total-file-size-bytes': 10485760, // 10MB - 'invalid-file-type-message': - 'Invalid file format. Please upload correct file type.', + errorMessages: { + invalidCount: 'You can add up to 3 files.', + invalidName: 'Filename must be 20 characters or fewer.', + invalidSize: 'Total file size must be 10MB or less.', + invalidType: 'Invalid file format. Please upload correct file type.', + }, + }, + parameters: { + docs: { + source: { + code: ` + +`, + }, + }, }, render: (args) => html` ` called 'dropzone' for adding custom content su ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------------------- | -------------------------------- | --------------------------------------------------------------------- | ---------------------- | ----------- | -| `acceptFileTypes` | `accept-file-types` | Accepted file types (e.g. '.jpg,.png' or 'image/*') | `string \| undefined` | `undefined` | -| `customClass` | `custom-class` | Custom CSS class to apply to the file dropzone element | `string \| undefined` | `''` | -| `disabled` | `disabled` | Disable the file input | `boolean \| undefined` | `undefined` | -| `fileDraggedOverInstructions` | `file-dragged-over-instructions` | Custom instructions shown when files are dragged over the dropzone | `string \| undefined` | `undefined` | -| `includeStateIcon` | `include-state-icon` | Include state icon (upload, success, error) | `boolean \| undefined` | `true` | -| `instructions` | `instructions` | Custom instructions shown as the default dropzone message | `string \| undefined` | `undefined` | -| `invalidFileTypeMessage` | `invalid-file-type-message` | Custom error message displayed when an invalid file type is selected | `string \| undefined` | `undefined` | -| `maxFileCount` | `max-file-count` | Maximum number of files allowed, will show error if exceeded | `number \| undefined` | `undefined` | -| `maxFileNameLength` | `max-file-name-length` | Maximum allowed length of filename, will show error if exceeded | `number \| undefined` | `undefined` | -| `maxTotalFileSizeBytes` | `max-total-file-size-bytes` | Maximum total file size in bytes allowed, will show error if exceeded | `number \| undefined` | `undefined` | -| `multiple` | `multiple` | Allow multiple file selection | `boolean \| undefined` | `undefined` | -| `successMessage` | `success-message` | Success message displayed when files are uploaded successfully | `string \| undefined` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ----------------------------- | -------------------------------- | --------------------------------------------------------------------- | ----------------------------------------- | ----------- | +| `acceptFileTypes` | `accept-file-types` | Accepted file types (e.g. '.jpg,.png' or 'image/*') | `string \| undefined` | `undefined` | +| `customClass` | `custom-class` | Custom CSS class to apply to the file dropzone element | `string \| undefined` | `''` | +| `disabled` | `disabled` | Disable the file input | `boolean \| undefined` | `undefined` | +| `errorMessages` | `error-messages` | Custom error messages displayed when file validation fails | `IFileDropzoneErrorMessages \| undefined` | `undefined` | +| `fileDraggedOverInstructions` | `file-dragged-over-instructions` | Custom instructions shown when files are dragged over the dropzone | `string \| undefined` | `undefined` | +| `includeStateIcon` | `include-state-icon` | Include state icon (upload, success, error) | `boolean \| undefined` | `true` | +| `instructions` | `instructions` | Custom instructions shown as the default dropzone message | `string \| undefined` | `undefined` | +| `invalidFileTypeMessage` | `invalid-file-type-message` | Custom error message displayed when an invalid file type is selected | `string \| undefined` | `undefined` | +| `maxFileCount` | `max-file-count` | Maximum number of files allowed, will show error if exceeded | `number \| undefined` | `undefined` | +| `maxFileNameLength` | `max-file-name-length` | Maximum allowed length of filename, will show error if exceeded | `number \| undefined` | `undefined` | +| `maxTotalFileSizeBytes` | `max-total-file-size-bytes` | Maximum total file size in bytes allowed, will show error if exceeded | `number \| undefined` | `undefined` | +| `multiple` | `multiple` | Allow multiple file selection | `boolean \| undefined` | `undefined` | +| `successMessage` | `success-message` | Success message displayed when files are uploaded successfully | `string \| undefined` | `undefined` | ## Events diff --git a/src/custom-elements.json b/src/custom-elements.json index b2eae9f2a..ad2993ab9 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -3019,6 +3019,14 @@ "text": "boolean" } }, + { + "name": "error-messages", + "fieldName": "errorMessages", + "description": "Custom error messages displayed when file validation fails", + "type": { + "text": "IFileDropzoneErrorMessages" + } + }, { "name": "file-dragged-over-instructions", "fieldName": "fileDraggedOverInstructions",