diff --git a/packages/components/timepicker/e2e.playwright-spec.ts b/packages/components/timepicker/e2e.playwright-spec.ts
index f78e94f10..5b7e0464c 100644
--- a/packages/components/timepicker/e2e.playwright-spec.ts
+++ b/packages/components/timepicker/e2e.playwright-spec.ts
@@ -11,5 +11,22 @@ test.describe('KbqTimepickerModule', () => {
await e2eEnableDarkTheme(page);
await expect(getComponent(page)).toHaveScreenshot('01-dark.png');
});
+
+ test('should not let digits grow beyond the mask on incomplete value', async ({ page }) => {
+ await page.goto('/E2eTimepickerStates');
+
+ const input = page.getByTestId('e2eTimepickerShort');
+
+ // an incomplete value: the minutes part has a single digit, so it never parses as time
+ await input.fill('11:1');
+ // place the caret at the very beginning, as a mouse click would
+ await input.evaluate((element: HTMLInputElement) => element.setSelectionRange(0, 0));
+
+ for (let index = 0; index < 10; index++) {
+ await input.press('9');
+ }
+
+ await expect(input).toHaveValue(/^\d{1,2}:\d{1,2}$/);
+ });
});
});
diff --git a/packages/components/timepicker/e2e.ts b/packages/components/timepicker/e2e.ts
index 8e6d670ff..f35da9911 100644
--- a/packages/components/timepicker/e2e.ts
+++ b/packages/components/timepicker/e2e.ts
@@ -19,7 +19,7 @@ import { DateTime } from 'luxon';
-
+
HH:mm
@@ -30,7 +30,7 @@ import { DateTime } from 'luxon';
-
+
HH:mm:ss
diff --git a/packages/components/timepicker/timepicker.directive.ts b/packages/components/timepicker/timepicker.directive.ts
index a05f4e0f5..dc970aacf 100644
--- a/packages/components/timepicker/timepicker.directive.ts
+++ b/packages/components/timepicker/timepicker.directive.ts
@@ -87,6 +87,9 @@ let uniqueComponentIdSuffix: number = 0;
const shortFormatSize: number = 5;
const fullFormatSize: number = 8;
+/** Maximum number of digits in a single time part */
+const timePartLength: number = 2;
+
@Directive({
selector: 'input[kbqTimepicker]',
providers: [
@@ -494,6 +497,25 @@ export class KbqTimepicker
this.lastValueValid = !!newTimeObj;
+ const selectionStart = this.selectionStart;
+ const selectionEnd = this.selectionEnd;
+ const nextViewValue = newTimeObj ? this.getTimeStringFromDate(newTimeObj, this.format) : formattedValue;
+ // A complete time is always rewritten, so that the caret keeps walking between the time parts.
+ // An incomplete one (e.g. `23:1`) is rewritten only when normalization trimmed it — otherwise
+ // the extra digits stay in the input and the value grows unbounded.
+ const shouldUpdateView = !!newTimeObj || nextViewValue !== this.viewValue;
+
+ if (shouldUpdateView) {
+ this.setViewValue(nextViewValue);
+
+ if (selectionStart !== null) {
+ this.selectionStart = selectionStart;
+ this.selectionEnd = newTimeObj ? selectionEnd : selectionStart;
+
+ this.createSelectionOfTimeComponentInInput(selectionStart + 1);
+ }
+ }
+
if (!newTimeObj) {
if (!this.viewValue) {
this.onChange(null);
@@ -502,16 +524,6 @@ export class KbqTimepicker
return;
}
- const selectionStart = this.selectionStart;
- const selectionEnd = this.selectionEnd;
-
- this.setViewValue(this.getTimeStringFromDate(newTimeObj, this.format));
-
- this.selectionStart = selectionStart;
- this.selectionEnd = selectionEnd;
-
- this.createSelectionOfTimeComponentInInput((selectionStart as number) + 1);
-
this.value = newTimeObj;
this.onChange(newTimeObj);
this.stateChanges.next();
@@ -640,29 +652,36 @@ export class KbqTimepicker
private replaceNumbers(value: string): string {
let formattedValue: string = value;
- const match: RegExpMatchArray | null = value.match(
- /^(?\d{0,4}):?(?\d{0,4}):?(?\d{0,4})$/
- );
+ const match: RegExpMatchArray | null = value.match(/^(?\d*):?(?\d*):?(?\d*)$/);
if (match?.groups) {
const { hours, minutes, seconds } = match.groups;
- if (hours.length && parseInt(hours) > HOURS_PER_DAY) {
- formattedValue = formattedValue.replace(hours, HOURS_PER_DAY.toString());
+ if (hours.length) {
+ formattedValue = formattedValue.replace(hours, this.normalizeTimePart(hours, HOURS_PER_DAY));
}
- if (minutes.length && parseInt(minutes) > MINUTES_PER_HOUR) {
- formattedValue = formattedValue.replace(minutes, MINUTES_PER_HOUR.toString());
+ if (minutes.length) {
+ formattedValue = formattedValue.replace(minutes, this.normalizeTimePart(minutes, MINUTES_PER_HOUR));
}
- if (seconds.length && parseInt(seconds) > SECONDS_PER_MINUTE) {
- formattedValue = formattedValue.replace(seconds, SECONDS_PER_MINUTE.toString());
+ if (seconds.length) {
+ formattedValue = formattedValue.replace(seconds, this.normalizeTimePart(seconds, SECONDS_PER_MINUTE));
}
}
return formattedValue;
}
+ /** Clamps a time part to the allowed maximum and trims it to two digits */
+ private normalizeTimePart(part: string, maxValue: number): string {
+ if (part.length <= timePartLength && parseInt(part) <= maxValue) {
+ return part;
+ }
+
+ return `${Math.min(parseInt(part), maxValue)}`.padStart(timePartLength, '0');
+ }
+
/** Checks whether the input is invalid based on the native validation. */
private isBadInput(): boolean {
const validity = (this.elementRef.nativeElement).validity;
diff --git a/packages/components/timepicker/timepicker.spec.ts b/packages/components/timepicker/timepicker.spec.ts
index 4d210e181..37bcc7801 100644
--- a/packages/components/timepicker/timepicker.spec.ts
+++ b/packages/components/timepicker/timepicker.spec.ts
@@ -580,6 +580,44 @@ describe(KbqTimepicker.name, () => {
expect(inputElementDebug.nativeElement.value).toBe('23:00:59');
}));
+ it('Should normalize incomplete value instead of letting it grow', fakeAsync(() => {
+ inputElementDebug.nativeElement.value = '911:1';
+ dispatchFakeEvent(inputElementDebug.nativeElement, 'keydown');
+ tick(1);
+
+ expect(inputElementDebug.nativeElement.value).toBe('23:1');
+ }));
+
+ it('Should trim time part longer than two digits', fakeAsync(() => {
+ inputElementDebug.nativeElement.value = '0001:1';
+ dispatchFakeEvent(inputElementDebug.nativeElement, 'keydown');
+ tick(1);
+
+ expect(inputElementDebug.nativeElement.value).toBe('01:1');
+ }));
+
+ it('Should normalize time part with more than four digits', fakeAsync(() => {
+ inputElementDebug.nativeElement.value = '123456';
+ dispatchFakeEvent(inputElementDebug.nativeElement, 'keydown');
+ tick(1);
+
+ expect(inputElementDebug.nativeElement.value).toBe('23:00:00');
+ }));
+
+ it('Should keep intermediate value untouched while typing', fakeAsync(() => {
+ inputElementDebug.nativeElement.value = '12:3';
+ dispatchFakeEvent(inputElementDebug.nativeElement, 'keydown');
+ tick(1);
+
+ expect(inputElementDebug.nativeElement.value).toBe('12:3');
+
+ inputElementDebug.nativeElement.value = '1';
+ dispatchFakeEvent(inputElementDebug.nativeElement, 'keydown');
+ tick(1);
+
+ expect(inputElementDebug.nativeElement.value).toBe('1');
+ }));
+
it('Increase hours by ArrowUp key and cycle from max to min', fakeAsync(() => {
expect(inputElementDebug.nativeElement.value).toBe('23:00:00');