Skip to content

Commit

Permalink
feat: add property to group selected overlay items at the top (#6685)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed Nov 8, 2023
1 parent a492882 commit ea5e199
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 12 deletions.
36 changes: 28 additions & 8 deletions packages/combo-box/src/vaadin-combo-box-mixin.js
Expand Up @@ -221,6 +221,14 @@ export const ComboBoxMixin = (subclass) =>
observer: '_toggleElementChanged',
},

/**
* Set of items to be rendered in the dropdown.
* @protected
*/
_dropdownItems: {
type: Array,
},

/** @private */
_closeOnBlurIsPrevented: Boolean,

Expand All @@ -238,8 +246,8 @@ export const ComboBoxMixin = (subclass) =>
static get observers() {
return [
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
'_openedOrItemsChanged(opened, filteredItems, loading)',
'_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
'_openedOrItemsChanged(opened, _dropdownItems, loading)',
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
];
}

Expand Down Expand Up @@ -497,7 +505,7 @@ export const ComboBoxMixin = (subclass) =>
this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));

this._onOpened();
} else if (wasOpened && this.filteredItems && this.filteredItems.length) {
} else if (wasOpened && this._dropdownItems && this._dropdownItems.length) {
this.close();

this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
Expand Down Expand Up @@ -683,7 +691,7 @@ export const ComboBoxMixin = (subclass) =>
/** @private */
_onArrowDown() {
if (this.opened) {
const items = this.filteredItems;
const items = this._dropdownItems;
if (items) {
this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
this._prefillFocusedItemLabel();
Expand All @@ -699,7 +707,7 @@ export const ComboBoxMixin = (subclass) =>
if (this._focusedIndex > -1) {
this._focusedIndex = Math.max(0, this._focusedIndex - 1);
} else {
const items = this.filteredItems;
const items = this._dropdownItems;
if (items) {
this._focusedIndex = items.length - 1;
}
Expand All @@ -714,7 +722,7 @@ export const ComboBoxMixin = (subclass) =>
/** @private */
_prefillFocusedItemLabel() {
if (this._focusedIndex > -1) {
const focusedItem = this.filteredItems[this._focusedIndex];
const focusedItem = this._dropdownItems[this._focusedIndex];
this._inputElementValue = this._getItemLabel(focusedItem);
this._markAllSelectionRange();
}
Expand Down Expand Up @@ -887,7 +895,7 @@ export const ComboBoxMixin = (subclass) =>
/** @private */
_commitValue() {
if (this._focusedIndex > -1) {
const focusedItem = this.filteredItems[this._focusedIndex];
const focusedItem = this._dropdownItems[this._focusedIndex];
if (this.selectedItem !== focusedItem) {
this.selectedItem = focusedItem;
}
Expand All @@ -902,7 +910,7 @@ export const ComboBoxMixin = (subclass) =>
}
} else {
// Try to find an item which label matches the input value.
const items = [...(this.filteredItems || []), this.selectedItem];
const items = [...(this._dropdownItems || []), this.selectedItem];
const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];

if (
Expand Down Expand Up @@ -1119,6 +1127,8 @@ export const ComboBoxMixin = (subclass) =>

/** @private */
_filteredItemsChanged(filteredItems, oldFilteredItems) {
this._setDropdownItems(filteredItems);

// Store the currently focused item if any. The focused index preserves
// in the case when more filtered items are loading but it is reset
// when the user types in a filter query.
Expand Down Expand Up @@ -1179,6 +1189,16 @@ export const ComboBoxMixin = (subclass) =>
}
}

/**
* Provide items to be rendered in the dropdown.
* Override this method to show custom items.
*
* @protected
*/
_setDropdownItems(items) {
this._dropdownItems = items;
}

/** @private */
_getItemElements() {
return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
Expand Down
Expand Up @@ -58,6 +58,15 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
notify: true,
},

/**
* Set to true to group selected items at the top of the overlay.
* @attr {boolean} group-selected-items
*/
groupSelectedItems: {
type: Boolean,
value: false,
},

/**
* When set to `true`, "loading" attribute is set
* on the host and the overlay element.
Expand Down Expand Up @@ -95,6 +104,14 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
notify: true,
},

/**
* A subset of items to be shown at the top of the overlay.
*/
topGroup: {
type: Array,
observer: '_topGroupChanged',
},

_target: {
type: Object,
},
Expand Down Expand Up @@ -138,6 +155,42 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
this._toggleElement = this.querySelector('.toggle-button');
}

/**
* Override combo-box method to group selected
* items at the top of the overlay.
*
* @protected
* @override
*/
_setDropdownItems(items) {
if (this.readonly || !this.groupSelectedItems) {
this._dropdownItems = items;
return;
}

if (this.topGroup) {
const filteredTopItems = [];
const filteredItems = [];

(items || []).forEach((item) => {
if (this.topGroup.some((selectedItem) => this._getItemValue(item) === this._getItemValue(selectedItem))) {
filteredTopItems.push(item);
} else {
filteredItems.push(item);
}
});

this._dropdownItems = [...filteredTopItems, ...filteredItems];
}
}

/** @private */
_topGroupChanged(topGroup) {
if (topGroup) {
this._setDropdownItems(this.filteredItems);
}
}

/**
* Override combo-box method to set correct owner for using by item renderers.
* This needs to be done before the scroller gets added to the DOM to ensure
Expand Down Expand Up @@ -193,9 +246,9 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
this.__enterPressed = null;

// Keep selected item focused after committing on Enter.
const focusedItem = this.filteredItems[this._focusedIndex];
const focusedItem = this._dropdownItems[this._focusedIndex];
this._commitValue();
this._focusedIndex = this.filteredItems.indexOf(focusedItem);
this._focusedIndex = this._dropdownItems.indexOf(focusedItem);

return;
}
Expand Down
Expand Up @@ -204,6 +204,12 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
*/
filter: string;

/**
* Set to true to group selected items at the top of the overlay.
* @attr {boolean} group-selected-items
*/
groupSelectedItems: boolean;

/**
* A full set of items to filter the visible options from.
* The items can be of either `String` or `Object` type.
Expand Down
Expand Up @@ -167,6 +167,8 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
size="{{size}}"
filtered-items="[[__effectiveFilteredItems]]"
selected-items="[[selectedItems]]"
group-selected-items="[[groupSelectedItems]]"
top-group="[[_topGroup]]"
opened="{{opened}}"
renderer="[[renderer]]"
theme$="[[_theme]]"
Expand Down Expand Up @@ -238,6 +240,15 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
value: false,
},

/**
* Set to true to group selected items at the top of the overlay.
* @attr {boolean} group-selected-items
*/
groupSelectedItems: {
type: Boolean,
value: false,
},

/**
* A full set of items to filter the visible options from.
* The items can be of either `String` or `Object` type.
Expand Down Expand Up @@ -465,11 +476,19 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
_lastFilter: {
type: String,
},

/** @private */
_topGroup: {
type: Array,
},
};
}

static get observers() {
return ['_selectedItemsChanged(selectedItems, selectedItems.*)'];
return [
'_selectedItemsChanged(selectedItems, selectedItems.*)',
'__updateTopGroup(groupSelectedItems, selectedItems, opened)',
];
}

/** @protected */
Expand Down Expand Up @@ -825,6 +844,15 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
}

/** @private */
__updateTopGroup(groupSelectedItems, selectedItems, opened) {
if (!groupSelectedItems) {
this._topGroup = [];
} else if (!opened) {
this._topGroup = [...selectedItems];
}
}

/** @private */
__createChip(item) {
const chip = document.createElement('vaadin-multi-select-combo-box-chip');
Expand Down
18 changes: 18 additions & 0 deletions packages/multi-select-combo-box/test/helpers.js
Expand Up @@ -15,3 +15,21 @@ export const getAsyncDataProvider = (allItems) => {
}, 0);
};
};

/**
* Returns all the items of the combo box dropdown.
*/
export const getAllItems = (comboBox) => {
const internal = comboBox.$.comboBox;
return Array.from(internal._scroller.querySelectorAll('vaadin-multi-select-combo-box-item'))
.filter((item) => !item.hidden)
.sort((a, b) => a.index - b.index);
};

/**
* Returns first item of the combo box dropdown.
*/
export const getFirstItem = (comboBox) => {
const internal = comboBox.$.comboBox;
return internal._scroller.querySelector('vaadin-multi-select-combo-box-item');
};

0 comments on commit ea5e199

Please sign in to comment.