diff --git a/packages/main/src/FileUploader.ts b/packages/main/src/FileUploader.ts index af9b1a380925..ee1fb88b8aa0 100644 --- a/packages/main/src/FileUploader.ts +++ b/packages/main/src/FileUploader.ts @@ -3,23 +3,36 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; +import query from "@ui5/webcomponents-base/dist/decorators/query.js"; import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; -import { isEnter, isSpace } from "@ui5/webcomponents-base/dist/Keys.js"; +import { + isUpAlt, + isDownAlt, + isEnter, + isEscape, + isF4, + isSpace, + isRight, + isLeft, +} from "@ui5/webcomponents-base/dist/Keys.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import { - FILEUPLOAD_BROWSE, - FILEUPLOADER_TITLE, + FILEUPLOADER_INPUT_TOOLTIP, + FILEUPLOADER_VALUE_HELP_TOOLTIP, + FILEUPLOADER_CLEAR_ICON_TOOLTIP, VALUE_STATE_SUCCESS, VALUE_STATE_INFORMATION, VALUE_STATE_ERROR, VALUE_STATE_WARNING, + FILEUPLOADER_DEFAULT_PLACEHOLDER, + FILEUPLOADER_ROLE_DESCRIPTION, } from "./generated/i18n/i18n-defaults.js"; -import type Input from "./Input.js"; import type Popover from "./Popover.js"; +import type Tokenizer from "./Tokenizer.js"; // Template import FileUploaderTemplate from "./FileUploaderTemplate.js"; @@ -185,6 +198,14 @@ class FileUploader extends UI5Element implements IFormInputElement { @property() valueState: `${ValueState}` = "None"; + /** + * Defines whether the component is required. + * @default false + * @private + */ + @property({ type: Boolean }) + required = false; + /** * @private */ @@ -214,6 +235,21 @@ class FileUploader extends UI5Element implements IFormInputElement { @slot() valueStateMessage!: Array; + @query(".ui5-file-uploader-form") + _from!: HTMLFormElement; + + @query("input[type=file]") + _input!: HTMLInputElement; + + @query("[ui5-tokenizer]") + _tokenizer!: Tokenizer; + + @query(".ui5-valuestatemessage-popover") + _messagePopover!: Popover; + + @property({ type: Array, noAttribute: true }) + private selectedFiles: Array = []; + static emptyInput: HTMLInputElement; @i18n("@ui5/webcomponents") @@ -227,7 +263,7 @@ class FileUploader extends UI5Element implements IFormInputElement { * @override */ getFocusDomRef(): HTMLElement | undefined { - return this.content[0]; + return this.hideInput ? this.content[0] : this._input; } get formFormattedValue() { @@ -244,34 +280,43 @@ class FileUploader extends UI5Element implements IFormInputElement { return null; } - _onmouseover() { - this.content.forEach(item => { - item.classList.add("ui5_hovered"); - }); - } - - _onmouseout() { - this.content.forEach(item => { - item.classList.remove("ui5_hovered"); - }); - } - _onclick() { - if (this.getFocusDomRef()?.matches(":focus-within")) { - this._input.click(); + if (this.getFocusDomRef()?.matches(":focus-within") && this.hideInput) { + this._openFileBrowser(); } } _onkeydown(e: KeyboardEvent) { if (isEnter(e)) { - this._input.click(); + this._openFileBrowser(); + e.preventDefault(); + } + + if (this.hideInput) { + return; + } + + const firstToken = this._tokenizer?.tokens.filter(token => !token.hasAttribute("overflows"))[0]; + const isToken = (e.target).hasAttribute("ui5-token"); + + if (isToken && e.target === firstToken && isLeft(e)) { + this._input.focus(); + e.preventDefault(); + } + + if (!isToken && isRight(e)) { + firstToken?.focus(); e.preventDefault(); } } _onkeyup(e: KeyboardEvent) { - if (isSpace(e)) { - this._input.click(); + const shouldOpenFileBrowser = (isF4(e) || isUpAlt(e) || isDownAlt(e)) && !this.hideInput; + if (isSpace(e) || shouldOpenFileBrowser) { + this._openFileBrowser(); + e.preventDefault(); + } else if (isEscape(e) && !this.hideInput) { + this._clearFileSelection(); e.preventDefault(); } } @@ -297,7 +342,8 @@ class FileUploader extends UI5Element implements IFormInputElement { } this._input.files = validatedFiles; - this._updateValue(validatedFiles); + this.selectedFiles = this._fileNamesList(files); + this.value = this.computedValue; this.fireDecoratorEvent("change", { files: validatedFiles, }); @@ -305,10 +351,32 @@ class FileUploader extends UI5Element implements IFormInputElement { _onfocusin() { this.focused = true; + // if (this.hideInput) { + // this._input.focus(); + // } + if (this._tokenizer) { + this._tokenizer.expanded = true; + } } _onfocusout() { this.focused = false; + if (this._tokenizer) { + this._tokenizer.expanded = false; + } + } + + _openFileBrowser() { + this._input.click(); + } + + _clearFileSelection() { + this.selectedFiles = []; + this.value = ""; + this._from?.reset(); + this.fireDecoratorEvent("change", { + files: this.files, + }); } /** @@ -324,14 +392,28 @@ class FileUploader extends UI5Element implements IFormInputElement { return FileUploader._emptyFilesList; } + get selectedFileNames(): Array { + return this.selectedFiles; + } + onAfterRendering() { if (!this.value) { this._input.value = ""; } + if (this.hideInput && this.content.length > 0) { + this.content.forEach(element => { + element.setAttribute("tabindex", "-1"); + }); + } + this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover); } + get computedValue(): string { + return this.selectedFiles.join(" "); + } + _onChange(e: Event) { let changedFiles = (e.target as HTMLInputElement).files; @@ -343,16 +425,15 @@ class FileUploader extends UI5Element implements IFormInputElement { return; } - this._updateValue(changedFiles); + this.selectedFiles = this._fileNamesList(changedFiles as FileList); + this.value = this.computedValue; this.fireDecoratorEvent("change", { files: changedFiles, }); } - _updateValue(files: FileList | null) { - this.value = Array.from(files || []).reduce((acc, currFile) => { - return `${acc}"${currFile.name}" `; - }, ""); + _fileNamesList(files: FileList) : Array { + return Array.from(files).map(file => file.name); } /** @@ -398,26 +479,18 @@ class FileUploader extends UI5Element implements IFormInputElement { } openValueStatePopover() { - const popover = this._getPopover(); - - if (popover) { - popover.opener = this; - popover.open = true; + if (this._messagePopover) { + this._messagePopover.opener = this; + this._messagePopover.open = true; } } closeValueStatePopover() { - const popover = this._getPopover(); - - if (popover) { - popover.open = false; + if (this._messagePopover) { + this._messagePopover.open = false; } } - _getPopover(): Popover { - return this.shadowRoot!.querySelector(".ui5-valuestatemessage-popover")!; - } - /** * in case when the component is not placed in the DOM, return empty FileList, like native input would do * @private @@ -430,16 +503,24 @@ class FileUploader extends UI5Element implements IFormInputElement { return this.emptyInput.files; } - get browseText(): string { - return FileUploader.i18nBundle.getText(FILEUPLOAD_BROWSE); + get inputTitle(): string { + return FileUploader.i18nBundle.getText(FILEUPLOADER_INPUT_TOOLTIP); + } + + get valueHelpTitle(): string { + return FileUploader.i18nBundle.getText(FILEUPLOADER_VALUE_HELP_TOOLTIP); + } + + get clearIconTitle(): string { + return FileUploader.i18nBundle.getText(FILEUPLOADER_CLEAR_ICON_TOOLTIP); } - get titleText(): string { - return FileUploader.i18nBundle.getText(FILEUPLOADER_TITLE); + get resolvedPlaceholder(): string { + return this.placeholder || FileUploader.i18nBundle.getText(FILEUPLOADER_DEFAULT_PLACEHOLDER); } - get _input(): HTMLInputElement { - return (this.shadowRoot!.querySelector("input[type=file]") || this.querySelector("input[type=file][data-ui5-form-support]"))!; + get roleDescription(): string { + return FileUploader.i18nBundle.getText(FILEUPLOADER_ROLE_DESCRIPTION); } get valueStateTextMappings(): Record { @@ -485,8 +566,8 @@ class FileUploader extends UI5Element implements IFormInputElement { return this.valueState !== ValueState.None ? iconPerValueState[this.valueState] : ""; } - get ui5Input() { - return this.shadowRoot!.querySelector(".ui5-file-uploader-input"); + get fromElement() : HTMLFormElement | undefined { + return this._from; } } diff --git a/packages/main/src/FileUploaderPopoverTemplate.tsx b/packages/main/src/FileUploaderPopoverTemplate.tsx index cc4187ea23d2..ebb4b28623e5 100644 --- a/packages/main/src/FileUploaderPopoverTemplate.tsx +++ b/packages/main/src/FileUploaderPopoverTemplate.tsx @@ -22,7 +22,7 @@ export default function FileUploaderPopoverTemplate(this: FileUploader) { "ui5-valuestatemessage--information": this.valueState === ValueState.Information, }} style={{ - "width": `${this.ui5Input ? this.ui5Input.offsetWidth : 0}px`, + "width": `${this.fromElement ? this.fromElement.offsetWidth : 0}px`, }} > { diff --git a/packages/main/src/FileUploaderTemplate.tsx b/packages/main/src/FileUploaderTemplate.tsx index 11576d16d695..85995ff87355 100644 --- a/packages/main/src/FileUploaderTemplate.tsx +++ b/packages/main/src/FileUploaderTemplate.tsx @@ -1,5 +1,7 @@ +import Icon from "./Icon.js"; +import Tokenizer from "./Tokenizer.js"; +import Token from "./Token.js"; import type FileUploader from "./FileUploader.js"; -import Input from "./Input.js"; import FileUploaderPopoverTemplate from "./FileUploaderPopoverTemplate.js"; export default function FileUploaderTemplate(this: FileUploader) { @@ -7,8 +9,6 @@ export default function FileUploaderTemplate(this: FileUploader) { <>
-
- {!this.hideInput && - + + + {!this.hideInput ? ( +
+ {this.selectedFileNames.length > 0 ? ( + <> + + {this.selectedFileNames.map(fileName => ( + + ))} + + + + ) : ( + + )} + - } +
+ ) : ( -
- + )}
{ FileUploaderPopoverTemplate.call(this) } diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 0e5b45f7a95a..91d9f96761ba 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -215,10 +215,20 @@ EXPANDABLE_TEXT_SHOW_MORE_POPOVER_ARIA_LABEL=Show the full text #XACT: ARIA-label text for link that allows the user to close the popover with the complete text EXPANDABLE_TEXT_SHOW_LESS_POPOVER_ARIA_LABEL=Close the popover -FILEUPLOAD_BROWSE=Browse... +#XACT: ARIA announcement for the FileUploader`s roledescription attribute +FILEUPLOADER_ROLE_DESCRIPTION=File Uploader -#XACT: File uploader title -FILEUPLOADER_TITLE=Upload File +#XACT: Default placeholder text for the ui5-file-uploader +FILEUPLOADER_DEFAULT_PLACEHOLDER=Select a file + +#XTOL: Default tooltip text for the ui5-file-uploader's input field +FILEUPLOADER_INPUT_TOOLTIP=All files will be replaced on each upload + +#XTOL: Default tooltip text for the ui5-file-uploader's value help icon +FILEUPLOADER_VALUE_HELP_TOOLTIP=Browse and replace all files + +#XTOL: Default tooltip text for the ui5-file-uploader's clear icon +FILEUPLOADER_CLEAR_ICON_TOOLTIP=Remove all files GROUP_HEADER_TEXT=Group Header diff --git a/packages/main/src/themes/FileUploader.css b/packages/main/src/themes/FileUploader.css index cc304159ce58..b69b0bcd7e3c 100644 --- a/packages/main/src/themes/FileUploader.css +++ b/packages/main/src/themes/FileUploader.css @@ -1,61 +1,83 @@ @import "./FormComponents.css"; +@import "./Input.css"; +@import "./InputIcon.css"; :host { display: inline-block; } -.ui5-file-uploader-root { +:host([hide-input]) { + width: max-content; + height: max-content; + box-shadow: none; + background: none; +} + +.ui5-file-uploader-root, +.ui5-file-uploader-form { position: relative; + width: inherit; + height: inherit; } -.ui5-file-uploader-root input[type=file] { - opacity: 0; +:host([hide-input]) .ui5-file-uploader-form { position: absolute; top: 0; left: 0; - height: 100%; width: 100%; - font-size: 0; + height: 100%; + display: block; } -.ui5-file-uploader-root input[type=file]:not([disabled]) { - cursor: pointer; +.ui5-file-uploader-root .ui5-file-uploader-native-input { + position: relative; + display: block; + width: inherit; + height: inherit; + opacity: 0; + font-size: 0; } -.ui5-file-uploader-mask { - display: flex; - align-items: center; +.ui5-file-uploader-display-container { + position: absolute; + top: 0; + inset-inline-start: 0; + width: inherit; + height: inherit; } -.ui5-file-uploader-mask [ui5-input] { - margin-right: 0.25rem; +.ui5-file-uploader-tokenizer, +.ui5-file-uploader-display-input { + position: absolute; + top: 0; + inset-inline-start: 0; + height: inherit; + border: none; + outline: none; + cursor: pointer; } -:host([value-state="None"]:not([disabled]):hover) [ui5-input], -:host(:not([value-state]):not([disabled]):hover) [ui5-input] { - border: var(--_ui5_file_uploader_hover_border); - background-color: var(--sapField_Hover_Background); - box-shadow: var(--sapField_Hover_Shadow); +/* Display parts styles */ +.ui5-file-uploader-close-icon, +.ui5-file-uploader-value-help-icon { + position: absolute; + cursor: pointer; } -:host([value-state="Negative"]:not([disabled]):hover) [ui5-input] { - background-color: var(--_ui5_file_uploader_value_state_error_hover_background_color); - box-shadow: var(--sapField_Hover_InvalidShadow); +.ui5-file-uploader-tokenizer { + max-width: var(--_ui5_file_uploader_tokenizer_width); } -:host([value-state="Critical"]:not([disabled]):hover) [ui5-input] { - background-color: var(--sapField_Hover_Background); - box-shadow: var(--sapField_Hover_WarningShadow); +.ui5-file-uploader-display-input { + width: var(--_ui5_file_uploader_display_input_width); } -:host([value-state="Positive"]:not([disabled]):hover) [ui5-input] { - background-color: var(--sapField_Hover_Background); - box-shadow: var(--sapField_Hover_SuccessShadow); +.ui5-file-uploader-close-icon { + inset-inline-end: var(--_ui5_input_icon_width); } -:host([value-state="Information"]:not([disabled]):hover) [ui5-input] { - background-color: var(--sapField_Hover_Background); - box-shadow: var(--sapField_Hover_InformationShadow); +.ui5-file-uploader-value-help-icon { + inset-inline-end: 0rem; } :host(:not([disabled]):active) [ui5-button] { @@ -64,3 +86,43 @@ color: var(--sapButton_Active_TextColor); text-shadow: none; } + +/* Input styles overrides */ +:host([focused]) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus)::after { + content: var(--ui5_input_focus_pseudo_element_content); + position: absolute; + pointer-events: none; + z-index: 2; + border: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--_ui5_input_focus_outline_color); + border-radius: var(--_ui5_input_focus_border_radius); + top: var(--_ui5_input_focus_offset); + bottom: var(--_ui5_input_focus_offset); + left: var(--_ui5_input_focus_offset); + right: var(--_ui5_input_focus_offset); +} + +:host(:not([value-state]):not([readonly])[focused]:not([opened]):hover), +:host([value-state="None"]:not([readonly])[focused]:not([opened]):hover) { + box-shadow: var(--sapField_Shadow); +} + +:host([value-state="Negative"][focused]:not([opened]):not([readonly]):not([hide-input])) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus)::after { + border-color: var(--_ui5_input_focused_value_state_error_focus_outline_color); +} + +:host([value-state="Critical"][focused]:not([opened]):not([readonly]):not([hide-input])) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus)::after { + border-color: var(--_ui5_input_focused_value_state_warning_focus_outline_color); +} + +:host([value-state="Positive"][focused]:not([opened]):not([readonly]):not([hide-input])) .ui5-file-uploader-root:has(.ui5-file-uploader-native-input:focus)::after { + border-color: var(--_ui5_input_focused_value_state_success_focus_outline_color); +} + +:host([value-state="None"][hide-input]:not([readonly]):hover), +:host([value-state="Negative"][hide-input]:not([readonly]):hover), +:host([value-state="Positive"][hide-input]:not([readonly]):hover), +:host([value-state="Information"][hide-input]:not([readonly]):hover), +:host([value-state="Critical"][hide-input]:not([readonly]):hover) { + box-shadow: none; + background: none; +} diff --git a/packages/main/src/themes/base/FileUploader-parameters.css b/packages/main/src/themes/base/FileUploader-parameters.css index c2998727e19f..eebe6b8aa075 100644 --- a/packages/main/src/themes/base/FileUploader-parameters.css +++ b/packages/main/src/themes/base/FileUploader-parameters.css @@ -1,4 +1,6 @@ :root { --_ui5_file_uploader_hover_border: 1px solid var(--sapField_Hover_BorderColor); --_ui5_file_uploader_value_state_error_hover_background_color: var(--sapField_Hover_Background); + --_ui5_file_uploader_display_input_width: calc(100% - var(--_ui5_input_icon_width)); + --_ui5_file_uploader_tokenizer_width: calc(100% - 2 * var(--_ui5_input_icon_width) - var(--_ui5_input_inner_space_to_tokenizer)); } \ No newline at end of file diff --git a/packages/main/test/pages/FileUploader.html b/packages/main/test/pages/FileUploader.html index fafd6e81cb2d..2442bea6adc5 100644 --- a/packages/main/test/pages/FileUploader.html +++ b/packages/main/test/pages/FileUploader.html @@ -1,8 +1,8 @@ - + - + FileUploader test page @@ -16,188 +16,91 @@
- open - - +
+ Choose files: + Upload -
- Close -
- -
+
-
- - - Upload - -
+
+ Choose files: + + Upload + +
-
- - - Upload - - - - Upload -
Information message. This is a Link. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.
-
Information message 2. This is a Link. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.
-
-
+
+ Choose files: + + + +
-
- - - Upload - -
-
- - - Upload - -
-
- - - Upload - -
-
- - -
-
- - - Upload - -
-
- - - - -
-
- - - - - -
-
- - - Upload File - -
-
- Maximum File Size -
- -
- - Upload - -
-
- - 0 -
-
+
+ Choose files: + + +
-
-
- Form support -
-
- - -
-
- - -
-
- - -
- Submit -
-
-
- Upload files async using fetch -
- - Upload + +
+ Choose files: +
-
- diff --git a/packages/main/test/pages/styles/FileUploader.css b/packages/main/test/pages/styles/FileUploader.css index fe5de4103ef3..4fe8559e2989 100644 --- a/packages/main/test/pages/styles/FileUploader.css +++ b/packages/main/test/pages/styles/FileUploader.css @@ -6,3 +6,9 @@ body > div { border: 1px solid black; padding: 1rem; } + + +.Test:has(button:focus) { + /* font-family: cursive; */ + background-color: red; +} \ No newline at end of file