Skip to content

Commit 367be61

Browse files
authored
feat: support partial i18n in multi-select-combo-box (#10147)
1 parent f235b39 commit 367be61

File tree

4 files changed

+142
-51
lines changed

4 files changed

+142
-51
lines changed

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ComboBoxBaseMixinClass } from '@vaadin/combo-box/src/vaadin-combo-
1313
import type { ComboBoxDataProviderMixinClass } from '@vaadin/combo-box/src/vaadin-combo-box-data-provider-mixin.js';
1414
import type { ComboBoxItemsMixinClass } from '@vaadin/combo-box/src/vaadin-combo-box-items-mixin.js';
1515
import type { DelegateStateMixinClass } from '@vaadin/component-base/src/delegate-state-mixin.js';
16+
import type { I18nMixinClass } from '@vaadin/component-base/src/i18n-mixin.js';
1617
import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';
1718
import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js';
1819
import type { ClearButtonMixinClass } from '@vaadin/field-base/src/clear-button-mixin.js';
@@ -31,11 +32,11 @@ export type MultiSelectComboBoxRenderer<TItem> = (
3132
) => void;
3233

3334
export interface MultiSelectComboBoxI18n {
34-
cleared: string;
35-
focused: string;
36-
selected: string;
37-
deselected: string;
38-
total: string;
35+
cleared?: string;
36+
focused?: string;
37+
selected?: string;
38+
deselected?: string;
39+
total?: string;
3940
}
4041

4142
export declare function MultiSelectComboBoxMixin<TItem, T extends Constructor<HTMLElement>>(
@@ -49,6 +50,7 @@ export declare function MultiSelectComboBoxMixin<TItem, T extends Constructor<HT
4950
Constructor<DisabledMixinClass> &
5051
Constructor<FieldMixinClass> &
5152
Constructor<FocusMixinClass> &
53+
Constructor<I18nMixinClass<MultiSelectComboBoxI18n>> &
5254
Constructor<InputConstraintsMixinClass> &
5355
Constructor<InputControlMixinClass> &
5456
Constructor<InputMixinClass> &
@@ -97,9 +99,9 @@ export declare class MultiSelectComboBoxMixinClass<TItem> {
9799
itemIdPath: string;
98100

99101
/**
100-
* The object used to localize this component.
101-
* To change the default localization, replace the entire
102-
* _i18n_ object or just the property you want to modify.
102+
* The object used to localize this component. To change the default
103+
* localization, replace this with an object that provides all properties, or
104+
* just the individual properties you want to change.
103105
*
104106
* The object has the following JSON structure and default values:
105107
* ```js

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

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,34 @@ import { announce } from '@vaadin/a11y-base/src/announce.js';
77
import { ComboBoxDataProviderMixin } from '@vaadin/combo-box/src/vaadin-combo-box-data-provider-mixin.js';
88
import { ComboBoxItemsMixin } from '@vaadin/combo-box/src/vaadin-combo-box-items-mixin.js';
99
import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
10+
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
1011
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
1112
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
1213
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
1314
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
1415
import { InputController } from '@vaadin/field-base/src/input-controller.js';
1516
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
1617

18+
const DEFAULT_I18N = {
19+
cleared: 'Selection cleared',
20+
focused: 'focused. Press Backspace to remove',
21+
selected: 'added to selection',
22+
deselected: 'removed from selection',
23+
total: '{count} items selected',
24+
};
25+
1726
/**
1827
* @polymerMixin
1928
* @mixes ComboBoxDataProviderMixin
2029
* @mixes ComboBoxItemsMixin
30+
* @mixes I18nMixin
2131
* @mixes InputControlMixin
2232
* @mixes ResizeMixin
2333
*/
2434
export const MultiSelectComboBoxMixin = (superClass) =>
25-
class MultiSelectComboBoxMixinClass extends ComboBoxDataProviderMixin(
26-
ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass))),
35+
class MultiSelectComboBoxMixinClass extends I18nMixin(
36+
DEFAULT_I18N,
37+
ComboBoxDataProviderMixin(ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass)))),
2738
) {
2839
static get properties() {
2940
return {
@@ -72,43 +83,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
7283
sync: true,
7384
},
7485

75-
/**
76-
* The object used to localize this component.
77-
* To change the default localization, replace the entire
78-
* _i18n_ object or just the property you want to modify.
79-
*
80-
* The object has the following JSON structure and default values:
81-
* ```js
82-
* {
83-
* // Screen reader announcement on clear button click.
84-
* cleared: 'Selection cleared',
85-
* // Screen reader announcement when a chip is focused.
86-
* focused: ' focused. Press Backspace to remove',
87-
* // Screen reader announcement when item is selected.
88-
* selected: 'added to selection',
89-
* // Screen reader announcement when item is deselected.
90-
* deselected: 'removed from selection',
91-
* // Screen reader announcement of the selected items count.
92-
* // {count} is replaced with the actual count of items.
93-
* total: '{count} items selected',
94-
* }
95-
* ```
96-
* @type {!MultiSelectComboBoxI18n}
97-
* @default {English/US}
98-
*/
99-
i18n: {
100-
type: Object,
101-
value: () => {
102-
return {
103-
cleared: 'Selection cleared',
104-
focused: 'focused. Press Backspace to remove',
105-
selected: 'added to selection',
106-
deselected: 'removed from selection',
107-
total: '{count} items selected',
108-
};
109-
},
110-
},
111-
11286
/**
11387
* When true, filter string isn't cleared after selecting an item.
11488
*/
@@ -244,6 +218,37 @@ export const MultiSelectComboBoxMixin = (superClass) =>
244218
];
245219
}
246220

221+
/**
222+
* The object used to localize this component. To change the default
223+
* localization, replace this with an object that provides all properties, or
224+
* just the individual properties you want to change.
225+
*
226+
* The object has the following JSON structure and default values:
227+
* ```js
228+
* {
229+
* // Screen reader announcement on clear button click.
230+
* cleared: 'Selection cleared',
231+
* // Screen reader announcement when a chip is focused.
232+
* focused: ' focused. Press Backspace to remove',
233+
* // Screen reader announcement when item is selected.
234+
* selected: 'added to selection',
235+
* // Screen reader announcement when item is deselected.
236+
* deselected: 'removed from selection',
237+
* // Screen reader announcement of the selected items count.
238+
* // {count} is replaced with the actual count of items.
239+
* total: '{count} items selected',
240+
* }
241+
* ```
242+
* @return {!MultiSelectComboBoxI18n}
243+
*/
244+
get i18n() {
245+
return super.i18n;
246+
}
247+
248+
set i18n(value) {
249+
super.i18n = value;
250+
}
251+
247252
/** @protected */
248253
get slotStyles() {
249254
const tag = this.localName;
@@ -384,7 +389,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
384389
clear() {
385390
this.__updateSelection([]);
386391

387-
announce(this.i18n.cleared);
392+
announce(this.__effectiveI18n.cleared);
388393
}
389394

390395
/**
@@ -750,8 +755,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
750755
/** @private */
751756
__announceItem(itemLabel, isSelected, itemCount) {
752757
const state = isSelected ? 'selected' : 'deselected';
753-
const total = this.i18n.total.replace('{count}', itemCount || 0);
754-
announce(`${itemLabel} ${this.i18n[state]} ${total}`);
758+
const total = this.__effectiveI18n.total.replace('{count}', itemCount || 0);
759+
announce(`${itemLabel} ${this.__effectiveI18n[state]} ${total}`);
755760
}
756761

757762
/** @private */
@@ -1218,7 +1223,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
12181223
if (focusedIndex > -1) {
12191224
const item = chips[focusedIndex].item;
12201225
const itemLabel = this._getItemLabel(item);
1221-
announce(`${itemLabel} ${this.i18n.focused}`);
1226+
announce(`${itemLabel} ${this.__effectiveI18n.focused}`);
12221227
}
12231228
}
12241229
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
3+
import sinon from 'sinon';
4+
import '../src/vaadin-multi-select-combo-box.js';
5+
6+
describe('i18n', () => {
7+
describe('announcements', () => {
8+
let comboBox, region, clock;
9+
10+
before(() => {
11+
region = document.querySelector('[aria-live]');
12+
});
13+
14+
beforeEach(async () => {
15+
comboBox = fixtureSync(`<vaadin-multi-select-combo-box></vaadin-multi-select-combo-box>`);
16+
comboBox.items = ['Apple', 'Banana', 'Lemon', 'Orange'];
17+
await nextRender();
18+
clock = sinon.useFakeTimers({ shouldClearNativeTimers: true });
19+
});
20+
21+
afterEach(() => {
22+
clock.restore();
23+
});
24+
25+
it('should use default i18n messages', () => {
26+
comboBox.__selectItem('Apple');
27+
clock.tick(150);
28+
expect(region.textContent).to.equal('Apple added to selection 1 items selected');
29+
30+
comboBox.__removeItem('Apple');
31+
clock.tick(150);
32+
expect(region.textContent).to.equal('Apple removed from selection 0 items selected');
33+
34+
comboBox.clear();
35+
clock.tick(150);
36+
expect(region.textContent).to.equal('Selection cleared');
37+
});
38+
39+
it('should use custom i18n messages', () => {
40+
comboBox.i18n = {
41+
cleared: 'Custom cleared message',
42+
selected: 'custom selected',
43+
deselected: 'custom deselected',
44+
total: 'Custom total: {count} selected items',
45+
};
46+
47+
comboBox.__selectItem('Apple');
48+
clock.tick(150);
49+
expect(region.textContent).to.equal('Apple custom selected Custom total: 1 selected items');
50+
51+
comboBox.__removeItem('Apple');
52+
clock.tick(150);
53+
expect(region.textContent).to.equal('Apple custom deselected Custom total: 0 selected items');
54+
55+
comboBox.clear();
56+
clock.tick(150);
57+
expect(region.textContent).to.equal('Custom cleared message');
58+
});
59+
60+
it('should fall back to defaults when custom messages are missing', () => {
61+
comboBox.i18n = {
62+
selected: 'custom selected',
63+
};
64+
65+
comboBox.__selectItem('Apple');
66+
clock.tick(150);
67+
expect(region.textContent).to.equal('Apple custom selected 1 items selected');
68+
69+
comboBox.__removeItem('Apple');
70+
clock.tick(150);
71+
expect(region.textContent).to.equal('Apple removed from selection 0 items selected');
72+
73+
comboBox.clear();
74+
clock.tick(150);
75+
expect(region.textContent).to.equal('Selection cleared');
76+
});
77+
});
78+
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { ComboBoxItemsMixinClass } from '@vaadin/combo-box/src/vaadin-combo
1212
import type { DelegateStateMixinClass } from '@vaadin/component-base/src/delegate-state-mixin.js';
1313
import type { DirMixinClass } from '@vaadin/component-base/src/dir-mixin.js';
1414
import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
15+
import type { I18nMixinClass } from '@vaadin/component-base/src/i18n-mixin.js';
1516
import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js';
1617
import type { ClearButtonMixinClass } from '@vaadin/field-base/src/clear-button-mixin.js';
1718
import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
@@ -120,6 +121,7 @@ assertType<DelegateStateMixinClass>(narrowedComboBox);
120121
assertType<DisabledMixinClass>(narrowedComboBox);
121122
assertType<FieldMixinClass>(narrowedComboBox);
122123
assertType<FocusMixinClass>(narrowedComboBox);
124+
assertType<I18nMixinClass<MultiSelectComboBoxI18n>>(narrowedComboBox);
123125
assertType<InputConstraintsMixinClass>(narrowedComboBox);
124126
assertType<InputControlMixinClass>(narrowedComboBox);
125127
assertType<ClearButtonMixinClass>(narrowedComboBox);
@@ -150,3 +152,7 @@ assertType<() => void>(narrowedItem.requestContentUpdate);
150152
assertType<ComboBoxItemMixinClass<TestComboBoxItem, MultiSelectComboBox>>(narrowedItem);
151153
assertType<DirMixinClass>(narrowedItem);
152154
assertType<ThemableMixinClass>(narrowedItem);
155+
156+
// I18n
157+
assertType<MultiSelectComboBoxI18n>({});
158+
assertType<MultiSelectComboBoxI18n>({ cleared: 'Cleared' });

0 commit comments

Comments
 (0)