Skip to content

Commit 0351925

Browse files
authored
feat: support partial i18n in time picker (#9141)
1 parent 693c98a commit 0351925

File tree

5 files changed

+94
-68
lines changed

5 files changed

+94
-68
lines changed

packages/time-picker/src/vaadin-time-picker-mixin.d.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js';
1010
import type { KeyboardMixinClass } from '@vaadin/a11y-base/src/keyboard-mixin.js';
1111
import type { ComboBoxBaseMixinClass } from '@vaadin/combo-box/src/vaadin-combo-box-base-mixin.js';
1212
import type { DelegateStateMixinClass } from '@vaadin/component-base/src/delegate-state-mixin.js';
13+
import type { I18nMixinClass } from '@vaadin/component-base/src/i18n-mixin.js';
1314
import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js';
1415
import type { ClearButtonMixinClass } from '@vaadin/field-base/src/clear-button-mixin.js';
1516
import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
@@ -22,8 +23,9 @@ import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.j
2223
import type { TimePickerTime } from './vaadin-time-picker-helper.js';
2324

2425
export interface TimePickerI18n {
25-
parseTime(time: string): TimePickerTime | undefined;
26-
formatTime(time: TimePickerTime | undefined): string;
26+
parseTime?(time: string): TimePickerTime | undefined;
27+
28+
formatTime?(time: TimePickerTime | undefined): string;
2729
}
2830

2931
/**
@@ -38,6 +40,7 @@ export declare function TimePickerMixin<T extends Constructor<HTMLElement>>(
3840
Constructor<DisabledMixinClass> &
3941
Constructor<FieldMixinClass> &
4042
Constructor<FocusMixinClass> &
43+
Constructor<I18nMixinClass<TimePickerI18n>> &
4144
Constructor<InputConstraintsMixinClass> &
4245
Constructor<InputControlMixinClass> &
4346
Constructor<InputMixinClass> &
@@ -99,9 +102,9 @@ export declare class TimePickerMixinClass {
99102
step: number | null | undefined;
100103

101104
/**
102-
* The object used to localize this component.
103-
* To change the default localization, replace the entire
104-
* _i18n_ object or just the property you want to modify.
105+
* The object used to localize this component. To change the default
106+
* localization, replace this with an object that provides both the
107+
* time parsing and formatting functions.
105108
*
106109
* The object has the following JSON structure:
107110
*
@@ -124,8 +127,8 @@ export declare class TimePickerMixinClass {
124127
* }
125128
* ```
126129
*
127-
* Both `formatTime` and `parseTime` need to be implemented
128-
* to ensure the component works properly.
130+
* NOTE: `formatTime` and `parseTime` must be implemented in a
131+
* compatible manner to ensure the component works properly.
129132
*/
130133
i18n: TimePickerI18n;
131134
}

packages/time-picker/src/vaadin-time-picker-mixin.js

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
66
import { ComboBoxBaseMixin } from '@vaadin/combo-box/src/vaadin-combo-box-base-mixin.js';
7+
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
78
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
89
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
910
import { InputController } from '@vaadin/field-base/src/input-controller.js';
@@ -24,11 +25,15 @@ const MAX_ALLOWED_TIME = '23:59:59.999';
2425
*
2526
* @polymerMixin
2627
* @mixes ComboBoxBaseMixin
28+
* @mixes I18nMixin
2729
* @mixes InputControlMixin
2830
* @mixes PatternMixin
2931
*/
3032
export const TimePickerMixin = (superClass) =>
31-
class TimePickerMixinClass extends PatternMixin(ComboBoxBaseMixin(InputControlMixin(superClass))) {
33+
class TimePickerMixinClass extends I18nMixin(
34+
timePickerI18nDefaults,
35+
PatternMixin(ComboBoxBaseMixin(InputControlMixin(superClass))),
36+
) {
3237
static get properties() {
3338
return {
3439
/**
@@ -98,43 +103,6 @@ export const TimePickerMixin = (superClass) =>
98103
sync: true,
99104
},
100105

101-
/**
102-
* The object used to localize this component.
103-
* To change the default localization, replace the entire
104-
* _i18n_ object or just the property you want to modify.
105-
*
106-
* The object has the following JSON structure:
107-
*
108-
* ```
109-
* {
110-
* // A function to format given `Object` as
111-
* // time string. Object is in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
112-
* formatTime: (time) => {
113-
* // returns a string representation of the given
114-
* // object in `hh` / 'hh:mm' / 'hh:mm:ss' / 'hh:mm:ss.fff' - formats
115-
* },
116-
*
117-
* // A function to parse the given text to an `Object` in the format
118-
* // `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`.
119-
* // Must properly parse (at least) text
120-
* // formatted by `formatTime`.
121-
* parseTime: text => {
122-
* // Parses a string in object/string that can be formatted by`formatTime`.
123-
* }
124-
* }
125-
* ```
126-
*
127-
* Both `formatTime` and `parseTime` need to be implemented
128-
* to ensure the component works properly.
129-
*
130-
* @type {!TimePickerI18n}
131-
*/
132-
i18n: {
133-
type: Object,
134-
sync: true,
135-
value: () => ({ ...timePickerI18nDefaults }),
136-
},
137-
138106
/** @private */
139107
_comboBoxValue: {
140108
type: String,
@@ -154,7 +122,7 @@ export const TimePickerMixin = (superClass) =>
154122
'_openedOrItemsChanged(opened, _dropdownItems)',
155123
'_updateScroller(opened, _dropdownItems, _focusedIndex, _theme)',
156124
'__updateAriaAttributes(_dropdownItems, opened, inputElement)',
157-
'__updateDropdownItems(i18n, min, max, step)',
125+
'__updateDropdownItems(__effectiveI18n, min, max, step)',
158126
];
159127
}
160128

@@ -180,14 +148,53 @@ export const TimePickerMixin = (superClass) =>
180148
return this.$.clearButton;
181149
}
182150

151+
/**
152+
* The object used to localize this component. To change the default
153+
* localization, replace this with an object that provides both the
154+
* time parsing and formatting functions.
155+
*
156+
* The object has the following JSON structure:
157+
*
158+
* ```
159+
* {
160+
* // A function to format given `Object` as
161+
* // time string. Object is in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
162+
* formatTime: (time) => {
163+
* // returns a string representation of the given
164+
* // object in `hh` / 'hh:mm' / 'hh:mm:ss' / 'hh:mm:ss.fff' - formats
165+
* },
166+
*
167+
* // A function to parse the given text to an `Object` in the format
168+
* // `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`.
169+
* // Must properly parse (at least) text
170+
* // formatted by `formatTime`.
171+
* parseTime: text => {
172+
* // Parses a string in object/string that can be formatted by`formatTime`.
173+
* }
174+
* }
175+
* ```
176+
*
177+
* NOTE: `formatTime` and `parseTime` must be implemented in a
178+
* compatible manner to ensure the component works properly.
179+
*
180+
* @return {!TimePickerI18n}
181+
*/
182+
get i18n() {
183+
return super.i18n;
184+
}
185+
186+
set i18n(value) {
187+
super.i18n = value;
188+
}
189+
183190
/**
184191
* The input element's value when it cannot be parsed as a time, and an empty string otherwise.
185192
*
186193
* @private
187194
* @return {string}
188195
*/
189196
get __unparsableValue() {
190-
if (this._inputElementValue && !this.i18n.parseTime(this._inputElementValue)) {
197+
if (this._inputElementValue && !this.__effectiveI18n.parseTime(this._inputElementValue)) {
191198
return this._inputElementValue;
192199
}
193200

@@ -245,8 +252,8 @@ export const TimePickerMixin = (superClass) =>
245252
checkValidity() {
246253
return !!(
247254
this.inputElement.checkValidity() &&
248-
(!this.value || this._timeAllowed(this.i18n.parseTime(this.value))) &&
249-
(!this._comboBoxValue || this.i18n.parseTime(this._comboBoxValue))
255+
(!this.value || this._timeAllowed(this.__effectiveI18n.parseTime(this.value))) &&
256+
(!this._comboBoxValue || this.__effectiveI18n.parseTime(this._comboBoxValue))
250257
);
251258
}
252259

@@ -411,7 +418,7 @@ export const TimePickerMixin = (superClass) =>
411418
// observer where the value can be parsed again, so we set
412419
// this flag to ensure it does not alter the value.
413420
this.__useMemo = true;
414-
this._comboBoxValue = this.i18n.formatTime(objWithStep);
421+
this._comboBoxValue = this.__effectiveI18n.formatTime(objWithStep);
415422
this.__useMemo = false;
416423

417424
this.__commitValueChange();
@@ -508,7 +515,7 @@ export const TimePickerMixin = (superClass) =>
508515
}
509516

510517
/** @private */
511-
__updateDropdownItems(i18n, min, max, step) {
518+
__updateDropdownItems(effectiveI18n, min, max, step) {
512519
const minTimeObj = validateTime(parseISOTime(min || MIN_ALLOWED_TIME), step);
513520
const minSec = this.__getSec(minTimeObj);
514521

@@ -524,7 +531,7 @@ export const TimePickerMixin = (superClass) =>
524531
}
525532

526533
if (this.value) {
527-
this._comboBoxValue = i18n.formatTime(i18n.parseTime(this.value));
534+
this._comboBoxValue = effectiveI18n.formatTime(effectiveI18n.parseTime(this.value));
528535
}
529536
}
530537

@@ -560,7 +567,7 @@ export const TimePickerMixin = (superClass) =>
560567
while (time + step >= minSec && time + step <= maxSec) {
561568
const timeObj = validateTime(this.__addStep(time * 1000, step), step);
562569
time += step;
563-
const formatted = this.i18n.formatTime(timeObj);
570+
const formatted = this.__effectiveI18n.formatTime(timeObj);
564571
generatedList.push({ label: formatted, value: formatted });
565572
}
566573

@@ -606,8 +613,8 @@ export const TimePickerMixin = (superClass) =>
606613
return;
607614
}
608615

609-
const parsedObj = this.__useMemo ? this.__memoValue : this.i18n.parseTime(value);
610-
const newValue = this.i18n.formatTime(parsedObj) || '';
616+
const parsedObj = this.__useMemo ? this.__memoValue : this.__effectiveI18n.parseTime(value);
617+
const newValue = this.__effectiveI18n.formatTime(parsedObj) || '';
611618

612619
if (parsedObj) {
613620
if (value !== newValue) {
@@ -644,7 +651,7 @@ export const TimePickerMixin = (superClass) =>
644651

645652
/** @private */
646653
__updateInputValue(obj) {
647-
const timeString = this.i18n.formatTime(validateTime(obj, this.step)) || '';
654+
const timeString = this.__effectiveI18n.formatTime(validateTime(obj, this.step)) || '';
648655
this._inputElementValue = timeString;
649656
this._comboBoxValue = timeString;
650657
}
@@ -657,8 +664,8 @@ export const TimePickerMixin = (superClass) =>
657664
* @protected
658665
*/
659666
_timeAllowed(time) {
660-
const parsedMin = this.i18n.parseTime(this.min || MIN_ALLOWED_TIME);
661-
const parsedMax = this.i18n.parseTime(this.max || MAX_ALLOWED_TIME);
667+
const parsedMin = this.__effectiveI18n.parseTime(this.min || MIN_ALLOWED_TIME);
668+
const parsedMax = this.__effectiveI18n.parseTime(this.max || MAX_ALLOWED_TIME);
662669

663670
return (
664671
(!this.__getMsec(parsedMin) || this.__getMsec(time) >= this.__getMsec(parsedMin)) &&

packages/time-picker/test/keyboard-navigation.test.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,17 @@ describe('keyboard navigation', () => {
108108

109109
describe('with custom parser and formatter', () => {
110110
beforeEach(() => {
111-
timePicker.i18n.parseTime = (text) => {
112-
const parts = text.split('.');
113-
return {
114-
hours: parts[0],
115-
minutes: parts[1],
116-
};
117-
};
118-
timePicker.i18n.formatTime = (time) => {
119-
return `${time.hours}.${time.minutes}`;
111+
timePicker.i18n = {
112+
parseTime(text) {
113+
const parts = text.split('.');
114+
return {
115+
hours: parts[0],
116+
minutes: parts[1],
117+
};
118+
},
119+
formatTime(time) {
120+
return `${time.hours}.${time.minutes}`;
121+
},
120122
};
121123
});
122124

packages/time-picker/test/time-picker.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ describe('time-picker', () => {
352352

353353
describe('custom functions', () => {
354354
it('should use custom parser if that exists', () => {
355-
timePicker.i18n = { ...timePicker.i18n, parseTime: sinon.stub().returns({ hours: 12, minutes: 0, seconds: 0 }) };
355+
timePicker.i18n = { parseTime: sinon.stub().returns({ hours: 12, minutes: 0, seconds: 0 }) };
356356
timePicker.value = '12';
357357
expect(timePicker.i18n.parseTime.args[0][0]).to.be.equal('12:00');
358358
expect(timePicker.value).to.be.equal('12:00');
@@ -387,6 +387,14 @@ describe('time-picker', () => {
387387
expect(inputElement.value).to.equal('1200');
388388
expect(timePicker.value).to.equal('12:00');
389389
});
390+
391+
it('should fallback to default functions if none are provided', () => {
392+
timePicker.i18n = {};
393+
394+
timePicker.value = '12:00';
395+
expect(inputElement.value).to.equal('12:00');
396+
expect(timePicker.value).to.equal('12:00');
397+
});
390398
});
391399

392400
describe('helper text', () => {

packages/time-picker/test/typings/time-picker.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
} from '@vaadin/combo-box/src/vaadin-combo-box-item-mixin.js';
66
import type { DirMixinClass } from '@vaadin/component-base/src/dir-mixin.js';
77
import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
8+
import type { I18nMixinClass } from '@vaadin/component-base/src/i18n-mixin.js';
89
import type { ClearButtonMixinClass } from '@vaadin/field-base/src/clear-button-mixin.js';
910
import type { InputControlMixinClass } from '@vaadin/field-base/src/input-control-mixin.js';
1011
import type { PatternMixinClass } from '@vaadin/field-base/src/pattern-mixin.js';
@@ -14,6 +15,7 @@ import type { TimePickerItem } from '../../src/vaadin-time-picker-item.js';
1415
import type {
1516
TimePicker,
1617
TimePickerChangeEvent,
18+
TimePickerI18n,
1719
TimePickerInvalidChangedEvent,
1820
TimePickerOpenedChangedEvent,
1921
TimePickerValidatedEvent,
@@ -26,6 +28,7 @@ const timePicker = document.createElement('vaadin-time-picker');
2628

2729
// Mixins
2830
assertType<ElementMixinClass>(timePicker);
31+
assertType<I18nMixinClass<TimePickerI18n>>(timePicker);
2932
assertType<InputControlMixinClass>(timePicker);
3033
assertType<ClearButtonMixinClass>(timePicker);
3134
assertType<PatternMixinClass>(timePicker);
@@ -66,6 +69,9 @@ timePicker.addEventListener('validated', (event) => {
6669
assertType<boolean>(event.detail.valid);
6770
});
6871

72+
// I18n
73+
assertType<TimePickerI18n>({});
74+
6975
// Item
7076
const item = document.createElement('vaadin-time-picker-item');
7177
assertType<TimePickerItem>(item);

0 commit comments

Comments
 (0)