Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-combobox): introduce nested grouping of items #8926

Merged
merged 25 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5abab93
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 9, 2024
6977ed1
feat(ui5-cb-group-item): introduce nested grouping (draft)
ndeshev May 10, 2024
970e138
feat(ui5-cb-group-item): introduce nested grouping (draft)
ndeshev May 13, 2024
dd6e411
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 14, 2024
03f88c3
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 16, 2024
c675a15
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 17, 2024
c730c00
Merge remote-tracking branch 'origin/main' into cb-item-grouping
ndeshev May 17, 2024
6b181be
feat(ui5-combobox): introduce nested grouping
ndeshev May 20, 2024
38da313
Merge branch 'main' into cb-item-grouping
ndeshev May 20, 2024
a42ae33
feat(ui5-cb-group-item): introduce nested grouping (draft)
ndeshev May 20, 2024
e888c72
feat(ui5-cb-group-item): introduce nested grouping (draft)
ndeshev May 20, 2024
ed34062
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 20, 2024
dd5dc05
Update ComboBox.mobile.spec.js
ndeshev May 20, 2024
3a744c8
Merge branch 'main' into cb-item-grouping
ndeshev May 21, 2024
313f34c
feat(ui5-cb-group-item): introduce nested grouping
ndeshev May 21, 2024
1eb42fe
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 22, 2024
981427a
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 22, 2024
69359df
Merge branch 'main' into cb-item-grouping
ndeshev May 22, 2024
fd8c952
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 22, 2024
9f18ba4
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 23, 2024
f576e7c
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 23, 2024
f190ec3
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 24, 2024
a32848b
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 25, 2024
d1b616a
Merge branch 'main' into cb-item-grouping
ndeshev May 28, 2024
9b8ba3d
feat(ui5-combobox): introduce nested grouping of items
ndeshev May 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 143 additions & 47 deletions packages/main/src/ComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -97,6 +98,8 @@ interface IComboBoxItem extends UI5Element {
selected?: boolean,
additionalText?: string,
stableDomRef: string,
_isVisible?: boolean,
items?: Array<IComboBoxItem>
}

type ValueStateAnnouncement = Record<Exclude<ValueState, ValueState.None>, string>;
Expand Down Expand Up @@ -176,9 +179,10 @@ type ComboBoxSelectionChangeEventDetail = {
BusyIndicator,
Button,
StandardListItem,
ListItemGroup,
ListItemGroupHeader,
Popover,
ComboBoxGroupItem,
ComboBoxItemGroup,
Input,
SuggestionItem,
],
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -656,7 +680,7 @@ class ComboBox extends UI5Element implements IFormInputElement {

if (value !== "" && (item && !item.selected && !item.isGroupItem)) {
this.fireEvent<ComboBoxSelectionChangeEventDetail>("selection-change", {
item,
item: item as ComboBoxItem,
});
}
}
Expand Down Expand Up @@ -695,36 +719,67 @@ class ComboBox extends UI5Element implements IFormInputElement {
}

_startsWithMatchingItems(str: string): Array<IComboBoxItem> {
return Filters.StartsWith(str, this._filteredItems, "text");
const filteredItems: Array<IComboBoxItem> = [];

this._filteredItems.forEach(item => {
if (isInstanceOfComboBoxItemGroup(item)) {
filteredItems.push(...item.items);
return;
}

filteredItems.push(item);
});

return Filters.StartsWith(str, filteredItems, "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<IComboBoxItem> = [];

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;
}

Expand All @@ -743,10 +798,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;
Expand All @@ -758,10 +815,11 @@ class ComboBox extends UI5Element implements IFormInputElement {
this._itemFocused = true;
this.value = isGroupItem ? "" : currentItem.text;
this.focused = false;

currentItem.focused = true;
} else {
this.focused = true;
this.value = isGroupItem ? nextItem.text : currentItem.text;
this.value = isGroupItem ? "" : currentItem.text;
currentItem.focused = false;
}

Expand All @@ -773,14 +831,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<ComboBoxSelectionChangeEventDetail>("selection-change", {
item,
item: item as ComboBoxItem,
});
}

Expand Down Expand Up @@ -848,11 +905,12 @@ 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 = allItems[indexOfItem].isGroupItem && !this.open;

this._handleItemNavigation(e, indexOfItem, shouldMoveForward);
}
Expand All @@ -872,7 +930,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() {
Expand All @@ -891,8 +949,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();
Expand Down Expand Up @@ -978,46 +1044,53 @@ 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<IComboBoxItem> = [];
const filteredItems: Array<IComboBoxItem> = [];
const filteredItemGroups: Array<IComboBoxItem> = [];

// 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<IComboBoxItem>, filteredItems: Array<IComboBoxItem>) {
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 {
_getFirstMatchingItem(current: string): IComboBoxItem | void {
const currentlyFocusedItem = this.items.find(item => item.focused === true);

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

const matchingItems: Array<ComboBoxItem> = (this._startsWithMatchingItems(current).filter(item => !item.isGroupItem) as Array<ComboBoxItem>);
const matchingItems: Array<IComboBoxItem> = (this._startsWithMatchingItems(current).map(item => (isInstanceOfComboBoxItemGroup(item) && item.items[0]) || 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;
Expand All @@ -1030,13 +1103,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;
});
}
Expand Down Expand Up @@ -1095,9 +1179,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);
Expand All @@ -1110,7 +1195,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");
Expand All @@ -1127,6 +1212,17 @@ 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;
Expand Down
7 changes: 7 additions & 0 deletions packages/main/src/ComboBoxItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading