Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 169 additions & 1 deletion packages/main/cypress/specs/MultiInput.mobile.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import MultiInput from "../../src/MultiInput.js";
import ResponsivePopover from "../../src/ResponsivePopover.js";
import Token from "../../src/Token.js";
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";

const createTokenFromText = (text: string): HTMLElement => {
const token = document.createElement("ui5-token");
token.setAttribute("text", text);
token.setAttribute("slot", "tokens");
return token;
};

const addTokenToMI = (token: HTMLElement, id: string) => {
document.getElementById(id)?.appendChild(token);
};

const handleTokenDelete = (event) => {
const mi = event.target;
event.detail.tokens.forEach(token => {
mi.removeChild(token);
});
};

describe("Multi Input on mobile device", () => {
beforeEach(() => {
Expand All @@ -18,6 +39,8 @@ describe("Multi Input on mobile device", () => {
.as("multiInput");

cy.get("@multiInput")
.shadow()
.find(".ui5-input-inner")
.realClick();

cy.get("@multiInput")
Expand All @@ -44,4 +67,149 @@ describe("Multi Input on mobile device", () => {
.should("have.value", "test");
cy.get("@onChange").should("not.have.been.called");
});

describe("Filter-Selected Button", () => {
it("Filter-selected button state changes when tokens are added/removed", () => {
cy.mount(
<>
<MultiInput id="test-multi-input" showSuggestions>
<SuggestionItem text="Argentina"></SuggestionItem>
<SuggestionItem text="Brazil"></SuggestionItem>
<SuggestionItem text="Canada"></SuggestionItem>
</MultiInput>
<Button id="add-token">Add Token</Button>
</>
);

cy.get("#add-token").then(button => {
button[0].addEventListener("click", () => {
addTokenToMI(createTokenFromText("Test Token"), "test-multi-input");
});
});

cy.get("#test-multi-input").then(multiInput => {
multiInput[0].addEventListener("ui5-token-delete", handleTokenDelete);
});

cy.get("#test-multi-input")
.shadow()
.find(".ui5-input-inner")
.realClick();

cy.get("#test-multi-input")
.shadow()
.find<ResponsivePopover>("[ui5-responsive-popover]")
.as("popover")
.ui5ResponsivePopoverOpened();

// Assert: Button should be initially disabled (no tokens)
cy.get("@popover")
.find("[ui5-toggle-button]")
.as("filterButton")
.should("have.attr", "disabled");

cy.get("#test-multi-input")
.shadow()
.find(".ui5-responsive-popover-close-btn")
.realClick();

cy.get("#add-token").realClick();

cy.get("#test-multi-input")
.find("[ui5-token]")
.should("have.length", 1);

cy.get("#test-multi-input")
.shadow()
.find(".ui5-input-inner")
.realClick();

cy.get<ResponsivePopover>("@popover")
.ui5ResponsivePopoverOpened();

// Assert: Button should be enabled after adding a token
cy.get("@filterButton")
.should("not.have.attr", "disabled")
cy.get("@filterButton")
.should("have.attr", "pressed");

cy.get("@popover")
.find("[ui5-li].ui5-suggestion-token-item")
.first()
.shadow()
.find("[ui5-button]")
.realClick();

cy.get("#test-multi-input")
.find("[ui5-token]")
.should("have.length", 0);

// Assert: Button should be disabled after removing all tokens
cy.get("@filterButton")
.should("have.attr", "disabled");
});

it("Filter-selected button affects list content display", () => {
cy.mount(
<MultiInput id="test-multi-input" showSuggestions>
<Token slot="tokens" text="Token 1"></Token>
<Token slot="tokens" text="Token 2"></Token>
<Token slot="tokens" text="Token 3"></Token>
<SuggestionItem text="Argentina"></SuggestionItem>
<SuggestionItem text="Brazil"></SuggestionItem>
</MultiInput>
);

cy.get("#test-multi-input")
.shadow()
.find(".ui5-input-inner")
.click();

cy.get("#test-multi-input")
.shadow()
.find("[ui5-responsive-popover]")
.as("popover")

cy.get("@popover")
.find("[ui5-toggle-button]")
.as("filterButton");

// Assert: Initially showing tokens (button pressed)
cy.get("@filterButton")
.should("have.attr", "pressed");

// Assert: Should see token list items
cy.get("@popover")
.find("[ui5-list].ui5-tokenizer-list")
.should("exist");

cy.get("@popover")
.find("[ui5-li].ui5-suggestion-token-item")
.should("have.length", 3);

// Act: Toggle to hide tokens
cy.get("@filterButton").realClick();

// Assert: Should see suggestion items instead
cy.get("@popover")
.find("[ui5-list]:not(.ui5-tokenizer-list)")
.should("exist");

cy.get("#test-multi-input")
.find("[ui5-suggestion-item]")
.should("have.length", 2);

// Act: Toggle back to show tokens
cy.get("@filterButton").realClick();

// Assert: Should see token list items again
cy.get("@popover")
.find("[ui5-list].ui5-tokenizer-list")
.should("exist");

cy.get("@popover")
.find("[ui5-li].ui5-suggestion-token-item")
.should("have.length", 3);
});
});
});
16 changes: 11 additions & 5 deletions packages/main/src/InputPopoverTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js";
import Popover from "./Popover.js";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";

export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsList?: (this: Input) => JsxTemplateResult }) {
export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsList?: (this: Input) => JsxTemplateResult, mobileHeader?: (this: Input) => JsxTemplateResult }) {
const suggestionsList = hooks?.suggestionsList;
const mobileHeader = hooks?.mobileHeader;

return (
<>
{this._effectiveShowSuggestions && this.Suggestions?.template.call(this, { suggestionsList, valueStateMessage, valueStateMessageInputIcon }) }
{this._effectiveShowSuggestions && this.Suggestions?.template.call(this, {
suggestionsList,
mobileHeader,
valueStateMessage,
valueStateMessageInputIcon
})}

{this.hasValueStateMessage &&
{this.hasValueStateMessage && (
<Popover
preventInitialFocus={true}
preventFocusRestore={true}
Expand All @@ -32,10 +38,10 @@ export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsL
>
<div slot="header" class={this.classes.popoverValueState}>
<Icon class="ui5-input-value-state-message-icon" name={valueStateMessageInputIcon.call(this)} />
{ this.valueStateOpen && valueStateMessage.call(this) }
{this.valueStateOpen && valueStateMessage.call(this)}
</div>
</Popover>
}
)}
</>
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/main/src/InputTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import InputPopoverTemplate from "./InputPopoverTemplate.js";

type TemplateHook = () => JsxTemplateResult;

export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook }) {
export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook, mobileHeader?: TemplateHook }) {
const suggestionsList = hooks?.suggestionsList;
const mobileHeader = hooks?.mobileHeader;
const preContent = hooks?.preContent || defaultPreContent;
const postContent = hooks?.postContent || defaultPostContent;

Expand Down Expand Up @@ -118,7 +119,7 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat
</div>
</div>

{ InputPopoverTemplate.call(this, { suggestionsList }) }
{ InputPopoverTemplate.call(this, { suggestionsList, mobileHeader }) }
</>
);
}
Expand Down
63 changes: 62 additions & 1 deletion packages/main/src/MultiInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import {
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
import { MULTIINPUT_ROLEDESCRIPTION_TEXT, MULTIINPUT_VALUE_HELP_LABEL, MULTIINPUT_VALUE_HELP } from "./generated/i18n/i18n-defaults.js";
import {
MULTIINPUT_ROLEDESCRIPTION_TEXT,
MULTIINPUT_VALUE_HELP_LABEL,
MULTIINPUT_VALUE_HELP,
MULTIINPUT_FILTER_BUTTON_LABEL,
} from "./generated/i18n/i18n-defaults.js";
import Input from "./Input.js";
import MultiInputTemplate from "./MultiInputTemplate.js";
import styles from "./generated/themes/MultiInput.css.js";
Expand Down Expand Up @@ -123,6 +128,22 @@ class MultiInput extends Input implements IFormInputElement {
@property()
declare name?: string;

/**
* Indicates whether to show tokens in suggestions popover
* @default false
* @private
*/
@property({ type: Boolean })
_showTokensInSuggestions = false;

/**
* Tracks whether user has explicitly toggled the show tokens state
* @default false
* @private
*/
@property({ type: Boolean })
_userToggledShowTokens = false;

/**
* Defines the component tokens.
* @public
Expand Down Expand Up @@ -343,6 +364,23 @@ class MultiInput extends Input implements IFormInputElement {
if (this.tokenizer) {
this.tokenizer.readonly = this.readonly;
}

// Reset toggle state if there are tokens and dialog is about to open
if (this.tokens.length > 0 && !this._userToggledShowTokens) {
this._showTokensInSuggestions = true;
}
}

/**
* Override the _handlePickerAfterOpen method to reset toggle state when dialog opens with tokens
*/
_handlePickerAfterOpen() {
if (this.tokens.length > 0) {
this._showTokensInSuggestions = true;
this._userToggledShowTokens = false;
}

super._handlePickerAfterOpen();
}

onAfterRendering() {
Expand Down Expand Up @@ -371,6 +409,10 @@ class MultiInput extends Input implements IFormInputElement {
return MultiInput.i18nBundle.getText(MULTIINPUT_VALUE_HELP);
}

get _filterButtonAccessibleName() {
return MultiInput.i18nBundle.getText(MULTIINPUT_FILTER_BUTTON_LABEL);
}

get _tokensCountTextId() {
return `hiddenText-nMore`;
}
Expand Down Expand Up @@ -419,6 +461,25 @@ class MultiInput extends Input implements IFormInputElement {
get shouldDisplayOnlyValueStateMessage() {
return this.hasValueStateMessage && !this.readonly && !this.open && this.focused && !this.tokenizer.open;
}

/**
* Computes the effective state for showing tokens in suggestions.
* Defaults to true when tokens exist, but respects explicit user toggle.
*/
get _effectiveShowTokensInSuggestions() {
// If no tokens exist, always false
if (this.tokens.length === 0) {
return false;
}

// If user has never interacted with the toggle, default to true when tokens exist
if (!this._userToggledShowTokens) {
return true;
}

// If user has interacted, respect their choice
return this._showTokensInSuggestions;
}
}

MultiInput.define();
Expand Down
Loading
Loading