Skip to content

Commit

Permalink
feat: add itemClassNameGenerator to generate CSS class names (#7305)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored May 13, 2024
1 parent b636efb commit df22622
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 2 deletions.
8 changes: 8 additions & 0 deletions packages/combo-box/src/vaadin-combo-box-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export declare class ComboBoxMixinClass<TItem> {
*/
items: TItem[] | undefined;

/**
* A function used to generate CSS class names for dropdown
* items based on the item. The return value should be the
* generated class name as a string, or multiple class names
* separated by whitespace characters.
*/
itemClassNameGenerator: (item: TItem) => string;

/**
* If `true`, the user can input a value that is not present in the items list.
* `value` property will be set to the input value in this case.
Expand Down
26 changes: 24 additions & 2 deletions packages/combo-box/src/vaadin-combo-box-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ export const ComboBoxMixin = (subclass) =>
sync: true,
},

/**
* A function used to generate CSS class names for dropdown
* items based on the item. The return value should be the
* generated class name as a string, or multiple class names
* separated by whitespace characters.
*/
itemClassNameGenerator: {
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
Expand Down Expand Up @@ -287,7 +297,7 @@ export const ComboBoxMixin = (subclass) =>
return [
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
'_openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme)',
'_updateScroller(_scroller, _dropdownItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, _theme, itemClassNameGenerator)',
];
}

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

/** @private */
// eslint-disable-next-line max-params
_updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
_updateScroller(
scroller,
items,
opened,
loading,
selectedItem,
itemIdPath,
focusedIndex,
renderer,
theme,
itemClassNameGenerator,
) {
if (scroller) {
if (opened) {
scroller.style.maxHeight =
Expand All @@ -519,6 +540,7 @@ export const ComboBoxMixin = (subclass) =>
focusedIndex,
renderer,
theme,
itemClassNameGenerator,
});

// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.
Expand Down
24 changes: 24 additions & 0 deletions packages/combo-box/src/vaadin-combo-box-scroller-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ export const ComboBoxScrollerMixin = (superClass) =>
observer: '__selectedItemChanged',
},

/**
* A function used to generate CSS class names for dropdown
* items based on the item. The return value should be the
* generated class name as a string, or multiple class names
* separated by whitespace characters.
*/
itemClassNameGenerator: {
type: Object,
observer: '__itemClassNameGeneratorChanged',
},

/**
* Path for the id of the item, used to detect whether the item is selected.
*/
Expand Down Expand Up @@ -254,6 +265,13 @@ export const ComboBoxScrollerMixin = (superClass) =>
this.requestContentUpdate();
}

/** @private */
__itemClassNameGeneratorChanged(generator, oldGenerator) {
if (generator || oldGenerator) {
this.requestContentUpdate();
}
}

/** @private */
__focusedIndexChanged(index, oldIndex) {
if (index !== oldIndex) {
Expand Down Expand Up @@ -305,6 +323,12 @@ export const ComboBoxScrollerMixin = (superClass) =>
focused: !this.loading && focusedIndex === index,
});

if (typeof this.itemClassNameGenerator === 'function') {
el.className = this.itemClassNameGenerator(item);
} else if (el.className !== '') {
el.className = '';
}

// NOTE: in PolylitMixin, setProperties() waits for `hasUpdated` to be set.
// However, this causes issues with virtualizer. So we enforce sync update.
if (el.performUpdate && !el.hasUpdated) {
Expand Down
3 changes: 3 additions & 0 deletions packages/combo-box/test/item-class-name-generator-lit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './not-animated-styles.js';
import '../theme/lumo/vaadin-lit-combo-box.js';
import './item-class-name-generator.common.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './not-animated-styles.js';
import '../vaadin-combo-box.js';
import './item-class-name-generator.common.js';
57 changes: 57 additions & 0 deletions packages/combo-box/test/item-class-name-generator.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
import { getAllItems } from './helpers.js';

describe('itemClassNameGenerator', () => {
let comboBox;

beforeEach(async () => {
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
await nextRender();
comboBox.items = ['foo', 'bar', 'baz'];
});

it('should set class name on dropdown items', async () => {
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
comboBox.open();
await nextRender();
const items = getAllItems(comboBox);
expect(items[0].className).to.equal('item-foo');
expect(items[1].className).to.equal('item-bar');
expect(items[2].className).to.equal('item-baz');
});

it('should remove class name when return value is empty string', async () => {
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
comboBox.open();
await nextRender();

comboBox.close();
comboBox.itemClassNameGenerator = () => '';

comboBox.open();
await nextRender();

const items = getAllItems(comboBox);
expect(items[0].className).to.equal('');
expect(items[1].className).to.equal('');
expect(items[2].className).to.equal('');
});

it('should remove class name when generator is set to null', async () => {
comboBox.itemClassNameGenerator = (item) => `item-${item}`;
comboBox.open();
await nextRender();

comboBox.close();
comboBox.itemClassNameGenerator = null;

comboBox.open();
await nextRender();

const items = getAllItems(comboBox);
expect(items[0].className).to.equal('');
expect(items[1].className).to.equal('');
expect(items[2].className).to.equal('');
});
});
1 change: 1 addition & 0 deletions packages/combo-box/test/typings/combo-box.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ assertType<boolean>(narrowedComboBox.opened);
assertType<string>(narrowedComboBox.filter);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
assertType<string>(narrowedComboBox.itemLabelPath);
assertType<string>(narrowedComboBox.itemValuePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
*/
items: TItem[] | undefined;

/**
* A function used to generate CSS class names for dropdown
* items and selected chips based on the item. The return
* value should be the generated class name as a string, or
* multiple class names separated by whitespace characters.
*/
itemClassNameGenerator: (item: TItem) => string;

/**
* The item property used for a visual representation of the item.
* @attr {string} item-label-path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
filtered-items="[[filteredItems]]"
selected-items="[[selectedItems]]"
selected-items-on-top="[[selectedItemsOnTop]]"
item-class-name-generator="[[itemClassNameGenerator]]"
top-group="[[_topGroup]]"
opened="{{opened}}"
renderer="[[renderer]]"
Expand Down Expand Up @@ -279,6 +280,17 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
type: Array,
},

/**
* A function used to generate CSS class names for dropdown
* items and selected chips based on the item. The return
* value should be the generated class name as a string, or
* multiple class names separated by whitespace characters.
*/
itemClassNameGenerator: {
type: Object,
observer: '__itemClassNameGeneratorChanged',
},

/**
* The item property used for a visual representation of the item.
* @attr {string} item-label-path
Expand Down Expand Up @@ -761,6 +773,13 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
}
}

/** @private */
__itemClassNameGeneratorChanged(generator, oldGenerator) {
if (generator || oldGenerator) {
this.__updateChips();
}
}

/** @private */
_pageSizeChanged(pageSize, oldPageSize) {
if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
Expand Down Expand Up @@ -934,6 +953,10 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
chip.label = label;
chip.setAttribute('title', label);

if (typeof this.itemClassNameGenerator === 'function') {
chip.className = this.itemClassNameGenerator(item);
}

chip.addEventListener('item-removed', (e) => this._onItemRemoved(e));
chip.addEventListener('mousedown', (e) => this._preventBlur(e));

Expand Down
6 changes: 6 additions & 0 deletions packages/multi-select-combo-box/test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ describe('basic', () => {
expect(comboBox.hasAttribute('loading')).to.be.false;
});

it('should propagate itemClassNameGenerator property to combo-box', () => {
const generator = (item) => item;
comboBox.itemClassNameGenerator = generator;
expect(internal.itemClassNameGenerator).to.equal(generator);
});

it('should update filteredItems when combo-box filteredItems changes', () => {
internal.filteredItems = ['apple'];
expect(comboBox.filteredItems).to.deep.equal(['apple']);
Expand Down
54 changes: 54 additions & 0 deletions packages/multi-select-combo-box/test/chips.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,4 +553,58 @@ describe('chips', () => {
expect(overlayPart.clientWidth).to.be.equal(comboBox.clientWidth);
});
});

describe('itemClassNameGenerator', () => {
beforeEach(() => {
comboBox.autoExpandHorizontally = true;
});

it('should set class name on the selected item chips', async () => {
comboBox.itemClassNameGenerator = (item) => item;
comboBox.selectedItems = ['apple', 'lemon'];
await nextRender();

const chips = getChips(comboBox);
expect(chips[1].className).to.equal('apple');
expect(chips[2].className).to.equal('lemon');
});

it('should set class name when generator set after selecting', async () => {
comboBox.selectedItems = ['apple', 'lemon'];
await nextRender();

comboBox.itemClassNameGenerator = (item) => item;
await nextRender();

const chips = getChips(comboBox);
expect(chips[1].className).to.equal('apple');
expect(chips[2].className).to.equal('lemon');
});

it('should remove class name when generator returns empty string', async () => {
comboBox.itemClassNameGenerator = (item) => item;
comboBox.selectedItems = ['apple', 'lemon'];
await nextRender();

comboBox.itemClassNameGenerator = () => '';
await nextRender();

const chips = getChips(comboBox);
expect(chips[1].className).to.equal('');
expect(chips[2].className).to.equal('');
});

it('should remove class name when generator is set to null', async () => {
comboBox.itemClassNameGenerator = (item) => item;
comboBox.selectedItems = ['apple', 'lemon'];
await nextRender();

comboBox.itemClassNameGenerator = null;
await nextRender();

const chips = getChips(comboBox);
expect(chips[1].className).to.equal('');
expect(chips[2].className).to.equal('');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ assertType<boolean | null | undefined>(narrowedComboBox.autoOpenDisabled);
assertType<string>(narrowedComboBox.filter);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
assertType<string>(narrowedComboBox.itemLabelPath);
assertType<string>(narrowedComboBox.itemValuePath);
Expand Down

0 comments on commit df22622

Please sign in to comment.