diff --git a/packages/main/cypress/specs/LitKeyFunction.cy.ts b/packages/main/cypress/specs/LitKeyFunction.cy.ts index 4134531cf14c..2ad10851c1f3 100644 --- a/packages/main/cypress/specs/LitKeyFunction.cy.ts +++ b/packages/main/cypress/specs/LitKeyFunction.cy.ts @@ -1,6 +1,7 @@ import { html } from "lit"; import "../../src/MultiComboBox.js"; import "../../src/MultiComboBoxItem.js"; +import type List from "../../src/List.js"; describe("Lit HTML key function for #each", () => { it("LIT HTML does not mess up keys when looping over lists", () => { @@ -21,15 +22,17 @@ describe("Lit HTML key function for #each", () => { cy.get("@mcb") .shadow() - .find(".ui5-multi-combobox-all-items-responsive-popover") - .as("popover"); + .find("[ui5-responsive-popover]") + .as("rpo"); - cy.get("@popover") - .find(".ui5-multi-combobox-all-items-list > ui5-li") - .as("items"); + cy.get("@rpo") + .find("[ui5-list]") + .as("list"); - cy.get("@items") - .eq(0) + cy.get("@list") + .then($el => { + return ($el[0] as List).getSlottedNodes("items"); + }) .realClick(); cy.get("@mcb") @@ -37,14 +40,18 @@ describe("Lit HTML key function for #each", () => { .find(".inputIcon") .realClick(); - // cy.get("@items") - // .eq(0) - // .should("contain.text", "") - // .should("not.have.attr", "selected"); - - // cy.get("@items") - // .eq(3) - // .should("contain.text", "USA") - // .should("have.attr", "selected"); + cy.get("@list") + .then($el => { + return ($el[0] as List).getSlottedNodes("items")[0]; + }) + .invoke("attr", "text", "") + .should("not.have.attr", "selected"); + + cy.get("@list") + .then($el => { + return ($el[0] as List).getSlottedNodes("items")[3]; + }) + .invoke("attr", "text", "USA") + .should("have.attr", "selected"); }); }); diff --git a/packages/main/cypress/specs/base/Events.cy.ts b/packages/main/cypress/specs/base/Events.cy.ts index 2ea8f3783624..18ba5d84c8b4 100644 --- a/packages/main/cypress/specs/base/Events.cy.ts +++ b/packages/main/cypress/specs/base/Events.cy.ts @@ -206,8 +206,7 @@ describe("Event bubbling", () => { .realClick(); cy.get("@multiCombobox") - .shadow() - .find("[ui5-responsive-popover]") + .find("[ui5-mcb-item]") .should("be.visible"); cy.get("@multiComboboxIcon") diff --git a/packages/main/src/ComboBox.ts b/packages/main/src/ComboBox.ts index 71a46c5a9710..3501380da787 100644 --- a/packages/main/src/ComboBox.ts +++ b/packages/main/src/ComboBox.ts @@ -98,7 +98,6 @@ interface IComboBoxItem extends UI5Element { isGroupItem?: boolean, selected?: boolean, additionalText?: string, - stableDomRef: string, _isVisible?: boolean, items?: Array } @@ -106,10 +105,6 @@ interface IComboBoxItem extends UI5Element { type ValueStateAnnouncement = Record, string>; type ValueStateTypeAnnouncement = Record, string>; -type ComboBoxListItem = ListItemStandard & { - mappedItem: ComboBoxItem -}; - enum ValueStateIconMapping { Negative = "error", Critical = "alert", @@ -398,7 +393,12 @@ class ComboBox extends UI5Element implements IFormInputElement { * Defines the component items. * @public */ - @slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true }) + @slot({ + type: HTMLElement, + "default": true, + individualSlots: true, + invalidateOnChildChange: true, + }) items!: Array; /** @@ -504,10 +504,6 @@ class ComboBox extends UI5Element implements IFormInputElement { } this.storeResponsivePopoverWidth(); - - this.items.forEach(item => { - item._getRealDomRef = () => this._getPicker().querySelector(`*[data-ui5-stable=${item.stableDomRef}]`)!; - }); } _focusin(e: FocusEvent) { @@ -1129,9 +1125,9 @@ class ComboBox extends UI5Element implements IFormInputElement { } _selectItem(e: CustomEvent) { - const listItem = e.detail.item as ComboBoxListItem; + const item = e.detail.item as ComboBoxItem; - this._selectedItemText = listItem.mappedItem.text || ""; + this._selectedItemText = item.text || ""; this._selectionPerformed = true; const sameItemSelected = this.value === this._selectedItemText; @@ -1144,17 +1140,12 @@ class ComboBox extends UI5Element implements IFormInputElement { this.value = this._selectedItemText; - if (!listItem.mappedItem.selected) { + if (!item.selected) { this.fireDecoratorEvent("selection-change", { - item: listItem.mappedItem, + item, }); } - this._filteredItems.map(item => { - item.selected = (item === listItem.mappedItem && !item.isGroupItem); - return item; - }); - this._fireChangeEvent(); this._closeRespPopover(); diff --git a/packages/main/src/ComboBoxItem.hbs b/packages/main/src/ComboBoxItem.hbs new file mode 100644 index 000000000000..cf3506e83bae --- /dev/null +++ b/packages/main/src/ComboBoxItem.hbs @@ -0,0 +1,18 @@ +{{>include "./ListItemBase.hbs"}} + +{{#*inline "listItemContent"}} +
+
+ + {{{text}}} + + {{#if additionalText}} + {{additionalText}} + {{/if}} +
+
+{{/inline}} + +{{#*inline "listItemAttributes"}} + role="option" +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/ComboBoxItem.ts b/packages/main/src/ComboBoxItem.ts index 426e141e6362..3406c8970040 100644 --- a/packages/main/src/ComboBoxItem.ts +++ b/packages/main/src/ComboBoxItem.ts @@ -1,19 +1,24 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import type { IComboBoxItem } from "./ComboBox.js"; +import ListItemBase from "./ListItemBase.js"; +import ComboBoxItemTemplate from "./generated/templates/ComboBoxItemTemplate.lit.js"; +import ComboboxItemCss from "./generated/themes/ComboBoxItem.css.js"; /** * @class * The `ui5-cb-item` represents the item for a `ui5-combobox`. * @constructor - * @extends UI5Element - * @abstract + * @extends ListItemBase * @implements {IComboBoxItem} * @public */ -@customElement("ui5-cb-item") -class ComboBoxItem extends UI5Element implements IComboBoxItem { +@customElement({ + tag: "ui5-cb-item", + template: ComboBoxItemTemplate, + styles: [ListItemBase.styles, ComboboxItemCss], +}) +class ComboBoxItem extends ListItemBase implements IComboBoxItem { /** * Defines the text of the component. * @default undefined @@ -52,9 +57,15 @@ class ComboBoxItem extends UI5Element implements IComboBoxItem { @property({ type: Boolean }) selected = false; - get stableDomRef() { - return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`; - } + /** + * Defines the markup text that will be displayed as suggestion. + * Used for highlighting the matching parts of the text. + * + * @since 2.4.0 + * @private + */ + @property() + markupText = ""; } ComboBoxItem.define(); diff --git a/packages/main/src/ComboBoxItemGroup.hbs b/packages/main/src/ComboBoxItemGroup.hbs new file mode 100644 index 000000000000..4cc129d71a5d --- /dev/null +++ b/packages/main/src/ComboBoxItemGroup.hbs @@ -0,0 +1,9 @@ +{{>include "./ListItemGroup.hbs"}} + +{{#*inline "items"}} + {{#each items}} + {{#if _isVisible}} + + {{/if}} + {{/each}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/ComboBoxItemGroup.ts b/packages/main/src/ComboBoxItemGroup.ts index 07f5859ce834..632b5d3e6079 100644 --- a/packages/main/src/ComboBoxItemGroup.ts +++ b/packages/main/src/ComboBoxItemGroup.ts @@ -1,63 +1,45 @@ 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"; +import ListItemGroup from "./ListItemGroup.js"; +import type ComboBoxItem from "./ComboBoxItem.js"; +import ComboBoxItemGroupTemplate from "./generated/templates/ComboBoxItemGroupTemplate.lit.js"; /** * @class * The `ui5-cb-group-item` is type of suggestion item, * that can be used to split the `ui5-combobox` suggestions into groups. * @constructor - * @extends UI5Element + * @extends ListItemGroup * @abstract * @public * @implements {IComboBoxItem} * @since 1.0.0-rc.15 */ -@customElement("ui5-cb-item-group") -class ComboBoxItemGroup extends UI5Element implements IComboBoxItem { - /** - * Defines the text of the component. - * @default undefined - * @public - */ - @property() - headerText?: string; - - /** - * Indicates whether the item is focused - * @protected - */ - @property({ type: Boolean }) - focused = false - - /** - * 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 - */ - get isGroupItem(): boolean { - return true; - } - - get stableDomRef() { - return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`; - } - - get _isVisible() { - return this.items.some(item => item._isVisible); - } +@customElement({ + tag: "ui5-cb-item-group", + template: ComboBoxItemGroupTemplate, +}) +class ComboBoxItemGroup extends ListItemGroup implements IComboBoxItem { + /** + * Defines the items of the ui5-cb-item-group. + * @public + */ + @slot({ + "default": true, + invalidateOnChildChange: true, + individualSlots: true, + type: HTMLElement, + }) + items!: Array; + + get isGroupItem(): boolean { + return true; + } + + get _isVisible() { + return this.items.some(item => item._isVisible); + } } ComboBoxItemGroup.define(); diff --git a/packages/main/src/ComboBoxPopover.hbs b/packages/main/src/ComboBoxPopover.hbs index 56dc7739c369..fb906e90e5fe 100644 --- a/packages/main/src/ComboBoxPopover.hbs +++ b/packages/main/src/ComboBoxPopover.hbs @@ -85,20 +85,7 @@ selection-mode="Single" > {{#each _filteredItems}} - {{#if isGroupItem}} - {{#if _isVisible}} - - {{#each this.items}} - {{#if _isVisible}} - {{> listItem}} - {{/if}} - {{/each}} - - {{/if}} - {{else}} - {{> listItem}} - {{/if}} - + {{/each}} @@ -140,20 +127,4 @@ {{else}} {{/if}} -{{/inline}} - -{{#*inline "listItem"}} - - {{this.text}} - -{{/inline}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index f257adf6321d..8d45a0842e80 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -872,10 +872,10 @@ class List extends UI5Element { slottedItems.forEach(item => { if (isInstanceOfListItemGroup(item)) { - const groupItems = [item.groupHeaderItem, ...item.items].filter(Boolean); + const groupItems = [item.groupHeaderItem, ...item.items.filter(listItem => listItem.assignedSlot)].filter(Boolean); items.push(...groupItems); } else { - items.push(item); + item.assignedSlot && items.push(item); } }); diff --git a/packages/main/src/ListItemGroup.hbs b/packages/main/src/ListItemGroup.hbs index c32938fee9da..d61eecb794d4 100644 --- a/packages/main/src/ListItemGroup.hbs +++ b/packages/main/src/ListItemGroup.hbs @@ -4,18 +4,25 @@ @dragover="{{_ondragover}}" @drop="{{_ondrop}}" @dragleave="{{_ondragleave}}"> + {{#if hasHeader}} {{#if hasFormattedHeader}} - + {{else}} - {{headerText}} + {{headerText}} {{/if}} {{/if}} - + + {{> items}} - \ No newline at end of file + + + +{{#*inline "items"}} + +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/MultiComboBox.hbs b/packages/main/src/MultiComboBox.hbs index fab3e94658b3..3ef27ca62d1f 100644 --- a/packages/main/src/MultiComboBox.hbs +++ b/packages/main/src/MultiComboBox.hbs @@ -39,18 +39,16 @@ {{/if}} {{/each}} {{else}} - {{#if selected}} - - - - {{/if}} - + {{#if selected}} + + + {{/if}} {{/if}} {{/each}} diff --git a/packages/main/src/MultiComboBox.ts b/packages/main/src/MultiComboBox.ts index de316f5927a3..dc5f0a61e696 100644 --- a/packages/main/src/MultiComboBox.ts +++ b/packages/main/src/MultiComboBox.ts @@ -101,7 +101,6 @@ import ValueStateMessageCss from "./generated/themes/ValueStateMessage.css.js"; import SuggestionsCss from "./generated/themes/Suggestions.css.js"; import MultiComboBoxPopover from "./generated/themes/MultiComboBoxPopover.css.js"; import type ComboBoxFilter from "./types/ComboBoxFilter.js"; -import type ListItemBase from "./ListItemBase.js"; import CheckBox from "./CheckBox.js"; import Input from "./Input.js"; import type { InputEventDetail } from "./Input.js"; @@ -117,7 +116,6 @@ interface IMultiComboBoxItem extends UI5Element { headerText?: string, selected: boolean, isGroupItem?: boolean, - stableDomRef: string, _isVisible?: boolean, items?: Array, } @@ -461,7 +459,12 @@ class MultiComboBox extends UI5Element implements IFormInputElement { * Defines the component items. * @public */ - @slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true }) + @slot({ + type: HTMLElement, + "default": true, + invalidateOnChildChange: true, + individualSlots: true, + }) items!: Array; /** @@ -992,7 +995,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { } _handleSelectAll() { - const filteredItems = this._filteredItems.filter(item => !item.isGroupItem); + const filteredItems = this._getItems().filter(item => item._isVisible && !item.isGroupItem); const allItemsSelected = filteredItems.every(item => item.selected); this._previouslySelectedItems = filteredItems.filter(item => item.selected).map(item => item); @@ -1007,7 +1010,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { } } - async _onListHeaderKeydown(e: KeyboardEvent) { + _onListHeaderKeydown(e: KeyboardEvent) { const isArrowDown = isDown(e); const isArrowUp = isUp(e); const isSelectAllFocused = (e.target as HTMLElement).classList.contains("ui5-mcb-select-all-checkbox"); @@ -1062,8 +1065,8 @@ class MultiComboBox extends UI5Element implements IFormInputElement { } _onItemKeydown(e: KeyboardEvent) { - const isFirstItemGroup = this.list?.items[1] === e.target && this.list?.items[0].hasAttribute("ui5-li-group"); - const isFirstItem = this.list?.items[0] === e.target || isFirstItemGroup; + const isFirstItemGroup = this.list?.getSlottedNodes("items")[1] === e.target && this.list?.getSlottedNodes("items")[0].hasAttribute("ui5-li-group"); + const isFirstItem = this.list?.getSlottedNodes("items")[0] === e.target || isFirstItemGroup; const isArrowUp = isUp(e) || isUpCtrl(e); if (this.hasValueStateMessage && !this.valueStateHeader) { @@ -1077,12 +1080,12 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if (isHomeCtrl(e)) { this.list?._itemNavigation._handleHome(); - this.list?.items[this.list?._itemNavigation._currentIndex].focus(); + this.list?.getSlottedNodes("items")[this.list?._itemNavigation._currentIndex].focus(); } if (isEndCtrl(e)) { this.list?._itemNavigation._handleEnd(); - this.list?.items[this.list?._itemNavigation._currentIndex].focus(); + this.list?.getSlottedNodes("items")[this.list?._itemNavigation._currentIndex].focus(); } e.preventDefault(); @@ -1094,12 +1097,12 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if ((isUpCtrl(e)) && !isFirstItem) { this.list?._itemNavigation._handleUp(); - this.list?.items[this.list?._itemNavigation._currentIndex].focus(); + this.list?.getSlottedNodes("items")[this.list?._itemNavigation._currentIndex].focus(); } if (isDownCtrl(e)) { this.list?._itemNavigation._handleDown(); - this.list?.items[this.list?._itemNavigation._currentIndex].focus(); + this.list?.getSlottedNodes("items")[this.list?._itemNavigation._currentIndex].focus(); } if (isShow(e)) { @@ -1114,7 +1117,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if (isFirstItem && isArrowUp) { if (this.showSelectAll) { if (isFirstItemGroup) { - this.list?.items[0].focus(); + this.list?.getSlottedNodes("items")[0].focus(); return; } @@ -1181,10 +1184,11 @@ class MultiComboBox extends UI5Element implements IFormInputElement { } async _handleArrowDown() { - const firstListItem = this.list?.listItems[0]; + const firstListItem = this.list?.getSlottedNodes("items")[0]; + const focusRef = firstListItem?.hasAttribute("ui5-mcb-item-group") ? (firstListItem as MultiComboBoxItemGroup).getFocusDomRef() : firstListItem; if (this.open) { - firstListItem && this.list?._itemNavigation.setCurrentItem(firstListItem); + firstListItem && focusRef && this.list?._itemNavigation.setCurrentItem(focusRef); this.value = this.valueBeforeAutoComplete || this.value; // wait item navigation to apply correct tabindex @@ -1197,8 +1201,8 @@ class MultiComboBox extends UI5Element implements IFormInputElement { _handleItemRangeSelection(e: KeyboardEvent) { const items = this._getItems(); - const listItems = this.list?.items; - const currentItemIdx = Number(listItems?.indexOf(e.target as ListItemBase)); + const listItems = this.list?.getSlottedNodes("items"); + const currentItemIdx = Number(listItems?.indexOf(e.target as IMultiComboBoxItem)); const nextItemIdx = currentItemIdx + 1; const prevItemIdx = currentItemIdx - 1; this._previouslySelectedItems = this._getSelectedItems(); @@ -1365,8 +1369,19 @@ class MultiComboBox extends UI5Element implements IFormInputElement { const itemsToFilter = this._getItems().filter(item => !item.isGroupItem); const filteredItems = (Filters[this.filter] || Filters.StartsWithPerTerm)(str, itemsToFilter, "text"); - // Return the filtered items and their group items - return this._getItems().filter((item, idx, allItems) => MultiComboBox._groupItemFilter(item, ++idx, allItems, filteredItems) || filteredItems.indexOf(item) !== -1); + this._getItems().forEach(item => { + if (isInstanceOfMultiComboBoxItem(item)) { + item._isVisible = filteredItems.includes(item); + } + }); + + return this.items.filter(item => { + if (item.isGroupItem) { + return (item as MultiComboBoxItemGroup).items.some(listItem => listItem._isVisible) ? item : false; + } + + return item._isVisible; + }); } /** @@ -1433,12 +1448,14 @@ class MultiComboBox extends UI5Element implements IFormInputElement { _listSelectionChange(e: CustomEvent) { let changePrevented; - if (!isPhone()) { - this._previouslySelectedItems = this._getSelectedItems(); + if (this.readonly) { + e.preventDefault(); + return; } - // sync list items and cb items - this.syncItems((e.target as List).listItems); + if (!isPhone()) { + this._previouslySelectedItems = e.detail.previouslySelectedItems; + } // don't call selection change right after selection as user can cancel it on phone if (!isPhone()) { @@ -1477,16 +1494,6 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this.value = this.valueBeforeAutoComplete || ""; } - syncItems(listItems: Array) { - listItems.forEach(item => { - this._getItems().forEach(mcbItem => { - if (mcbItem._id === item.getAttribute("data-ui5-token-id")) { - mcbItem.selected = item.selected; - } - }); - }); - } - fireSelectionChange() { const changePrevented = !this.fireDecoratorEvent("selection-change", { items: this._getSelectedItems(), @@ -1609,11 +1616,8 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if (this.open) { const list = this._getList(); - const selectedListItemsCount = list?.querySelectorAll("[ui5-li][selected]")?.length; - const items = this._getItems().filter(item => !item.isGroupItem); - const listItems = list?.querySelectorAll("[ui5-li]")?.length; - const selectedItemsCount = items.filter(item => item.selected).length; - this._allSelected = selectedItemsCount === items.length || selectedListItemsCount === listItems; + const selectedListItemsCount = this.items.filter(item => item.selected).length; + this._allSelected = selectedListItemsCount > 0 && ((selectedListItemsCount === this.items.length) || (list?.getSlottedNodes("items").length === selectedListItemsCount)); } this._effectiveShowClearIcon = (this.showClearIcon && !!this.value && !this.readonly && !this.disabled); @@ -1629,6 +1633,12 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this.style.setProperty(getScopedVarName("--_ui5-input-icons-count"), `${this.iconsCount}`); if (!input || !value) { + this._getItems().forEach(item => { + if (isInstanceOfMultiComboBoxItem(item)) { + item._isVisible = true; + item._readonly = this.readonly; + } + }); return; } // Typehead causes issues on Android devices, so we disable it for now @@ -1665,10 +1675,6 @@ class MultiComboBox extends UI5Element implements IFormInputElement { if (this._tokenizer.expanded && this.hasAttribute("focused")) { this._tokenizer.scrollToEnd(); } - - this._getItems().forEach(item => { - item._getRealDomRef = () => this._getResponsivePopover()!.querySelector(`*[data-ui5-stable=${item.stableDomRef}]`)!; - }); } get _isPhone() { @@ -1769,7 +1775,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { this._clearingValue = false; - if (!isPhone() && (((e.relatedTarget as HTMLElement)?.tagName !== "UI5-STATIC-AREA-ITEM") || !e.relatedTarget)) { + if (!isPhone() && e.target === this._innerInput) { this._innerInput.setSelectionRange(0, this.value.length); } this._tokenizer.tokens.forEach(token => { @@ -1786,6 +1792,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement { const focusIsGoingInValueStatePopup = this?.contains(e.relatedTarget as Node); if (focusIsGoingInValueStatePopup) { + this.focused = false; e.stopImmediatePropagation(); return; } diff --git a/packages/main/src/MultiComboBoxItem.hbs b/packages/main/src/MultiComboBoxItem.hbs new file mode 100644 index 000000000000..2b3a2b996c80 --- /dev/null +++ b/packages/main/src/MultiComboBoxItem.hbs @@ -0,0 +1,19 @@ +{{>include "./ComboBoxItem.hbs"}} + +{{#*inline "listItemContent"}} + +
+
+ + {{{text}}} + + {{#if additionalText}} + {{additionalText}} + {{/if}} +
+
+{{/inline}} + +{{#*inline "listItemAttributes"}} + role="option" +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/MultiComboBoxItem.ts b/packages/main/src/MultiComboBoxItem.ts index 726742708340..c00d027ac43b 100644 --- a/packages/main/src/MultiComboBoxItem.ts +++ b/packages/main/src/MultiComboBoxItem.ts @@ -1,18 +1,38 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import { event } from "@ui5/webcomponents-base/dist/decorators.js"; import ComboBoxItem from "./ComboBoxItem.js"; +import CheckBox from "./CheckBox.js"; import type { IMultiComboBoxItem } from "./MultiComboBox.js"; +import { + ARIA_LABEL_LIST_ITEM_CHECKBOX, +} from "./generated/i18n/i18n-defaults.js"; + +import styles from "./generated/themes/MultiComboBoxItem.css.js"; +import MultiComboBoxItemTemplate from "./generated/templates/MultiComboBoxItemTemplate.lit.js"; +import type { SelectionRequestEventDetail } from "./ListItem.js"; /** * @class * The `ui5-mcb-item` represents the item for a `ui5-multi-combobox`. * @constructor * @extends ComboBoxItem - * @abstract * @implements {IMultiComboBoxItem} * @public */ -@customElement("ui5-mcb-item") +@customElement({ + tag: "ui5-mcb-item", + template: MultiComboBoxItemTemplate, + styles: [ComboBoxItem.styles, styles], + dependencies: [...ComboBoxItem.dependencies, CheckBox], +}) + +@event("_selection-requested", { + bubbles: true, +}) + class MultiComboBoxItem extends ComboBoxItem implements IMultiComboBoxItem { /** * Defines the selected state of the component. @@ -29,9 +49,27 @@ class MultiComboBoxItem extends ComboBoxItem implements IMultiComboBoxItem { @property({ type: Boolean, noAttribute: true }) _isVisible = false; + @property({ type: Boolean, noAttribute: true }) + _readonly = false; + + @i18n("@ui5/webcomponents") + static i18nBundle: I18nBundle; + get isMultiComboBoxItem() { return true; } + + _onclick(e: MouseEvent) { + if ((e.target as HTMLElement)?.hasAttribute("ui5-checkbox")) { + return this.fireDecoratorEvent("_selection-requested", { item: this, selected: (e.target as CheckBox).checked, selectionComponentPressed: true }); + } + + super._onclick(e); + } + + get _accessibleName() { + return MultiComboBoxItem.i18nBundle.getText(ARIA_LABEL_LIST_ITEM_CHECKBOX); + } } const isInstanceOfMultiComboBoxItem = (object: any): object is MultiComboBoxItem => { diff --git a/packages/main/src/MultiComboBoxItemGroup.hbs b/packages/main/src/MultiComboBoxItemGroup.hbs new file mode 100644 index 000000000000..4cc129d71a5d --- /dev/null +++ b/packages/main/src/MultiComboBoxItemGroup.hbs @@ -0,0 +1,9 @@ +{{>include "./ListItemGroup.hbs"}} + +{{#*inline "items"}} + {{#each items}} + {{#if _isVisible}} + + {{/if}} + {{/each}} +{{/inline}} \ No newline at end of file diff --git a/packages/main/src/MultiComboBoxItemGroup.ts b/packages/main/src/MultiComboBoxItemGroup.ts index 2997679a0a9a..6fbc2a1bbb73 100644 --- a/packages/main/src/MultiComboBoxItemGroup.ts +++ b/packages/main/src/MultiComboBoxItemGroup.ts @@ -1,31 +1,26 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; -import property from "@ui5/webcomponents-base/dist/decorators/property.js"; -import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import type { IMultiComboBoxItem } from "./MultiComboBox.js"; import type MultiComboBoxItem from "./MultiComboBoxItem.js"; +import MultiComboBoxItemGroupTemplate from "./generated/templates/MultiComboBoxItemGroupTemplate.lit.js"; +import type ListItemGroupHeader from "./ListItemGroupHeader.js"; +import ComboBoxItemGroup from "./ComboBoxItemGroup.js"; /** * @class * The `ui5-mcb-item-group` is type of suggestion item, * that can be used to split the `ui5-multi-combobox` suggestions into groups. * @constructor - * @extends UI5Element - * @abstract + * @extends ComboBoxItemGroup * @public * @implements {IMultiComboBoxItem} * @since 2.0.0 */ -@customElement("ui5-mcb-item-group") -class MultiComboBoxItemGroup extends UI5Element implements IMultiComboBoxItem { - /** - * Defines the text of the component. - * @default undefined - * @public - */ - @property() - headerText?: string; - +@customElement({ + tag: "ui5-mcb-item-group", + template: MultiComboBoxItemGroupTemplate, +}) +class MultiComboBoxItemGroup extends ComboBoxItemGroup implements IMultiComboBoxItem { /** * Defines the items of the ui5-mcb-item-group. * @public @@ -33,6 +28,7 @@ class MultiComboBoxItemGroup extends UI5Element implements IMultiComboBoxItem { @slot({ "default": true, invalidateOnChildChange: true, + individualSlots: true, type: HTMLElement, }) items!: Array; @@ -52,6 +48,10 @@ class MultiComboBoxItemGroup extends UI5Element implements IMultiComboBoxItem { get stableDomRef() { return this.getAttribute("stable-dom-ref") || `${this._id}-stable-dom-ref`; } + + getFocusDomRef() { + return this.shadowRoot!.querySelector("[ui5-li-group-header]") as ListItemGroupHeader; + } } MultiComboBoxItemGroup.define(); diff --git a/packages/main/src/MultiComboBoxPopover.hbs b/packages/main/src/MultiComboBoxPopover.hbs index ee5fc1308e79..79ffc2c77421 100644 --- a/packages/main/src/MultiComboBoxPopover.hbs +++ b/packages/main/src/MultiComboBoxPopover.hbs @@ -80,33 +80,13 @@ {{#if filterSelected}} {{#each selectedItems}} - {{#if isGroupItem}} - - {{#each this.items}} - {{#if _isVisible}} - {{> listItem}} - {{/if}} - {{/each}} - - {{else}} - {{> listItem}} - {{/if}} + {{> listItem}} {{/each}} {{else}} - + {{#each _filteredItems}} - {{#if isGroupItem}} - - {{#each this.items}} - {{#if _isVisible}} - {{> listItem}} - {{/if}} - {{/each}} - - {{else}} - {{> listItem}} - {{/if}} + {{> listItem}} {{/each}} {{/if}} @@ -152,24 +132,13 @@ {{/inline}} {{#*inline "listItem"}} - - {{this.text}} - + {{/inline}} {{#*inline "selectAllWrapper"}} {{#if showSelectAll}}
- +
{{/if}} {{/inline}} diff --git a/packages/main/src/themes/ComboBoxItem.css b/packages/main/src/themes/ComboBoxItem.css new file mode 100644 index 000000000000..c500bb0dd498 --- /dev/null +++ b/packages/main/src/themes/ComboBoxItem.css @@ -0,0 +1,14 @@ +:host([ui5-cb-item]) { + height: auto; + min-height: var(--_ui5_list_item_base_height); +} + +:host([ui5-cb-item]) .ui5-li-root { + min-height: var(--_ui5_list_item_base_height); +} + +:host([ui5-cb-item]) .ui5-li-content { + padding-bottom: .875rem; + padding-top: .875rem; + box-sizing: border-box; +} \ No newline at end of file diff --git a/packages/main/src/themes/MultiComboBoxItem.css b/packages/main/src/themes/MultiComboBoxItem.css new file mode 100644 index 000000000000..c9256a465239 --- /dev/null +++ b/packages/main/src/themes/MultiComboBoxItem.css @@ -0,0 +1,19 @@ +:host([ui5-mcb-item]) { + height: auto; + min-height: var(--_ui5_list_item_base_height); +} + +:host([ui5-mcb-item]) .ui5-li-root { + padding-inline-start: 0; + min-height: var(--_ui5_list_item_base_height); +} + +:host([ui5-mcb-item]) .ui5-li-content { + padding-bottom: .875rem; + padding-top: .875rem; + box-sizing: border-box; +} + +:host([ui5-mcb-item]) [ui5-checkbox] { + overflow: visible; +} \ No newline at end of file diff --git a/packages/main/src/themes/MultiComboBoxPopover.css b/packages/main/src/themes/MultiComboBoxPopover.css index d6ca89bbb8d7..573e396b4767 100644 --- a/packages/main/src/themes/MultiComboBoxPopover.css +++ b/packages/main/src/themes/MultiComboBoxPopover.css @@ -31,4 +31,8 @@ .ui5-mcb-select-all-checkbox::part(label) { font-family: var(--sapFontBoldFamily); -} \ No newline at end of file +} + +:host([readonly]) ::slotted([ui5-mcb-item]) { + cursor: auto; +} diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index 61e6c8906bbe..d6725c22c9b9 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -389,7 +389,9 @@

ComboBox in Compact

// remove busy state event.target.loading = false; - }); + + // load the items once or turn off filtering + }, { once: true });