diff --git a/packages/main/cypress/specs/MultiInput.cy.tsx b/packages/main/cypress/specs/MultiInput.cy.tsx
index 7734b4d81319..2406d0e60cee 100644
--- a/packages/main/cypress/specs/MultiInput.cy.tsx
+++ b/packages/main/cypress/specs/MultiInput.cy.tsx
@@ -1502,6 +1502,145 @@ describe("Keyboard handling", () => {
cy.get("@changeSpy")
.should("have.been.calledOnce");
});
+
+ it("should focus last token on ArrowLeft at start of input, keep suggestions open, and not fire change event", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ const changeSpy = cy.stub().as("changeSpy");
+
+ cy.get("[ui5-multi-input]")
+ .then(multiInput => {
+ multiInput[0].addEventListener("ui5-change", changeSpy);
+ });
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ cy.get("@input")
+ .realClick();
+
+ cy.realType("a");
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .ui5ResponsivePopoverOpened();
+
+ cy.realPress("ArrowLeft"); // cursor: pos 1 → pos 0
+ cy.realPress("ArrowLeft"); // cursor at pos 0 → focuses last token
+
+ cy.get("[ui5-token]")
+ .should("have.length", 1);
+
+ cy.get("[ui5-token]")
+ .should("be.focused");
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("[ui5-responsive-popover]")
+ .ui5ResponsivePopoverOpened();
+
+ cy.get("@changeSpy")
+ .should("not.have.been.called");
+ });
+
+ it("should fire change event when returning from tokenizer to input via ArrowRight and pressing Tab", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ const changeSpy = cy.stub().as("changeSpy");
+
+ cy.get("[ui5-multi-input]")
+ .then(multiInput => {
+ multiInput[0].addEventListener("ui5-change", changeSpy);
+ });
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ cy.get("@input")
+ .realClick();
+
+ cy.realType("a");
+
+ // focus last token
+ cy.realPress("ArrowLeft");
+ cy.realPress("ArrowLeft");
+
+ cy.get("[ui5-token]")
+ .should("be.focused");
+
+ // return to input
+ cy.realPress("ArrowRight");
+
+ cy.get("[ui5-multi-input]")
+ .should("be.focused");
+
+ cy.realPress("Tab");
+
+ cy.get("@changeSpy")
+ .should("have.been.calledOnce");
+ });
+
+ it("should fire change event when returning from tokenizer to input via Tab and pressing Enter", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ const changeSpy = cy.stub().as("changeSpy");
+
+ cy.get("[ui5-multi-input]")
+ .then(multiInput => {
+ multiInput[0].addEventListener("ui5-change", changeSpy);
+ });
+
+ cy.get("[ui5-multi-input]")
+ .shadow()
+ .find("input")
+ .as("input");
+
+ cy.get("@input")
+ .realClick();
+
+ cy.realType("b");
+
+ // focus last token
+ cy.realPress("ArrowLeft");
+ cy.realPress("ArrowLeft");
+
+ cy.get("[ui5-token]")
+ .should("be.focused");
+
+ // return to input
+ cy.realPress("Tab");
+
+ cy.get("[ui5-multi-input]")
+ .should("be.focused");
+
+ cy.realPress("Enter");
+
+ cy.get("@changeSpy")
+ .should("have.been.calledOnce");
+ });
});
describe("MultiInput Composition", () => {
diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts
index 9a5f995204d6..2b16598d01f6 100644
--- a/packages/main/src/Input.ts
+++ b/packages/main/src/Input.ts
@@ -631,8 +631,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement
previousValue: string;
firstRendering: boolean;
typedInValue: string;
- lastConfirmedValue: string
- isTyping: boolean
+ lastConfirmedValue: string;
+ isTyping: boolean;
_handleResizeBound: ResizeObserverCallback;
_shouldAutocomplete?: boolean;
_enterKeyDown?: boolean;
diff --git a/packages/main/src/MultiInput.ts b/packages/main/src/MultiInput.ts
index d5a99d692714..bb455b1d33e4 100644
--- a/packages/main/src/MultiInput.ts
+++ b/packages/main/src/MultiInput.ts
@@ -13,7 +13,8 @@ import {
isHome,
isEnd,
isDown,
-
+ isEnter,
+ isTabNext,
} from "@ui5/webcomponents-base/dist/Keys.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
@@ -153,6 +154,8 @@ class MultiInput extends Input implements IFormInputElement {
_skipOpenSuggestions: boolean;
_valueHelpIconPressed: boolean;
+ _focusInTokenizer: boolean;
+ _returningFromTokenizer: boolean;
get formValidityMessage() {
return MultiInput.i18nBundle.getText(FORM_MIXED_TEXTFIELD_REQUIRED);
@@ -188,6 +191,8 @@ class MultiInput extends Input implements IFormInputElement {
// Prevent suggestions' opening.
this._skipOpenSuggestions = false;
this._valueHelpIconPressed = false;
+ this._focusInTokenizer = false;
+ this._returningFromTokenizer = false;
}
valueHelpPress() {
@@ -225,6 +230,10 @@ class MultiInput extends Input implements IFormInputElement {
if (!this.contains(e.relatedTarget as HTMLElement) && !this.shadowRoot!.contains(e.relatedTarget as HTMLElement)) {
this.tokenizer._tokens.forEach(token => { token.selected = false; });
}
+ if (this.shadowRoot!.contains(e.relatedTarget as HTMLElement)) {
+ this._returningFromTokenizer = true;
+ }
+ this._focusInTokenizer = false;
}
valueHelpMouseUp() {
@@ -248,6 +257,7 @@ class MultiInput extends Input implements IFormInputElement {
_onkeydown(e: KeyboardEvent) {
!this._isComposing && super._onkeydown(e);
+ this._isKeyNavigation = true;
const target = e.target as HTMLInputElement;
const isHomeInBeginning = isHome(e) && target.selectionStart === 0;
@@ -269,9 +279,16 @@ class MultiInput extends Input implements IFormInputElement {
this._skipOpenSuggestions = false;
+ if ((isEnter(e) || isTabNext(e)) && this.previousValue !== this.value) {
+ this._handleChange();
+ return;
+ }
+
if (isShow(e)) {
this.valueHelpPress();
}
+
+ this._isKeyNavigation = false;
}
_onTokenizerKeydown(e: KeyboardEvent) {
@@ -281,6 +298,7 @@ class MultiInput extends Input implements IFormInputElement {
const lastTokenIndex = this.tokens.length - 1;
if (e.target === this.tokens[lastTokenIndex] && this.tokens[lastTokenIndex] === document.activeElement) {
+ this._returningFromTokenizer = true;
setTimeout(() => {
this.focus();
}, 0);
@@ -296,9 +314,25 @@ class MultiInput extends Input implements IFormInputElement {
// selectionStart property applies only to inputs of types text, search, URL, tel, and password
if (((cursorPosition === null && !this.value) || cursorPosition === 0) && lastToken) {
e.preventDefault();
- lastToken.focus();
- this.tokenizer._itemNav.setCurrentItem(lastToken);
+ this._focusToken(lastToken);
+ }
+ }
+
+ _focusToken(tokenToFocus: IToken) {
+ this._focusInTokenizer = true;
+ tokenToFocus.focus();
+ this.tokenizer._itemNav.setCurrentItem(tokenToFocus);
+ }
+
+ /**
+ * @override
+ */
+ _handleChange() {
+ if (this._focusInTokenizer) {
+ return;
}
+
+ super._handleChange();
}
_handleBackspace(e: KeyboardEvent) {
@@ -308,8 +342,7 @@ class MultiInput extends Input implements IFormInputElement {
// Only move focus to the last token if the input is empty
if (!this.value && lastToken) {
e.preventDefault();
- lastToken.focus();
- this.tokenizer._itemNav.setCurrentItem(lastToken);
+ this._focusToken(lastToken);
}
}
@@ -319,9 +352,7 @@ class MultiInput extends Input implements IFormInputElement {
if (firstToken) {
e.preventDefault();
-
- firstToken.focus();
- this.tokenizer._itemNav.setCurrentItem(firstToken);
+ this._focusToken(firstToken);
}
}
@@ -347,7 +378,15 @@ class MultiInput extends Input implements IFormInputElement {
const inputDomRef = this.getInputDOMRef();
if (e.target === inputDomRef) {
- super._onfocusin(e);
+ if (this._returningFromTokenizer) {
+ this._returningFromTokenizer = false;
+ this.focused = true;
+ this.open = true;
+ this._inputIconFocused = false;
+ this._focusedAfterClear = false;
+ } else {
+ super._onfocusin(e);
+ }
}
}