From 5c346d2e4e4687210eb76e5af02f455d82e8f29f Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Wed, 22 Jul 2026 17:32:05 +0300 Subject: [PATCH 1/3] fix(ui5-tokenizer): fix rare case when n-more button cause loop error --- .../main/cypress/specs/MultiComboBox.cy.tsx | 84 ++++++++++++++++++- packages/main/cypress/specs/MultiInput.cy.tsx | 4 +- packages/main/cypress/specs/Tokenizer.cy.tsx | 2 +- packages/main/src/Tokenizer.ts | 26 ++++-- 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index 273b523ca49c8..f700bc9d06bd8 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -1,3 +1,4 @@ +import "../../src/Assets.js"; import MultiComboBox from "../../src/MultiComboBox.js"; import MultiComboBoxItem from "../../src/MultiComboBoxItem.js"; import MultiComboBoxItemCustom from "../../src/MultiComboBoxItemCustom.js"; @@ -6,6 +7,7 @@ import ResponsivePopover from "../../src/ResponsivePopover.js"; import Button from "../../src/Button.js"; import Link from "../../src/Link.js"; import Input from "../../src/Input.js"; +import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_CONTAIN_ONE_TOKEN, TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, TOKENIZER_ARIA_CONTAIN_TOKEN, TOKENIZER_SHOW_ALL_ITEMS, VALUE_STATE_ERROR, VALUE_STATE_TYPE_ERROR, VALUE_STATE_TYPE_SUCCESS, VALUE_STATE_TYPE_WARNING, VALUE_STATE_WARNING } from "../../src/generated/i18n/i18n-defaults.js"; describe("Security", () => { @@ -372,7 +374,7 @@ describe("General", () => { it("Should delete token after focus change when tokenizer collapses", () => { cy.mount( - + @@ -5503,3 +5505,83 @@ describe("MultiComboBoxItemCustom - Mixed Selection", () => { cy.get("@multiCombobox").shadow().find("[ui5-token]").should("have.length", 2); }); }); + +describe("Tokenizer overflow calculation", () => { + it("should not cause render loop at specific widths with medium-length tokens", () => { + // This test verifies the fix for a render loop that occurred when: + // - MultiComboBox has a specific width (208px) + // - First token is medium-length (3-4 characters) + // - The "n more" indicator in Korean (wider text) causes oscillation + // Korean "1 more" = "1개 더" which is significantly wider than English + // The loop is reproducable in english as well but we need to change the english + // translation to be 1 character longer (instead of "more" use "moree") + + // Set language to Korean and wait for it to load + cy.wrap({ setLanguage }) + .then(api => api.setLanguage("ko")); + + cy.mount( + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("mcb"); + + // Get the tokenizer element + cy.get("@mcb") + .shadow() + .find("[ui5-tokenizer]") + .as("tokenizer"); + + // Get the "n more" text element + cy.get("@tokenizer") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("nMoreText"); + + // Wait a bit for language to fully apply + cy.wait(200); + + // Verify "n more" indicator is shown (some tokens should overflow) + cy.get("@nMoreText") + .should("be.visible"); + + // Verify Korean text is shown (contains Korean characters) + // Korean shows "2개 항목" (wider than English "2 more") + cy.get("@nMoreText") + .invoke("text") + .should("match", /개|항목/); // Korean text contains these characters + + // Capture the initial text of the "n more" indicator + cy.get("@nMoreText") + .invoke("text") + .as("initialText"); + + // Wait a bit to ensure no render loop is happening + cy.wait(500); + + // Verify the "n more" text hasn't changed (would change continuously in a render loop) + cy.get("@nMoreText") + .invoke("text") + .then(currentText => { + cy.get("@initialText").should("equal", currentText); + }); + + // Verify the tokenizer state is stable + cy.get("@tokenizer") + .should("exist"); + + // Verify tokens exist in the tokenizer (tokens are created from selected items) + cy.get("@tokenizer") + .find("[ui5-token]") + .should("have.length.at.least", 1); + + // Reset language + cy.wrap({ setLanguage }) + .then(api => api.setLanguage("en")); + }); +}); diff --git a/packages/main/cypress/specs/MultiInput.cy.tsx b/packages/main/cypress/specs/MultiInput.cy.tsx index f3f603373d6df..e05902fa83665 100644 --- a/packages/main/cypress/specs/MultiInput.cy.tsx +++ b/packages/main/cypress/specs/MultiInput.cy.tsx @@ -355,7 +355,7 @@ describe("MultiInput tokens", () => { cy.get("[ui5-token]") .eq(1) - .should("not.have.attr", "overflows"); + .should("have.attr", "overflows"); cy.get("[ui5-token]") .eq(2) @@ -571,7 +571,7 @@ describe("MultiInput tokens", () => { - + diff --git a/packages/main/cypress/specs/Tokenizer.cy.tsx b/packages/main/cypress/specs/Tokenizer.cy.tsx index 48bedecca72d6..184490575e5eb 100755 --- a/packages/main/cypress/specs/Tokenizer.cy.tsx +++ b/packages/main/cypress/specs/Tokenizer.cy.tsx @@ -798,7 +798,7 @@ describe("Accessibility", () => { const resourceBundle = (tokenizer.constructor as any).i18nBundle; cy.get("@nMoreLabel") - .should("have.text", resourceBundle.getText(MULTIINPUT_SHOW_MORE_TOKENS.defaultText, 2)); + .should("have.text", resourceBundle.getText(MULTIINPUT_SHOW_MORE_TOKENS.defaultText, 3)); }); }); }); diff --git a/packages/main/src/Tokenizer.ts b/packages/main/src/Tokenizer.ts index a7d43780d5cfa..8cd2430b945f9 100644 --- a/packages/main/src/Tokenizer.ts +++ b/packages/main/src/Tokenizer.ts @@ -1272,22 +1272,32 @@ class Tokenizer extends UI5Element implements IFormInputElement { const tokensArray = this._tokens; - // Reset the overflow prop of the tokens first in order - // to use their dimensions for calculation because already - // hidden tokens are set to 'display: none' + // Reset overflow to measure all tokens tokensArray.forEach(token => { token.overflows = false; }); - return tokensArray.filter(token => { - const parentRect = this.contentDom.getBoundingClientRect(); + const parentRect = this.contentDom.getBoundingClientRect(); + const parentEnd = Number(parentRect.right.toFixed(2)); + const parentStart = Number(parentRect.left.toFixed(2)); + + // Measure "n more" width + let nMoreWidth = 0; + const nMoreElement = this.moreLink; + if (nMoreElement) { + nMoreWidth = nMoreElement.getBoundingClientRect().width; + } + + // Calculate overflow, reserving space for "n more" except on last token + return tokensArray.filter((token, index) => { const tokenRect = token.getBoundingClientRect(); const tokenEnd = Number(tokenRect.right.toFixed(2)); - const parentEnd = Number(parentRect.right.toFixed(2)); const tokenStart = Number(tokenRect.left.toFixed(2)); - const parentStart = Number(parentRect.left.toFixed(2)); - token.overflows = !this.expanded && ((tokenStart < parentStart) || (tokenEnd > parentEnd)); + const isLastToken = index === tokensArray.length - 1; + const effectiveParentEnd = isLastToken ? parentEnd : Number((parentRect.right - nMoreWidth).toFixed(2)); + + token.overflows = !this.expanded && ((tokenStart < parentStart) || (tokenEnd > effectiveParentEnd)); return token.overflows; }); From f7b7036a5fc78821e8253e0ad09e2282d53af236 Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Thu, 23 Jul 2026 08:42:22 +0300 Subject: [PATCH 2/3] fix(ui5-tokenizer): fix rare case when n-more button cause loop error --- .../main/cypress/specs/MultiComboBox.cy.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index f700bc9d06bd8..0f0e5fda1486b 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -5515,7 +5515,7 @@ describe("Tokenizer overflow calculation", () => { // Korean "1 more" = "1개 더" which is significantly wider than English // The loop is reproducable in english as well but we need to change the english // translation to be 1 character longer (instead of "more" use "moree") - + // Set language to Korean and wait for it to load cy.wrap({ setLanguage }) .then(api => api.setLanguage("ko")); @@ -5524,7 +5524,8 @@ describe("Tokenizer overflow calculation", () => { - + + ); @@ -5537,21 +5538,21 @@ describe("Tokenizer overflow calculation", () => { .find("[ui5-tokenizer]") .as("tokenizer"); - // Get the "n more" text element + // Wait a bit for language to fully apply + cy.wait(200); + + // Get the "n more" text element (if it exists) cy.get("@tokenizer") .shadow() .find(".ui5-tokenizer-more-text") .as("nMoreText"); - // Wait a bit for language to fully apply - cy.wait(200); - - // Verify "n more" indicator is shown (some tokens should overflow) + // Verify "n more" indicator is shown (some tokens should overflow at 208px with 3 tokens) cy.get("@nMoreText") .should("be.visible"); // Verify Korean text is shown (contains Korean characters) - // Korean shows "2개 항목" (wider than English "2 more") + // Korean shows "N개 항목" (wider than English "N more") cy.get("@nMoreText") .invoke("text") .should("match", /개|항목/); // Korean text contains these characters From c374f09e7706c9a02175ad8b8f9f1b80e8dde162 Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Thu, 23 Jul 2026 08:58:27 +0300 Subject: [PATCH 3/3] fix(ui5-tokenizer): fix rare case when n-more button cause loop error --- packages/main/cypress/specs/MultiComboBox.cy.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index 0f0e5fda1486b..edcb773768537 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -5525,7 +5525,6 @@ describe("Tokenizer overflow calculation", () => { - ); @@ -5538,19 +5537,16 @@ describe("Tokenizer overflow calculation", () => { .find("[ui5-tokenizer]") .as("tokenizer"); - // Wait a bit for language to fully apply - cy.wait(200); + // Wait for language to fully apply and rendering to stabilize + cy.wait(300); - // Get the "n more" text element (if it exists) + // Get the "n more" text element with timeout - should exist with 3 selected tokens at 208px cy.get("@tokenizer") .shadow() .find(".ui5-tokenizer-more-text") + .should("exist") .as("nMoreText"); - // Verify "n more" indicator is shown (some tokens should overflow at 208px with 3 tokens) - cy.get("@nMoreText") - .should("be.visible"); - // Verify Korean text is shown (contains Korean characters) // Korean shows "N개 항목" (wider than English "N more") cy.get("@nMoreText")