From 7fda2de1c4c9877ef2823a22fabf390061662f80 Mon Sep 17 00:00:00 2001 From: Nikolay Deshev Date: Fri, 8 May 2026 08:13:40 +0300 Subject: [PATCH] feat(ui5-tokenizer): deselect tokens on ESC deselect all selected tokens on ESC key press --- .../main/cypress/specs/MultiComboBox.cy.tsx | 48 +++++++++ packages/main/cypress/specs/MultiInput.cy.tsx | 33 +++++++ packages/main/cypress/specs/Tokenizer.cy.tsx | 98 +++++++++++++++++++ packages/main/src/Tokenizer.ts | 15 +++ 4 files changed, 194 insertions(+) diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index 2e704bf87590..6dacca7e109e 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -3430,6 +3430,54 @@ describe("Keyboard Handling", () => { .should("have.value", "I"); }); + it("should deselect all tokens on [Escape] key when focus is on tokenizer", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .shadow() + .find("[ui5-tokenizer]") + .find("[ui5-token]") + .should("have.length", 3); + + cy.get("[ui5-multi-combobox]") + .realClick(); + + cy.realPress("Backspace"); + + cy.get("[ui5-multi-combobox]") + .shadow() + .find("[ui5-tokenizer]") + .find("[ui5-token]") + .last() + .should("be.focused"); + + cy.realPress(["Shift", "Home"]); + + cy.get("[ui5-multi-combobox]") + .shadow() + .find("[ui5-tokenizer]") + .find("[ui5-token]") + .each($token => { + cy.wrap($token).should("have.attr", "selected"); + }); + + cy.realPress("Escape"); + + cy.get("[ui5-multi-combobox]") + .shadow() + .find("[ui5-tokenizer]") + .find("[ui5-token]") + .each($token => { + cy.wrap($token).should("not.have.attr", "selected"); + }); + }); + it("Selects an item when enter is pressed and value matches a text of an item in the list", () => { cy.mount( <> diff --git a/packages/main/cypress/specs/MultiInput.cy.tsx b/packages/main/cypress/specs/MultiInput.cy.tsx index 7734b4d81319..6dc87533e408 100644 --- a/packages/main/cypress/specs/MultiInput.cy.tsx +++ b/packages/main/cypress/specs/MultiInput.cy.tsx @@ -1502,6 +1502,39 @@ describe("Keyboard handling", () => { cy.get("@changeSpy") .should("have.been.calledOnce"); }); + + it("should deselect all tokens on [Escape] key", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-multi-input]") + .shadow() + .find("input") + .realClick(); + + cy.realPress("Home"); + + cy.get("[ui5-token]") + .eq(0) + .should("be.focused"); + + cy.realPress(["Shift", "End"]); + + cy.get("[ui5-token]").each($token => { + cy.wrap($token).should("have.attr", "selected"); + }); + + cy.realPress("Escape"); + + cy.get("[ui5-token]").each($token => { + cy.wrap($token).should("not.have.attr", "selected"); + }); + }); }); describe("MultiInput Composition", () => { diff --git a/packages/main/cypress/specs/Tokenizer.cy.tsx b/packages/main/cypress/specs/Tokenizer.cy.tsx index 0dd97b5ce12a..c8495e8af866 100755 --- a/packages/main/cypress/specs/Tokenizer.cy.tsx +++ b/packages/main/cypress/specs/Tokenizer.cy.tsx @@ -1590,6 +1590,104 @@ describe("Keyboard Handling", () => { .eq(1) .should("have.attr", "selected"); }); + + it("should deselect all tokens on [Escape] key", () => { + cy.get("[ui5-token]") + .eq(0) + .as("firstToken"); + + cy.get("[ui5-token]") + .eq(1) + .as("secondToken"); + + cy.get("[ui5-token]") + .eq(2) + .as("thirdToken"); + + cy.get("@firstToken") + .realClick(); + + cy.realPress(["Shift", "End"]); + + cy.get("@firstToken") + .should("have.attr", "selected"); + + cy.get("@secondToken") + .should("have.attr", "selected"); + + cy.get("@thirdToken") + .should("have.attr", "selected"); + + cy.realPress("Escape"); + + cy.get("@firstToken") + .should("not.have.attr", "selected"); + + cy.get("@secondToken") + .should("not.have.attr", "selected"); + + cy.get("@thirdToken") + .should("not.have.attr", "selected"); + }); + + it("should fire selection-change event on [Escape] when tokens are selected", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-token]") + .eq(0) + .realClick(); + + cy.get("@selectionChange") + .should("have.been.called"); + + cy.get("@selectionChange") + .invoke("resetHistory"); + + cy.realPress("Escape"); + + cy.get("@selectionChange") + .should("have.been.calledOnce"); + + cy.get("@selectionChange") + .its("firstCall.args.0.detail.tokens") + .should("have.length", 0); + }); + + it("should not fire selection-change on [Escape] when no tokens are selected", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-token]") + .eq(0) + .realClick(); + + cy.get("@selectionChange") + .invoke("resetHistory"); + + cy.realPress("Space"); + + cy.get("[ui5-token]") + .eq(0) + .should("not.have.attr", "selected"); + + cy.get("@selectionChange") + .invoke("resetHistory"); + + cy.realPress("Escape"); + + cy.get("@selectionChange") + .should("not.have.been.called"); + }); }); describe("Clipboard Operations", () => { diff --git a/packages/main/src/Tokenizer.ts b/packages/main/src/Tokenizer.ts index 0cdd39e560a1..6a4f96c00f1e 100644 --- a/packages/main/src/Tokenizer.ts +++ b/packages/main/src/Tokenizer.ts @@ -716,6 +716,10 @@ class Tokenizer extends UI5Element implements IFormInputElement { _onkeydown(e: KeyboardEvent) { const isCtrl = !!(e.metaKey || e.ctrlKey); + if (isEscape(e)) { + return this._deselectAllTokens(); + } + if ((isCtrl && ["c", "x"].includes(e.key.toLowerCase())) || isDeleteShift(e) || isInsertCtrl(e)) { e.preventDefault(); @@ -1070,6 +1074,17 @@ class Tokenizer extends UI5Element implements IFormInputElement { } } + _deselectAllTokens() { + const hadSelection = this._selectedTokens.length > 0; + this._tokens.forEach(token => { token.selected = false; }); + + if (hadSelection) { + this.fireDecoratorEvent("selection-change", { + tokens: [], + }); + } + } + get hasTokens() { return this._tokens.length > 0; }