diff --git a/packages/main/src/ComboBox.ts b/packages/main/src/ComboBox.ts index 6234d2c2c831..3011f0175a2c 100644 --- a/packages/main/src/ComboBox.ts +++ b/packages/main/src/ComboBox.ts @@ -77,7 +77,8 @@ import type { ListItemClickEventDetail } from "./List.js"; import BusyIndicator from "./BusyIndicator.js"; import Button from "./Button.js"; import StandardListItem from "./StandardListItem.js"; -import ComboBoxGroupItem from "./ComboBoxGroupItem.js"; +import ComboBoxItemGroup, { isInstanceOfComboBoxItemGroup } from "./ComboBoxItemGroup.js"; +import ListItemGroup from "./ListItemGroup.js"; import ListItemGroupHeader from "./ListItemGroupHeader.js"; import ComboBoxFilter from "./types/ComboBoxFilter.js"; import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js"; @@ -93,10 +94,12 @@ const SKIP_ITEMS_SIZE = 10; interface IComboBoxItem extends UI5Element { text: string, focused: boolean, - isGroupItem: boolean, + isGroupItem?: boolean, selected?: boolean, additionalText?: string, stableDomRef: string, + _isVisible?: boolean, + items?: Array } type ValueStateAnnouncement = Record, string>; @@ -176,9 +179,10 @@ type ComboBoxSelectionChangeEventDetail = { BusyIndicator, Button, StandardListItem, + ListItemGroup, ListItemGroupHeader, Popover, - ComboBoxGroupItem, + ComboBoxItemGroup, Input, SuggestionItem, ], @@ -452,8 +456,15 @@ class ComboBox extends UI5Element implements IFormInputElement { if (this.open && !this._isKeyNavigation) { const items = this._filterItems(this.filterValue); + this._filteredItems = (items.length && items) || []; + } + + const hasNoVisibleItems = !this._filteredItems.length || !this._filteredItems.some(i => i._isVisible); - this._filteredItems = items.length ? items : this.items; + // If there is no filtered items matching the value, show all items when the arrow is pressed + if (((hasNoVisibleItems && !isPhone()) && this.value)) { + this.items.forEach(this._makeAllVisible.bind(this)); + this._filteredItems = this.items; } if (!this._initialRendering && document.activeElement === this && !this._filteredItems.length && popover) { @@ -613,6 +624,19 @@ class ComboBox extends UI5Element implements IFormInputElement { this._selectMatchingItem(); } + _resetItemVisibility() { + this.items.forEach(item => { + if (isInstanceOfComboBoxItemGroup(item)) { + item.items?.forEach(i => { + i._isVisible = false; + }); + return; + } + + item._isVisible = false; + }); + } + _arrowClick() { this.inner.focus(); this._resetFilter(); @@ -656,7 +680,7 @@ class ComboBox extends UI5Element implements IFormInputElement { if (value !== "" && (item && !item.selected && !item.isGroupItem)) { this.fireEvent("selection-change", { - item, + item: item as ComboBoxItem, }); } } @@ -695,36 +719,57 @@ class ComboBox extends UI5Element implements IFormInputElement { } _startsWithMatchingItems(str: string): Array { - return Filters.StartsWith(str, this._filteredItems, "text"); + const allItems:Array = this._getItems(); + return Filters.StartsWith(str, allItems, "text"); } _clearFocus() { - this._filteredItems.map(item => { + const allItems = this._getItems(); + + allItems.map(item => { item.focused = false; return item; }); } + // Get groups and items as a flat array for filtering + _getItems() { + const allItems: Array = []; + + this._filteredItems.forEach(item => { + if (isInstanceOfComboBoxItemGroup(item)) { + const groupedItems = [item, ...item.items]; + allItems.push(...groupedItems); + return; + } + + allItems.push(item); + }); + + return allItems; + } + handleNavKeyPress(e: KeyboardEvent) { + const allItems = this._getItems(); + if (this.focused && (isHome(e) || isEnd(e)) && this.value) { return; } const isOpen = this.open; - const currentItem = this._filteredItems.find(item => { + const currentItem = allItems.find(item => { return isOpen ? item.focused : item.selected; }); - const indexOfItem = currentItem ? this._filteredItems.indexOf(currentItem) : -1; - + const indexOfItem = currentItem ? allItems.indexOf(currentItem) : -1; e.preventDefault(); if (this.focused && isOpen && (isUp(e) || isPageUp(e) || isPageDown(e))) { return; } - if (this._filteredItems.length - 1 === indexOfItem && isDown(e)) { + if (allItems.length - 1 === indexOfItem && isDown(e)) { return; } @@ -743,10 +788,12 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handleItemNavigation(e: KeyboardEvent, indexOfItem: number, isForward: boolean) { + const allItems = this._getItems(); + const isOpen = this.open; - const currentItem = this._filteredItems[indexOfItem]; - const nextItem = isForward ? this._filteredItems[indexOfItem + 1] : this._filteredItems[indexOfItem - 1]; + const currentItem: IComboBoxItem = allItems[indexOfItem]; const isGroupItem = currentItem && currentItem.isGroupItem; + const nextItem = isForward ? allItems[indexOfItem + 1] : allItems[indexOfItem - 1]; if ((!isOpen) && ((isGroupItem && !nextItem) || (!isGroupItem && !currentItem))) { return; @@ -758,6 +805,7 @@ class ComboBox extends UI5Element implements IFormInputElement { this._itemFocused = true; this.value = isGroupItem ? "" : currentItem.text; this.focused = false; + currentItem.focused = true; } else { this.focused = true; @@ -773,14 +821,13 @@ class ComboBox extends UI5Element implements IFormInputElement { if (isGroupItem && isOpen) { return; } - // autocomplete const item = this._getFirstMatchingItem(this.value); item && this._applyAtomicValueAndSelection(item, (this.open ? this._userTypedValue : ""), true); if ((item && !item.selected)) { this.fireEvent("selection-change", { - item, + item: item as ComboBoxItem, }); } @@ -832,9 +879,10 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handlePageUp(e: KeyboardEvent, indexOfItem: number) { + const allItems = this._getItems(); const isProposedIndexValid = indexOfItem - SKIP_ITEMS_SIZE > -1; indexOfItem = isProposedIndexValid ? indexOfItem - SKIP_ITEMS_SIZE : 0; - const shouldMoveForward = this._filteredItems[indexOfItem].isGroupItem && !this.open; + const shouldMoveForward = isInstanceOfComboBoxItemGroup(allItems[indexOfItem]) && !this.open; if (!isProposedIndexValid && this.hasValueStateText && this.open) { this._clearFocus(); @@ -848,17 +896,18 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handlePageDown(e: KeyboardEvent, indexOfItem: number) { - const itemsLength = this._filteredItems.length; + const allItems = this._getItems(); + const itemsLength = allItems.length; const isProposedIndexValid = indexOfItem + SKIP_ITEMS_SIZE < itemsLength; indexOfItem = isProposedIndexValid ? indexOfItem + SKIP_ITEMS_SIZE : itemsLength - 1; - const shouldMoveForward = this._filteredItems[indexOfItem].isGroupItem && !this.open; + const shouldMoveForward = isInstanceOfComboBoxItemGroup(allItems[indexOfItem]) && !this.open; this._handleItemNavigation(e, indexOfItem, shouldMoveForward); } _handleHome(e: KeyboardEvent) { - const shouldMoveForward = this._filteredItems[0].isGroupItem && !this.open; + const shouldMoveForward = isInstanceOfComboBoxItemGroup(this._filteredItems[0]) && !this.open; if (this.hasValueStateText && this.open) { this._clearFocus(); @@ -872,7 +921,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handleEnd(e: KeyboardEvent) { - this._handleItemNavigation(e, this._filteredItems.length - 1, true /* isForward */); + this._handleItemNavigation(e, this._getItems().length - 1, true /* isForward */); } _keyup() { @@ -882,6 +931,7 @@ class ComboBox extends UI5Element implements IFormInputElement { _keydown(e: KeyboardEvent) { const isNavKey = isDown(e) || isUp(e) || isPageUp(e) || isPageDown(e) || isHome(e) || isEnd(e); const picker = this.responsivePopover; + const allItems: Array = this._getItems(); this._autocomplete = !(isBackSpace(e) || isDelete(e)); this._isKeyNavigation = false; @@ -891,8 +941,16 @@ class ComboBox extends UI5Element implements IFormInputElement { } if (isEnter(e)) { - const focusedItem = this._filteredItems.find(item => { - return item.focused; + let focusedItem: IComboBoxItem | undefined; + + this._filteredItems.forEach(item => { + if (isInstanceOfComboBoxItemGroup(item) && !focusedItem) { + focusedItem = item.items.find(groupItem => groupItem.focused); + } + + if (item.focused) { + focusedItem = item; + } }); this._fireChangeEvent(); @@ -922,7 +980,7 @@ class ComboBox extends UI5Element implements IFormInputElement { this._resetFilter(); this._toggleRespPopover(); - const selectedItem = this._filteredItems.find(item => { + const selectedItem = allItems.find(item => { return item.selected; }); @@ -978,46 +1036,54 @@ class ComboBox extends UI5Element implements IFormInputElement { } _filterItems(str: string) { - const itemsToFilter = this.items.filter(item => !item.isGroupItem); - const filteredItems = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, itemsToFilter, "text"); + let filteredItem:IComboBoxItem; + let filteredGroupItems: Array = []; + const filteredItems: Array = []; + const filteredItemGroups: Array = []; - // 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); - } + this._resetItemVisibility(); + this.items.forEach(item => { + if (isInstanceOfComboBoxItemGroup(item)) { + filteredGroupItems = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, item.items, "text"); + filteredGroupItems.forEach(i => { + i._isVisible = true; + }); - /** - * Returns true if the group header should be shown (if there is a filtered suggestion item for this group item) - * @private - */ - static _groupItemFilter(item: IComboBoxItem, idx: number, allItems: Array, filteredItems: Array) { - if (item.isGroupItem) { - let groupHasFilteredItems; + if (filteredGroupItems.length) { + filteredItemGroups.push(item); + } - while (allItems[idx] && !allItems[idx].isGroupItem && !groupHasFilteredItems) { - groupHasFilteredItems = filteredItems.indexOf(allItems[idx]) !== -1; - idx++; + return; } - return groupHasFilteredItems; - } + [filteredItem] = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, [item], "text"); + + if (filteredItem) { + filteredItem._isVisible = true; + filteredItems.push(filteredItem); + } + }); + + return [...filteredItemGroups, ...filteredItems]; } - _getFirstMatchingItem(current: string): ComboBoxItem | undefined { - const currentlyFocusedItem = this.items.find(item => item.focused === true); + _getFirstMatchingItem(current: string): IComboBoxItem | void { + const allItems = this._getItems(); + const currentlyFocusedItem = allItems.find(item => item.focused === true); if (currentlyFocusedItem?.isGroupItem) { this.value = this.filterValue; return; } - const matchingItems: Array = (this._startsWithMatchingItems(current).filter(item => !item.isGroupItem) as Array); + const matchingItems: Array = (this._startsWithMatchingItems(current).filter(item => !isInstanceOfComboBoxItemGroup(item))); if (matchingItems.length) { return matchingItems[0]; } } - _applyAtomicValueAndSelection(item: ComboBoxItem, filterValue: string, highlightValue: boolean) { + _applyAtomicValueAndSelection(item: IComboBoxItem, filterValue: string, highlightValue: boolean) { const value = (item && item.text) || ""; this.inner.value = value; @@ -1030,13 +1096,24 @@ class ComboBox extends UI5Element implements IFormInputElement { _selectMatchingItem() { const currentlyFocusedItem = this.items.find(item => item.focused); const shouldSelectionBeCleared = currentlyFocusedItem && currentlyFocusedItem.isGroupItem; + let itemToBeSelected: IComboBoxItem | undefined; - const itemToBeSelected = this._filteredItems.find(item => { - return !item.isGroupItem && (item.text === this.value) && !shouldSelectionBeCleared; + this._filteredItems.forEach(item => { + if (!shouldSelectionBeCleared && !itemToBeSelected) { + itemToBeSelected = ((!item.isGroupItem && (item.text === this.value)) ? item : item?.items?.find(i => i.text === this.value)); + } }); this._filteredItems = this._filteredItems.map(item => { - item.selected = item === itemToBeSelected; + if (!isInstanceOfComboBoxItemGroup(item)) { + item.selected = item === itemToBeSelected; + return item; + } + + item.items?.forEach(groupItem => { + groupItem.selected = itemToBeSelected === groupItem; + }); + return item; }); } @@ -1095,9 +1172,10 @@ class ComboBox extends UI5Element implements IFormInputElement { } _announceSelectedItem(indexOfItem: number) { - const currentItem = this._filteredItems[indexOfItem]; - const nonGroupItems = this._filteredItems.filter(item => !item.isGroupItem); - const currentItemAdditionalText = currentItem.additionalText || ""; + const allItems = this._getItems(); + const currentItem = allItems[indexOfItem]; + const nonGroupItems = allItems.filter(item => !item.isGroupItem); + const currentItemAdditionalText = currentItem?.additionalText || ""; const isGroupItem = currentItem?.isGroupItem; const itemPositionText = ComboBox.i18nBundle.getText(LIST_ITEM_POSITION, nonGroupItems.indexOf(currentItem) + 1, nonGroupItems.length); const groupHeaderText = ComboBox.i18nBundle.getText(LIST_ITEM_GROUP_HEADER); @@ -1110,7 +1188,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _clear() { - const selectedItem = this.items.find(item => item.selected) as (ComboBoxItem | undefined); + const selectedItem = this.items.find(item => item.selected); if (selectedItem?.text === this.value) { this.fireEvent("change"); @@ -1127,10 +1205,21 @@ class ComboBox extends UI5Element implements IFormInputElement { } } + _makeAllVisible(item: IComboBoxItem) { + if (isInstanceOfComboBoxItemGroup(item)) { + item.items.forEach(groupItem => { + groupItem._isVisible = true; + }); + return; + } + + item._isVisible = true; + } + async _scrollToItem(indexOfItem: number, forward: boolean) { const picker = await this._getPicker(); const list = picker.querySelector(".ui5-combobox-items-list") as List; - const listItem = list?.items[indexOfItem]; + const listItem = list?.listItems[indexOfItem]; if (listItem) { const pickerRect = picker.getBoundingClientRect(); diff --git a/packages/main/src/ComboBoxItem.ts b/packages/main/src/ComboBoxItem.ts index 22ecf6aeccce..3b923b1a0379 100644 --- a/packages/main/src/ComboBoxItem.ts +++ b/packages/main/src/ComboBoxItem.ts @@ -31,6 +31,13 @@ class ComboBoxItem extends UI5Element implements IComboBoxItem { @property() additionalText!: string + /** + * Indicates whether the item is filtered + * @private + */ + @property({ type: Boolean, noAttribute: true }) + _isVisible!: boolean; + /** * Indicates whether the item is focssed * @protected @@ -45,14 +52,6 @@ class ComboBoxItem extends UI5Element implements IComboBoxItem { @property({ type: Boolean }) selected!: boolean; - /** - * Used to avoid tag name checks - * @protected - */ - get isGroupItem(): boolean { - return false; - } - get stableDomRef() { return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`; } diff --git a/packages/main/src/ComboBoxGroupItem.ts b/packages/main/src/ComboBoxItemGroup.ts similarity index 57% rename from packages/main/src/ComboBoxGroupItem.ts rename to packages/main/src/ComboBoxItemGroup.ts index b571098f0c42..3b97e032a8c9 100644 --- a/packages/main/src/ComboBoxGroupItem.ts +++ b/packages/main/src/ComboBoxItemGroup.ts @@ -1,5 +1,6 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import type { IComboBoxItem } from "./ComboBox.js"; @@ -14,8 +15,8 @@ import type { IComboBoxItem } from "./ComboBox.js"; * @implements {IComboBoxItem} * @since 1.0.0-rc.15 */ -@customElement("ui5-cb-group-item") -class ComboBoxGroupItem extends UI5Element implements IComboBoxItem { +@customElement("ui5-cb-item-group") +class ComboBoxItemGroup extends UI5Element implements IComboBoxItem { /** * Defines the text of the component. * @default "" @@ -25,12 +26,23 @@ class ComboBoxGroupItem extends UI5Element implements IComboBoxItem { text!: string; /** - * Indicates whether the item is focssed + * Indicates whether the item is focused * @protected */ @property({ type: Boolean }) focused!: boolean; + /** + * Defines the items of the ui5-cb-item-group. + * @public + */ + @slot({ + "default": true, + invalidateOnChildChange: true, + type: HTMLElement, + }) + items!: Array; + /** * Used to avoid tag name checks * @protected @@ -42,8 +54,17 @@ class ComboBoxGroupItem extends UI5Element implements IComboBoxItem { get stableDomRef() { return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`; } + + get _isVisible() { + return this.items.some(item => item._isVisible); + } } -ComboBoxGroupItem.define(); +ComboBoxItemGroup.define(); + +const isInstanceOfComboBoxItemGroup = (object: any): object is ComboBoxItemGroup => { + return "isGroupItem" in object; +}; -export default ComboBoxGroupItem; +export { isInstanceOfComboBoxItemGroup }; +export default ComboBoxItemGroup; diff --git a/packages/main/src/ComboBoxPopover.hbs b/packages/main/src/ComboBoxPopover.hbs index a3a7999d39ed..098be08e7c6d 100644 --- a/packages/main/src/ComboBoxPopover.hbs +++ b/packages/main/src/ComboBoxPopover.hbs @@ -62,6 +62,7 @@ {{/unless}} {{#each _filteredItems}} {{#if isGroupItem}} - {{ this.text }} + {{#if _isVisible}} + + {{#each this.items}} + {{#if _isVisible}} + {{> listItem}} + {{/if}} + {{/each}} + + {{/if}} {{else}} {{> listItem}} {{/if}} @@ -118,6 +127,7 @@ {{#*inline "listItem"}} Items with grouping and value state: - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -105,26 +111,27 @@
Items with grouping: - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + +
diff --git a/packages/main/test/specs/ComboBox.mobile.spec.js b/packages/main/test/specs/ComboBox.mobile.spec.js index fcea70fb51dd..79b515848015 100644 --- a/packages/main/test/specs/ComboBox.mobile.spec.js +++ b/packages/main/test/specs/ComboBox.mobile.spec.js @@ -243,9 +243,9 @@ describe("Picker filtering", () => { const dialogInput = await combo.shadow$("ui5-responsive-popover").$("[ui5-input]"); const dialogList = await combo.shadow$("ui5-responsive-popover").$('ui5-list') - assert.strictEqual(await dialogList.$$('ui5-li').length, 8, "All of the items are shown (8)"); + assert.strictEqual(await dialogList.$$('ui5-li').length, 9, "All of the items are shown (8)"); await dialogInput.keys("B"); - assert.strictEqual(await dialogList.$$('ui5-li').length, 3, "There are 3 filtered items"); + assert.strictEqual(await dialogList.$$('ui5-li').length, 4, "There are 4 filtered items"); }); it("Should filter group header list items", async () => { @@ -259,9 +259,9 @@ describe("Picker filtering", () => { const dialogInput = await combo.shadow$("ui5-responsive-popover").$("[ui5-input]"); const dialogList = await combo.shadow$("ui5-responsive-popover").$('ui5-list') - assert.strictEqual(await dialogList.$$('ui5-li-group-header').length, 3, "All of the group header list items are shown (3)"); + assert.strictEqual(await dialogList.$$('ui5-li-group').length, 3, "All of the group header list items are shown (3)"); await dialogInput.keys("B"); - assert.strictEqual(await dialogList.$$('ui5-li-group-header').length, 1, "There is only 1 visible group header"); + assert.strictEqual(await dialogList.$$('ui5-li-group').length, 2, "There is only 1 visible group"); }); }); diff --git a/packages/main/test/specs/ComboBox.spec.js b/packages/main/test/specs/ComboBox.spec.js index f5e3dbb70e2c..2e7c1fddc82c 100644 --- a/packages/main/test/specs/ComboBox.spec.js +++ b/packages/main/test/specs/ComboBox.spec.js @@ -145,7 +145,7 @@ describe("General interaction", () => { listItems = await popover.$("ui5-list").$$("ui5-li"); // assert - assert.strictEqual(listItems.length, 0, "Items should be 0"); + assert.notOk(listItems.some(item => item._isVisible), "Rendered items should be 0"); assert.notOk(await popover.getProperty("open"), "Popover should close"); }); @@ -655,17 +655,21 @@ describe("Grouping", () => { const input = await combo.shadow$("#ui5-combobox-input"); const arrow = await combo.shadow$(".inputIcon"); let popover = await combo.shadow$("ui5-responsive-popover"); - let groupItems = await popover.$("ui5-list").$$("ui5-li-group-header"); - let listItems = await popover.$("ui5-list").$$("ui5-li"); + let listItems; + let groupItems; await arrow.click(); + + groupItems = await popover.$("ui5-list").$$("ui5-li-group"); + listItems = await popover.$("ui5-list").$$("ui5-li-group ui5-li"); + assert.strictEqual(groupItems.length, 4, "Group items should be 4"); assert.strictEqual(listItems.length, 13, "Items should be 13"); await input.keys("c"); popover = await combo.shadow$("ui5-responsive-popover"); - groupItems = await popover.$("ui5-list").$$("ui5-li-group-header"); + groupItems = await popover.$("ui5-list").$$("ui5-li-group"); listItems = await popover.$("ui5-list").$$("ui5-li"); assert.strictEqual(groupItems.length, 1, "Filtered group items should be 1"); @@ -684,7 +688,7 @@ describe("Grouping", () => { await arrow.click(); await input.keys("ArrowDown"); - groupItem = await popover.$("ui5-list").$$("ui5-li-group-header")[0]; + groupItem = await popover.$("ui5-list").$("ui5-li-group").shadow$("ui5-li-group-header"); assert.ok(await groupItem.getProperty("focused"), "The first group header should be focused"); }); @@ -692,7 +696,6 @@ describe("Grouping", () => { it ("Tests input value while group item is focused", async () => { const combo = await browser.$("#combo-grouping"); const input = await combo.shadow$("#ui5-combobox-input"); - const arrow = await combo.shadow$(".inputIcon"); const popover = await combo.shadow$("ui5-responsive-popover"); let groupItem; @@ -704,7 +707,7 @@ describe("Grouping", () => { await input.keys("ArrowDown"); await input.keys("ArrowDown"); - groupItem = await popover.$("ui5-list").$$("ui5-li-group-header")[1]; + groupItem = await popover.$("ui5-list").$$("ui5-li-group")[1]; assert.ok(await groupItem.getProperty("focused"), "The second group header should be focused"); assert.strictEqual(await combo.getProperty("filterValue"), "a", "Filter value should be the initial one"); @@ -725,6 +728,24 @@ describe("Grouping", () => { assert.ok(await popover.getProperty("open"), "Popover remains open"); }); + + it ("Grouped items should be filtered and with the correct role attributes", async () => { + await browser.url(`test/pages/ComboBox.html`); + + const combo = await browser.$("#combo-grouping"); + const input = await combo.shadow$("#ui5-combobox-input"); + const popover = await combo.shadow$("ui5-responsive-popover"); + const list = await popover.$("ui5-list"); + const listItem = await list.$("ui5-li"); + + await input.click(); + await input.keys("a"); + + assert.ok(await listItem, "The filtered item is shown"); + assert.strictEqual(await list.getAttribute("accessible-role"), "ListBox", "The list item has the correct role attribute"); + assert.strictEqual(await listItem.getAttribute("accessible-role"), "Option", "The list item has the correct role attribute"); + assert.strictEqual(await list.$$("ui5-li").length, 5, "Group items are filtered correctly"); + }); }); describe("Accessibility", async () => { @@ -922,7 +943,7 @@ describe("Keyboard navigation", async () => { await arrow.click(); await input.keys("ArrowDown"); - groupItem = await popover.$("ui5-list").$$("ui5-li-group-header")[0]; + groupItem = await popover.$("ui5-list").$$("ui5-li-group")[0]; assert.strictEqual(await groupItem.getProperty("focused"), true, "The first group header should be focused"); @@ -932,7 +953,7 @@ describe("Keyboard navigation", async () => { await input.keys("ArrowDown"); await input.keys("ArrowDown"); - listItem = await popover.$("ui5-list").$$("ui5-li")[0]; + listItem = await popover.$("ui5-list").$("ui5-li-group ui5-li"); assert.strictEqual(await listItem.getProperty("focused"), true, "The first list item after the group header should be focused"); @@ -967,7 +988,7 @@ describe("Keyboard navigation", async () => { await input.keys("ArrowDown"); await input.keys("ArrowDown"); - groupItem = await popover.$("ui5-list").$$("ui5-li-group-header")[0]; + groupItem = await popover.$("ui5-list").$("ui5-li-group"); assert.strictEqual(await groupItem.getProperty("focused"), true, "The first group header should be focused"); }); @@ -1019,7 +1040,7 @@ describe("Keyboard navigation", async () => { const input = await combo.shadow$("#ui5-combobox-input"); const arrow = await combo.shadow$(".inputIcon"); const popover = await combo.shadow$("ui5-responsive-popover"); - let listItem, prevListItem; + let prevListItem; await input.click(); await input.keys("ArrowDown"); @@ -1049,6 +1070,28 @@ describe("Keyboard navigation", async () => { assert.strictEqual(await prevListItem.getProperty("focused"), false, "The previously focused item is no longer focused"); }); + it ("Navigates back and forward through items in multiple groups", async () => { + await browser.url(`test/pages/ComboBox.html`); + + const combo = await browser.$("#value-state-grouping"); + const input = await combo.shadow$("#ui5-combobox-input"); + const arrow = await combo.shadow$(".inputIcon"); + + await input.click(); + + for (let i = 0; i < 5; i++) { + await input.keys("ArrowDown"); + } + + assert.equal(await combo.getProperty("value"), "Bahrain", "The value is updated with the first suggestion item of the second group"); + + for (let i = 0; i < 4; i++) { + await input.keys("ArrowUp"); + } + + assert.strictEqual(await combo.getProperty("value"), "Argentina", "The value is updated with the first suggestion item of the first group"); + }); + it ("Should focus the next/previous focusable element on TAB/SHIFT+TAB", async () => { await browser.url(`test/pages/ComboBox.html`); @@ -1122,6 +1165,36 @@ describe("Keyboard navigation", async () => { assert.strictEqual(await input.getProperty("value"), "Chile", "The +10 item should be selected on PAGEDOWN"); }); + it ("Should select previous item when the last suggestion is selected and the picker is closed", async () => { + await browser.url(`test/pages/ComboBox.html`); + + const combo = await browser.$("#combo-grouping"); + + await combo.click(); + await combo.keys("PageDown"); + await combo.keys("PageDown"); + await combo.keys("ArrowUp"); + + assert.strictEqual(await combo.getProperty("value"), "Albania", "The value is updated correctly"); + }); + + it ("Should keep the value from the selected items after closing the picker", async () => { + await browser.url(`test/pages/ComboBox.html`); + + const combo = await browser.$("#combo-grouping"); + const arrow = await combo.shadow$(".inputIcon"); + + await arrow.click(); + await combo.keys("ArrowDown"); + await combo.keys("ArrowDown"); + + assert.strictEqual(await combo.getProperty("value"), "Algeria", "The value is updated correctly"); + + await combo.keys("F4"); + + assert.strictEqual(await combo.getProperty("value"), "Algeria", "The value remains in the input field after picker is closed"); + }); + it ("Should select first matching item", async () => { await browser.url(`test/pages/ComboBox.html`); @@ -1180,7 +1253,7 @@ describe("Keyboard navigation", async () => { let isInVisibleArea = await browser.executeAsync(async done => { const combobox = document.getElementById("combo-grouping"); const picker = await combobox._getPicker(); - const listItem = picker.querySelector(".ui5-combobox-items-list ui5-li:last-child"); + const listItem = picker.querySelector(".ui5-combobox-items-list ui5-li-group:last-child ui5-li:last-child"); const scrollableRect = picker.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); const elementRect = listItem.getBoundingClientRect(); @@ -1195,7 +1268,7 @@ describe("Keyboard navigation", async () => { !isElementBelowViewport && !isElementLeftOfViewport && !isElementRightOfViewport - ); + ); done(isListItemInVisibleArea); }); @@ -1209,7 +1282,7 @@ describe("Keyboard navigation", async () => { isInVisibleArea = await browser.executeAsync(async done => { const combobox = document.getElementById("combo-grouping"); const picker = await combobox._getPicker(); - const listItem = picker.querySelector(".ui5-combobox-items-list ui5-li:last-child"); + const listItem = picker.querySelector(".ui5-combobox-items-list ui5-li-group:last-child ui5-li:last-child"); const scrollableRect = picker.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); const elementRect = listItem.getBoundingClientRect(); @@ -1224,12 +1297,64 @@ describe("Keyboard navigation", async () => { !isElementBelowViewport && !isElementLeftOfViewport && !isElementRightOfViewport - ); + ); done(isListItemInVisibleArea); }); assert.ok(isInVisibleArea, "Item should be displayed in the viewport"); + + await input.keys("Home"); + + let isFirstItemInVisibleArea = await browser.executeAsync(async done => { + const combobox = document.getElementById("combo-grouping"); + const picker = await combobox._getPicker(); + const firstListItem = picker.querySelector(".ui5-combobox-items-list ui5-li-group:first-child ui5-li:first-child"); + const scrollableRect = picker.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); + const firstItemBoundingClientRect = firstListItem.getBoundingClientRect(); + + // Check if the element is within the visible area + const isFirstItemAboveViewport = firstItemBoundingClientRect.bottom < scrollableRect.top; + const isFirstItemBelowViewport = firstItemBoundingClientRect.top > scrollableRect.bottom; + const isFirstItemLeftOfViewport = firstItemBoundingClientRect.right < scrollableRect.left; + const isFirstItemRightOfViewport = firstItemBoundingClientRect.left > scrollableRect.right; + + const isFirstItemInVisibleArea = ( + !isFirstItemAboveViewport && + !isFirstItemBelowViewport && + !isFirstItemLeftOfViewport && + !isFirstItemRightOfViewport + ); + + done(isFirstItemInVisibleArea); + }); + + assert.ok(isFirstItemInVisibleArea, "The first item should be displayed in the viewport"); + + let isLastItemInVisibleArea = await browser.executeAsync(async done => { + const combobox = document.getElementById("combo-grouping"); + const picker = await combobox._getPicker(); + const lastListItem = picker.querySelector(".ui5-combobox-items-list ui5-li-group:first-child ui5-li:first-child"); + const scrollableRect = picker.shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); + const lastItemBoundingClientRect = lastListItem.getBoundingClientRect(); + + // Check if the element is within the visible area + const isLastItemAboveViewport = lastItemBoundingClientRect.bottom < scrollableRect.top; + const isLastItemBelowViewport = lastItemBoundingClientRect.top > scrollableRect.bottom; + const isLastItemLeftOfViewport = lastItemBoundingClientRect.right < scrollableRect.left; + const isLastItemRightOfViewport = lastItemBoundingClientRect.left > scrollableRect.right; + + const isLastItemInVisibleArea = ( + !isLastItemAboveViewport && + !isLastItemBelowViewport && + !isLastItemLeftOfViewport && + !isLastItemRightOfViewport + ); + + done(isLastItemInVisibleArea); + }); + + assert.ok(isLastItemInVisibleArea, "The first item should be displayed in the viewport"); }); it ("Should get the physical DOM reference for the cb item", async () => {