Skip to content

Commit 30908b5

Browse files
authored
feat: upload thumbnails theme variant (#10607)
1 parent cdb86d7 commit 30908b5

10 files changed

+193
-5
lines changed

packages/upload/src/styles/vaadin-upload-file-base-styles.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,42 @@ export const uploadFileStyles = css`
2525
display: none;
2626
}
2727
28+
/* Hide thumbnail and file-icon by default, only show with thumbnails theme */
29+
[part='thumbnail'],
30+
[part='file-icon'] {
31+
display: none;
32+
}
33+
34+
:host([theme~='thumbnails']) {
35+
grid-template-columns: 3rem minmax(0, 1fr) auto;
36+
}
37+
38+
:host([theme~='thumbnails']) [part='thumbnail']:not([hidden]),
39+
:host([theme~='thumbnails']) [part='file-icon']:not([hidden]) {
40+
display: block;
41+
width: 3rem;
42+
height: 3rem;
43+
align-self: center;
44+
}
45+
46+
[part='thumbnail'] {
47+
object-fit: cover;
48+
}
49+
50+
[part='file-icon'] {
51+
background: #e0e0e0;
52+
}
53+
2854
[part='done-icon']:not([hidden]),
2955
[part='warning-icon']:not([hidden]) {
3056
display: flex;
3157
}
3258
59+
:host([theme~='thumbnails']) [part='done-icon'],
60+
:host([theme~='thumbnails']) [part='warning-icon'] {
61+
display: none !important;
62+
}
63+
3364
[part='done-icon']::before,
3465
[part='warning-icon']::before {
3566
content: '\\2003' / '';

packages/upload/src/styles/vaadin-upload-file-list-base-styles.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,19 @@ export const uploadFileListStyles = css`
3030
border-bottom: var(--vaadin-upload-file-list-divider-width, 1px) solid
3131
var(--vaadin-upload-file-list-divider-color, var(--vaadin-border-color-secondary));
3232
}
33+
34+
/* Thumbnails theme variant */
35+
:host([theme~='thumbnails']) [part='list'] {
36+
display: flex;
37+
flex-wrap: wrap;
38+
gap: var(--vaadin-gap-s);
39+
}
40+
41+
:host([theme~='thumbnails']) ::slotted(:first-child) {
42+
margin-top: 0;
43+
}
44+
45+
:host([theme~='thumbnails']) ::slotted(li) {
46+
border-bottom: none;
47+
}
3348
`;

packages/upload/src/vaadin-upload-file-list-mixin.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
66
import { html, render } from 'lit';
7+
import { ifDefined } from 'lit/directives/if-defined.js';
78
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
89
import { UploadManager } from './vaadin-upload-manager.js';
910

@@ -81,7 +82,7 @@ export const UploadFileListMixin = (superClass) =>
8182
}
8283

8384
static get observers() {
84-
return ['__updateItems(items, __effectiveI18n, disabled)'];
85+
return ['__updateItems(items, __effectiveI18n, disabled, _theme)'];
8586
}
8687

8788
constructor() {
@@ -191,7 +192,7 @@ export const UploadFileListMixin = (superClass) =>
191192
}
192193

193194
/** @private */
194-
__updateItems(items, i18n) {
195+
__updateItems(items, i18n, _disabled, _theme) {
195196
if (items && i18n) {
196197
// Apply i18n formatting to each file
197198
items.forEach((file) => this.__applyI18nToFile(file));
@@ -339,6 +340,7 @@ export const UploadFileListMixin = (superClass) =>
339340
.status="${file.status}"
340341
.uploading="${file.uploading}"
341342
.i18n="${i18n}"
343+
theme="${ifDefined(this._theme)}"
342344
></vaadin-upload-file>
343345
</li>
344346
`,

packages/upload/src/vaadin-upload-file-mixin.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,20 @@ export const UploadFileMixin = (superClass) =>
115115
_progress: {
116116
type: Object,
117117
},
118+
119+
/** @private */
120+
__thumbnail: {
121+
type: String,
122+
},
118123
};
119124
}
120125

121126
static get observers() {
122-
return ['__updateTabindex(tabindex, disabled)', '__updateProgress(_progress, progress, indeterminate)'];
127+
return [
128+
'__updateTabindex(tabindex, disabled)',
129+
'__updateProgress(_progress, progress, indeterminate)',
130+
'__updateThumbnail(file)',
131+
];
123132
}
124133

125134
/** @protected */
@@ -204,4 +213,31 @@ export const UploadFileMixin = (superClass) =>
204213
}),
205214
);
206215
}
216+
217+
/** @private */
218+
__updateThumbnail(file) {
219+
// Abort any pending read
220+
if (this.__thumbnailReader) {
221+
this.__thumbnailReader.abort();
222+
this.__thumbnailReader = null;
223+
}
224+
225+
if (!file) {
226+
this.__thumbnail = '';
227+
return;
228+
}
229+
230+
// Check if file is an image and is an actual File/Blob object
231+
if (file.type && file.type.startsWith('image/') && file instanceof Blob) {
232+
const reader = new FileReader();
233+
this.__thumbnailReader = reader;
234+
reader.onload = (e) => {
235+
this.__thumbnail = e.target.result;
236+
this.__thumbnailReader = null;
237+
};
238+
reader.readAsDataURL(file);
239+
} else {
240+
this.__thumbnail = '';
241+
}
242+
}
207243
};

packages/upload/src/vaadin-upload-file.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface UploadFileEventMap extends HTMLElementEventMap, UploadFileCusto
4242
*
4343
* Part name | Description
4444
* -----------------|-------------
45+
* `thumbnail` | Image thumbnail for image files (used with `thumbnails` theme)
46+
* `file-icon` | Generic file icon for non-image files (used with `thumbnails` theme)
4547
* `done-icon` | File done status icon
4648
* `warning-icon` | File warning status icon
4749
* `meta` | Container for file name, status and error messages

packages/upload/src/vaadin-upload-file.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { UploadFileMixin } from './vaadin-upload-file-mixin.js';
2222
*
2323
* Part name | Description
2424
* -----------------|-------------
25+
* `thumbnail` | Image thumbnail for image files (used with `thumbnails` theme)
26+
* `file-icon` | Generic file icon for non-image files (used with `thumbnails` theme)
2527
* `done-icon` | File done status icon
2628
* `warning-icon` | File warning status icon
2729
* `meta` | Container for file name, status and error messages
@@ -99,6 +101,9 @@ class UploadFile extends UploadFileMixin(ThemableMixin(PolylitMixin(LumoInjectio
99101
const isFileRetryVisible = this.errorMessage;
100102

101103
return html`
104+
<img src="${this.__thumbnail}" alt="${this.fileName}" part="thumbnail" ?hidden="${!this.__thumbnail}" />
105+
<div part="file-icon" ?hidden="${!!this.__thumbnail}" aria-hidden="true"></div>
106+
102107
<div part="done-icon" ?hidden="${!this.complete}" aria-hidden="true"></div>
103108
<div part="warning-icon" ?hidden="${!this.errorMessage}" aria-hidden="true"></div>
104109

packages/upload/src/vaadin-upload-mixin.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export const UploadMixin = (superClass) =>
381381
return [
382382
'__updateAddButton(_addButton, maxFiles, __effectiveI18n, maxFilesReached, disabled)',
383383
'__updateDropLabel(_dropLabel, maxFiles, __effectiveI18n)',
384-
'__updateFileList(_fileList, files, __effectiveI18n, disabled)',
384+
'__updateFileList(_fileList, files, __effectiveI18n, disabled, _theme)',
385385
'__updateMaxFilesReached(maxFiles, files)',
386386
];
387387
}
@@ -590,6 +590,11 @@ export const UploadMixin = (superClass) =>
590590
list.items = [...files];
591591
list.i18n = effectiveI18n;
592592
list.disabled = disabled;
593+
if (this._theme) {
594+
list.setAttribute('theme', this._theme);
595+
} else {
596+
list.removeAttribute('theme');
597+
}
593598
}
594599
}
595600

packages/upload/test/dom/__snapshots__/vaadin-upload-file.test.snap.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22
export const snapshots = {};
33

44
snapshots["vaadin-upload-file shadow default"] =
5-
`<div
5+
`<img
6+
alt="Workflow.pdf"
7+
hidden=""
8+
part="thumbnail"
9+
src=""
10+
>
11+
<div
12+
aria-hidden="true"
13+
part="file-icon"
14+
>
15+
</div>
16+
<div
617
aria-hidden="true"
718
hidden=""
819
part="done-icon"

packages/upload/test/upload-file-list.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,4 +523,57 @@ describe('vaadin-upload-file-list', () => {
523523
expect(getUploadFiles().length).to.equal(3);
524524
});
525525
});
526+
527+
describe('theme propagation', () => {
528+
it('should propagate theme to upload-file elements', async () => {
529+
fileList.setAttribute('theme', 'thumbnails');
530+
manager.addFiles([createFile(100, 'text/plain')]);
531+
fileList.manager = manager;
532+
await nextFrame();
533+
534+
const uploadFile = getUploadFile();
535+
expect(uploadFile.getAttribute('theme')).to.equal('thumbnails');
536+
});
537+
538+
it('should propagate theme to multiple upload-file elements', async () => {
539+
fileList.setAttribute('theme', 'thumbnails');
540+
manager.addFiles(createFiles(3, 100, 'text/plain'));
541+
fileList.manager = manager;
542+
await nextFrame();
543+
544+
const uploadFiles = getUploadFiles();
545+
uploadFiles.forEach((file) => {
546+
expect(file.getAttribute('theme')).to.equal('thumbnails');
547+
});
548+
});
549+
550+
it('should update upload-file elements when theme changes', async () => {
551+
manager.addFiles([createFile(100, 'text/plain')]);
552+
fileList.manager = manager;
553+
await nextFrame();
554+
555+
const uploadFile = getUploadFile();
556+
expect(uploadFile.getAttribute('theme')).to.be.null;
557+
558+
fileList.setAttribute('theme', 'thumbnails');
559+
await nextFrame();
560+
561+
expect(uploadFile.getAttribute('theme')).to.equal('thumbnails');
562+
});
563+
564+
it('should remove theme from upload-file elements when theme is removed', async () => {
565+
fileList.setAttribute('theme', 'thumbnails');
566+
manager.addFiles([createFile(100, 'text/plain')]);
567+
fileList.manager = manager;
568+
await nextFrame();
569+
570+
const uploadFile = getUploadFile();
571+
expect(uploadFile.getAttribute('theme')).to.equal('thumbnails');
572+
573+
fileList.removeAttribute('theme');
574+
await nextFrame();
575+
576+
expect(uploadFile.getAttribute('theme')).to.be.null;
577+
});
578+
});
526579
});

packages/upload/test/upload.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,32 @@ describe('upload', () => {
690690
upload._queueFileUpload(file);
691691
});
692692
});
693+
694+
describe('theme', () => {
695+
it('should propagate theme to file list', async () => {
696+
upload.setAttribute('theme', 'thumbnails');
697+
await nextUpdate(upload);
698+
expect(upload._fileList.getAttribute('theme')).to.equal('thumbnails');
699+
});
700+
701+
it('should update file list theme when theme changes', async () => {
702+
upload.setAttribute('theme', 'thumbnails');
703+
await nextUpdate(upload);
704+
expect(upload._fileList.getAttribute('theme')).to.equal('thumbnails');
705+
706+
upload.setAttribute('theme', 'other');
707+
await nextUpdate(upload);
708+
expect(upload._fileList.getAttribute('theme')).to.equal('other');
709+
});
710+
711+
it('should remove file list theme when theme is removed', async () => {
712+
upload.setAttribute('theme', 'thumbnails');
713+
await nextUpdate(upload);
714+
expect(upload._fileList.getAttribute('theme')).to.equal('thumbnails');
715+
716+
upload.removeAttribute('theme');
717+
await nextUpdate(upload);
718+
expect(upload._fileList.getAttribute('theme')).to.be.null;
719+
});
720+
});
693721
});

0 commit comments

Comments
 (0)