Skip to content

Commit df22622

Browse files
authored
feat: add itemClassNameGenerator to generate CSS class names (#7305)
1 parent b636efb commit df22622

12 files changed

+212
-2
lines changed

packages/combo-box/src/vaadin-combo-box-mixin.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export declare class ComboBoxMixinClass<TItem> {
6464
*/
6565
items: TItem[] | undefined;
6666

67+
/**
68+
* A function used to generate CSS class names for dropdown
69+
* items based on the item. The return value should be the
70+
* generated class name as a string, or multiple class names
71+
* separated by whitespace characters.
72+
*/
73+
itemClassNameGenerator: (item: TItem) => string;
74+
6775
/**
6876
* If `true`, the user can input a value that is not present in the items list.
6977
* `value` property will be set to the input value in this case.

packages/combo-box/src/vaadin-combo-box-mixin.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ export const ComboBoxMixin = (subclass) =>
196196
sync: true,
197197
},
198198

199+
/**
200+
* A function used to generate CSS class names for dropdown
201+
* items based on the item. The return value should be the
202+
* generated class name as a string, or multiple class names
203+
* separated by whitespace characters.
204+
*/
205+
itemClassNameGenerator: {
206+
type: Object,
207+
},
208+
199209
/**
200210
* Path for label of the item. If `items` is an array of objects, the
201211
* `itemLabelPath` is used to fetch the displayed string label for each
@@ -287,7 +297,7 @@ export const ComboBoxMixin = (subclass) =>
287297
return [
288298
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
289299
'_openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
290-
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme)',
300+
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme, itemClassNameGenerator)',
291301
];
292302
}
293303

@@ -503,7 +513,18 @@ export const ComboBoxMixin = (subclass) =>
503513

504514
/** @private */
505515
// eslint-disable-next-line max-params
506-
_updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
516+
_updateScroller(
517+
scroller,
518+
items,
519+
opened,
520+
loading,
521+
selectedItem,
522+
itemIdPath,
523+
focusedIndex,
524+
renderer,
525+
theme,
526+
itemClassNameGenerator,
527+
) {
507528
if (scroller) {
508529
if (opened) {
509530
scroller.style.maxHeight =
@@ -519,6 +540,7 @@ export const ComboBoxMixin = (subclass) =>
519540
focusedIndex,
520541
renderer,
521542
theme,
543+
itemClassNameGenerator,
522544
});
523545

524546
// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.

packages/combo-box/src/vaadin-combo-box-scroller-mixin.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ export const ComboBoxScrollerMixin = (superClass) =>
6363
observer: '__selectedItemChanged',
6464
},
6565

66+
/**
67+
* A function used to generate CSS class names for dropdown
68+
* items based on the item. The return value should be the
69+
* generated class name as a string, or multiple class names
70+
* separated by whitespace characters.
71+
*/
72+
itemClassNameGenerator: {
73+
type: Object,
74+
observer: '__itemClassNameGeneratorChanged',
75+
},
76+
6677
/**
6778
* Path for the id of the item, used to detect whether the item is selected.
6879
*/
@@ -254,6 +265,13 @@ export const ComboBoxScrollerMixin = (superClass) =>
254265
this.requestContentUpdate();
255266
}
256267

268+
/** @private */
269+
__itemClassNameGeneratorChanged(generator, oldGenerator) {
270+
if (generator || oldGenerator) {
271+
this.requestContentUpdate();
272+
}
273+
}
274+
257275
/** @private */
258276
__focusedIndexChanged(index, oldIndex) {
259277
if (index !== oldIndex) {
@@ -305,6 +323,12 @@ export const ComboBoxScrollerMixin = (superClass) =>
305323
focused: !this.loading && focusedIndex === index,
306324
});
307325

326+
if (typeof this.itemClassNameGenerator === 'function') {
327+
el.className = this.itemClassNameGenerator(item);
328+
} else if (el.className !== '') {
329+
el.className = '';
330+
}
331+
308332
// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.
309333
// However, this causes issues with virtualizer. So we enforce sync update.
310334
if (el.performUpdate && !el.hasUpdated) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './not-animated-styles.js';
2+
import '../theme/lumo/vaadin-lit-combo-box.js';
3+
import './item-class-name-generator.common.js';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './not-animated-styles.js';
2+
import '../vaadin-combo-box.js';
3+
import './item-class-name-generator.common.js';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect } from '@esm-bundle/chai';
2+
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
3+
import { getAllItems } from './helpers.js';
4+
5+
describe('itemClassNameGenerator', () => {
6+
let comboBox;
7+
8+
beforeEach(async () => {
9+
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
10+
await nextRender();
11+
comboBox.items = ['foo', 'bar', 'baz'];
12+
});
13+
14+
it('should set class name on dropdown items', async () => {
15+
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
16+
comboBox.open();
17+
await nextRender();
18+
const items = getAllItems(comboBox);
19+
expect(items[0].className).to.equal('item-foo');
20+
expect(items[1].className).to.equal('item-bar');
21+
expect(items[2].className).to.equal('item-baz');
22+
});
23+
24+
it('should remove class name when return value is empty string', async () => {
25+
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
26+
comboBox.open();
27+
await nextRender();
28+
29+
comboBox.close();
30+
comboBox.itemClassNameGenerator = () => '';
31+
32+
comboBox.open();
33+
await nextRender();
34+
35+
const items = getAllItems(comboBox);
36+
expect(items[0].className).to.equal('');
37+
expect(items[1].className).to.equal('');
38+
expect(items[2].className).to.equal('');
39+
});
40+
41+
it('should remove class name when generator is set to null', async () => {
42+
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
43+
comboBox.open();
44+
await nextRender();
45+
46+
comboBox.close();
47+
comboBox.itemClassNameGenerator = null;
48+
49+
comboBox.open();
50+
await nextRender();
51+
52+
const items = getAllItems(comboBox);
53+
expect(items[0].className).to.equal('');
54+
expect(items[1].className).to.equal('');
55+
expect(items[2].className).to.equal('');
56+
});
57+
});

packages/combo-box/test/typings/combo-box.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ assertType<boolean>(narrowedComboBox.opened);
111111
assertType<string>(narrowedComboBox.filter);
112112
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
113113
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
114+
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
114115
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
115116
assertType<string>(narrowedComboBox.itemLabelPath);
116117
assertType<string>(narrowedComboBox.itemValuePath);

packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
231231
*/
232232
items: TItem[] | undefined;
233233

234+
/**
235+
* A function used to generate CSS class names for dropdown
236+
* items and selected chips based on the item. The return
237+
* value should be the generated class name as a string, or
238+
* multiple class names separated by whitespace characters.
239+
*/
240+
itemClassNameGenerator: (item: TItem) => string;
241+
234242
/**
235243
* The item property used for a visual representation of the item.
236244
* @attr {string} item-label-path

packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
180180
filtered-items="[[filteredItems]]"
181181
selected-items="[[selectedItems]]"
182182
selected-items-on-top="[[selectedItemsOnTop]]"
183+
item-class-name-generator="[[itemClassNameGenerator]]"
183184
top-group="[[_topGroup]]"
184185
opened="{{opened}}"
185186
renderer="[[renderer]]"
@@ -279,6 +280,17 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
279280
type: Array,
280281
},
281282

283+
/**
284+
* A function used to generate CSS class names for dropdown
285+
* items and selected chips based on the item. The return
286+
* value should be the generated class name as a string, or
287+
* multiple class names separated by whitespace characters.
288+
*/
289+
itemClassNameGenerator: {
290+
type: Object,
291+
observer: '__itemClassNameGeneratorChanged',
292+
},
293+
282294
/**
283295
* The item property used for a visual representation of the item.
284296
* @attr {string} item-label-path
@@ -761,6 +773,13 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
761773
}
762774
}
763775

776+
/** @private */
777+
__itemClassNameGeneratorChanged(generator, oldGenerator) {
778+
if (generator || oldGenerator) {
779+
this.__updateChips();
780+
}
781+
}
782+
764783
/** @private */
765784
_pageSizeChanged(pageSize, oldPageSize) {
766785
if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
@@ -934,6 +953,10 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
934953
chip.label = label;
935954
chip.setAttribute('title', label);
936955

956+
if (typeof this.itemClassNameGenerator === 'function') {
957+
chip.className = this.itemClassNameGenerator(item);
958+
}
959+
937960
chip.addEventListener('item-removed', (e) => this._onItemRemoved(e));
938961
chip.addEventListener('mousedown', (e) => this._preventBlur(e));
939962

packages/multi-select-combo-box/test/basic.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ describe('basic', () => {
128128
expect(comboBox.hasAttribute('loading')).to.be.false;
129129
});
130130

131+
it('should propagate itemClassNameGenerator property to combo-box', () => {
132+
const generator = (item) => item;
133+
comboBox.itemClassNameGenerator = generator;
134+
expect(internal.itemClassNameGenerator).to.equal(generator);
135+
});
136+
131137
it('should update filteredItems when combo-box filteredItems changes', () => {
132138
internal.filteredItems = ['apple'];
133139
expect(comboBox.filteredItems).to.deep.equal(['apple']);

packages/multi-select-combo-box/test/chips.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,4 +553,58 @@ describe('chips', () => {
553553
expect(overlayPart.clientWidth).to.be.equal(comboBox.clientWidth);
554554
});
555555
});
556+
557+
describe('itemClassNameGenerator', () => {
558+
beforeEach(() => {
559+
comboBox.autoExpandHorizontally = true;
560+
});
561+
562+
it('should set class name on the selected item chips', async () => {
563+
comboBox.itemClassNameGenerator = (item) => item;
564+
comboBox.selectedItems = ['apple', 'lemon'];
565+
await nextRender();
566+
567+
const chips = getChips(comboBox);
568+
expect(chips[1].className).to.equal('apple');
569+
expect(chips[2].className).to.equal('lemon');
570+
});
571+
572+
it('should set class name when generator set after selecting', async () => {
573+
comboBox.selectedItems = ['apple', 'lemon'];
574+
await nextRender();
575+
576+
comboBox.itemClassNameGenerator = (item) => item;
577+
await nextRender();
578+
579+
const chips = getChips(comboBox);
580+
expect(chips[1].className).to.equal('apple');
581+
expect(chips[2].className).to.equal('lemon');
582+
});
583+
584+
it('should remove class name when generator returns empty string', async () => {
585+
comboBox.itemClassNameGenerator = (item) => item;
586+
comboBox.selectedItems = ['apple', 'lemon'];
587+
await nextRender();
588+
589+
comboBox.itemClassNameGenerator = () => '';
590+
await nextRender();
591+
592+
const chips = getChips(comboBox);
593+
expect(chips[1].className).to.equal('');
594+
expect(chips[2].className).to.equal('');
595+
});
596+
597+
it('should remove class name when generator is set to null', async () => {
598+
comboBox.itemClassNameGenerator = (item) => item;
599+
comboBox.selectedItems = ['apple', 'lemon'];
600+
await nextRender();
601+
602+
comboBox.itemClassNameGenerator = null;
603+
await nextRender();
604+
605+
const chips = getChips(comboBox);
606+
expect(chips[1].className).to.equal('');
607+
expect(chips[2].className).to.equal('');
608+
});
609+
});
556610
});

packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ assertType<boolean | null | undefined>(narrowedComboBox.autoOpenDisabled);
8888
assertType<string>(narrowedComboBox.filter);
8989
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
9090
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
91+
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
9192
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
9293
assertType<string>(narrowedComboBox.itemLabelPath);
9394
assertType<string>(narrowedComboBox.itemValuePath);

0 commit comments

Comments
 (0)