diff --git a/packages/main/bundle.esm.js b/packages/main/bundle.esm.js index 3d7789c2f187..01592500f224 100644 --- a/packages/main/bundle.esm.js +++ b/packages/main/bundle.esm.js @@ -47,6 +47,7 @@ import Dialog from "./dist/Dialog.js"; import FileUploader from "./dist/FileUploader.js"; import Icon from "./dist/Icon.js"; import Input from "./dist/Input.js"; +import MultiInput from "./dist/MultiInput.js"; import Label from "./dist/Label.js"; import Link from "./dist/Link.js"; import Popover from "./dist/Popover.js"; diff --git a/packages/main/src/DatePicker.js b/packages/main/src/DatePicker.js index 99da791fa2ad..276374f08ea6 100644 --- a/packages/main/src/DatePicker.js +++ b/packages/main/src/DatePicker.js @@ -475,8 +475,8 @@ class DatePicker extends UI5Element { return this.shadowRoot.querySelector("ui5-input"); } - _handleInputChange() { - let nextValue = this._getInput().getInputValue(); + async _handleInputChange() { + let nextValue = await this._getInput().getInputValue(); const emptyValue = nextValue === ""; const isValid = emptyValue || this._checkValueValidity(nextValue); @@ -494,8 +494,8 @@ class DatePicker extends UI5Element { this.fireEvent("value-changed", { value: nextValue, valid: isValid }); } - _handleInputLiveChange() { - const nextValue = this._getInput().getInputValue(); + async _handleInputLiveChange() { + const nextValue = await this._getInput().getInputValue(); const emptyValue = nextValue === ""; const isValid = emptyValue || this._checkValueValidity(nextValue); diff --git a/packages/main/src/DateRangePicker.js b/packages/main/src/DateRangePicker.js index 63d3d491de90..5aab31b42616 100644 --- a/packages/main/src/DateRangePicker.js +++ b/packages/main/src/DateRangePicker.js @@ -249,10 +249,10 @@ class DateRangePicker extends DatePicker { return this.placeholder !== undefined ? this.placeholder : this._displayFormat.concat(" ", this.delimiter, " ", this._displayFormat); } - _handleInputChange() { - const nextValue = this._getInput().getInputValue(), - emptyValue = nextValue === "", - isValid = emptyValue || this._checkValueValidity(nextValue); + async _handleInputChange() { + const nextValue = await this._getInput().getInputValue(); + const emptyValue = nextValue === ""; + const isValid = emptyValue || this._checkValueValidity(nextValue); if (isValid) { this._setValue(nextValue); diff --git a/packages/main/src/Input.hbs b/packages/main/src/Input.hbs index 6966a05e1475..97813b3d3212 100644 --- a/packages/main/src/Input.hbs +++ b/packages/main/src/Input.hbs @@ -29,6 +29,7 @@ @keydown="{{_onkeydown}}" @keyup="{{_onkeyup}}" @click={{_click}} + @focusin={{innerFocusIn}} data-sap-no-tab-ref data-sap-focus-ref step="{{step}}" @@ -39,6 +40,8 @@ {{/if}} + {{> postContent }} + {{#if showSuggestions}} {{suggestionsText}} @@ -58,4 +61,5 @@ -{{#*inline "preContent"}}{{/inline}} \ No newline at end of file +{{#*inline "preContent"}}{{/inline}} +{{#*inline "postContent"}}{{/inline}} \ No newline at end of file diff --git a/packages/main/src/Input.js b/packages/main/src/Input.js index 18b0cbb04906..4bfca853078d 100644 --- a/packages/main/src/Input.js +++ b/packages/main/src/Input.js @@ -547,7 +547,7 @@ class Input extends UI5Element { } } - onAfterRendering() { + async onAfterRendering() { if (!this.firstRendering && !isPhone() && this.Suggestions) { const shouldOpenSuggestions = this.shouldOpenSuggestions(); @@ -562,7 +562,8 @@ class Input extends UI5Element { if (!isPhone() && shouldOpenSuggestions) { // Set initial focus to the native input - this.inputDomRef && this.inputDomRef.focus(); + + (await this.getInputDOMRef()).focus(); } } @@ -648,9 +649,7 @@ class Input extends UI5Element { return; } - if (this.popover) { - this.popover.close(); - } + this.closePopover(); this.previousValue = ""; this.focused = false; // invalidating property @@ -677,8 +676,9 @@ class Input extends UI5Element { } async _handleInput(event) { - await this.getInputDOMRef(); - if (event.target === this.inputDomRef) { + const inputDomRef = await this.getInputDOMRef(); + + if (event.target === inputDomRef) { // stop the native event, as the semantic "input" would be fired. event.stopImmediatePropagation(); } @@ -687,7 +687,7 @@ class Input extends UI5Element { - value of the host and the internal input should be differnt in case of actual input - input is called when a key is pressed => keyup should not be called yet */ - const skipFiring = (this.inputDomRef.value === this.value) && isIE() && !this._keyDown && !!this.placeholder; + const skipFiring = (inputDomRef.value === this.value) && isIE() && !this._keyDown && !!this.placeholder; !skipFiring && this.fireEventByAction(this.ACTION_USER_INPUT); @@ -709,8 +709,7 @@ class Input extends UI5Element { async _afterOpenPopover() { // Set initial focus to the native input if (isPhone()) { - await this.getInputDOMRef(); - this.inputDomRef.focus(); + (await this.getInputDOMRef()).focus(); } } @@ -741,18 +740,18 @@ class Input extends UI5Element { } async openPopover() { - this.popover = await this._getPopover(); - if (this.popover) { + const popover = await this._getPopover(); + + if (popover) { this._isPopoverOpen = true; - this.popover.openBy(this); + popover.openBy(this); } } - closePopover() { - if (this.isOpen()) { - this._isPopoverOpen = false; - this.popover && this.popover.close(); - } + async closePopover() { + const popover = await this._getPopover(); + + popover && popover.close(); } async _getPopover() { @@ -791,7 +790,6 @@ class Input extends UI5Element { ? this.valueBeforeItemSelection !== itemText : this.value !== itemText; this.hasSuggestionItemSelected = true; - this.fireEvent(this.EVENT_SUGGESTION_ITEM_SELECT, { item }); if (fireInput) { this.value = itemText; @@ -799,6 +797,8 @@ class Input extends UI5Element { this.fireEvent(this.EVENT_INPUT); this.fireEvent(this.EVENT_CHANGE); } + + this.fireEvent(this.EVENT_SUGGESTION_ITEM_SELECT, { item }); } previewSuggestion(item) { @@ -839,7 +839,7 @@ class Input extends UI5Element { return; } - const inputValue = this.getInputValue(); + const inputValue = await this.getInputValue(); const isSubmit = action === this.ACTION_ENTER; const isUserInput = action === this.ACTION_USER_INPUT; @@ -875,28 +875,22 @@ class Input extends UI5Element { } } - getInputValue() { - const inputDOM = this.getDomRef(); - if (inputDOM) { - return this.inputDomRef.value; + async getInputValue() { + const domRef = this.getDomRef(); + + if (domRef) { + return (await this.getInputDOMRef()).value; } return ""; } async getInputDOMRef() { - let inputDomRef; - - if (isPhone() && this.Suggestions) { + if (isPhone() && this.Suggestions && this.suggestionItems.length) { await this.Suggestions._respPopover(); - inputDomRef = this.Suggestions && this.Suggestions.responsivePopover.querySelector(".ui5-input-inner-phone"); - } - - if (!inputDomRef) { - inputDomRef = this.getDomRef().querySelector(`#${this.getInputId()}`); + return this.Suggestions && this.Suggestions.responsivePopover.querySelector(".ui5-input-inner-phone"); } - this.inputDomRef = inputDomRef; - return this.inputDomRef; + return this.getDomRef().querySelector(`input`); } getLabelableElementId() { diff --git a/packages/main/src/MultiComboBox.hbs b/packages/main/src/MultiComboBox.hbs index dd93118ecbdc..b93c1f0148c6 100644 --- a/packages/main/src/MultiComboBox.hbs +++ b/packages/main/src/MultiComboBox.hbs @@ -21,12 +21,12 @@ {{#each items}} {{#if this.selected}} - {{this.text}} {{/if}} {{/each}} diff --git a/packages/main/src/MultiComboBox.js b/packages/main/src/MultiComboBox.js index 8219928a6ba3..8bd958ce3034 100644 --- a/packages/main/src/MultiComboBox.js +++ b/packages/main/src/MultiComboBox.js @@ -410,7 +410,7 @@ class MultiComboBox extends UI5Element { tokenizer.tokens.forEach(token => { token.selected = false; }); - this._tokenizer.contentDom.scrollLeft = 0; + this._tokenizer.scrollToStart(); if (tokensCount === 0 && this._deleting) { setTimeout(() => { diff --git a/packages/main/src/MultiInput.hbs b/packages/main/src/MultiInput.hbs new file mode 100644 index 000000000000..a637b5eb0d46 --- /dev/null +++ b/packages/main/src/MultiInput.hbs @@ -0,0 +1,29 @@ +{{>include "./Input.hbs"}} + +{{#*inline "preContent"}} + + + +{{/inline}} + + +{{#*inline "postContent"}} + {{#if showValueHelpIcon}} + + {{/if}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/MultiInput.js b/packages/main/src/MultiInput.js new file mode 100644 index 000000000000..3824f586c285 --- /dev/null +++ b/packages/main/src/MultiInput.js @@ -0,0 +1,202 @@ +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import Input from "./Input.js"; +import MultiInputTemplate from "./generated/templates/MultiInputTemplate.lit.js"; +import styles from "./generated/themes/MultiInput.css.js"; +import Token from "./Token.js"; +import Tokenizer from "./Tokenizer.js"; +import Icon from "./Icon.js"; + +/** + * @public + */ +const metadata = { + tag: "ui5-multi-input", + properties: /** @lends sap.ui.webcomponents.main.MultiInput.prototype */ { + /** + * Determines whether a value help icon will be should in the end of the input. + * Pressing the icon will fire value-help-icon-press event. + * + * @type {boolean} + * @defaultvalue false + * @public + */ + showValueHelpIcon: { + type: Boolean, + }, + + /** + * Indicates whether the tokenizer is expanded or collapsed(shows the n more label) + * @private + */ + expandedTokenizer: { + type: Boolean, + }, + }, + slots: /** @lends sap.ui.webcomponents.main.MultiInput.prototype */ { + /** + * Defines the ui5-multi-input tokens. + *

+ * Example:
+ * <ui5-multi-input>
+ *     <ui5-token slot="tokens" text="Token 1"></ui5-token>
+ *     <ui5-token slot="tokens" text="Token 2"></ui5-token>
+ * </ui5-multi-input> + *

+ * + * @type {HTMLElement[]} + * @slot + * @public + */ + tokens: { + type: HTMLElement, + multiple: true, + }, + }, + events: /** @lends sap.ui.webcomponents.main.MultiInput.prototype */ { + /** + * Fired when value state icon is pressed. + * + * @event sap.ui.webcomponents.main.MultiInput#value-help-icon-press + * @public + */ + "value-help-icon-press": {}, + + /** + * Fired when a token is about to be deleted. + * + * @event sap.ui.webcomponents.main.MultiInput#token-delete + * @param {HTMLElement} token deleted token. + * @public + */ + "token-delete": { + detail: { + token: { type: HTMLElement }, + }, + }, + }, +}; + +/** + * @class + *

Overview

+ * A ui5-multi-input field allows the user to enter multiple values, which are displayed as ui5-token. + * + * User can choose interaction for creating tokens. + * Fiori Guidelines say that user should create tokens when: + * + * + *

ES6 Module Import

+ * + * import "@ui5/webcomponents/dist/MultiInput"; + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.MultiInput + * @extends Input + * @tagname ui5-multi-input + * @appenddocs Token + * @since 1.0.0-rc.9 + * @public + */ +class MultiInput extends Input { + static get metadata() { + return metadata; + } + + static get render() { + return litRender; + } + + static get template() { + return MultiInputTemplate; + } + + static get styles() { + return [Input.styles, styles]; + } + + valueHelpPress(event) { + this.closePopover(); + this.fireEvent("value-help-icon-press", {}); + } + + showMorePress(event) { + this.expandedTokenizer = false; + this.focus(); + } + + tokenDelete(event) { + this.fireEvent("token-delete", { + token: event.detail.ref, + }); + + this.focus(); + } + + valueHelpMouseDown(event) { + this.closePopover(); + this.tokenizer.closeMorePopover(); + this._valueHelpIconPressed = true; + event.target.focus(); + } + + _tokenizerFocusOut(event) { + if (!this.contains(event.relatedTarget)) { + this.tokenizer.scrollToStart(); + } + } + + valueHelpMouseUp(event) { + setTimeout(() => { + this._valueHelpIconPressed = false; + }, 0); + } + + innerFocusIn() { + this.expandedTokenizer = true; + } + + _onfocusout(event) { + super._onfocusout(event); + const relatedTarget = event.relatedTarget; + const insideDOM = this.contains(relatedTarget); + const insideShadowDom = this.shadowRoot.contains(relatedTarget); + + if (!insideDOM && !insideShadowDom) { + this.expandedTokenizer = false; + } + } + + shouldOpenSuggestions() { + const parent = super.shouldOpenSuggestions(); + const valueHelpPressed = this._valueHelpIconPressed; + const nonEmptyValue = this.value !== ""; + + return parent && nonEmptyValue && !valueHelpPressed; + } + + lastItemDeleted() { + setTimeout(() => { + this.focus(); + }, 0); + } + + get tokenizer() { + return this.shadowRoot.querySelector("ui5-tokenizer"); + } + + static async onDefine() { + await Promise.all([ + Tokenizer.define(), + Token.define(), + Icon.define(), + ]); + } +} + +MultiInput.define(); + +export default MultiInput; diff --git a/packages/main/src/TimePicker.js b/packages/main/src/TimePicker.js index b24e8440af29..21eb0f2b3f70 100644 --- a/packages/main/src/TimePicker.js +++ b/packages/main/src/TimePicker.js @@ -330,18 +330,18 @@ class TimePicker extends UI5Element { } } - _handleInputChange() { - const nextValue = this._getInput().getInputValue(), - isValid = this.isValid(nextValue); + async _handleInputChange() { + const nextValue = await this._getInput().getInputValue(); + const isValid = this.isValid(nextValue); this.setValue(nextValue); this.fireEvent("change", { value: nextValue, valid: isValid }); this.fireEvent("value-changed", { value: nextValue, valid: isValid }); } - _handleInputLiveChange() { - const nextValue = this._getInput().getInputValue(), - isValid = this.isValid(nextValue); + async _handleInputLiveChange() { + const nextValue = await this._getInput().getInputValue(); + const isValid = this.isValid(nextValue); this.value = nextValue; this.setSlidersValue(); diff --git a/packages/main/src/Token.hbs b/packages/main/src/Token.hbs index 38f3f3a38f78..d99ecdf4ef17 100644 --- a/packages/main/src/Token.hbs +++ b/packages/main/src/Token.hbs @@ -7,7 +7,7 @@ role="option" aria-selected="{{selected}}" > - + {{this.text}} {{#unless readonly}} ui5-token. - *

- * Note: Аlthough this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design. - * - * @type {Node[]} - * @slot - * @public - */ - "default": { - type: Node, - }, - }, properties: /** @lends sap.ui.webcomponents.main.Token.prototype */ { /** - * Defines whether the ui5-token is selected or not. + * Defines the text of the token. * - * @type {boolean} + * @type {string} + * @defaultvalue "" * @public */ - selected: { type: Boolean }, + text: { type: String }, /** * Defines whether the ui5-token is read-only. @@ -59,8 +46,11 @@ const metadata = { */ readonly: { type: Boolean }, - _tabIndex: { type: String, defaultValue: "-1", noAttribute: true }, - + /** + * Set by the tokenizer when a token is in the "more" area (overflowing) + * @type {boolean} + * @private + */ overflows: { type: Boolean }, }, @@ -72,7 +62,7 @@ const metadata = { * @event * @param {boolean} backSpace indicates whether token is deleted by backspace key * @param {boolean} delete indicates whether token is deleted by delete key - * @public + * @private */ "delete": { detail: { @@ -80,14 +70,6 @@ const metadata = { "delete": { type: Boolean }, }, }, - - /** - * Fired when the a token is selected by user interaction with mouse, clicking space or enter - * - * @event - * @public - */ - select: {}, }, }; @@ -98,13 +80,16 @@ const metadata = { * * Tokens are small items of information (similar to tags) that mainly serve to visualize previously selected items. * + *

ES6 Module Import

+ * + * import "@ui5/webcomponents/dist/Token.js"; * @constructor * @author SAP SE * @alias sap.ui.webcomponents.main.Token - * @extends UI5Element + * @extends sap.ui.webcomponents.base.UI5Element * @tagname ui5-token - * @usestextcontent - * @private + * @since 1.0.0-rc.9 + * @public */ class Token extends UI5Element { static get metadata() { diff --git a/packages/main/src/Tokenizer.js b/packages/main/src/Tokenizer.js index 169ed26fc93f..326fa6d0884c 100644 --- a/packages/main/src/Tokenizer.js +++ b/packages/main/src/Tokenizer.js @@ -3,10 +3,11 @@ import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; import ScrollEnablement from "@ui5/webcomponents-base/dist/delegate/ScrollEnablement.js"; -import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; +import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import TokenizerTemplate from "./generated/templates/TokenizerTemplate.lit.js"; -import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_LABEL } from "./generated/i18n/i18n-defaults.js"; +import TokenizerPopoverTemplate from "./generated/templates/TokenizerPopoverTemplate.lit.js"; +import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_LABEL, TOKENIZER_POPOVER_REMOVE } from "./generated/i18n/i18n-defaults.js"; // Styles import styles from "./generated/themes/Tokenizer.css.js"; @@ -27,6 +28,7 @@ const metadata = { }, properties: /** @lends sap.ui.webcomponents.main.Tokenizer.prototype */ { showMore: { type: Boolean }, + disabled: { type: Boolean }, /** @@ -35,6 +37,13 @@ const metadata = { * @private */ expanded: { type: Boolean }, + + morePopoverOpener: { type: Object }, + + popoverMinWidth: { + type: Integer, + }, + _nMoreCount: { type: Integer }, }, events: /** @lends sap.ui.webcomponents.main.Tokenizer.prototype */ { @@ -84,6 +93,10 @@ class Tokenizer extends UI5Element { return styles; } + static get staticAreaTemplate() { + return TokenizerPopoverTemplate; + } + _handleResize() { this._nMoreCount = this.overflownTokens.length; } @@ -98,6 +111,13 @@ class Tokenizer extends UI5Element { this.i18nBundle = getI18nBundle("@ui5/webcomponents"); } + async onBeforeRendering() { + if (this.showPopover && !this._getTokens().length) { + const popover = await this.getPopover(); + popover.close(); + } + } + onEnterDOM() { ResizeHandler.register(this.shadowRoot.querySelector(".ui5-tokenizer--content"), this._resizeHandler); } @@ -106,20 +126,38 @@ class Tokenizer extends UI5Element { ResizeHandler.deregister(this.shadowRoot.querySelector(".ui5-tokenizer--content"), this._resizeHandler); } + async _openOverflowPopover() { + if (this.showPopover) { + const popover = await this.getPopover(); + + popover.open(this.morePopoverOpener || this); + } + + this.fireEvent("show-more-items-press"); + } + + _getTokens() { + return this.getSlottedNodes("tokens"); + } + + get _tokens() { + return this.getSlottedNodes("tokens"); + } + + get showPopover() { + return Object.keys(this.morePopoverOpener).length; + } + _getVisibleTokens() { if (this.disabled) { return []; } - return this.tokens.filter((token, index) => { - return index < (this.tokens.length - this._nMoreCount); + return this._tokens.filter((token, index) => { + return index < (this._tokens.length - this._nMoreCount); }); } - _openOverflowPopover() { - this.fireEvent("show-more-items-press"); - } - onAfterRendering() { this._nMoreCount = this.overflownTokens.length; this._scrollEnablement.scrollContainer = this.expanded ? this.contentDom : this; @@ -134,10 +172,16 @@ class Tokenizer extends UI5Element { this.fireEvent("token-delete", { ref: event.target }); } + itemDelete(event) { + const token = event.detail.item.tokenRef; + + this.fireEvent("token-delete", { ref: token }); + } + /* Keyboard handling */ _updateAndFocus() { - if (this.tokens.length) { + if (this._tokens.length) { this._itemNav.update(); setTimeout(() => { @@ -156,14 +200,29 @@ class Tokenizer extends UI5Element { } } - get showNMore() { - return !this.expanded && this.showMore && this.overflownTokens.length; + /** + * Scrolls the container of the tokens to its beginning. + * This method is used by MultiInput and MultiComboBox. + * @private + */ + scrollToStart() { + this.contentDom.scrollLeft = 0; + } + + async closeMorePopover() { + const popover = await this.getPopover(); + + popover.close(); } get _nMoreText() { return this.i18nBundle.getText(MULTIINPUT_SHOW_MORE_TOKENS, [this._nMoreCount]); } + get showNMore() { + return !this.expanded && this.showMore && this.overflownTokens.length; + } + get contentDom() { return this.shadowRoot.querySelector(".ui5-tokenizer--content"); } @@ -172,12 +231,16 @@ class Tokenizer extends UI5Element { return this.i18nBundle.getText(TOKENIZER_ARIA_LABEL); } + get morePopoverTitle() { + return this.i18nBundle.getText(TOKENIZER_POPOVER_REMOVE); + } + get overflownTokens() { if (!this.contentDom) { return []; } - return this.tokens.filter(token => { + return this._getTokens().filter(token => { const parentRect = this.contentDom.getBoundingClientRect(); const tokenRect = token.getBoundingClientRect(); const tokenLeft = tokenRect.left + tokenRect.width; @@ -194,7 +257,7 @@ class Tokenizer extends UI5Element { wrapper: { "ui5-tokenizer-root": true, "ui5-tokenizer-nmore--wrapper": this.showMore, - "ui5-tokenizer-no-padding": !this.tokens.length, + "ui5-tokenizer-no-padding": !this._getTokens().length, }, content: { "ui5-tokenizer--content": true, @@ -203,9 +266,21 @@ class Tokenizer extends UI5Element { }; } + get styles() { + return { + popover: { + "min-width": `${this.popoverMinWidth}px`, + }, + }; + } + static async onDefine() { await fetchI18nBundle("@ui5/webcomponents"); } + + async getPopover() { + return (await this.getStaticAreaItemDomRef()).querySelector("ui5-responsive-popover"); + } } Tokenizer.define(); diff --git a/packages/main/src/TokenizerPopover.hbs b/packages/main/src/TokenizerPopover.hbs new file mode 100644 index 000000000000..d06435867c61 --- /dev/null +++ b/packages/main/src/TokenizerPopover.hbs @@ -0,0 +1,21 @@ + + + {{#each _tokens}} + + {{this.text}} + + {{/each}} + + \ No newline at end of file diff --git a/packages/main/src/features/InputSuggestions.js b/packages/main/src/features/InputSuggestions.js index c60d7f4943e0..705073858e5c 100644 --- a/packages/main/src/features/InputSuggestions.js +++ b/packages/main/src/features/InputSuggestions.js @@ -118,7 +118,10 @@ class Suggestions { async open() { this.responsivePopover = await this._respPopover(); this._beforeOpen(); - this.responsivePopover.open(this._getComponent()); + + if (this._getItems().length) { + this.responsivePopover.open(this._getComponent()); + } } async close(preventFocusRestore = false) { diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index bb8f46423027..4b8ffe8461c2 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -346,6 +346,9 @@ TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS=Contains {0} tokens #XACT: ARIA announcement for tokenizer label TOKENIZER_ARIA_LABEL=Tokenizer +#XFLD: Remove label for Tokenizer's Dialog on phone +TOKENIZER_POPOVER_REMOVE=Remove + #XTOL: text that is appended to the tooltips of input fields etc. which are marked to have an error VALUE_STATE_ERROR=Invalid entry diff --git a/packages/main/src/themes/MultiInput.css b/packages/main/src/themes/MultiInput.css new file mode 100644 index 000000000000..e218c1ceda83 --- /dev/null +++ b/packages/main/src/themes/MultiInput.css @@ -0,0 +1,14 @@ +@import "./InputIcon.css"; + +.ui5-multi-input-tokenizer { + max-width: calc(100% - 3rem - var(--_ui5_input_icon_min_width)); + border: none; + width: auto; + min-width: 0px; + height: 100%; +} + +/* Workaround for IE */ +ui5-multi-input ui5-tokenizer { + flex: 3; +} \ No newline at end of file diff --git a/packages/main/src/themes/ResponsivePopover.css b/packages/main/src/themes/ResponsivePopover.css index 2a39e038a5c4..16b6fd26eac6 100644 --- a/packages/main/src/themes/ResponsivePopover.css +++ b/packages/main/src/themes/ResponsivePopover.css @@ -17,7 +17,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 0 0 0 1rem; + padding: 0 1rem; box-shadow: var(--sapContent_HeaderShadow); } diff --git a/packages/main/test/pages/MultiInput.html b/packages/main/test/pages/MultiInput.html new file mode 100644 index 000000000000..bbdbcb6200ab --- /dev/null +++ b/packages/main/test/pages/MultiInput.html @@ -0,0 +1,258 @@ + + + + + + + + ui5-multi-input + + + + + + + + + + + + +
+ +

Basic API

+ +

Empty Multi Input

+ + +
+
+ +

Multi Input with Value Help Icon

+ + +
+
+ +

Multi Input with Value Help Icon and a custom icon

+ + + + +
+
+ +

Multi Input with 1 token

+ + + + +
+
+ +

Multi Input with 11 tokens (overflowing)

+ + + + + + + + + + + + + + +
+
+ +

Multi Input with 11 tokens (overflowing) and value help icon

+ + + + + + + + + + + + + + + +
+
+ + +
+ +
+

Tokens

+ +

Multi Input with 1 token

+ + + +
+
+ + Add more tokens + +
+
+ +

Multi Input with 5 tokens

+ + + + + + + + +
+
+ + + Add more tokens +
+ +
+

Tokens + Suggestions

+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/packages/main/test/pages/MultiInput_Suggestions.html b/packages/main/test/pages/MultiInput_Suggestions.html new file mode 100644 index 000000000000..fb70a7e63055 --- /dev/null +++ b/packages/main/test/pages/MultiInput_Suggestions.html @@ -0,0 +1,191 @@ + + + + + + + + ui5-multi-input + + + + + + + + + + + + + +
+

Tokens + Suggestions

+ + + + + + + + + + + + + + + + + + + + +
+ +
+ +

Token Creation on enter or focus out

+ + +
+ +
+ +

Token Creation onChange (unique)

+ + +
Token is already in the list
+
+
+ + + + + \ No newline at end of file diff --git a/packages/main/test/samples/MultiInput.sample.html b/packages/main/test/samples/MultiInput.sample.html new file mode 100644 index 000000000000..0f85ce8fe791 --- /dev/null +++ b/packages/main/test/samples/MultiInput.sample.html @@ -0,0 +1,171 @@ + + +
+

Multi Input

+
+ +
@ui5/webcomponents
+ +
<ui5-multi-input>
+ +
+

Basic Multi Input

+
+ + +
+

+<ui5-multi-input value="basic input"></ui5-multi-input>
+<ui5-multi-input show-value-help-icon value="value help icon"></ui5-multi-input>
+	
+
+ +
+

Multi Input with tokens

+
+ + + + + + + + + + + + + + + + + + +
+

+<ui5-multi-input>
+	<ui5-token slot="tokens" text="Bulgaria"></ui5-token>
+</ui5-multi-input>
+
+<ui5-multi-input>
+	<ui5-token slot="tokens" text="Argentina"></ui5-token>
+	<ui5-token slot="tokens" text="Bulgaria"></ui5-token>
+	<ui5-token slot="tokens" text="England"></ui5-token>
+	<ui5-token slot="tokens" text="Finland"></ui5-token>
+	<ui5-token slot="tokens" text="Germany"></ui5-token>
+	<ui5-token slot="tokens" text="Hungary"></ui5-token>
+	<ui5-token slot="tokens" text="Italy"></ui5-token>
+	<ui5-token slot="tokens" text="Luxembourg"></ui5-token>
+	<ui5-token slot="tokens" text="Mexico"></ui5-token>
+	<ui5-token slot="tokens" text="Philippines"></ui5-token>
+	<ui5-token slot="tokens" text="Sweden"></ui5-token>
+	<ui5-token slot="tokens" text="USA"></ui5-token>
+</ui5-multi-input>
+	
+
+ +
+

Multi Input and token creation onChange

+
+ +
Token is already in the list
+
+ + +
+

+<ui5-multi-input show-suggestions id="token-unique">
+		<div slot="valueStateMessage">Token is already in the list</div>
+</ui5-multi-input>
+
+<script>
+	var createTokenFromText = function (text) {
+		var token = document.createElement("ui5-token");
+
+		token.setAttribute("text", text);
+		token.setAttribute("slot", "tokens");
+
+		return token;
+	};
+
+	document.getElementById("token-unique").addEventListener("change", function (event) {
+		if (!event.target.value) {
+			return;
+		};
+
+		var isDuplicate = event.target.tokens.some(function(token) {
+			return token.text === event.target.value
+		});
+
+		if (isDuplicate) {
+			event.target.valueState = "Error";
+
+			setTimeout(function () {
+				event.target.valueState = "Normal";
+			}, 2000);
+
+			return;
+		}
+
+		event.target.appendChild(createTokenFromText(event.target.value));
+
+		event.target.value = "";
+	});
+</script>
+	
+
+ + + + diff --git a/packages/main/test/specs/Input.spec.js b/packages/main/test/specs/Input.spec.js index d47ebe700aa5..f2134e49eb6e 100644 --- a/packages/main/test/specs/Input.spec.js +++ b/packages/main/test/specs/Input.spec.js @@ -234,6 +234,7 @@ describe("Input general interaction", () => { const listItem = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover").$("ui5-li-suggestion-item"); nativeInput.click(); + nativeInput.keys("a"); assert.strictEqual(input.getSize('width'), listItem.getSize('width')); }) @@ -257,7 +258,7 @@ describe("Input general interaction", () => { const inputShadowRef = browser.$("#inputError").shadow$("input"); const staticAreaItemClassName = browser.getStaticAreaItemClassName("#inputError"); const popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-popover"); - const respPopover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover .ui5-responsive-popover-header"); + const respPopover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover").$(".ui5-responsive-popover-header"); inputShadowRef.click(); @@ -304,6 +305,8 @@ describe("Input general interaction", () => { // act inputItemPreview.click(); + inputItemPreview.keys("c"); + inputItemPreview.keys("ArrowDown"); // assert diff --git a/packages/main/test/specs/MultiInput.spec.js b/packages/main/test/specs/MultiInput.spec.js new file mode 100644 index 000000000000..fd647234d9eb --- /dev/null +++ b/packages/main/test/specs/MultiInput.spec.js @@ -0,0 +1,104 @@ +const assert = require("chai").assert; + +const getTokenizerPopoverId = (inputId) => { + return browser.execute(async (inputId) => { + const input = await document.querySelector(`#${inputId}`); + const staticAreaItem = await (input.shadowRoot.querySelector("ui5-tokenizer").getStaticAreaItemDomRef()); + + return staticAreaItem.host.classList[0]; + }, inputId); +} + +describe("MultiInput general interaction", () => { + browser.url("http://localhost:8080/test-resources/pages/MultiInput.html"); + + it("tests expanding of tokenizer", () => { + const basic = $("#basic-overflow"); + const basicInner = basic.shadow$("input"); + const basicTokenizer = basic.shadow$("ui5-tokenizer"); + + basicInner.click(); + basicInner.keys("Tab"); + + assert.ok(!basicTokenizer.getProperty("expanded"), "Tokenizer should not be expanded"); + }); + + it ("tests opening of tokenizer Popover", () => { + const tokenizer = $("#basic-overflow").shadow$("ui5-tokenizer"); + const nMoreLable = tokenizer.shadow$(".ui5-tokenizer-more-text"); + + nMoreLable.click(); + + const rpoClassName = getTokenizerPopoverId("basic-overflow"); + const rpo = $(`.${rpoClassName}`).shadow$("ui5-responsive-popover"); + + assert.ok(rpo.getProperty("opened"), "More Popover should be open"); + }); + + it ("fires value help icon press", () => { + const lable = $("#basic-event-listener"); + const icon = $("#basic-overflow-and-icon").shadow$("ui5-icon"); + + assert.strictEqual(lable.getText(), "", "event is not fired"); + + icon.click(); + + assert.strictEqual(lable.getText(), "value help icon press", "value help press event is fired"); + }); + + it ("adds a token to multi input", () => { + const mi = $("#single-token"); + const btn = $("#add-to-single"); + + assert.strictEqual(mi.$$("ui5-token").length, 1, "should have 1 token"); + $("#suggestion-token").scrollIntoView(); + + assert.ok(!mi.$$("ui5-token")[0].getProperty("overflows"), "Token should not overflow"); + + btn.click(); + + assert.strictEqual(mi.$$("ui5-token").length, 2, "should have 2 tokens"); + assert.ok(!mi.$$("ui5-token")[0].getProperty("overflows"), "Token should not overflow"); + assert.ok(!mi.$$("ui5-token")[1].getProperty("overflows"), "Token should not overflow"); + }); + + it ("adds an overflowing token to multi input", () => { + const mi = $("#multiple-token"); + const btn = $("#add-to-multiple"); + + assert.strictEqual(mi.$$("ui5-token").length, 5, "should have 5 token"); + $("#suggestion-token").scrollIntoView(); + + assert.ok(!mi.$$("ui5-token")[0].getProperty("overflows"), "Token should not overflow"); + + for (let i = 1; i <= 4; i++) { + assert.ok(mi.$$("ui5-token")[i].getProperty("overflows"), "Token should overflow"); + } + + btn.click(); + + assert.strictEqual(mi.$$("ui5-token").length, 6, "should have 6 tokens"); + + for (let i = 1; i <= 5; i++) { + assert.ok(mi.$$("ui5-token")[i].getProperty("overflows"), "Token should overflow"); + } + }); + + it ("adds a token after selection change", () => { + const mi = $("#suggestion-token"); + const input = mi.shadow$("input"); + const staticAreaItemClassName = browser.getStaticAreaItemClassName("#suggestion-token"); + const popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + + input.click(); + input.keys("c"); + + assert.ok(popover.getProperty("opened"), "Suggestion Popovoer is open"); + assert.strictEqual(mi.$$("ui5-token").length, 0, "0 tokens"); + + popover.$("ui5-li-suggestion-item").click(); + + assert.ok(!popover.getProperty("opened"), "Suggestion Popovoer is closed"); + assert.strictEqual(mi.$$("ui5-token").length, 1, "a token is added after selection"); + }); +}); diff --git a/packages/playground/build-scripts/samples-prepare.js b/packages/playground/build-scripts/samples-prepare.js index aa0a25141921..023f215e5c33 100644 --- a/packages/playground/build-scripts/samples-prepare.js +++ b/packages/playground/build-scripts/samples-prepare.js @@ -19,7 +19,8 @@ const newComponents = [ "Tree", "RatingIndicator", "SideNavigation", - "ProgressIndicator", + "ProgressIndicator", + "MultiInput" ]; packages.forEach(package => {