Skip to content

Commit d046cb3

Browse files
authored
feat(ui5-multiinput): implement selected token indicator in filter dialog (#12698)
1 parent 6e48084 commit d046cb3

8 files changed

Lines changed: 349 additions & 28 deletions

File tree

packages/main/cypress/specs/MultiInput.mobile.cy.tsx

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
import MultiInput from "../../src/MultiInput.js";
2-
import ResponsivePopover from "../../src/ResponsivePopover.js";
2+
import Token from "../../src/Token.js";
3+
import SuggestionItem from "../../src/SuggestionItem.js";
4+
import Button from "../../src/Button.js";
35
import "../../src/features/InputSuggestions.js";
6+
import type ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js";
7+
8+
const createTokenFromText = (text: string): HTMLElement => {
9+
const token = document.createElement("ui5-token");
10+
token.setAttribute("text", text);
11+
token.setAttribute("slot", "tokens");
12+
return token;
13+
};
14+
15+
const addTokenToMI = (token: HTMLElement, id: string) => {
16+
document.getElementById(id)?.appendChild(token);
17+
};
18+
19+
const handleTokenDelete = (event) => {
20+
const mi = event.target;
21+
event.detail.tokens.forEach(token => {
22+
mi.removeChild(token);
23+
});
24+
};
425

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

2041
cy.get("@multiInput")
42+
.shadow()
43+
.find(".ui5-input-inner")
2144
.realClick();
2245

2346
cy.get("@multiInput")
@@ -44,4 +67,149 @@ describe("Multi Input on mobile device", () => {
4467
.should("have.value", "test");
4568
cy.get("@onChange").should("not.have.been.called");
4669
});
70+
71+
describe("Filter-Selected Button", () => {
72+
it("Filter-selected button state changes when tokens are added/removed", () => {
73+
cy.mount(
74+
<>
75+
<MultiInput id="test-multi-input" showSuggestions>
76+
<SuggestionItem text="Argentina"></SuggestionItem>
77+
<SuggestionItem text="Brazil"></SuggestionItem>
78+
<SuggestionItem text="Canada"></SuggestionItem>
79+
</MultiInput>
80+
<Button id="add-token">Add Token</Button>
81+
</>
82+
);
83+
84+
cy.get("#add-token").then(button => {
85+
button[0].addEventListener("click", () => {
86+
addTokenToMI(createTokenFromText("Test Token"), "test-multi-input");
87+
});
88+
});
89+
90+
cy.get("#test-multi-input").then(multiInput => {
91+
multiInput[0].addEventListener("ui5-token-delete", handleTokenDelete);
92+
});
93+
94+
cy.get("#test-multi-input")
95+
.shadow()
96+
.find(".ui5-input-inner")
97+
.realClick();
98+
99+
cy.get("#test-multi-input")
100+
.shadow()
101+
.find<ResponsivePopover>("[ui5-responsive-popover]")
102+
.as("popover")
103+
.ui5ResponsivePopoverOpened();
104+
105+
// Assert: Button should be initially disabled (no tokens)
106+
cy.get("@popover")
107+
.find("[ui5-toggle-button]")
108+
.as("filterButton")
109+
.should("have.attr", "disabled");
110+
111+
cy.get("#test-multi-input")
112+
.shadow()
113+
.find(".ui5-responsive-popover-close-btn")
114+
.realClick();
115+
116+
cy.get("#add-token").realClick();
117+
118+
cy.get("#test-multi-input")
119+
.find("[ui5-token]")
120+
.should("have.length", 1);
121+
122+
cy.get("#test-multi-input")
123+
.shadow()
124+
.find(".ui5-input-inner")
125+
.realClick();
126+
127+
cy.get<ResponsivePopover>("@popover")
128+
.ui5ResponsivePopoverOpened();
129+
130+
// Assert: Button should be enabled after adding a token
131+
cy.get("@filterButton")
132+
.should("not.have.attr", "disabled")
133+
cy.get("@filterButton")
134+
.should("have.attr", "pressed");
135+
136+
cy.get("@popover")
137+
.find("[ui5-li].ui5-suggestion-token-item")
138+
.first()
139+
.shadow()
140+
.find("[ui5-button]")
141+
.realClick();
142+
143+
cy.get("#test-multi-input")
144+
.find("[ui5-token]")
145+
.should("have.length", 0);
146+
147+
// Assert: Button should be disabled after removing all tokens
148+
cy.get("@filterButton")
149+
.should("have.attr", "disabled");
150+
});
151+
152+
it("Filter-selected button affects list content display", () => {
153+
cy.mount(
154+
<MultiInput id="test-multi-input" showSuggestions>
155+
<Token slot="tokens" text="Token 1"></Token>
156+
<Token slot="tokens" text="Token 2"></Token>
157+
<Token slot="tokens" text="Token 3"></Token>
158+
<SuggestionItem text="Argentina"></SuggestionItem>
159+
<SuggestionItem text="Brazil"></SuggestionItem>
160+
</MultiInput>
161+
);
162+
163+
cy.get("#test-multi-input")
164+
.shadow()
165+
.find(".ui5-input-inner")
166+
.click();
167+
168+
cy.get("#test-multi-input")
169+
.shadow()
170+
.find("[ui5-responsive-popover]")
171+
.as("popover")
172+
173+
cy.get("@popover")
174+
.find("[ui5-toggle-button]")
175+
.as("filterButton");
176+
177+
// Assert: Initially showing tokens (button pressed)
178+
cy.get("@filterButton")
179+
.should("have.attr", "pressed");
180+
181+
// Assert: Should see token list items
182+
cy.get("@popover")
183+
.find("[ui5-list].ui5-tokenizer-list")
184+
.should("exist");
185+
186+
cy.get("@popover")
187+
.find("[ui5-li].ui5-suggestion-token-item")
188+
.should("have.length", 3);
189+
190+
// Act: Toggle to hide tokens
191+
cy.get("@filterButton").realClick();
192+
193+
// Assert: Should see suggestion items instead
194+
cy.get("@popover")
195+
.find("[ui5-list]:not(.ui5-tokenizer-list)")
196+
.should("exist");
197+
198+
cy.get("#test-multi-input")
199+
.find("[ui5-suggestion-item]")
200+
.should("have.length", 2);
201+
202+
// Act: Toggle back to show tokens
203+
cy.get("@filterButton").realClick();
204+
205+
// Assert: Should see token list items again
206+
cy.get("@popover")
207+
.find("[ui5-list].ui5-tokenizer-list")
208+
.should("exist");
209+
210+
cy.get("@popover")
211+
.find("[ui5-li].ui5-suggestion-token-item")
212+
.should("have.length", 3);
213+
});
214+
});
47215
});

packages/main/src/InputPopoverTemplate.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js";
1010
import Popover from "./Popover.js";
1111
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
1212

13-
export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsList?: (this: Input) => JsxTemplateResult }) {
13+
export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsList?: (this: Input) => JsxTemplateResult, mobileHeader?: (this: Input) => JsxTemplateResult }) {
1414
const suggestionsList = hooks?.suggestionsList;
15+
const mobileHeader = hooks?.mobileHeader;
1516

1617
return (
1718
<>
18-
{this._effectiveShowSuggestions && this.Suggestions?.template.call(this, { suggestionsList, valueStateMessage, valueStateMessageInputIcon }) }
19+
{this._effectiveShowSuggestions && this.Suggestions?.template.call(this, {
20+
suggestionsList,
21+
mobileHeader,
22+
valueStateMessage,
23+
valueStateMessageInputIcon
24+
})}
1925

20-
{this.hasValueStateMessage &&
26+
{this.hasValueStateMessage && (
2127
<Popover
2228
preventInitialFocus={true}
2329
preventFocusRestore={true}
@@ -32,10 +38,10 @@ export default function InputPopoverTemplate(this: Input, hooks?: { suggestionsL
3238
>
3339
<div slot="header" class={this.classes.popoverValueState}>
3440
<Icon class="ui5-input-value-state-message-icon" name={valueStateMessageInputIcon.call(this)} />
35-
{ this.valueStateOpen && valueStateMessage.call(this) }
41+
{this.valueStateOpen && valueStateMessage.call(this)}
3642
</div>
3743
</Popover>
38-
}
44+
)}
3945
</>
4046
);
4147
}

packages/main/src/InputTemplate.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import InputPopoverTemplate from "./InputPopoverTemplate.js";
66

77
type TemplateHook = () => JsxTemplateResult;
88

9-
export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook }) {
9+
export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook, mobileHeader?: TemplateHook }) {
1010
const suggestionsList = hooks?.suggestionsList;
11+
const mobileHeader = hooks?.mobileHeader;
1112
const preContent = hooks?.preContent || defaultPreContent;
1213
const postContent = hooks?.postContent || defaultPostContent;
1314

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

121-
{ InputPopoverTemplate.call(this, { suggestionsList }) }
122+
{ InputPopoverTemplate.call(this, { suggestionsList, mobileHeader }) }
122123
</>
123124
);
124125
}

packages/main/src/MultiInput.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import {
1919
import type { ITabbable } from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
2020
import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
2121
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
22-
import { MULTIINPUT_ROLEDESCRIPTION_TEXT, MULTIINPUT_VALUE_HELP_LABEL, MULTIINPUT_VALUE_HELP } from "./generated/i18n/i18n-defaults.js";
22+
import {
23+
MULTIINPUT_ROLEDESCRIPTION_TEXT,
24+
MULTIINPUT_VALUE_HELP_LABEL,
25+
MULTIINPUT_VALUE_HELP,
26+
MULTIINPUT_FILTER_BUTTON_LABEL,
27+
} from "./generated/i18n/i18n-defaults.js";
2328
import Input from "./Input.js";
2429
import MultiInputTemplate from "./MultiInputTemplate.js";
2530
import styles from "./generated/themes/MultiInput.css.js";
@@ -123,6 +128,22 @@ class MultiInput extends Input implements IFormInputElement {
123128
@property()
124129
declare name?: string;
125130

131+
/**
132+
* Indicates whether to show tokens in suggestions popover
133+
* @default false
134+
* @private
135+
*/
136+
@property({ type: Boolean })
137+
_showTokensInSuggestions = false;
138+
139+
/**
140+
* Tracks whether user has explicitly toggled the show tokens state
141+
* @default false
142+
* @private
143+
*/
144+
@property({ type: Boolean })
145+
_userToggledShowTokens = false;
146+
126147
/**
127148
* Defines the component tokens.
128149
* @public
@@ -343,6 +364,23 @@ class MultiInput extends Input implements IFormInputElement {
343364
if (this.tokenizer) {
344365
this.tokenizer.readonly = this.readonly;
345366
}
367+
368+
// Reset toggle state if there are tokens and dialog is about to open
369+
if (this.tokens.length > 0 && !this._userToggledShowTokens) {
370+
this._showTokensInSuggestions = true;
371+
}
372+
}
373+
374+
/**
375+
* Override the _handlePickerAfterOpen method to reset toggle state when dialog opens with tokens
376+
*/
377+
_handlePickerAfterOpen() {
378+
if (this.tokens.length > 0) {
379+
this._showTokensInSuggestions = true;
380+
this._userToggledShowTokens = false;
381+
}
382+
383+
super._handlePickerAfterOpen();
346384
}
347385

348386
onAfterRendering() {
@@ -371,6 +409,10 @@ class MultiInput extends Input implements IFormInputElement {
371409
return MultiInput.i18nBundle.getText(MULTIINPUT_VALUE_HELP);
372410
}
373411

412+
get _filterButtonAccessibleName() {
413+
return MultiInput.i18nBundle.getText(MULTIINPUT_FILTER_BUTTON_LABEL);
414+
}
415+
374416
get _tokensCountTextId() {
375417
return `hiddenText-nMore`;
376418
}
@@ -419,6 +461,25 @@ class MultiInput extends Input implements IFormInputElement {
419461
get shouldDisplayOnlyValueStateMessage() {
420462
return this.hasValueStateMessage && !this.readonly && !this.open && this.focused && !this.tokenizer.open;
421463
}
464+
465+
/**
466+
* Computes the effective state for showing tokens in suggestions.
467+
* Defaults to true when tokens exist, but respects explicit user toggle.
468+
*/
469+
get _effectiveShowTokensInSuggestions() {
470+
// If no tokens exist, always false
471+
if (this.tokens.length === 0) {
472+
return false;
473+
}
474+
475+
// If user has never interacted with the toggle, default to true when tokens exist
476+
if (!this._userToggledShowTokens) {
477+
return true;
478+
}
479+
480+
// If user has interacted, respect their choice
481+
return this._showTokensInSuggestions;
482+
}
422483
}
423484

424485
MultiInput.define();

0 commit comments

Comments
 (0)