diff --git a/packages/main/src/ComboBox.js b/packages/main/src/ComboBox.js index 0427a3e0e707..6e33ae38ca4e 100644 --- a/packages/main/src/ComboBox.js +++ b/packages/main/src/ComboBox.js @@ -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 @@ -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 */ @@ -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"); } @@ -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; } @@ -405,7 +411,6 @@ class ComboBox extends UI5Element { } this._itemFocused = false; - this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover); this.storeResponsivePopoverWidth(); } @@ -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()) { @@ -508,6 +512,7 @@ class ComboBox extends UI5Element { } this._filteredItems = this._filterItems(value); + this.value = value; this.filterValue = value; @@ -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, }); @@ -553,7 +558,7 @@ class ComboBox extends UI5Element { }); } - handleArrowKeyPress(event) { + async handleArrowKeyPress(event) { if (this.readonly || !this._filteredItems.length) { return; } @@ -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); @@ -594,12 +607,8 @@ class ComboBox extends UI5Element { }); } - this._isKeyNavigation = true; - this._itemFocused = true; this.fireEvent("input"); this._fireChangeEvent(); - - this._selectionChanged = true; } _keydown(event) { @@ -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; @@ -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(() => { @@ -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; }); } @@ -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; }); @@ -825,6 +864,7 @@ class ComboBox extends UI5Element { Button, StandardListItem, Popover, + ComboBoxGroupItem, ]; } diff --git a/packages/main/src/ComboBoxGroupItem.js b/packages/main/src/ComboBoxGroupItem.js new file mode 100644 index 000000000000..61505a085e0c --- /dev/null +++ b/packages/main/src/ComboBoxGroupItem.js @@ -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 ui5-combobox-group-item is type of suggestion item, + * that can be used to split the ui5-combobox 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; diff --git a/packages/main/src/ComboBoxPopover.hbs b/packages/main/src/ComboBoxPopover.hbs index 2f0318116654..bfad81f5b47b 100644 --- a/packages/main/src/ComboBoxPopover.hbs +++ b/packages/main/src/ComboBoxPopover.hbs @@ -68,16 +68,22 @@ mode="SingleSelect" > {{#each _filteredItems}} - - {{this.text}} - + {{#if isGroupItem}} + {{ this.text }} + {{else}} + + {{this.text}} + + {{/if}} + {{/each}} diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index 22c69d7425d7..e6ec96b390d8 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -84,6 +84,7 @@ +
Information message. This is a Link. 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.
Information message 2. This is a Link. 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.
@@ -100,7 +101,33 @@
- Items with additional text: + Items with grouping: + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ Items with additional text: diff --git a/packages/main/test/samples/ComboBox.sample.html b/packages/main/test/samples/ComboBox.sample.html index 56c228282a07..e222e0d8ddd7 100644 --- a/packages/main/test/samples/ComboBox.sample.html +++ b/packages/main/test/samples/ComboBox.sample.html @@ -176,6 +176,43 @@

ComboBox with Two-Column Layout Items

+
+

ComboBox with Grouping of Items

+
+ + + + + + + + + + + + + +
+ +

+
+<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>
+
+
+ +

Lazy loading

diff --git a/packages/main/test/specs/ComboBox.spec.js b/packages/main/test/specs/ComboBox.spec.js index ddfe91956612..c3b3cb73cb55 100644 --- a/packages/main/test/specs/ComboBox.spec.js +++ b/packages/main/test/specs/ComboBox.spec.js @@ -384,9 +384,80 @@ describe("General interaction", () => { }); }); +describe("Grouping", () => { + + it ("Tests group filtering", () => { + browser.url(`http://localhost:${PORT}/test-resources/pages/ComboBox.html`); + + const combo = $("#combo-grouping"); + const input = combo.shadow$("#ui5-combobox-input"); + const arrow = combo.shadow$("[input-icon]"); + const staticAreaItemClassName = browser.getStaticAreaItemClassName("#combo-grouping"); + let popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + let groupItems = popover.$("ui5-list").$$("ui5-li-groupheader"); + let listItems = popover.$("ui5-list").$$("ui5-li"); + + arrow.click(); + assert.strictEqual(groupItems.length, 4, "Group items should be 4"); + assert.strictEqual(listItems.length, 13, "Items should be 13"); + + input.keys("c"); + + popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + groupItems = popover.$("ui5-list").$$("ui5-li-groupheader"); + listItems = popover.$("ui5-list").$$("ui5-li"); + + assert.strictEqual(groupItems.length, 1, "Filtered group items should be 1"); + assert.strictEqual(listItems.length, 2, "Filtered items should be 2"); + }); + + it ("Tests group item focusability", () => { + browser.url(`http://localhost:${PORT}/test-resources/pages/ComboBox.html`); + + const combo = $("#combo-grouping"); + const input = combo.shadow$("#ui5-combobox-input"); + const arrow = combo.shadow$("[input-icon]"); + const staticAreaItemClassName = browser.getStaticAreaItemClassName("#combo-grouping"); + const popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + let groupItem; + + arrow.click(); + input.keys("ArrowDown"); + + groupItem = popover.$("ui5-list").$$("ui5-li-groupheader")[0]; + + assert.strictEqual(groupItem.getProperty("focused"), true, "The first group header should be focused"); + }); + + it ("Tests input value while group item is focused", () => { + const combo = $("#combo-grouping"); + const input = combo.shadow$("#ui5-combobox-input"); + const arrow = combo.shadow$("[input-icon]"); + const staticAreaItemClassName = browser.getStaticAreaItemClassName("#combo-grouping"); + const popover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover"); + let groupItem; + + input.keys("a"); + input.keys("ArrowDown"); + input.keys("ArrowDown"); + input.keys("ArrowDown"); + input.keys("ArrowDown"); + input.keys("ArrowDown"); + input.keys("ArrowDown"); + + groupItem = popover.$("ui5-list").$$("ui5-li-groupheader")[1]; + + assert.strictEqual(groupItem.getProperty("focused"), true, "The second group header should be focused"); + assert.strictEqual(combo.getProperty("filterValue"), "a", "Filter value should be the initial one"); + assert.strictEqual(combo.getProperty("value"), "a", "Temp value should be reset to the initial filter value - no autocomplete"); + }); +}); + describe("Accessibility", () => { it ("Announce item on selection", () => { + browser.url(`http://localhost:${PORT}/test-resources/pages/ComboBox.html`); + const combo = $("#combo"); const arrow = combo.shadow$("[input-icon]"); const input = combo.shadow$("#ui5-combobox-input");