diff --git a/dev/playground/combo-box.html b/dev/playground/combo-box.html index dc5def7a54d..15945f1f4b4 100644 --- a/dev/playground/combo-box.html +++ b/dev/playground/combo-box.html @@ -59,24 +59,47 @@

States

+
+

Item Label Generator

+ +
+ diff --git a/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts b/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts index 286e130590c..6be65eb0ef6 100644 --- a/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts +++ b/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts @@ -29,6 +29,13 @@ export declare class ComboBoxItemsMixinClass { */ filter: string; + /** + * A function that is used to generate the label for dropdown + * items based on the item. Receives one argument: + * - `item` The item to generate the label for. + */ + itemLabelGenerator: ((item: TItem) => string) | undefined; + /** * Path for label of the item. If `items` is an array of objects, the * `itemLabelPath` is used to fetch the displayed string label for each diff --git a/packages/combo-box/src/vaadin-combo-box-items-mixin.js b/packages/combo-box/src/vaadin-combo-box-items-mixin.js index 2bbc39e2f55..825b3d890d4 100644 --- a/packages/combo-box/src/vaadin-combo-box-items-mixin.js +++ b/packages/combo-box/src/vaadin-combo-box-items-mixin.js @@ -77,6 +77,15 @@ export const ComboBoxItemsMixin = (superClass) => sync: true, }, + /** + * A function that is used to generate the label for dropdown + * items based on the item. Receives one argument: + * - `item` The item to generate the label for. + */ + itemLabelGenerator: { + type: Object, + }, + /** * Path for label of the item. If `items` is an array of objects, the * `itemLabelPath` is used to fetch the displayed string label for each @@ -122,6 +131,10 @@ export const ComboBoxItemsMixin = (superClass) => if (props.has('filter')) { this._filterChanged(this.filter); } + + if (props.has('itemLabelGenerator')) { + this.requestContentUpdate(); + } } /** @@ -161,6 +174,10 @@ export const ComboBoxItemsMixin = (superClass) => * @override */ _getItemLabel(item) { + if (typeof this.itemLabelGenerator === 'function' && item) { + return this.itemLabelGenerator(item) || ''; + } + let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined; if (label === undefined || label === null) { label = item ? item.toString() : ''; diff --git a/packages/combo-box/test/items.test.js b/packages/combo-box/test/items.test.js index dadc0fb5334..c4984f9f2aa 100644 --- a/packages/combo-box/test/items.test.js +++ b/packages/combo-box/test/items.test.js @@ -142,8 +142,6 @@ describe('items', () => { }); describe('itemClassNameGenerator', () => { - let comboBox; - beforeEach(async () => { comboBox = fixtureSync(''); await nextRender(); @@ -194,4 +192,76 @@ describe('items', () => { expect(items[2].className).to.equal(''); }); }); + + describe('itemLabelGenerator', () => { + beforeEach(async () => { + comboBox = fixtureSync(''); + comboBox.items = [ + { id: 1, name: 'John', surname: 'Doe', age: 30 }, + { id: 2, name: 'Jane', surname: 'Smith', age: 25 }, + { id: 3, name: 'Bob', surname: 'Johnson', age: 35 }, + ]; + comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`; + await nextRender(); + }); + + it('should generate items text content using itemLabelGenerator', async () => { + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John Doe'); + expect(items[1].textContent).to.equal('Jane Smith'); + expect(items[2].textContent).to.equal('Bob Johnson'); + }); + + it('should set generated label as input value when item is selected', async () => { + comboBox.itemValuePath = 'id'; + comboBox.value = 2; + await nextRender(); + expect(comboBox.inputElement.value).to.equal('Jane Smith'); + }); + + it('should filter items using generated label', () => { + setInputValue(comboBox, 'john'); + + expect(comboBox.filteredItems.length).to.equal(2); + expect(comboBox.filteredItems[0]).to.deep.equal(comboBox.items[0]); + expect(comboBox.filteredItems[1]).to.deep.equal(comboBox.items[2]); + }); + + it('should use itemLabelGenerator over itemLabelPath', async () => { + comboBox.itemLabelPath = 'surname'; + comboBox.itemLabelGenerator = (item) => item.name; + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John'); + expect(items[1].textContent).to.equal('Jane'); + }); + + it('should accept empty string returned from itemLabelGenerator', async () => { + comboBox.itemLabelGenerator = (item) => (item.id === 2 ? '' : `${item.name} ${item.surname}`); + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John Doe'); + expect(items[1].textContent).to.equal(''); + expect(items[2].textContent).to.equal('Bob Johnson'); + }); + + it('should update dropdown when itemLabelGenerator changes', async () => { + comboBox.open(); + await nextRender(); + + expect(getFirstItem(comboBox).textContent).to.equal('John Doe'); + + comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`; + await nextRender(); + + expect(getFirstItem(comboBox).textContent).to.equal('John (30)'); + }); + }); }); diff --git a/packages/combo-box/test/typings/combo-box.types.ts b/packages/combo-box/test/typings/combo-box.types.ts index 50caf56517f..eda20a33df8 100644 --- a/packages/combo-box/test/typings/combo-box.types.ts +++ b/packages/combo-box/test/typings/combo-box.types.ts @@ -100,6 +100,7 @@ assertType(narrowedComboBox.filter); assertType(narrowedComboBox.filteredItems); assertType(narrowedComboBox.items); assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator); +assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator); assertType(narrowedComboBox.itemIdPath); assertType(narrowedComboBox.itemLabelPath); assertType(narrowedComboBox.itemValuePath); diff --git a/packages/multi-select-combo-box/test/chips.test.js b/packages/multi-select-combo-box/test/chips.test.js index 513d98d8ec5..1cc0021f055 100644 --- a/packages/multi-select-combo-box/test/chips.test.js +++ b/packages/multi-select-combo-box/test/chips.test.js @@ -2,12 +2,11 @@ import { expect } from '@vaadin/chai-plugins'; import { sendKeys } from '@vaadin/test-runner-commands'; import { fixtureSync, nextRender, nextResize, nextUpdate } from '@vaadin/testing-helpers'; import '../src/vaadin-multi-select-combo-box.js'; +import { getChips } from './helpers.js'; describe('chips', () => { let comboBox, inputElement; - const getChips = (combo) => combo.querySelectorAll('vaadin-multi-select-combo-box-chip'); - const getChipContent = (chip) => chip.shadowRoot.querySelector('[part="label"]').textContent; beforeEach(async () => { diff --git a/packages/multi-select-combo-box/test/helpers.js b/packages/multi-select-combo-box/test/helpers.js index fb820862d8c..f8339a123a9 100644 --- a/packages/multi-select-combo-box/test/helpers.js +++ b/packages/multi-select-combo-box/test/helpers.js @@ -1,3 +1,5 @@ +import { fire } from '@vaadin/testing-helpers'; + export const getDataProvider = (allItems) => (params, callback) => { const offset = params.page * params.pageSize; const filteredItems = allItems.filter((item) => item.indexOf(params.filter) > -1); @@ -31,3 +33,19 @@ export const getAllItems = (comboBox) => { export const getFirstItem = (comboBox) => { return comboBox._scroller.querySelector('vaadin-multi-select-combo-box-item'); }; + +/** + * Emulates the user filling in something in the combo-box input. + * + * @param {Element} comboBox + * @param {string} value + */ +export function setInputValue(comboBox, value) { + comboBox.inputElement.value = value; + fire(comboBox.inputElement, 'input'); +} + +/** + * Returns all the chips of the combo-box. + */ +export const getChips = (comboBox) => comboBox.querySelectorAll('vaadin-multi-select-combo-box-chip'); diff --git a/packages/multi-select-combo-box/test/items.test.js b/packages/multi-select-combo-box/test/items.test.js index 9e2cfc327f7..7f69e1dae8b 100644 --- a/packages/multi-select-combo-box/test/items.test.js +++ b/packages/multi-select-combo-box/test/items.test.js @@ -2,7 +2,7 @@ import { expect } from '@vaadin/chai-plugins'; import { fixtureSync, nextRender } from '@vaadin/testing-helpers'; import sinon from 'sinon'; import '../src/vaadin-multi-select-combo-box.js'; -import { getAllItems, getFirstItem } from './helpers.js'; +import { getAllItems, getChips, getFirstItem, setInputValue } from './helpers.js'; describe('items', () => { let comboBox; @@ -81,10 +81,6 @@ describe('items', () => { }); describe('itemClassNameGenerator', () => { - let comboBox; - - const getChips = (combo) => combo.querySelectorAll('vaadin-multi-select-combo-box-chip'); - beforeEach(async () => { comboBox = fixtureSync(''); await nextRender(); @@ -184,4 +180,95 @@ describe('items', () => { expect(chips[2].className).to.equal(''); }); }); + + describe('itemLabelGenerator', () => { + beforeEach(async () => { + comboBox = fixtureSync(` + + `); + comboBox.items = [ + { id: 1, name: 'John', surname: 'Doe', age: 30 }, + { id: 2, name: 'Jane', surname: 'Smith', age: 25 }, + { id: 3, name: 'Bob', surname: 'Johnson', age: 35 }, + ]; + comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`; + await nextRender(); + }); + + it('should generate items text content using itemLabelGenerator', async () => { + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John Doe'); + expect(items[1].textContent).to.equal('Jane Smith'); + expect(items[2].textContent).to.equal('Bob Johnson'); + }); + + it('should generate chips text content using itemLabelGenerator', async () => { + comboBox.selectedItems = [comboBox.items[0], comboBox.items[1]]; + await nextRender(); + + const chips = getChips(comboBox); + expect(chips[1].label).to.equal('John Doe'); + expect(chips[2].label).to.equal('Jane Smith'); + }); + + it('should filter items using generated label', () => { + setInputValue(comboBox, 'john'); + + expect(comboBox.filteredItems.length).to.equal(2); + expect(comboBox.filteredItems[0]).to.deep.equal(comboBox.items[0]); + expect(comboBox.filteredItems[1]).to.deep.equal(comboBox.items[2]); + }); + + it('should use itemLabelGenerator over itemLabelPath', async () => { + comboBox.itemLabelPath = 'surname'; + comboBox.itemLabelGenerator = (item) => item.name; + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John'); + expect(items[1].textContent).to.equal('Jane'); + }); + + it('should accept empty string returned from itemLabelGenerator', async () => { + comboBox.itemLabelGenerator = (item) => (item.id === 2 ? '' : `${item.name} ${item.surname}`); + comboBox.open(); + await nextRender(); + + const items = getAllItems(comboBox); + expect(items[0].textContent).to.equal('John Doe'); + expect(items[1].textContent).to.equal(''); + expect(items[2].textContent).to.equal('Bob Johnson'); + }); + + it('should update dropdown items when itemLabelGenerator changes', async () => { + comboBox.open(); + await nextRender(); + + expect(getFirstItem(comboBox).textContent).to.equal('John Doe'); + + comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`; + await nextRender(); + + expect(getFirstItem(comboBox).textContent).to.equal('John (30)'); + }); + + it('should update chips when itemLabelGenerator changes', async () => { + comboBox.selectedItems = [comboBox.items[0]]; + await nextRender(); + + expect(getChips(comboBox)[1].label).to.equal('John Doe'); + + comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`; + await nextRender(); + + expect(getChips(comboBox)[1].label).to.equal('John (30)'); + }); + }); }); diff --git a/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts index 05c540aecde..97e0e15ff77 100644 --- a/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts +++ b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts @@ -93,6 +93,7 @@ assertType(narrowedComboBox.filter); assertType(narrowedComboBox.filteredItems); assertType(narrowedComboBox.items); assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator); +assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator); assertType(narrowedComboBox.itemIdPath); assertType(narrowedComboBox.itemLabelPath); assertType(narrowedComboBox.itemValuePath);