Skip to content

Commit 2dbd6d9

Browse files
authored
feat: add upload-button (#10938)
1 parent 144b0ad commit 2dbd6d9

File tree

102 files changed

+1439
-57
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+1439
-57
lines changed

dev/upload-manager.html

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,6 @@
88
<script type="module" src="./common.js"></script>
99

1010
<style>
11-
/* Temporary Upload Button */
12-
.temp-upload-button {
13-
display: inline-flex;
14-
align-items: center;
15-
gap: 8px;
16-
padding: 8px 16px;
17-
background: #1976d2;
18-
color: white;
19-
border: none;
20-
border-radius: 4px;
21-
font-size: 14px;
22-
cursor: pointer;
23-
transition: background 0.2s;
24-
}
25-
26-
.temp-upload-button:hover {
27-
background: #1565c0;
28-
}
29-
30-
.temp-upload-button:disabled {
31-
background: #ccc;
32-
cursor: not-allowed;
33-
}
34-
35-
.temp-upload-button input[type='file'] {
36-
display: none;
37-
}
38-
3911
/* Temporary Drop Zone */
4012
.temp-drop-zone {
4113
border: 2px dashed #ccc;
@@ -175,10 +147,7 @@ <h1>Upload Manager Demo</h1>
175147
<p>This page demonstrates the headless <code>UploadManager</code> class.</p>
176148

177149
<div class="demo-controls">
178-
<label class="temp-upload-button" id="upload-button">
179-
Upload Files...
180-
<input type="file" multiple />
181-
</label>
150+
<vaadin-upload-button id="upload-button">Upload Files...</vaadin-upload-button>
182151
</div>
183152

184153
<div class="temp-drop-zone" id="dropzone">
@@ -188,6 +157,7 @@ <h1>Upload Manager Demo</h1>
188157
<ul class="temp-file-list" id="file-list"></ul>
189158

190159
<script type="module">
160+
import '@vaadin/upload/vaadin-upload-button.js';
191161
import { xhrCreator } from '@vaadin/upload/test/helpers.js';
192162
import { UploadManager } from '@vaadin/upload/vaadin-upload-manager.js';
193163

@@ -213,13 +183,9 @@ <h1>Upload Manager Demo</h1>
213183
// Use mock XHR
214184
manager._createXhr = xhrCreator({ size: 512, uploadTime: 3000, stepTime: 500 });
215185

216-
// Setup file input
186+
// Link upload button to manager
217187
const button = document.querySelector('#upload-button');
218-
const input = button.querySelector('input');
219-
input.addEventListener('change', () => {
220-
manager.addFiles(input.files);
221-
input.value = '';
222-
});
188+
button.manager = manager;
223189

224190
// Setup drop zone
225191
const dropzone = document.querySelector('#dropzone');

packages/aura/src/color.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ vaadin-app-layout > :not([slot]),
5757
vaadin-notification-container,
5858
vaadin-avatar,
5959
vaadin-button,
60+
vaadin-upload-button,
6061
vaadin-context-menu-item,
6162
vaadin-crud-edit,
6263
vaadin-drawer-toggle,
@@ -222,7 +223,7 @@ vaadin-tab,
222223
}
223224

224225
/* Restore accent color for child elements like badges */
225-
:where(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle) > *,
226+
:where(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle) > *,
226227
:where(vaadin-tab) > *,
227228
:where(vaadin-side-nav-item) > * {
228229
--aura-accent-color-light: var(--aura-accent-color-light-initial);
@@ -231,7 +232,7 @@ vaadin-tab,
231232
}
232233

233234
/* Badges and other content that uses contrast color inside filled variants */
234-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle)[theme~='primary'] > *,
235+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle)[theme~='primary'] > *,
235236
vaadin-tabs[theme~='filled'] > vaadin-tab[selected] > :not([slot='tooltip']),
236237
vaadin-side-nav[theme~='filled'] > vaadin-side-nav-item[current] > :not([slot='children']):not([slot='tooltip']),
237238
:is(

packages/aura/src/components/button.css

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
--vaadin-button-shadow: var(--aura-shadow-xs);
44
}
55

6-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit) {
6+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit) {
77
transition: scale 180ms;
88
position: relative;
99
--_background: var(--aura-accent-surface) padding-box;
@@ -14,19 +14,23 @@ vaadin-drawer-toggle[theme~='tertiary']::part(icon) {
1414
opacity: 0.6;
1515
}
1616

17-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where(:not([theme~='tertiary'])) {
17+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where(
18+
:not([theme~='tertiary'])
19+
) {
1820
--aura-surface-level: 6;
1921
--aura-surface-opacity: 0.3;
2022
box-shadow: var(--vaadin-button-shadow);
2123
}
2224

2325
/* prettier-ignore */
24-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not([theme~='primary'], [theme~='tertiary']) {
26+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not([theme~='primary'], [theme~='tertiary']) {
2527
background: var(--vaadin-button-background, var(--_background));
2628
--vaadin-button-border-color: var(--aura-accent-border-color);
2729
}
2830

29-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where(:not([theme~='primary'])) {
31+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where(
32+
:not([theme~='primary'])
33+
) {
3034
outline-offset: calc(var(--vaadin-button-border-width, 1px) * -1);
3135
}
3236

@@ -35,24 +39,30 @@ Increase padding, but only for buttons that don't have an icon in the default sl
3539
Buttons that place an icon in the default slot are assumed to be icon-only buttons.
3640
*/
3741
/* prettier-ignore */
38-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not(:has(> :is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar):not([slot]))) {
42+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not(:has(> :is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar):not([slot]))) {
3943
--vaadin-button-padding: round(var(--vaadin-padding-s) / 1.4, 1px)
4044
max(var(--vaadin-padding-m), round(var(--vaadin-radius-m) / 1.5, 1px));
4145
}
4246

4347
/* Decrease padding when an icon is placed in the prefix or suffix slot */
44-
:is(vaadin-button, vaadin-menu-bar-button):has(> [slot='prefix']:is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar)),
48+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button):has(
49+
> [slot='prefix']:is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar)
50+
),
4551
vaadin-drawer-toggle:empty {
4652
padding-inline-start: max(var(--vaadin-padding-s), round(var(--vaadin-radius-m) / 1.75, 1px));
4753
}
4854

49-
:is(vaadin-button, vaadin-menu-bar-button):has(> [slot='suffix']:is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar)),
55+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button):has(
56+
> [slot='suffix']:is(vaadin-icon, svg, i[class*='fa-'], vaadin-avatar)
57+
),
5058
vaadin-drawer-toggle:empty,
5159
vaadin-menu-bar-button[aria-haspopup='true']:not([slot='overflow']) {
5260
padding-inline-end: max(var(--vaadin-padding-s), round(var(--vaadin-radius-m) / 1.75, 1px));
5361
}
5462

55-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where([theme~='primary']) {
63+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):where(
64+
[theme~='primary']
65+
) {
5666
outline-offset: 2px;
5767
--vaadin-button-font-weight: var(--aura-font-weight-semibold);
5868
--vaadin-button-text-color: var(--aura-accent-contrast-color);
@@ -61,11 +71,13 @@ vaadin-menu-bar-button[aria-haspopup='true']:not([slot='overflow']) {
6171
--vaadin-button-shadow: var(--aura-shadow-s);
6272
}
6373

64-
:is(vaadin-button, vaadin-menu-bar-button)[disabled][theme~='primary']::part(label) {
74+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button)[disabled][theme~='primary']::part(label) {
6575
color: color-mix(in srgb, currentColor 50%, transparent);
6676
}
6777

68-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not([disabled])::before {
78+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not(
79+
[disabled]
80+
)::before {
6981
content: '';
7082
position: absolute;
7183
inset: calc(var(--vaadin-button-border-width, 1px) * -1);
@@ -79,43 +91,49 @@ vaadin-menu-bar-button[aria-haspopup='true']:not([slot='overflow']) {
7991
}
8092

8193
@supports (color: hsl(0 0 0)) {
82-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not([disabled])::before {
94+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):not(
95+
[disabled]
96+
)::before {
8397
background-color: oklch(from currentColor calc(l + 0.4 - c) c h / calc(1 - l / 2));
8498
}
8599
}
86100

87101
@media (any-hover: hover) {
88102
/* prettier-ignore */
89-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):hover:not([disabled], [active])::before {
103+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit):hover:not([disabled], [active])::before {
90104
opacity: 0.03;
91105
}
92106

93107
/* prettier-ignore */
94-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[theme~='primary']:hover:not([disabled], [active])::before {
108+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[theme~='primary']:hover:not([disabled], [active])::before {
95109
opacity: 0.12;
96110
}
97111
}
98112

99113
@media (min-resolution: 2x) {
100114
/* prettier-ignore */
101-
:is(vaadin-button, vaadin-menu-bar-button[first-visible][last-visible], vaadin-drawer-toggle)[active]:not([disabled], [aria-disabled='true']) {
115+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button[first-visible][last-visible], vaadin-drawer-toggle)[active]:not([disabled], [aria-disabled='true']) {
102116
scale: 0.98;
103117
transition-duration: 50ms;
104118
}
105119
}
106120

107-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[active]:not([disabled]) {
121+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[active]:not(
122+
[disabled]
123+
) {
108124
box-shadow: none;
109125
}
110126

111-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[active]:not([disabled])::before {
127+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[active]:not(
128+
[disabled]
129+
)::before {
112130
transition-duration: 0s;
113131
opacity: 0.08;
114132
background: oklch(from currentColor min(c, 1 - l + c) calc(c * 0.9) h);
115133
}
116134

117135
/* prettier-ignore */
118-
:is(vaadin-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[theme~='primary'][active]:not([disabled])::before {
136+
:is(vaadin-button, vaadin-upload-button, vaadin-menu-bar-button, vaadin-drawer-toggle, vaadin-crud-edit)[theme~='primary'][active]:not([disabled])::before {
119137
opacity: 0.16;
120138
}
121139

packages/aura/src/surface.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ vaadin-app-layout,
1313
vaadin-app-layout::part(navbar),
1414
vaadin-app-layout::part(drawer),
1515
vaadin-button,
16+
vaadin-upload-button,
1617
vaadin-card,
1718
vaadin-checkbox::part(checkbox),
1819
vaadin-details[theme~='filled'],
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2000 - 2026 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import type { CSSResult } from 'lit';
7+
8+
export declare const uploadButtonStyles: CSSResult[];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2000 - 2026 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { css } from 'lit';
7+
import { buttonStyles } from '@vaadin/button/src/styles/vaadin-button-base-styles.js';
8+
9+
const uploadButton = css``;
10+
11+
export const uploadButtonStyles = [buttonStyles, uploadButton];
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2000 - 2026 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { ButtonMixin } from '@vaadin/button/src/vaadin-button-mixin.js';
7+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
8+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
9+
import type { UploadManager } from './vaadin-upload-manager.js';
10+
11+
/**
12+
* `<vaadin-upload-button>` is a button component for file uploads.
13+
* When clicked, it opens a file picker dialog and calls addFiles
14+
* on a linked UploadManager.
15+
*
16+
* ```html
17+
* <vaadin-upload-button>Upload Files</vaadin-upload-button>
18+
* ```
19+
*
20+
* The button must be linked to an UploadManager by setting the
21+
* `manager` property:
22+
*
23+
* ```javascript
24+
* const button = document.querySelector('vaadin-upload-button');
25+
* button.manager = uploadManager;
26+
* ```
27+
*
28+
* ### Styling
29+
*
30+
* The following shadow DOM parts are available for styling:
31+
*
32+
* Part name | Description
33+
* ----------|-------------
34+
* `label` | The label (text) inside the button.
35+
* `prefix` | A slot for content before the label (e.g. an icon).
36+
* `suffix` | A slot for content after the label (e.g. an icon).
37+
*
38+
* The following state attributes are available for styling:
39+
*
40+
* Attribute | Description
41+
* -------------------|-------------
42+
* `active` | Set when the button is pressed down, either with mouse, touch or the keyboard
43+
* `disabled` | Set when the button is disabled
44+
* `focus-ring` | Set when the button is focused using the keyboard
45+
* `focused` | Set when the button is focused
46+
* `has-tooltip` | Set when the button has a slotted tooltip
47+
* `max-files-reached`| Set when the manager has reached maxFiles
48+
*
49+
* The following custom CSS properties are available for styling:
50+
*
51+
* Custom CSS property |
52+
* :----------------------------------|
53+
* | `--vaadin-button-background` |
54+
* | `--vaadin-button-border-color` |
55+
* | `--vaadin-button-border-radius` |
56+
* | `--vaadin-button-border-width` |
57+
* | `--vaadin-button-font-size` |
58+
* | `--vaadin-button-font-weight` |
59+
* | `--vaadin-button-gap` |
60+
* | `--vaadin-button-height` |
61+
* | `--vaadin-button-line-height` |
62+
* | `--vaadin-button-margin` |
63+
* | `--vaadin-button-padding` |
64+
* | `--vaadin-button-text-color` |
65+
*
66+
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
67+
*/
68+
declare class UploadButton extends ButtonMixin(ElementMixin(ThemableMixin(HTMLElement))) {
69+
/**
70+
* Reference to an UploadManager.
71+
* When set, the button will automatically disable when maxFilesReached
72+
* becomes true on the manager. The file picker will also use the manager's
73+
* `accept` and `maxFiles` settings.
74+
*/
75+
manager: UploadManager | null;
76+
77+
/**
78+
* Capture attribute for mobile file input.
79+
*/
80+
capture: string | undefined;
81+
82+
/**
83+
* True when max files has been reached on the manager.
84+
* The button will not open the file picker when this is true.
85+
*/
86+
maxFilesReached: boolean;
87+
88+
/**
89+
* Opens the file picker dialog.
90+
*/
91+
openFilePicker(): void;
92+
}
93+
94+
declare global {
95+
interface HTMLElementTagNameMap {
96+
'vaadin-upload-button': UploadButton;
97+
}
98+
}
99+
100+
export { UploadButton };

0 commit comments

Comments
 (0)