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: add keepFilter option #7063

Merged
merged 7 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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 @@ -305,6 +306,14 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
type: String,
},

/**
* When true, filter string isn't cleared after selecting an item.
sissbruecker marked this conversation as resolved.
Show resolved Hide resolved
*/
keepFilter: {
type: Boolean,
value: false,
},

/**
* The object used to localize this component.
* To change the default localization, replace the entire
Expand Down Expand Up @@ -824,10 +833,26 @@ 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 */
sissbruecker marked this conversation as resolved.
Show resolved Hide resolved
__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 +884,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 +897,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 +1257,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('');
});
});
});
});