Skip to content

Commit

Permalink
feat(ui5-combobox): add suggestions grouping (#3469)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ndeshev authored Jul 1, 2021
1 parent a5f27f2 commit 5e3f391
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 31 deletions.
80 changes: 60 additions & 20 deletions packages/main/src/ComboBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import List from "./List.js";
import BusyIndicator from "./BusyIndicator.js";
import Button from "./Button.js";
import StandardListItem from "./StandardListItem.js";
import ComboBoxGroupItem from "./ComboBoxGroupItem.js";

/**
* @public
Expand Down Expand Up @@ -338,7 +339,7 @@ const metadata = {
* @alias sap.ui.webcomponents.main.ComboBox
* @extends UI5Element
* @tagname ui5-combobox
* @appenddocs ComboBoxItem
* @appenddocs ComboBoxItem ComboBoxGroupItem
* @public
* @since 1.0.0-rc.6
*/
Expand Down Expand Up @@ -373,7 +374,6 @@ class ComboBox extends UI5Element {
this._filteredItems = [];
this._initialRendering = true;
this._itemFocused = false;
this._tempFilterValue = "";
this._selectionChanged = false;
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
}
Expand All @@ -389,6 +389,12 @@ class ComboBox extends UI5Element {

this._selectMatchingItem();

if (this._isKeyNavigation && this.responsivePopover && this.responsivePopover.opened) {
this.focused = false;
} else if (this.shadowRoot.activeElement) {
this.focused = this.shadowRoot.activeElement.id === "ui5-combobox-input";
}

this._initialRendering = false;
this._isKeyNavigation = false;
}
Expand All @@ -405,7 +411,6 @@ class ComboBox extends UI5Element {
}

this._itemFocused = false;

this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover);
this.storeResponsivePopoverWidth();
}
Expand Down Expand Up @@ -437,7 +442,6 @@ class ComboBox extends UI5Element {
_afterClosePopover() {
this._iconPressed = false;
this._filteredItems = this.items;
this._tempFilterValue = "";

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

this._filteredItems = this._filterItems(value);

this.value = value;
this.filterValue = value;

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

if (!this._selectionChanged && (item && !item.selected)) {
if (!this._selectionChanged && (item && !item.selected && !item.isGroupItem)) {
this.fireEvent("selection-change", {
item,
});
Expand Down Expand Up @@ -553,7 +558,7 @@ class ComboBox extends UI5Element {
});
}

handleArrowKeyPress(event) {
async handleArrowKeyPress(event) {
if (this.readonly || !this._filteredItems.length) {
return;
}
Expand All @@ -575,15 +580,23 @@ class ComboBox extends UI5Element {

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

if (this.responsivePopover.opened) {
this.announceSelectedItem(indexOfItem);
}

this._filteredItems[indexOfItem].focused = true;
this._filteredItems[indexOfItem].selected = true;
this.value = this._filteredItems[indexOfItem].isGroupItem ? this.filterValue : this._filteredItems[indexOfItem].text;

this.value = this._filteredItems[indexOfItem].text;
this._isKeyNavigation = true;
this._itemFocused = true;
this._selectionChanged = true;

if (this._filteredItems[indexOfItem].isGroupItem) {
return;
}

this._filteredItems[indexOfItem].selected = true;

// autocomplete
const item = this._autoCompleteValue(this.value);
Expand All @@ -594,12 +607,8 @@ class ComboBox extends UI5Element {
});
}

this._isKeyNavigation = true;
this._itemFocused = true;
this.fireEvent("input");
this._fireChangeEvent();

this._selectionChanged = true;
}

_keydown(event) {
Expand Down Expand Up @@ -642,11 +651,40 @@ class ComboBox extends UI5Element {
}

_filterItems(str) {
return (Filters[this.filter] || Filters.StartsWithPerTerm)(str, this.items);
const itemsToFilter = this.items.filter(item => !item.isGroupItem);
const filteredItems = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, itemsToFilter);

// Return the filtered items and their group items
return this.items.filter((item, idx, allItems) => ComboBox._groupItemFilter(item, ++idx, allItems, filteredItems) || filteredItems.indexOf(item) !== -1);
}

/**
* Returns true if the group header should be shown (if there is a filtered suggestion item for this group item)
*
* @private
*/
static _groupItemFilter(item, idx, allItems, filteredItems) {
if (item.isGroupItem) {
let groupHasFilteredItems;

while (allItems[idx] && !allItems[idx].isGroupItem && !groupHasFilteredItems) {
groupHasFilteredItems = filteredItems.indexOf(allItems[idx]) !== -1;
idx++;
}

return groupHasFilteredItems;
}
}

_autoCompleteValue(current) {
const matchingItems = this._startsWithMatchingItems(current);
const currentlyFocusedItem = this.items.find(item => item.focused === true);

if (currentlyFocusedItem && currentlyFocusedItem.isGroupItem) {
this.value = this.filterValue;
return;
}

const matchingItems = this._startsWithMatchingItems(current).filter(item => !item.isGroupItem);

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

if (this._isKeyNavigation) {
setTimeout(() => {
this.inner.setSelectionRange(0, this.value.length);
this.inner.setSelectionRange(this.filterValue.length, this.value.length);
}, 0);
} else if (matchingItems.length) {
setTimeout(() => {
Expand All @@ -670,9 +708,11 @@ class ComboBox extends UI5Element {
}

_selectMatchingItem() {
this._filteredItems = this._filteredItems.map(item => {
item.selected = (item.text === this.value);
const currentlyFocusedItem = this.items.find(item => item.focused);
const shouldSelectionBeCleared = currentlyFocusedItem && currentlyFocusedItem.isGroupItem;

this._filteredItems = this._filteredItems.map(item => {
item.selected = !item.isGroupItem && (item.text === this.value) && !shouldSelectionBeCleared;
return item;
});
}
Expand Down Expand Up @@ -716,8 +756,7 @@ class ComboBox extends UI5Element {
}

this._filteredItems.map(item => {
item.selected = (item === listItem.mappedItem);

item.selected = (item === listItem.mappedItem && !item.isGroupItem);
return item;
});

Expand Down Expand Up @@ -825,6 +864,7 @@ class ComboBox extends UI5Element {
Button,
StandardListItem,
Popover,
ComboBoxGroupItem,
];
}

Expand Down
69 changes: 69 additions & 0 deletions packages/main/src/ComboBoxGroupItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import GroupHeaderListItem from "./GroupHeaderListItem.js";

/**
* @public
*/
const metadata = {
tag: "ui5-cb-group-item",
properties: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
/**
* Defines the text of the component.
*
* @type {string}
* @defaultvalue ""
* @public
*/
text: {
type: String,
},
/**
* Indicates whether the input is focssed
* @private
*/
focused: {
type: Boolean,
},
},
slots: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
},
events: /** @lends sap.ui.webcomponents.main.ComboBoxGroupItem.prototype */ {
},
};

/**
* @class
* The <code>ui5-combobox-group-item</code> is type of suggestion item,
* that can be used to split the <code>ui5-combobox</code> suggestions into groups.
*
* @constructor
* @author SAP SE
* @alias sap.ui.webcomponents.main.ComboBoxGroupItem
* @extends UI5Element
* @tagname ui5-cb-group-item
* @public
* @since 1.0.0-rc.15
*/
class ComboBoxGroupItem extends UI5Element {
static get metadata() {
return metadata;
}

static get dependencies() {
return [
GroupHeaderListItem,
];
}

/**
* Used to avoid tag name checks
* @protected
*/
get isGroupItem() {
return true;
}
}

ComboBoxGroupItem.define();

export default ComboBoxGroupItem;
26 changes: 16 additions & 10 deletions packages/main/src/ComboBoxPopover.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,22 @@
mode="SingleSelect"
>
{{#each _filteredItems}}
<ui5-li
type="Active"
additional-text={{this.additionalText}}
._tabIndex={{itemTabIndex}}
.mappedItem={{this}}
?selected={{this.selected}}
?focused={{this.focused}}
>
{{this.text}}
</ui5-li>
{{#if isGroupItem}}
<ui5-li-groupheader ?focused={{this.focused}}>{{ this.text }}</ui5-li-groupheader>
{{else}}
<ui5-li
type="Active"
additional-text={{this.additionalText}}
group-name={{this.groupName}}
._tabIndex={{itemTabIndex}}
.mappedItem={{this}}
?selected={{this.selected}}
?focused={{this.focused}}
>
{{this.text}}
</ui5-li>
{{/if}}

{{/each}}
</ui5-list>

Expand Down
29 changes: 28 additions & 1 deletion packages/main/test/pages/ComboBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<ui5-cb-item text="Austria"></ui5-cb-item>
<ui5-cb-item text="Bahrain"></ui5-cb-item>
<ui5-cb-item text="Belgium"></ui5-cb-item>
<ui5-cb-item text="Bosnia and Herzegovina"></ui5-cb-item>
<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>
<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>
</ui5-combobox>
Expand All @@ -100,7 +101,33 @@
</div>

<div class="demo-section">
<ui5-label id="combo-additional-text">Items with additional text: </ui5-label>
<ui5-label id="combo-grouping-label">Items with grouping:</ui5-label>
<ui5-combobox filter="StartsWith" id="combo-grouping" style="width: 360px;" aria-label="Select destination:">
<ui5-cb-group-item text="A"></ui5-cb-group-item>
<ui5-cb-item text="Algeria"></ui5-cb-item>
<ui5-cb-item text="Argentina"></ui5-cb-item>
<ui5-cb-item text="Australia"></ui5-cb-item>
<ui5-cb-item text="Austria"></ui5-cb-item>

<ui5-cb-group-item text="Donut"></ui5-cb-group-item>
<ui5-cb-item text="Bahrain"></ui5-cb-item>
<ui5-cb-item text="Belgium"></ui5-cb-item>
<ui5-cb-item text="Bosnia and Herzegovina"></ui5-cb-item>
<ui5-cb-item text="Brazil"></ui5-cb-item>

<ui5-cb-group-item text="C"></ui5-cb-group-item>
<ui5-cb-item text="Canada"></ui5-cb-item>
<ui5-cb-item text="Chile"></ui5-cb-item>

<ui5-cb-group-item text="Random Group Item Text"></ui5-cb-group-item>
<ui5-cb-item text="Zimbabve"></ui5-cb-item>
<ui5-cb-item text="Albania"></ui5-cb-item>
<ui5-cb-item text="Madagascar"></ui5-cb-item>
</ui5-combobox>
</div>

<div class="demo-section">
<ui5-label id="combo-additional-text">Items with additional text:</ui5-label>
<ui5-combobox id="combobox-two-column-layout" style="width: 360px;" value="Bulgaria" aria-labelledby="countryLabel">
<ui5-cb-item text="Algeria" additional-text="DZ"></ui5-cb-item>
<ui5-cb-item text="Argentina" additional-text="AR"></ui5-cb-item>
Expand Down
37 changes: 37 additions & 0 deletions packages/main/test/samples/ComboBox.sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,43 @@ <h3>ComboBox with Two-Column Layout Items</h3>

</section>

<section>
<h3>ComboBox with Grouping of Items</h3>
<div class="snippet responsive-snippet">
<ui5-combobox placeholder="ComboBox with grouping of suggestions">
<ui5-cb-group-item text="A"></ui5-cb-group-item>
<ui5-cb-item text="Argentina"></ui5-cb-item>
<ui5-cb-item text="Australia"></ui5-cb-item>
<ui5-cb-item text="Austria"></ui5-cb-item>
<ui5-cb-group-item text="B"></ui5-cb-group-item>
<ui5-cb-item text="Bahrain"></ui5-cb-item>
<ui5-cb-item text="Belgium"></ui5-cb-item>
<ui5-cb-item text="Brazil"></ui5-cb-item>
<ui5-cb-group-item text="C"></ui5-cb-group-item>
<ui5-cb-item text="Canada"></ui5-cb-item>
<ui5-cb-item text="Chile"></ui5-cb-item>
</ui5-combobox>
</div>

<pre class="prettyprint lang-html"><xmp>

<ui5-combobox placeholder="ComboBox with grouping of suggestions">
<ui5-cb-group-item text="A"></ui5-cb-group-item>
<ui5-cb-item text="Argentina"></ui5-cb-item>
<ui5-cb-item text="Australia"></ui5-cb-item>
<ui5-cb-item text="Austria"></ui5-cb-item>
<ui5-cb-group-item text="B"></ui5-cb-group-item>
<ui5-cb-item text="Bahrain"></ui5-cb-item>
<ui5-cb-item text="Belgium"></ui5-cb-item>
<ui5-cb-item text="Brazil"></ui5-cb-item>
<ui5-cb-group-item text="C"></ui5-cb-group-item>
<ui5-cb-item text="Canada"></ui5-cb-item>
<ui5-cb-item text="Chile"></ui5-cb-item>
</ui5-combobox>

</xmp></pre>

</section>

<section>
<h3>Lazy loading</h3>
Expand Down
Loading

0 comments on commit 5e3f391

Please sign in to comment.