Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<modus-wc-file-dropzone max-file-count="1"></modus-wc-file-dropzone>',
});

const component = page.rootInstance;
const fileSelectSpy = jest.spyOn(component.fileSelect, 'emit');
component.errorMessages = {
invalidCount: 'Custom file count error',
};

Comment thread
prashanthr6383 marked this conversation as resolved.
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],
Expand Down Expand Up @@ -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: '<modus-wc-file-dropzone invalid-file-type-message="Legacy type message"></modus-wc-file-dropzone>',
});

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: '<modus-wc-file-dropzone invalid-file-type-message="Legacy type message"></modus-wc-file-dropzone>',
});

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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -54,6 +56,10 @@ const meta: Meta<FileDropzoneArgs> = {
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:
Expand Down Expand Up @@ -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'])}
></modus-wc-file-dropzone>
`,
Expand Down Expand Up @@ -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: `<modus-wc-file-dropzone
id="file-validations-dropzone"
accept-file-types=".doc, .docx, .pdf"
max-file-name-length="20"
max-file-count="3"
max-total-file-size-bytes="10485760"
multiple
instructions="Upload files (max 3 files, 10MB total, filename ≀ 20 chars)"
></modus-wc-file-dropzone>

<script>
const dropzone = document.getElementById('file-validations-dropzone');

dropzone.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.',
};
</script>`,
},
},
},
render: (args) => html`
<modus-wc-file-dropzone
accept-file-types=${ifDefined(args['accept-file-types'])}
.errorMessages=${args.errorMessages}
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'])}
Expand Down Expand Up @@ -241,6 +279,7 @@ export const ShadowDomParent: Story = {
includeStateIcon: boolean;
instructions: string;
invalidFileTypeMessage: string;
errorMessages: IFileDropzoneErrorMessages;
maxFileCount: number;
maxFileNameLength: number;
maxTotalFileSizeBytes: number;
Expand All @@ -256,6 +295,7 @@ export const ShadowDomParent: Story = {
dropzoneEl.instructions = v.instructions ?? '';
dropzoneEl.invalidFileTypeMessage =
v['invalid-file-type-message'] ?? '';
dropzoneEl.errorMessages = v['error-messages'] ?? {};
dropzoneEl.maxFileCount = v['max-file-count'] ?? 0;
dropzoneEl.maxFileNameLength = v['max-file-name-length'] ?? 0;
dropzoneEl.maxTotalFileSizeBytes =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import { convertPropsToClasses } from './modus-wc-file-dropzone.tailwind';
import { handleShadowDOMStyles } from '../base-component';
import { Attributes, inheritAriaAttributes } from '../utils';

/** Custom messages shown when file validation fails. Messages are rendered as provided; include any limit values directly in the string. */
export interface IFileDropzoneErrorMessages {
/** Message shown when selected or dropped file count exceeds maxFileCount. */
invalidCount?: string;
/** Message shown when a filename exceeds maxFileNameLength. */
invalidName?: string;
/** Message shown when total selected or dropped file size exceeds maxTotalFileSizeBytes. */
invalidSize?: string;
Comment thread
prashanthr6383 marked this conversation as resolved.
/** Message shown when a file does not match acceptFileTypes. */
invalidType?: string;
}

/**
* File dropzone component that allows users to drag and drop files for upload.
*
Expand Down Expand Up @@ -64,6 +76,9 @@ export class ModusWcFileDropzone {
/** Custom error message displayed when an invalid file type is selected */
@Prop() invalidFileTypeMessage?: string;

/** Custom error messages displayed when file validation fails */
@Prop() errorMessages?: IFileDropzoneErrorMessages;

/** Maximum allowed length of filename, will show error if exceeded */
@Prop() maxFileNameLength?: number;

Expand Down Expand Up @@ -110,13 +125,25 @@ export class ModusWcFileDropzone {
): string {
switch (errorType) {
case 'type':
return this.invalidFileTypeMessage || 'File format not accepted';
return (
this.errorMessages?.invalidType ||
this.invalidFileTypeMessage ||
'File format not accepted'
);
case 'name':
return 'Filename exceeds maximum length';
return (
this.errorMessages?.invalidName || 'Filename exceeds maximum length'
);
case 'count':
return `Maximum number of files allowed is ${this.maxFileCount}`;
return (
this.errorMessages?.invalidCount ||
`Maximum number of files allowed is ${this.maxFileCount}`
);
case 'size':
return `Total file size exceeds ${this.formatFileSize(this.maxTotalFileSizeBytes)}`;
return (
this.errorMessages?.invalidSize ||
`Total file size exceeds ${this.formatFileSize(this.maxTotalFileSizeBytes)}`
);
default:
return 'Validation error';
}
Expand Down
29 changes: 15 additions & 14 deletions src/components/modus-wc-file-dropzone/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@ The component supports a `<slot>` 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
Expand Down
8 changes: 8 additions & 0 deletions src/custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading