Skip to content

Commit 5e3f391

Browse files
authored
feat(ui5-combobox): add suggestions grouping (#3469)
Enable users to set items to groups. Groups are visually identified in the suggestions list by group headers. For group headers a new component is introduced - ComboBoxGroupItem. The grouping is based on the items order as they are declared in the markup - the items between two group headers are considered to belong to the first one. Fixes: #3371
1 parent a5f27f2 commit 5e3f391

File tree

6 files changed

+281
-31
lines changed

6 files changed

+281
-31
lines changed

packages/main/src/ComboBox.js

+60-20
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import List from "./List.js";
4747
import BusyIndicator from "./BusyIndicator.js";
4848
import Button from "./Button.js";
4949
import StandardListItem from "./StandardListItem.js";
50+
import ComboBoxGroupItem from "./ComboBoxGroupItem.js";
5051

5152
/**
5253
* @public
@@ -338,7 +339,7 @@ const metadata = {
338339
* @alias sap.ui.webcomponents.main.ComboBox
339340
* @extends UI5Element
340341
* @tagname ui5-combobox
341-
* @appenddocs ComboBoxItem
342+
* @appenddocs ComboBoxItem ComboBoxGroupItem
342343
* @public
343344
* @since 1.0.0-rc.6
344345
*/
@@ -373,7 +374,6 @@ class ComboBox extends UI5Element {
373374
this._filteredItems = [];
374375
this._initialRendering = true;
375376
this._itemFocused = false;
376-
this._tempFilterValue = "";
377377
this._selectionChanged = false;
378378
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
379379
}
@@ -389,6 +389,12 @@ class ComboBox extends UI5Element {
389389

390390
this._selectMatchingItem();
391391

392+
if (this._isKeyNavigation && this.responsivePopover && this.responsivePopover.opened) {
393+
this.focused = false;
394+
} else if (this.shadowRoot.activeElement) {
395+
this.focused = this.shadowRoot.activeElement.id === "ui5-combobox-input";
396+
}
397+
392398
this._initialRendering = false;
393399
this._isKeyNavigation = false;
394400
}
@@ -405,7 +411,6 @@ class ComboBox extends UI5Element {
405411
}
406412

407413
this._itemFocused = false;
408-
409414
this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover);
410415
this.storeResponsivePopoverWidth();
411416
}
@@ -437,7 +442,6 @@ class ComboBox extends UI5Element {
437442
_afterClosePopover() {
438443
this._iconPressed = false;
439444
this._filteredItems = this.items;
440-
this._tempFilterValue = "";
441445

442446
// close device's keyboard and prevent further typing
443447
if (isPhone()) {
@@ -508,6 +512,7 @@ class ComboBox extends UI5Element {
508512
}
509513

510514
this._filteredItems = this._filterItems(value);
515+
511516
this.value = value;
512517
this.filterValue = value;
513518

@@ -517,7 +522,7 @@ class ComboBox extends UI5Element {
517522
if (this._autocomplete && value !== "") {
518523
const item = this._autoCompleteValue(value);
519524

520-
if (!this._selectionChanged && (item && !item.selected)) {
525+
if (!this._selectionChanged && (item && !item.selected && !item.isGroupItem)) {
521526
this.fireEvent("selection-change", {
522527
item,
523528
});
@@ -553,7 +558,7 @@ class ComboBox extends UI5Element {
553558
});
554559
}
555560

556-
handleArrowKeyPress(event) {
561+
async handleArrowKeyPress(event) {
557562
if (this.readonly || !this._filteredItems.length) {
558563
return;
559564
}
@@ -575,15 +580,23 @@ class ComboBox extends UI5Element {
575580

576581
indexOfItem += isArrowDown ? 1 : -1;
577582
indexOfItem = indexOfItem < 0 ? 0 : indexOfItem;
583+
this._filteredItems[indexOfItem].focused = true;
578584

579585
if (this.responsivePopover.opened) {
580586
this.announceSelectedItem(indexOfItem);
581587
}
582588

583-
this._filteredItems[indexOfItem].focused = true;
584-
this._filteredItems[indexOfItem].selected = true;
589+
this.value = this._filteredItems[indexOfItem].isGroupItem ? this.filterValue : this._filteredItems[indexOfItem].text;
585590

586-
this.value = this._filteredItems[indexOfItem].text;
591+
this._isKeyNavigation = true;
592+
this._itemFocused = true;
593+
this._selectionChanged = true;
594+
595+
if (this._filteredItems[indexOfItem].isGroupItem) {
596+
return;
597+
}
598+
599+
this._filteredItems[indexOfItem].selected = true;
587600

588601
// autocomplete
589602
const item = this._autoCompleteValue(this.value);
@@ -594,12 +607,8 @@ class ComboBox extends UI5Element {
594607
});
595608
}
596609

597-
this._isKeyNavigation = true;
598-
this._itemFocused = true;
599610
this.fireEvent("input");
600611
this._fireChangeEvent();
601-
602-
this._selectionChanged = true;
603612
}
604613

605614
_keydown(event) {
@@ -642,11 +651,40 @@ class ComboBox extends UI5Element {
642651
}
643652

644653
_filterItems(str) {
645-
return (Filters[this.filter] || Filters.StartsWithPerTerm)(str, this.items);
654+
const itemsToFilter = this.items.filter(item => !item.isGroupItem);
655+
const filteredItems = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, itemsToFilter);
656+
657+
// Return the filtered items and their group items
658+
return this.items.filter((item, idx, allItems) => ComboBox._groupItemFilter(item, ++idx, allItems, filteredItems) || filteredItems.indexOf(item) !== -1);
659+
}
660+
661+
/**
662+
* Returns true if the group header should be shown (if there is a filtered suggestion item for this group item)
663+
*
664+
* @private
665+
*/
666+
static _groupItemFilter(item, idx, allItems, filteredItems) {
667+
if (item.isGroupItem) {
668+
let groupHasFilteredItems;
669+
670+
while (allItems[idx] && !allItems[idx].isGroupItem && !groupHasFilteredItems) {
671+
groupHasFilteredItems = filteredItems.indexOf(allItems[idx]) !== -1;
672+
idx++;
673+
}
674+
675+
return groupHasFilteredItems;
676+
}
646677
}
647678

648679
_autoCompleteValue(current) {
649-
const matchingItems = this._startsWithMatchingItems(current);
680+
const currentlyFocusedItem = this.items.find(item => item.focused === true);
681+
682+
if (currentlyFocusedItem && currentlyFocusedItem.isGroupItem) {
683+
this.value = this.filterValue;
684+
return;
685+
}
686+
687+
const matchingItems = this._startsWithMatchingItems(current).filter(item => !item.isGroupItem);
650688

651689
if (matchingItems.length) {
652690
this.value = matchingItems[0] ? matchingItems[0].text : current;
@@ -656,7 +694,7 @@ class ComboBox extends UI5Element {
656694

657695
if (this._isKeyNavigation) {
658696
setTimeout(() => {
659-
this.inner.setSelectionRange(0, this.value.length);
697+
this.inner.setSelectionRange(this.filterValue.length, this.value.length);
660698
}, 0);
661699
} else if (matchingItems.length) {
662700
setTimeout(() => {
@@ -670,9 +708,11 @@ class ComboBox extends UI5Element {
670708
}
671709

672710
_selectMatchingItem() {
673-
this._filteredItems = this._filteredItems.map(item => {
674-
item.selected = (item.text === this.value);
711+
const currentlyFocusedItem = this.items.find(item => item.focused);
712+
const shouldSelectionBeCleared = currentlyFocusedItem && currentlyFocusedItem.isGroupItem;
675713

714+
this._filteredItems = this._filteredItems.map(item => {
715+
item.selected = !item.isGroupItem && (item.text === this.value) && !shouldSelectionBeCleared;
676716
return item;
677717
});
678718
}
@@ -716,8 +756,7 @@ class ComboBox extends UI5Element {
716756
}
717757

718758
this._filteredItems.map(item => {
719-
item.selected = (item === listItem.mappedItem);
720-
759+
item.selected = (item === listItem.mappedItem && !item.isGroupItem);
721760
return item;
722761
});
723762

@@ -825,6 +864,7 @@ class ComboBox extends UI5Element {
825864
Button,
826865
StandardListItem,
827866
Popover,
867+
ComboBoxGroupItem,
828868
];
829869
}
830870

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
2+
import GroupHeaderListItem from "./GroupHeaderListItem.js";
3+
4+
/**
5+
* @public
6+
*/
7+
const metadata = {
8+
tag: "ui5-cb-group-item",
9+
properties: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
10+
/**
11+
* Defines the text of the component.
12+
*
13+
* @type {string}
14+
* @defaultvalue ""
15+
* @public
16+
*/
17+
text: {
18+
type: String,
19+
},
20+
/**
21+
* Indicates whether the input is focssed
22+
* @private
23+
*/
24+
focused: {
25+
type: Boolean,
26+
},
27+
},
28+
slots: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
29+
},
30+
events: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
31+
},
32+
};
33+
34+
/**
35+
* @class
36+
* The <code>ui5-combobox-group-item</code> is type of suggestion item,
37+
* that can be used to split the <code>ui5-combobox</code> suggestions into groups.
38+
*
39+
* @constructor
40+
* @author SAP SE
41+
* @alias sap.ui.webcomponents.main.ComboBoxGroupItem
42+
* @extends UI5Element
43+
* @tagname ui5-cb-group-item
44+
* @public
45+
* @since 1.0.0-rc.15
46+
*/
47+
class ComboBoxGroupItem extends UI5Element {
48+
static get metadata() {
49+
return metadata;
50+
}
51+
52+
static get dependencies() {
53+
return [
54+
GroupHeaderListItem,
55+
];
56+
}
57+
58+
/**
59+
* Used to avoid tag name checks
60+
* @protected
61+
*/
62+
get isGroupItem() {
63+
return true;
64+
}
65+
}
66+
67+
ComboBoxGroupItem.define();
68+
69+
export default ComboBoxGroupItem;

packages/main/src/ComboBoxPopover.hbs

+16-10
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,22 @@
6868
mode="SingleSelect"
6969
>
7070
{{#each _filteredItems}}
71-
<ui5-li
72-
type="Active"
73-
additional-text={{this.additionalText}}
74-
._tabIndex={{itemTabIndex}}
75-
.mappedItem={{this}}
76-
?selected={{this.selected}}
77-
?focused={{this.focused}}
78-
>
79-
{{this.text}}
80-
</ui5-li>
71+
{{#if isGroupItem}}
72+
<ui5-li-groupheader ?focused={{this.focused}}>{{ this.text }}</ui5-li-groupheader>
73+
{{else}}
74+
<ui5-li
75+
type="Active"
76+
additional-text={{this.additionalText}}
77+
group-name={{this.groupName}}
78+
._tabIndex={{itemTabIndex}}
79+
.mappedItem={{this}}
80+
?selected={{this.selected}}
81+
?focused={{this.focused}}
82+
>
83+
{{this.text}}
84+
</ui5-li>
85+
{{/if}}
86+
8187
{{/each}}
8288
</ui5-list>
8389

packages/main/test/pages/ComboBox.html

+28-1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<ui5-cb-item text="Austria"></ui5-cb-item>
8585
<ui5-cb-item text="Bahrain"></ui5-cb-item>
8686
<ui5-cb-item text="Belgium"></ui5-cb-item>
87+
<ui5-cb-item text="Bosnia and Herzegovina"></ui5-cb-item>
8788
<div slot="valueStateMessage">Information message. This is a <a href="#">Link</a>. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.</div>
8889
<div slot="valueStateMessage">Information message 2. This is a <a href="#">Link</a>. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.</div>
8990
</ui5-combobox>
@@ -100,7 +101,33 @@
100101
</div>
101102

102103
<div class="demo-section">
103-
<ui5-label id="combo-additional-text">Items with additional text: </ui5-label>
104+
<ui5-label id="combo-grouping-label">Items with grouping:</ui5-label>
105+
<ui5-combobox filter="StartsWith" id="combo-grouping" style="width: 360px;" aria-label="Select destination:">
106+
<ui5-cb-group-item text="A"></ui5-cb-group-item>
107+
<ui5-cb-item text="Algeria"></ui5-cb-item>
108+
<ui5-cb-item text="Argentina"></ui5-cb-item>
109+
<ui5-cb-item text="Australia"></ui5-cb-item>
110+
<ui5-cb-item text="Austria"></ui5-cb-item>
111+
112+
<ui5-cb-group-item text="Donut"></ui5-cb-group-item>
113+
<ui5-cb-item text="Bahrain"></ui5-cb-item>
114+
<ui5-cb-item text="Belgium"></ui5-cb-item>
115+
<ui5-cb-item text="Bosnia and Herzegovina"></ui5-cb-item>
116+
<ui5-cb-item text="Brazil"></ui5-cb-item>
117+
118+
<ui5-cb-group-item text="C"></ui5-cb-group-item>
119+
<ui5-cb-item text="Canada"></ui5-cb-item>
120+
<ui5-cb-item text="Chile"></ui5-cb-item>
121+
122+
<ui5-cb-group-item text="Random Group Item Text"></ui5-cb-group-item>
123+
<ui5-cb-item text="Zimbabve"></ui5-cb-item>
124+
<ui5-cb-item text="Albania"></ui5-cb-item>
125+
<ui5-cb-item text="Madagascar"></ui5-cb-item>
126+
</ui5-combobox>
127+
</div>
128+
129+
<div class="demo-section">
130+
<ui5-label id="combo-additional-text">Items with additional text:</ui5-label>
104131
<ui5-combobox id="combobox-two-column-layout" style="width: 360px;" value="Bulgaria" aria-labelledby="countryLabel">
105132
<ui5-cb-item text="Algeria" additional-text="DZ"></ui5-cb-item>
106133
<ui5-cb-item text="Argentina" additional-text="AR"></ui5-cb-item>

packages/main/test/samples/ComboBox.sample.html

+37
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,43 @@ <h3>ComboBox with Two-Column Layout Items</h3>
176176

177177
</section>
178178

179+
<section>
180+
<h3>ComboBox with Grouping of Items</h3>
181+
<div class="snippet responsive-snippet">
182+
<ui5-combobox placeholder="ComboBox with grouping of suggestions">
183+
<ui5-cb-group-item text="A"></ui5-cb-group-item>
184+
<ui5-cb-item text="Argentina"></ui5-cb-item>
185+
<ui5-cb-item text="Australia"></ui5-cb-item>
186+
<ui5-cb-item text="Austria"></ui5-cb-item>
187+
<ui5-cb-group-item text="B"></ui5-cb-group-item>
188+
<ui5-cb-item text="Bahrain"></ui5-cb-item>
189+
<ui5-cb-item text="Belgium"></ui5-cb-item>
190+
<ui5-cb-item text="Brazil"></ui5-cb-item>
191+
<ui5-cb-group-item text="C"></ui5-cb-group-item>
192+
<ui5-cb-item text="Canada"></ui5-cb-item>
193+
<ui5-cb-item text="Chile"></ui5-cb-item>
194+
</ui5-combobox>
195+
</div>
196+
197+
<pre class="prettyprint lang-html"><xmp>
198+
199+
<ui5-combobox placeholder="ComboBox with grouping of suggestions">
200+
<ui5-cb-group-item text="A"></ui5-cb-group-item>
201+
<ui5-cb-item text="Argentina"></ui5-cb-item>
202+
<ui5-cb-item text="Australia"></ui5-cb-item>
203+
<ui5-cb-item text="Austria"></ui5-cb-item>
204+
<ui5-cb-group-item text="B"></ui5-cb-group-item>
205+
<ui5-cb-item text="Bahrain"></ui5-cb-item>
206+
<ui5-cb-item text="Belgium"></ui5-cb-item>
207+
<ui5-cb-item text="Brazil"></ui5-cb-item>
208+
<ui5-cb-group-item text="C"></ui5-cb-group-item>
209+
<ui5-cb-item text="Canada"></ui5-cb-item>
210+
<ui5-cb-item text="Chile"></ui5-cb-item>
211+
</ui5-combobox>
212+
213+
</xmp></pre>
214+
215+
</section>
179216

180217
<section>
181218
<h3>Lazy loading</h3>

0 commit comments

Comments
 (0)