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}}
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
+ *
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:
+ * change
event is fired)
+ * suggestion-item-select
event is fired)
+ * 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}}"
>
- 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.
*
+ * 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 @@
+++ + +
++ + + ++ + ++ + + + + + + + + + + +
++ + + + +Token is already in the list+