From 2a798218692ff5f33b6c158521430bbc620ca3e2 Mon Sep 17 00:00:00 2001 From: Milen Karmidzhanov Date: Wed, 28 Jan 2026 17:48:08 +0200 Subject: [PATCH 1/4] feat(ui5-tokenizer/ui5-multi-input/ui5-multi-combobox): align mobile popover title with spec --- .../cypress/specs/MultiComboBox.mobile.cy.tsx | 171 +++++++++++++++- .../cypress/specs/MultiInput.mobile.cy.tsx | 184 +++++++++++++++++- .../cypress/specs/Tokenizer.mobile.cy.tsx | 72 ++++++- packages/main/src/Input.ts | 3 +- packages/main/src/MultiComboBox.ts | 2 +- .../main/src/MultiComboBoxPopoverTemplate.tsx | 9 +- packages/main/src/MultiInputTemplate.tsx | 2 + packages/main/src/Tokenizer.ts | 4 +- .../src/features/InputSuggestionsTemplate.tsx | 11 +- .../main/src/i18n/messagebundle.properties | 7 +- .../src/themes/ResponsivePopoverCommon.css | 2 +- packages/main/test/pages/MultiInput.html | 13 +- packages/main/test/pages/Tokenizer.html | 5 +- 13 files changed, 460 insertions(+), 25 deletions(-) diff --git a/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx b/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx index 67726bb4707d..8d477370b65d 100644 --- a/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.mobile.cy.tsx @@ -1,4 +1,5 @@ -import { SHOW_SELECTED_BUTTON } from "../../src/generated/i18n/i18n-defaults.js"; +import { INPUT_SUGGESTIONS_TITLE, SHOW_SELECTED_BUTTON } from "../../src/generated/i18n/i18n-defaults.js"; +import Label from "../../src/Label.js"; import MultiComboBox from "../../src/MultiComboBox.js"; import MultiComboBoxItem from "../../src/MultiComboBoxItem.js"; import ResponsivePopover from "../../src/ResponsivePopover.js"; @@ -874,4 +875,172 @@ describe("Accessibility", () => { .should("have.attr", "accessible-name", SHOW_SELECTED_BUTTON.defaultText); }); + + it("Should test dialog with default title", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("multiComboBox"); + + cy.get("@multiComboBox") + .shadow() + .find("#ui5-multi-combobox-input") + .realClick(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", INPUT_SUGGESTIONS_TITLE.defaultText); + }); + + it("Should test dialog title with accessibleName present and tokenizer n-more is pressed", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("multiComboBox"); + + cy.get("@multiComboBox") + .shadow() + .find('.ui5-multi-combobox-tokenizer') + .as("tokenizer") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("moreText") + .realClick(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleName is present, it should be used as the mobile dialog title + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleName present and MultiComboBox is focused", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("multiComboBox"); + + cy.get("@multiComboBox") + .shadow() + .find("#ui5-multi-combobox-input") + .realClick(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleName is present, it should be used as the mobile dialog title + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleNameRef is present and tokenizer n-more is pressed", () => { + cy.mount( + <> + + + + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("multiComboBox"); + + cy.get("@multiComboBox") + .shadow() + .find('.ui5-multi-combobox-tokenizer') + .as("tokenizer") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("moreText") + .realClick(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleNameRef is present, it should be used as the mobile dialog title + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleNameRef is present and MultiComboBox is focused", () => { + cy.mount( + <> + + + + + + + + + ); + + cy.get("[ui5-multi-combobox]") + .as("multiComboBox"); + + cy.get("@multiComboBox") + .shadow() + .find("#ui5-multi-combobox-input") + .realClick(); + + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleNameRef is present, it should be used as the mobile dialog title + cy.get("@multiComboBox") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/specs/MultiInput.mobile.cy.tsx b/packages/main/cypress/specs/MultiInput.mobile.cy.tsx index 375fe099fa7b..766c6134dd59 100644 --- a/packages/main/cypress/specs/MultiInput.mobile.cy.tsx +++ b/packages/main/cypress/specs/MultiInput.mobile.cy.tsx @@ -4,6 +4,8 @@ import SuggestionItem from "../../src/SuggestionItem.js"; import Button from "../../src/Button.js"; import "../../src/features/InputSuggestions.js"; import type ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js"; +import { INPUT_SUGGESTIONS_TITLE } from "../../src/generated/i18n/i18n-defaults.js"; +import Label from "../../src/Label.js"; const createTokenFromText = (text: string): HTMLElement => { const token = document.createElement("ui5-token"); @@ -124,7 +126,7 @@ describe("Multi Input on mobile device", () => { .find(".ui5-input-inner") .realClick(); - cy.get("@popover") + cy.get("@popover") .ui5ResponsivePopoverOpened(); // Assert: Button should be enabled after adding a token @@ -212,4 +214,184 @@ describe("Multi Input on mobile device", () => { .should("have.length", 3); }); }); + + describe("Accessibility", () => { + it("Should test dialog with default title", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-multi-input]") + .as("multiInput"); + + cy.get("@multiInput") + .shadow() + .find(".ui5-input-inner") + .realClick(); + + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", INPUT_SUGGESTIONS_TITLE.defaultText); + }); + + it("Should test dialog title with accessibleName is present and n-more button of tokenizer is pressed", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-multi-input]") + .as("multiInput"); + + cy.get("@multiInput") + .shadow() + .find('.ui5-multi-input-tokenizer') + .as("tokenizer") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("moreText") + .realClick(); + + cy.get("@tokenizer") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleName is present, it should be used as the mobile dialog title + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleName is present and input is focused", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-multi-input]") + .as("multiInput"); + + cy.get("@multiInput") + .shadow() + .find(".ui5-input-inner") + .realClick(); + + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleName is present, it should be used as the mobile dialog title + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleNameRef is present and and n-more button of tokenizer is pressed", () => { + cy.mount( + <> + + + + + + + + + + + ); + + cy.get("[ui5-multi-input]") + .as("multiInput"); + + cy.get("@multiInput") + .shadow() + .find('.ui5-multi-input-tokenizer') + .as("tokenizer") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("moreText") + .realClick(); + + cy.get("@tokenizer") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleNameRef is present, it should be used as the mobile dialog title + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + + it("Should test dialog title with accessibleNameRef is present and input is focused", () => { + cy.mount( + <> + + + + + + + + + + + ); + + cy.get("[ui5-multi-input]") + .as("multiInput"); + + cy.get("@multiInput") + .shadow() + .find(".ui5-input-inner") + .realClick(); + + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleNameRef is present, it should be used as the mobile dialog title + cy.get("@multiInput") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Custom Title"); + }); + }); }); diff --git a/packages/main/cypress/specs/Tokenizer.mobile.cy.tsx b/packages/main/cypress/specs/Tokenizer.mobile.cy.tsx index be7344dd308b..23a65bab0f7c 100644 --- a/packages/main/cypress/specs/Tokenizer.mobile.cy.tsx +++ b/packages/main/cypress/specs/Tokenizer.mobile.cy.tsx @@ -1,7 +1,9 @@ import Tokenizer from "../../src/Tokenizer.js"; import Button from "../../src/Button.js"; import Token from "../../src/Token.js"; -import { TOKENIZER_DIALOG_OK_BUTTON, TOKENIZER_DIALOG_CANCEL_BUTTON, TOKENIZER_POPOVER_REMOVE } from "../../src/generated/i18n/i18n-defaults.js"; +import { TOKENIZER_DIALOG_OK_BUTTON, TOKENIZER_DIALOG_CANCEL_BUTTON, INPUT_SUGGESTIONS_TITLE } from "../../src/generated/i18n/i18n-defaults.js"; +import Label from "../../src/Label.js"; +import ResponsivePopover from "../../src/ResponsivePopover.js"; describe("Phone mode", () => { beforeEach(() => { @@ -133,7 +135,7 @@ describe("Phone mode", () => { cy.get("@nMoreDialog") .find(".ui5-responsive-popover-header .ui5-responsive-popover-header-text") - .should("have.text", TOKENIZER_POPOVER_REMOVE.defaultText); + .should("have.text", INPUT_SUGGESTIONS_TITLE.defaultText); }); it("Should fire the ui5-token-delete event when the 'X' is pressed in the n-more picker and confirmed with OK", () => { @@ -213,6 +215,7 @@ describe("Phone mode", () => { cy.get("@tokenDeleteSpy") .should("not.have.been.called"); }); + it("Should NOT fire the ui5-token-delete event when the 'X' is pressed in the n-more picker and canceled", () => { cy.mount( @@ -255,4 +258,69 @@ describe("Phone mode", () => { cy.get("@tokenDeleteSpy") .should("not.have.been.called"); }); + + it("Should test popover title when accessibleName is set", () => { + cy.mount( + <> + + + + + + + ); + + cy.get("[ui5-tokenizer]") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("nMoreLabel"); + + cy.get("@nMoreLabel") + .realClick(); + + cy.get("[ui5-tokenizer]") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleName is present, it should be used as the mobile dialog title + cy.get("[ui5-tokenizer]") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Animals"); + }); + + it("Should test popover title when accessibleNameRef is set", () => { + cy.mount( + <> + + + + + + + + ); + + cy.get("[ui5-tokenizer]") + .shadow() + .find(".ui5-tokenizer-more-text") + .as("nMoreLabel"); + + cy.get("@nMoreLabel") + .realClick(); + + cy.get("[ui5-tokenizer]") + .shadow() + .find("[ui5-responsive-popover]") + .as("popover") + .ui5ResponsivePopoverOpened(); + + // When accessibleNameRef is present, it should be used as the mobile dialog title + cy.get("[ui5-tokenizer]") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] [ui5-title]") + .should("have.text", "Countries"); + }); }) \ No newline at end of file diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index e3437e7b8aeb..27fc3b853c24 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -52,6 +52,7 @@ import { deregisterUI5Element, getEffectiveAriaDescriptionText, getAllAccessibleDescriptionRefTexts, + getEffectiveAriaLabelText, } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; import { getCaretPosition, setCaretPosition } from "@ui5/webcomponents-base/dist/util/Caret.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; @@ -1732,7 +1733,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } get _headerTitleText() { - return Input.i18nBundle.getText(INPUT_SUGGESTIONS_TITLE); + return getEffectiveAriaLabelText(this) || Input.i18nBundle.getText(INPUT_SUGGESTIONS_TITLE); } get _suggestionsOkButtonText() { diff --git a/packages/main/src/MultiComboBox.ts b/packages/main/src/MultiComboBox.ts index adfd142ed14a..fe1aca931d82 100644 --- a/packages/main/src/MultiComboBox.ts +++ b/packages/main/src/MultiComboBox.ts @@ -2190,7 +2190,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { } get _headerTitleText() { - return MultiComboBox.i18nBundle.getText(INPUT_SUGGESTIONS_TITLE); + return getEffectiveAriaLabelText(this) || MultiComboBox.i18nBundle.getText(INPUT_SUGGESTIONS_TITLE); } get _iconAccessibleNameText() { diff --git a/packages/main/src/MultiComboBoxPopoverTemplate.tsx b/packages/main/src/MultiComboBoxPopoverTemplate.tsx index af570b19acf2..aca650d6f810 100644 --- a/packages/main/src/MultiComboBoxPopoverTemplate.tsx +++ b/packages/main/src/MultiComboBoxPopoverTemplate.tsx @@ -10,6 +10,7 @@ import List from "./List.js"; import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js"; import Popover from "./Popover.js"; import CheckBox from "./CheckBox.js"; +import Title from "./Title.js"; export default function MultiComboBoxPopoverTemplate(this: MultiComboBox) { return (<> @@ -34,7 +35,13 @@ export default function MultiComboBoxPopoverTemplate(this: MultiComboBox) { {this._isPhone && <>
- {this._headerTitleText} + + {this._headerTitleText} +
{ this.tokens.map(token => )} diff --git a/packages/main/src/Tokenizer.ts b/packages/main/src/Tokenizer.ts index 095904de05bd..ca260af2f424 100644 --- a/packages/main/src/Tokenizer.ts +++ b/packages/main/src/Tokenizer.ts @@ -61,7 +61,6 @@ import type Button from "./Button.js"; import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_LABEL, - TOKENIZER_POPOVER_REMOVE, TOKENIZER_ARIA_CONTAIN_TOKEN, TOKENIZER_ARIA_CONTAIN_ONE_TOKEN, TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, @@ -69,6 +68,7 @@ import { TOKENIZER_CLEAR_ALL, TOKENIZER_DIALOG_OK_BUTTON, TOKENIZER_DIALOG_CANCEL_BUTTON, + INPUT_SUGGESTIONS_TITLE, } from "./generated/i18n/i18n-defaults.js"; // Styles @@ -1178,7 +1178,7 @@ class Tokenizer extends UI5Element implements IFormInputElement { } get morePopoverTitle() { - return Tokenizer.i18nBundle.getText(TOKENIZER_POPOVER_REMOVE); + return getEffectiveAriaLabelText(this) || Tokenizer.i18nBundle.getText(INPUT_SUGGESTIONS_TITLE); } get overflownTokens() { diff --git a/packages/main/src/features/InputSuggestionsTemplate.tsx b/packages/main/src/features/InputSuggestionsTemplate.tsx index 846500799446..b16bd7c69175 100644 --- a/packages/main/src/features/InputSuggestionsTemplate.tsx +++ b/packages/main/src/features/InputSuggestionsTemplate.tsx @@ -6,6 +6,7 @@ import List from "../List.js"; import ResponsivePopover from "../ResponsivePopover.js"; import Button from "../Button.js"; import ListAccessibleRole from "../types/ListAccessibleRole.js"; +import Title from "../Title.js"; export default function InputSuggestionsTemplate(this: Input, hooks?: { suggestionsList?: (this: Input) => JsxTemplateResult, mobileHeader?: (this: Input) => JsxTemplateResult, valueStateMessage: (this: Input) => JsxTemplateResult, valueStateMessageInputIcon: (this: Input) => string }) { const suggestionsList = hooks?.suggestionsList || defaultSuggestionsList; @@ -35,7 +36,13 @@ export default function InputSuggestionsTemplate(this: Input, hooks?: { suggesti <>
- {this._headerTitleText} + + {this._headerTitleText} +
@@ -75,7 +82,7 @@ export default function InputSuggestionsTemplate(this: Input, hooks?: { suggesti
} - { this.showSuggestions && suggestionsList.call(this) } + {this.showSuggestions && suggestionsList.call(this)} {this._isPhone &&