Skip to content

Commit

Permalink
feat: add keepFilter option (#7063)
Browse files Browse the repository at this point in the history
* --wip-- [skip ci]

* fix some use cases and add tests

* handle more cases

* some refactoring

* cleanup tests

* revert multi-select-combo-box.html

* fix API docs

---------

Co-authored-by: web-padawan <iamkulykov@gmail.com>
  • Loading branch information
sissbruecker and web-padawan authored Jan 11, 2024
1 parent e6d8660 commit d731b62
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 18 deletions.
15 changes: 12 additions & 3 deletions packages/combo-box/src/vaadin-combo-box-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,15 @@ export const ComboBoxMixin = (subclass) =>
this._detectAndDispatchChange();
}

/**
* Clears the current filter. Should be used instead of setting the property
* directly in order to allow overriding this in multi-select combo box.
* @protected
*/
_clearFilter() {
this.filter = '';
}

/**
* Reverts back to original value.
*/
Expand Down Expand Up @@ -938,15 +947,15 @@ export const ComboBoxMixin = (subclass) =>
this.value = this._getItemValue(itemMatchingInputValue);
} else {
// Revert the input value
this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
this._revertInputValueToValue();
}
}

this._detectAndDispatchChange();

this._clearSelectionRange();

this.filter = '';
this._clearFilter();
}

/**
Expand Down Expand Up @@ -1087,7 +1096,7 @@ export const ComboBoxMixin = (subclass) =>
this.selectedItem = null;
}

this.filter = '';
this._clearFilter();

// In the next _detectAndDispatchChange() call, the change detection should pass
this._lastCommittedValue = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
notify: true,
},

/**
* When true, filter string isn't cleared after selecting an item.
*/
keepFilter: {
type: Boolean,
value: false,
},

/**
* When set to `true`, "loading" attribute is set
* on the host and the overlay element.
Expand Down Expand Up @@ -268,6 +276,30 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
super._onEscape(event);
}

/**
* Override from combo-box to ignore requests to clear the filter if the
* keepFilter option is enabled. Exceptions are when the dropdown is closed,
* so the filter is still cleared on cancel and focus out.
* @protected
* @override
*/
_clearFilter() {
if (!this.keepFilter || !this.opened) {
super._clearFilter();
}
}

/**
* Override method from combo-box to always clear the filter when reverting
* the input value, regardless of the keepFilter option.
* @override
* @protected
*/
_revertInputValueToValue() {
super._revertInputValueToValue();
this.filter = '';
}

/**
* @protected
* @override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
*/
i18n: MultiSelectComboBoxI18n;

/**
* When true, filter string isn't cleared after selecting an item.
* @attr {boolean} keep-filter
*/
keepFilter: boolean;

/**
* True when loading items from the data provider, false otherwise.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
top-group="[[_topGroup]]"
opened="{{opened}}"
renderer="[[renderer]]"
keep-filter="[[keepFilter]]"
theme$="[[_theme]]"
on-combo-box-item-selected="_onComboBoxItemSelected"
on-change="_onComboBoxChange"
Expand Down Expand Up @@ -342,6 +343,14 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
},
},

/**
* When true, filter string isn't cleared after selecting an item.
*/
keepFilter: {
type: Boolean,
value: false,
},

/**
* True when loading items from the data provider, false otherwise.
*/
Expand Down Expand Up @@ -824,10 +833,27 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
return selectedItems.indexOf(item);
}

/** @private */
__clearFilter() {
this.filter = '';
this.$.comboBox.clear();
/**
* Clear the internal combo box value and filter. Filter will not be cleared
* when the `keepFilter` option is enabled. Using `force` can enforce clearing
* the filter.
* @param {boolean} force overrides the keepFilter option
* @private
*/
__clearInternalValue(force = false) {
if (!this.keepFilter || force) {
// Clear both combo box value and filter.
this.filter = '';
this.$.comboBox.clear();
} else {
// Only clear combo box value. This effectively resets _lastCommittedValue
// which allows toggling the same item multiple times via keyboard.
this.$.comboBox.clear();
// Restore input to the filter value. Needed when items are
// navigated with keyboard, which overrides the input value
// with the item label.
this._inputElementValue = this.filter;
}
}

/** @private */
Expand Down Expand Up @@ -859,7 +885,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
const lastFilter = this._lastFilter;
// Do not unselect when manually typing and committing an already selected item.
if (lastFilter && lastFilter.toLowerCase() === itemLabel.toLowerCase()) {
this.__clearFilter();
this.__clearInternalValue();
return;
}

Expand All @@ -872,7 +898,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
this.__updateSelection(itemsCopy);

// Suppress `value-changed` event.
this.__clearFilter();
this.__clearInternalValue();

this.__announceItem(itemLabel, isSelected, itemsCopy.length);
}
Expand Down Expand Up @@ -1232,7 +1258,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
// Stop the original event
event.stopPropagation();

this.__clearFilter();
this.__clearInternalValue(true);

this.dispatchEvent(
new CustomEvent('custom-value-set', {
Expand Down
122 changes: 114 additions & 8 deletions packages/multi-select-combo-box/test/selecting-items.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { getAllItems, getDataProvider, getFirstItem } from './helpers.js';
describe('selecting items', () => {
let comboBox, internal, inputElement;

function expectItems(values) {
const items = getAllItems(comboBox);
expect(items.length).to.equal(values.length);
values.forEach((value, idx) => {
expect(items[idx].textContent).to.equal(value);
});
}

beforeEach(() => {
comboBox = fixtureSync(`<vaadin-multi-select-combo-box></vaadin-multi-select-combo-box>`);
internal = comboBox.$.comboBox;
Expand Down Expand Up @@ -163,14 +171,6 @@ describe('selecting items', () => {
});

describe('selected items on top', () => {
function expectItems(values) {
const items = getAllItems(comboBox);
expect(items.length).to.equal(values.length);
values.forEach((value, idx) => {
expect(items[idx].textContent).to.equal(value);
});
}

beforeEach(() => {
comboBox.selectedItemsOnTop = true;
});
Expand Down Expand Up @@ -345,4 +345,110 @@ describe('selecting items', () => {
});
});
});

describe('keep filter', () => {
beforeEach(() => {
comboBox.items = ['apple', 'banana', 'lemon', 'orange'];
comboBox.keepFilter = true;
});

it('should keep the filter after selecting items', async () => {
await sendKeys({ type: 'an' });
expectItems(['banana', 'orange']);

const filterChangeSpy = sinon.spy();
comboBox.addEventListener('filter-changed', filterChangeSpy);

await sendKeys({ down: 'ArrowDown' });
await sendKeys({ down: 'Enter' });
expect(comboBox.selectedItems).to.deep.equal(['banana']);
expect(comboBox.filter).to.equal('an');
expect(inputElement.value).to.equal('an');
expectItems(['banana', 'orange']);
// Filter should never change, otherwise data provider would be called
expect(filterChangeSpy.notCalled).to.be.true;
});

it('should clear the filter when closing the overlay', async () => {
await sendKeys({ type: 'an' });
expectItems(['banana', 'orange']);

inputElement.blur();
expect(comboBox.filter).to.equal('');
expect(inputElement.value).to.equal('');
});

it('should clear a matching filter when closing the overlay', async () => {
await sendKeys({ type: 'apple' });

inputElement.blur();
expect(comboBox.selectedItems).to.deep.equal([]);
expect(comboBox.filter).to.equal('');
expect(inputElement.value).to.equal('');
});

it('should clear the filter when pressing escape', async () => {
await sendKeys({ type: 'an' });
expectItems(['banana', 'orange']);

await sendKeys({ down: 'Escape' });
expect(comboBox.filter).to.equal('');
expect(inputElement.value).to.equal('');
});

it('should clear the filter when pressing escape after selecting an item', async () => {
await sendKeys({ type: 'an' });
expectItems(['banana', 'orange']);

await sendKeys({ down: 'ArrowDown' });
await sendKeys({ down: 'Enter' });
// Pressing escape twice to first deselect item and then close the overlay
await sendKeys({ down: 'Escape' });
await sendKeys({ down: 'Escape' });
expect(comboBox.opened).to.be.false;
expect(comboBox.filter).to.equal('');
expect(inputElement.value).to.equal('');
});

it('should clear the filter when committing a non-existing item', async () => {
await sendKeys({ type: 'an' });
expectItems(['banana', 'orange']);

await sendKeys({ down: 'Enter' });
expect(comboBox.opened).to.be.true;
expect(inputElement.value).to.equal('');
expect(comboBox.filter).to.equal('');
});

it('should allow toggling items via keyboard', async () => {
await sendKeys({ down: 'ArrowDown' });
await sendKeys({ down: 'ArrowDown' });
await sendKeys({ down: 'Enter' });
expect(comboBox.selectedItems).to.deep.equal(['apple']);

await sendKeys({ down: 'Enter' });
expect(comboBox.selectedItems).to.deep.equal([]);
});

it('should restore the input value to the filter after selecting an item', async () => {
await sendKeys({ type: 'an' });
await sendKeys({ down: 'ArrowDown' });
await sendKeys({ down: 'Enter' });
expect(comboBox.selectedItems).to.deep.equal(['banana']);
expect(inputElement.value).to.equal('an');
});

describe('with allowCustomValue', () => {
beforeEach(() => {
comboBox.allowCustomValue = true;
});

it('should clear the filter value after entering custom value', async () => {
await sendKeys({ type: 'pear' });
await sendKeys({ down: 'Enter' });
expect(comboBox.filter).to.equal('');
expect(inputElement.value).to.equal('');
});
});
});
});

0 comments on commit d731b62

Please sign in to comment.