Skip to content

Commit 6f61cad

Browse files
authored
feat(ui5-file-uploader): apply new design concept (#11589)
- Redesigned the FileUploader component to display a ui5-tokenizer for selected files, providing a clearer and more interactive visual representation. - Improved accessibility for both visual and screen reader users. - Added support for accessibleName, accessibleNameRef and required properties to enhance ARIA labeling and compliance. - Updated keyboard navigation and focus management for better usability. - Ensured all interactive elements are accessible and properly described for assistive technologies. Fixes: #11578 Fixes: #11604 Fixes: #10549 Fixes: #11611 Fixes: #5893 Fixes: #10909 Fixes: #8395
1 parent 3f80faa commit 6f61cad

File tree

18 files changed

+1044
-380
lines changed

18 files changed

+1044
-380
lines changed

packages/main/cypress/specs/FileUploader.cy.tsx

Lines changed: 470 additions & 0 deletions
Large diffs are not rendered by default.

packages/main/src/FileUploader.ts

Lines changed: 217 additions & 59 deletions
Large diffs are not rendered by default.

packages/main/src/FileUploaderPopoverTemplate.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ export default function FileUploaderPopoverTemplate(this: FileUploader) {
2121
"ui5-valuestatemessage--warning": this.valueState === ValueState.Critical,
2222
"ui5-valuestatemessage--information": this.valueState === ValueState.Information,
2323
}}
24-
style={{
25-
"width": `${this.ui5Input ? this.ui5Input.offsetWidth : 0}px`,
26-
}}
24+
style={{ width: `${this._formWidth}px` }}
2725
>
2826
{
2927
this._valueStateMessageInputIcon &&

packages/main/src/FileUploaderTemplate.tsx

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,97 @@
1+
import Icon from "./Icon.js";
2+
import Tokenizer from "./Tokenizer.js";
3+
import Token from "./Token.js";
14
import type FileUploader from "./FileUploader.js";
2-
import Input from "./Input.js";
35
import FileUploaderPopoverTemplate from "./FileUploaderPopoverTemplate.js";
46

57
export default function FileUploaderTemplate(this: FileUploader) {
68
return (
79
<>
810
<div
911
class="ui5-file-uploader-root"
10-
onMouseOver={this._onmouseover}
11-
onMouseOut={this._onmouseout}
1212
onFocusIn={this._onfocusin}
1313
onFocusOut={this._onfocusout}
1414
onKeyDown={this._onkeydown}
1515
onKeyUp={this._onkeyup}
1616
onClick={this._onclick}
17+
onMouseDown={this._onmousedown}
1718
onDragOver={this._ondrag}
1819
onDrop={this._ondrop}
1920
>
20-
<div class="ui5-file-uploader-mask">
21-
{!this.hideInput &&
22-
<Input
23-
value={this.value}
24-
valueState={this.valueState}
25-
placeholder={this.placeholder}
26-
disabled={this.disabled}
27-
tabindex={-1}
28-
class="ui5-file-uploader-input"
29-
/>
30-
}
21+
<form class="ui5-file-uploader-form" onSubmit={this._onFormSubmit}>
22+
<input
23+
type="file"
24+
class="ui5-file-uploader-native-input"
25+
name={this.name}
26+
multiple={this.multiple}
27+
accept={this.accept}
28+
disabled={this.disabled}
29+
title={this.inputTitle}
30+
aria-roledescription={this.accInfo.ariaRoledescription}
31+
aria-haspopup={this.accInfo.ariaHasPopup}
32+
aria-label={this.accInfo.ariaLabel}
33+
aria-required={this.accInfo.ariaRequired}
34+
aria-invalid={this.accInfo.ariaInvalid}
35+
onClick={this._onNativeInputClick}
36+
onChange={this._onChange}
37+
data-sap-focus-ref
38+
/>
39+
</form>
40+
41+
{this.hideInput ? (
3142
<slot></slot>
32-
</div>
33-
<input
34-
type="file"
35-
tabindex={-1}
36-
aria-hidden="true"
37-
multiple={this.multiple}
38-
accept={this.accept}
39-
title={this.titleText}
40-
disabled={this.disabled}
41-
onChange={this._onChange}
42-
/>
43+
) : (
44+
<div class="ui5-file-uploader-display-container">
45+
<div class="ui5-file-uploader-display-elements">
46+
{this._selectedFilesNames.length > 0 ? (
47+
<>
48+
<Tokenizer
49+
class="ui5-file-uploader-tokenizer"
50+
expanded={this._tokenizerExpanded}
51+
open={this._tokenizerOpen}
52+
popoverMinWidth={this._formWidth}
53+
onClick={this._onTokenizerClick}
54+
onMouseDown={this._onTokenizerMouseDown}
55+
onKeyDown={this._onTokenizerKeyDown}
56+
onKeyUp={this._onTokenizerKeyUp}
57+
preventInitialFocus
58+
readonly
59+
>
60+
{this._selectedFilesNames.map(fileName => (
61+
<Token
62+
text={fileName}
63+
/>
64+
))}
65+
</Tokenizer>
66+
<Icon
67+
name="decline"
68+
class="ui5-file-uploader-clear-icon inputIcon"
69+
onClick={this._onClearIconClick}
70+
title={this.clearIconTitle}
71+
/>
72+
</>
73+
) : (
74+
<input
75+
class="ui5-file-uploader-display-input"
76+
tabindex={-1}
77+
aria-hidden="true"
78+
title={this.inputTitle}
79+
placeholder={this.resolvedPlaceholder}
80+
inner-input
81+
readonly
82+
/>
83+
)}
84+
85+
<Icon
86+
name="value-help"
87+
class="ui5-file-uploader-value-help-icon inputIcon"
88+
title={this.valueHelpTitle}
89+
/>
90+
</div>
91+
92+
<slot></slot>
93+
</div>
94+
)}
4395
</div>
4496

4597
{ FileUploaderPopoverTemplate.call(this) }

packages/main/src/i18n/messagebundle.properties

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,23 @@ EXPANDABLE_TEXT_SHOW_MORE_POPOVER_ARIA_LABEL=Show the full text
218218
#XACT: ARIA-label text for link that allows the user to close the popover with the complete text
219219
EXPANDABLE_TEXT_SHOW_LESS_POPOVER_ARIA_LABEL=Close the popover
220220

221-
FILEUPLOAD_BROWSE=Browse...
221+
#XACT: ARIA announcement for the FileUploader's roledescription attribute
222+
FILEUPLOADER_ROLE_DESCRIPTION=File Uploader
222223

223-
#XACT: File uploader title
224-
FILEUPLOADER_TITLE=Upload File
224+
#XACT: Default placeholder text for the ui5-file-uploader
225+
FILEUPLOADER_DEFAULT_PLACEHOLDER=Browse or drop a file
226+
227+
#XACT: Default placeholder text for the multiple ui5-file-uploader
228+
FILEUPLOADER_DEFAULT_MULTIPLE_PLACEHOLDER=Browse or drop multiple files
229+
230+
#XTOL: Default tooltip text for the ui5-file-uploader's input field
231+
FILEUPLOADER_INPUT_TOOLTIP=All files will be replaced on each upload
232+
233+
#XTOL: Default tooltip text for the ui5-file-uploader's value help icon
234+
FILEUPLOADER_VALUE_HELP_TOOLTIP=Browse and replace all files
235+
236+
#XTOL: Default tooltip text for the ui5-file-uploader's clear icon
237+
FILEUPLOADER_CLEAR_ICON_TOOLTIP=Remove all files
225238

226239
GROUP_HEADER_TEXT=Group Header
227240

Lines changed: 140 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,174 @@
11
@import "./FormComponents.css";
2+
@import "./Input.css";
3+
@import "./InputIcon.css";
4+
@import "./ValueStateVariables.css";
25

36
:host {
47
display: inline-block;
58
}
69

7-
.ui5-file-uploader-root {
8-
position: relative;
10+
:host([hide-input]) {
11+
width: max-content;
12+
height: max-content;
913
}
1014

11-
.ui5-file-uploader-root input[type=file] {
12-
opacity: 0;
15+
:host([hide-input]) .ui5-file-uploader-form {
1316
position: absolute;
1417
top: 0;
1518
left: 0;
16-
height: 100%;
1719
width: 100%;
18-
font-size: 0;
20+
height: 100%;
21+
display: block;
1922
}
2023

21-
.ui5-file-uploader-root input[type=file]:not([disabled]) {
22-
cursor: pointer;
24+
.ui5-file-uploader-root,
25+
.ui5-file-uploader-form {
26+
position: relative;
27+
width: inherit;
28+
height: inherit;
29+
}
30+
31+
.ui5-file-uploader-root .ui5-file-uploader-native-input {
32+
position: relative;
33+
display: block;
34+
width: inherit;
35+
height: inherit;
36+
opacity: 0;
37+
font-size: 0;
2338
}
2439

25-
.ui5-file-uploader-mask {
40+
.ui5-file-uploader-display-container {
41+
position: absolute;
42+
top: 0;
43+
inset-inline-start: 0;
44+
width: inherit;
45+
height: inherit;
2646
display: flex;
47+
column-gap: 0.5rem;
2748
align-items: center;
2849
}
2950

30-
.ui5-file-uploader-mask [ui5-input] {
31-
margin-right: 0.25rem;
51+
.ui5-file-uploader-display-elements {
52+
height: inherit;
53+
flex: 1;
54+
display: flex;
55+
position: relative;
3256
}
3357

34-
:host([value-state="None"]:not([disabled]):hover) [ui5-input],
35-
:host(:not([value-state]):not([disabled]):hover) [ui5-input] {
36-
border: var(--_ui5_file_uploader_hover_border);
37-
background-color: var(--sapField_Hover_Background);
38-
box-shadow: var(--sapField_Hover_Shadow);
58+
.ui5-file-uploader-tokenizer,
59+
.ui5-file-uploader-display-input {
60+
position: absolute;
61+
top: 0;
62+
inset-inline-start: 0;
63+
height: inherit;
64+
border: none;
65+
outline: none;
66+
cursor: pointer;
3967
}
4068

41-
:host([value-state="Negative"]:not([disabled]):hover) [ui5-input] {
42-
background-color: var(--_ui5_file_uploader_value_state_error_hover_background_color);
43-
box-shadow: var(--sapField_Hover_InvalidShadow);
69+
.ui5-file-uploader-clear-icon,
70+
.ui5-file-uploader-value-help-icon {
71+
position: absolute;
72+
cursor: pointer;
4473
}
4574

46-
:host([value-state="Critical"]:not([disabled]):hover) [ui5-input] {
47-
background-color: var(--sapField_Hover_Background);
48-
box-shadow: var(--sapField_Hover_WarningShadow);
75+
.ui5-file-uploader-tokenizer {
76+
max-width: var(--_ui5_file_uploader_tokenizer_width);
4977
}
5078

51-
:host([value-state="Positive"]:not([disabled]):hover) [ui5-input] {
52-
background-color: var(--sapField_Hover_Background);
53-
box-shadow: var(--sapField_Hover_SuccessShadow);
79+
.ui5-file-uploader-display-input {
80+
width: var(--_ui5_file_uploader_display_input_width);
81+
}
82+
83+
.ui5-file-uploader-clear-icon {
84+
inset-inline-end: var(--_ui5_input_icon_width);
85+
}
86+
87+
.ui5-file-uploader-value-help-icon {
88+
inset-inline-end: 0rem;
89+
}
90+
91+
/* Input styles overrides */
92+
:host(:not([readonly])),
93+
:host([readonly][disabled]),
94+
:host([value-state="None"]:not([readonly]):hover),
95+
:host([value-state="Negative"]:not([readonly]):not([disabled])),
96+
:host([value-state="Negative"]:not([readonly])[focused][opened]:hover),
97+
:host([value-state="Negative"]:not([readonly]):not([focused]):hover),
98+
:host([value-state="Critical"]:not([readonly]):not([disabled])),
99+
:host([value-state="Critical"]:not([readonly])[focused][opened]:hover),
100+
:host([value-state="Critical"]:not([readonly]):not([focused]):hover),
101+
:host([value-state="Positive"]:not([readonly]):not([disabled])),
102+
:host([value-state="Positive"]:not([readonly])[focused][opened]:hover),
103+
:host([value-state="Positive"]:not([readonly]):not([focused]):hover),
104+
:host([value-state="Information"]:not([readonly]):not([disabled])),
105+
:host([value-state="Information"]:not([readonly])[focused][opened]:hover),
106+
:host([value-state="Information"]:not([readonly]):not([focused]):hover),
107+
:host(:not([value-state]):not([readonly]):hover) {
108+
box-shadow: none;
109+
background: none;
110+
}
111+
112+
:host(:not([readonly])) .ui5-file-uploader-display-elements,
113+
:host([readonly][disabled]) .ui5-file-uploader-display-elements {
114+
box-shadow: var(--sapField_Shadow);
115+
border: var(--_ui5-input-border);
116+
border-radius: var(--_ui5_input_border_radius);
117+
background: var(--sapField_BackgroundStyle);
54118
}
55119

56-
:host([value-state="Information"]:not([disabled]):hover) [ui5-input] {
120+
/* Value state styles */
121+
:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-file-uploader-display-elements,
122+
:host([value-state="Critical"]:not([readonly]):not([disabled])) .ui5-file-uploader-display-elements,
123+
:host([value-state="Positive"]:not([readonly]):not([disabled])) .ui5-file-uploader-display-elements,
124+
:host([value-state="Information"]:not([readonly]):not([disabled])) .ui5-file-uploader-display-elements {
125+
box-shadow: var(--ui5_value_state-box-shadow);
126+
background: var(--ui5_value_state-background);
127+
background-color: var(--ui5_value_state-background-color);
128+
}
129+
130+
/* Value state hover styles */
131+
:host([value-state="None"]:not([readonly]):hover) .ui5-file-uploader-display-elements,
132+
:host(:not([value-state]):not([readonly]):hover) .ui5-file-uploader-display-elements {
133+
box-shadow: var(--sapField_Hover_Shadow);
134+
background: var(--sapField_Hover_BackgroundStyle);
135+
}
136+
137+
:host([value-state="Negative"]:not([readonly]):not([focused]):hover) .ui5-file-uploader-display-elements,
138+
:host([value-state="Negative"]:not([readonly])[focused][opened]:hover) .ui5-file-uploader-display-elements {
139+
background-color: var(--_ui5_input_value_state_error_hover_background);
140+
box-shadow: var(--sapField_Hover_InvalidShadow);
141+
}
142+
143+
:host([value-state="Negative"][focused]:not([opened]):not([readonly])) .ui5-file-uploader-display-elements,
144+
:host([value-state="Positive"]:not([readonly]):not([focused]):hover) .ui5-file-uploader-display-elements,
145+
:host([value-state="Positive"]:not([readonly])[focused][opened]:hover) .ui5-file-uploader-display-elements,
146+
:host([value-state="Positive"][focused]:not([opened]):not([readonly])) .ui5-file-uploader-display-elements,
147+
:host([value-state="Critical"]:not([readonly]):not([focused]):hover) .ui5-file-uploader-display-elements,
148+
:host([value-state="Critical"]:not([readonly])[focused][opened]:hover) .ui5-file-uploader-display-elements,
149+
:host([value-state="Critical"][focused]:not([opened]):not([readonly])) .ui5-file-uploader-display-elements,
150+
:host([value-state="Information"]:not([readonly]):not([focused]):hover) .ui5-file-uploader-display-elements,
151+
:host([value-state="Information"]:not([readonly])[focused][opened]:hover) .ui5-file-uploader-display-elements,
152+
:host([value-state="Information"][focused]:not([opened]):not([readonly])) .ui5-file-uploader-display-elements{
57153
background-color: var(--sapField_Hover_Background);
58-
box-shadow: var(--sapField_Hover_InformationShadow);
59154
}
60155

61-
:host(:not([disabled]):active) [ui5-button] {
62-
background-color: var(--sapButton_Active_Background);
63-
border-color: var(--sapButton_Active_BorderColor);
64-
color: var(--sapButton_Active_TextColor);
65-
text-shadow: none;
156+
/* Value state focus styles */
157+
:host([focused][hide-input]) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus)::after,
158+
:host([focused]) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus) .ui5-file-uploader-display-elements::after {
159+
content: var(--ui5_input_focus_pseudo_element_content);
160+
position: absolute;
161+
pointer-events: none;
162+
z-index: 2;
163+
border: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--_ui5_input_focus_outline_color);
164+
border-radius: var(--_ui5_input_focus_border_radius);
165+
top: var(--_ui5_input_focus_offset);
166+
bottom: var(--_ui5_input_focus_offset);
167+
left: var(--_ui5_input_focus_offset);
168+
right: var(--_ui5_input_focus_offset);
169+
}
170+
171+
:host([focused]:not([opened]):not([readonly]):not([hide-input]))
172+
.ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus) .ui5-file-uploader-display-elements::after {
173+
border-color: var(--ui5_value_state-focus-outline, var(--_ui5_input_focus_outline_color));
66174
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
:host([value-state="Negative"]:not([readonly]):not([disabled])) {
2+
--ui5_value_state-background: var(--sapField_InvalidBackgroundStyle);
3+
--ui5_value_state-background-color: var(--sapField_InvalidBackground);
4+
--ui5_value_state-box-shadow: var(--sapField_InvalidShadow);
5+
--ui5_value_state-focus-outline: var(--_ui5_input_focused_value_state_error_focus_outline_color);
6+
}
7+
8+
:host([value-state="Critical"]:not([readonly]):not([disabled])) {
9+
--ui5_value_state-background: var(--sapField_WarningBackgroundStyle);
10+
--ui5_value_state-background-color: var(--sapField_WarningBackground);
11+
--ui5_value_state-box-shadow: var(--sapField_WarningShadow);
12+
--ui5_value_state-focus-outline: var(--_ui5_input_focused_value_state_warning_focus_outline_color);
13+
}
14+
15+
:host([value-state="Positive"]:not([readonly]):not([disabled])) {
16+
--ui5_value_state-background: var(--sapField_SuccessBackgroundStyle);
17+
--ui5_value_state-background-color: var(--sapField_SuccessBackground);
18+
--ui5_value_state-box-shadow: var(--sapField_SuccessShadow);
19+
--ui5_value_state-focus-outline: var(--_ui5_input_focused_value_state_success_focus_outline_color);
20+
}
21+
22+
:host([value-state="Information"]:not([readonly]):not([disabled])) {
23+
--ui5_value_state-background: var(--sapField_InformationBackgroundStyle);
24+
--ui5_value_state-background-color: var(--sapField_InformationBackground);
25+
--ui5_value_state-box-shadow: var(--sapField_InformationShadow);
26+
--ui5_value_state-focus-outline: var(--_ui5_input_focused_value_state_information_focus_outline_color);
27+
}

0 commit comments

Comments
 (0)