diff --git a/dev/upload.html b/dev/upload.html index 60ceb299df7..cf47355160a 100644 --- a/dev/upload.html +++ b/dev/upload.html @@ -49,6 +49,11 @@ }); upload.files = files; + + const selectionMode = document.querySelector('#selection-mode'); + selectionMode.addEventListener('value-changed', (e) => { + upload.directory = e.detail.value === 'directory'; + }); @@ -62,5 +67,12 @@ + +
+ + + + + diff --git a/packages/upload/src/vaadin-lit-upload.js b/packages/upload/src/vaadin-lit-upload.js index 12238c72516..af573a682cb 100644 --- a/packages/upload/src/vaadin-lit-upload.js +++ b/packages/upload/src/vaadin-lit-upload.js @@ -67,6 +67,7 @@ class Upload extends UploadMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElem accept="${this.accept}" ?multiple="${this._isMultiple(this.maxFiles)}" capture="${ifDefined(this.capture)}" + ?webkitdirectory="${this.directory}" /> `; } diff --git a/packages/upload/src/vaadin-upload-mixin.d.ts b/packages/upload/src/vaadin-upload-mixin.d.ts index 4e19437c466..f02aada1818 100644 --- a/packages/upload/src/vaadin-upload-mixin.d.ts +++ b/packages/upload/src/vaadin-upload-mixin.d.ts @@ -33,6 +33,9 @@ export interface UploadI18n { one: string; many: string; }; + addDirectories: { + one: string; + }; error: { tooManyFiles: string; fileIsTooBig: string; @@ -189,6 +192,20 @@ export declare class UploadMixinClass { */ capture: string | null | undefined; + /** + * In directory mode, the user can select a directory instead of files. + * When selecting a directory, all files in the directory will be added + * to the upload list. Files are still filtered by the `accept` filter, + * and any non-matching files will be rejected. + * + * Note that this only allows selecting a single directory, and that + * selecting files is not supported in this mode. Browsers may request + * a confirmation from the user before allowing to upload a directory. + * In this mode it is still possible to add a combination of files and + * directories using drag and drop. + */ + directory: boolean; + /** * The object used to localize this component. * For changing the default localization, change the entire @@ -206,6 +223,9 @@ export declare class UploadMixinClass { * one: 'Upload File...', * many: 'Upload Files...' * }, + * addDirectories: { + * one: 'Upload Directory...', + * }, * error: { * tooManyFiles: 'Too Many Files.', * fileIsTooBig: 'File is Too Big.', diff --git a/packages/upload/src/vaadin-upload-mixin.js b/packages/upload/src/vaadin-upload-mixin.js index 8bd5b11d1f4..41248889d80 100644 --- a/packages/upload/src/vaadin-upload-mixin.js +++ b/packages/upload/src/vaadin-upload-mixin.js @@ -272,6 +272,23 @@ export const UploadMixin = (superClass) => */ capture: String, + /** + * In directory mode, the user can select a directory instead of files. + * When selecting a directory, all files in the directory will be added + * to the upload list. Files are still filtered by the `accept` filter, + * and any non-matching files will be rejected. + * + * Note that this only allows selecting a single directory, and that + * selecting files is not supported in this mode. Browsers may request + * a confirmation from the user before allowing to upload a directory. + * In this mode it is still possible to add a combination of files and + * directories using drag and drop. + */ + directory: { + type: Boolean, + value: false, + }, + /** * The object used to localize this component. * For changing the default localization, change the entire @@ -289,6 +306,9 @@ export const UploadMixin = (superClass) => * one: 'Upload File...', * many: 'Upload Files...' * }, + * addDirectories: { + * one: 'Upload Directory...', + * }, * error: { * tooManyFiles: 'Too Many Files.', * fileIsTooBig: 'File is Too Big.', @@ -344,6 +364,9 @@ export const UploadMixin = (superClass) => one: 'Upload File...', many: 'Upload Files...', }, + addDirectories: { + one: 'Upload Directory...', + }, error: { tooManyFiles: 'Too Many Files.', fileIsTooBig: 'File is Too Big.', @@ -402,7 +425,7 @@ export const UploadMixin = (superClass) => static get observers() { return [ - '__updateAddButton(_addButton, maxFiles, i18n, maxFilesReached, disabled)', + '__updateAddButton(_addButton, maxFiles, i18n, maxFilesReached, disabled, directory)', '__updateDropLabel(_dropLabel, maxFiles, i18n)', '__updateFileList(_fileList, files, i18n, disabled)', '__updateMaxFilesReached(maxFiles, files)', @@ -528,7 +551,11 @@ export const UploadMixin = (superClass) => // Only update text content for the default button element if (addButton === this._addButtonController.defaultNode) { - addButton.textContent = this._i18nPlural(maxFiles, i18n.addFiles); + if (this.directory) { + addButton.textContent = i18n.addDirectories.one; + } else { + addButton.textContent = this._i18nPlural(maxFiles, i18n.addFiles); + } } } } diff --git a/packages/upload/src/vaadin-upload.js b/packages/upload/src/vaadin-upload.js index 9eb8db0ed2c..40fd6e27695 100644 --- a/packages/upload/src/vaadin-upload.js +++ b/packages/upload/src/vaadin-upload.js @@ -100,6 +100,7 @@ class Upload extends UploadMixin(ElementMixin(ThemableMixin(ControllerMixin(Poly accept$="{{accept}}" multiple$="[[_isMultiple(maxFiles)]]" capture$="[[capture]]" + webkitdirectory$="[[directory]]" /> `; } diff --git a/packages/upload/test/upload.common.js b/packages/upload/test/upload.common.js index 835e1557f4f..9b6e4baf6d3 100644 --- a/packages/upload/test/upload.common.js +++ b/packages/upload/test/upload.common.js @@ -160,6 +160,7 @@ describe('upload', () => { function MockFormData() { this.data = []; } + MockFormData.prototype.append = function (name, value, filename) { this.data.push({ name, value, filename }); }; @@ -539,4 +540,28 @@ describe('upload', () => { expect(upload.files.length).to.equal(0); }); }); + + describe('Directory mode', () => { + it('should not set webkitdirectory attribute on the file input by default', () => { + expect(upload.$.fileInput.hasAttribute('webkitdirectory')).to.be.false; + }); + + it('should set webkitdirectory attribute on the file input when directory mode is enabled', async () => { + upload.directory = true; + await nextRender(upload); + + expect(upload.$.fileInput.hasAttribute('webkitdirectory')).to.be.true; + }); + + it('should use add files translation when not using directory mode', () => { + expect(upload.querySelector('[slot="add-button"]').textContent).to.be.equal(upload.i18n.addFiles.many); + }); + + it('should use add directories translation when using directory mode', async () => { + upload.directory = true; + await nextRender(upload); + + expect(upload.querySelector('[slot="add-button"]').textContent).to.be.equal(upload.i18n.addDirectories.one); + }); + }); });