From 0a65630ef2c38de3a226daa58e41f892fc193ee1 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova <35433937+ddaribo@users.noreply.github.com> Date: Tue, 21 May 2024 12:41:40 +0300 Subject: [PATCH] Date/time editors: Add support for same input formats as Angular Date Pipe - AmPm (Period), Fractional Seconds parts (#14065) --------- Co-authored-by: Stamen Stoychev --- CHANGELOG.md | 4 + .../date-common/util/date-time.util.spec.ts | 89 +++++++--- .../lib/date-common/util/date-time.util.ts | 107 ++++++++++-- .../lib/directives/date-time-editor/README.md | 2 +- .../date-time-editor.common.ts | 2 + .../date-time-editor.directive.spec.ts | 153 ++++++++++++------ .../date-time-editor.directive.ts | 20 ++- .../src/lib/grids/columns/column.component.ts | 4 +- .../query-builder/query-builder.component.ts | 4 +- .../src/lib/time-picker/README.md | 4 +- .../src/lib/time-picker/time-picker.common.ts | 2 +- .../time-picker/time-picker.component.spec.ts | 54 ++++++- .../lib/time-picker/time-picker.component.ts | 34 ++-- .../lib/time-picker/time-picker.directives.ts | 8 +- .../src/lib/time-picker/time-picker.pipes.ts | 9 +- .../date-time-editor.sample.html | 2 +- src/app/time-picker/time-picker.sample.html | 6 +- src/app/time-picker/time-picker.sample.ts | 2 +- 18 files changed, 373 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00dd45768ca..a2550f54d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes for each version of this project will be documented in this ### New Features - `IgxCombo`, `IgxSimpleCombo`: - Introduced abillity for hiding the clear icon button when the custom clear icon template is empty. +- `IgxDateTimeEditor`, `IgxTimePicker`: + - Now accept the following custom `inputFormat` options, as Angular's DatePipe: + - Fractional seconds: S, SS, SSS. + - Period (Am/Pm): a, aa, aaa, aaaa, aaaaa ## 17.2.0 ### New Features diff --git a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts index 79747451032..f9f4974bed6 100644 --- a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts +++ b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.spec.ts @@ -9,7 +9,7 @@ const reduceToDictionary = (parts: DatePartInfo[]) => parts.reduce((obj, x) => { describe(`DateTimeUtil Unit tests`, () => { describe('Date Time Parsing', () => { it('should correctly parse all date time parts (base)', () => { - const result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss tt'); + let result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS a'); const expected = [ { start: 0, end: 2, type: DatePart.Date, format: 'dd' }, { start: 2, end: 3, type: DatePart.Literal, format: '/' }, @@ -22,10 +22,16 @@ describe(`DateTimeUtil Unit tests`, () => { { start: 14, end: 16, type: DatePart.Minutes, format: 'mm' }, { start: 16, end: 17, type: DatePart.Literal, format: ':' }, { start: 17, end: 19, type: DatePart.Seconds, format: 'ss' }, - { start: 19, end: 20, type: DatePart.Literal, format: ' ' }, - { start: 20, end: 22, type: DatePart.AmPm, format: 'tt' } + { start: 19, end: 20, type: DatePart.Literal, format: ':' }, + { start: 20, end: 23, type: DatePart.FractionalSeconds, format: 'SSS' }, + { start: 23, end: 24, type: DatePart.Literal, format: ' ' }, + { start: 24, end: 26, type: DatePart.AmPm, format: 'aa' } ]; expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + + result = DateTimeUtil.parseDateTimeFormat('dd/MM/yyyy HH:mm:ss:SS tt'); + expected[expected.length - 1] = { start: 24, end: 26, type: DatePart.AmPm, format: 'tt' } + expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); }); it('should correctly parse date parts of with short formats', () => { @@ -112,7 +118,7 @@ describe(`DateTimeUtil Unit tests`, () => { expect(result).toEqual(new Date(2020, 9, 31)); }); - it('should correctly parse values in h:m:s tt format', () => { + it('should correctly parse values in h:m:s a, aa,.. or h:m:s tt format', () => { const verifyTime = (val: Date, hours = 0, minutes = 0, seconds = 0, milliseconds = 0) => { expect(val.getHours()).toEqual(hours); expect(val.getMinutes()).toEqual(minutes); @@ -120,19 +126,29 @@ describe(`DateTimeUtil Unit tests`, () => { expect(val.getMilliseconds()).toEqual(milliseconds); }; - const parts = DateTimeUtil.parseDateTimeFormat('h:m:s tt'); - let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts); - verifyTime(result, 11, 34, 12); - result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts); - verifyTime(result, 16, 12, 15); - result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts); - verifyTime(result, 11, 0, 0); - result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts); - verifyTime(result, 22, 0, 0); - result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts); - verifyTime(result, 12, 0, 0); - result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts); - verifyTime(result, 0, 0, 0); + const runTestsForParts = (parts: DatePartInfo[]) => { + let result = DateTimeUtil.parseValueFromMask('11:34:12 AM', parts); + verifyTime(result, 11, 34, 12); + result = DateTimeUtil.parseValueFromMask('04:12:15 PM', parts); + verifyTime(result, 16, 12, 15); + result = DateTimeUtil.parseValueFromMask('11:00:00 AM', parts); + verifyTime(result, 11, 0, 0); + result = DateTimeUtil.parseValueFromMask('10:00:00 PM', parts); + verifyTime(result, 22, 0, 0); + result = DateTimeUtil.parseValueFromMask('12:00:00 PM', parts); + verifyTime(result, 12, 0, 0); + result = DateTimeUtil.parseValueFromMask('12:00:00 AM', parts); + verifyTime(result, 0, 0, 0); + } + + const inputFormat = 'h:m:s'; + let parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} tt`); + runTestsForParts(parts); + + for (let i = 0; i < 5; i++) { + parts = DateTimeUtil.parseDateTimeFormat(`${inputFormat} ${'a'.repeat(i + 1)}`); + runTestsForParts(parts); + } }); }); @@ -159,7 +175,7 @@ describe(`DateTimeUtil Unit tests`, () => { { start: 5, end: 6, type: DatePart.Literal, format: ':' }, { start: 6, end: 8, type: DatePart.Seconds, format: 'ss' }, { start: 8, end: 9, type: DatePart.Literal, format: ' ' }, - { start: 9, end: 11, type: DatePart.AmPm, format: 'tt' } + { start: 9, end: 11, type: DatePart.AmPm, format: 'a' } ]; result = DateTimeUtil.parseValueFromMask(input, dateParts); @@ -225,6 +241,7 @@ describe(`DateTimeUtil Unit tests`, () => { expect(DateTimeUtil.isDateOrTimeChar('h')).toBeTrue(); expect(DateTimeUtil.isDateOrTimeChar('m')).toBeTrue(); expect(DateTimeUtil.isDateOrTimeChar('s')).toBeTrue(); + expect(DateTimeUtil.isDateOrTimeChar('S')).toBeTrue(); expect(DateTimeUtil.isDateOrTimeChar(':')).toBeFalse(); expect(DateTimeUtil.isDateOrTimeChar('/')).toBeFalse(); expect(DateTimeUtil.isDateOrTimeChar('.')).toBeFalse(); @@ -404,7 +421,35 @@ describe(`DateTimeUtil Unit tests`, () => { expect(date.getTime()).toEqual(new Date(2015, 4, 20, 12, 59, 57).getTime()); }); - it('should spin AM/PM portion correctly', () => { + it('should spin fractional seconds portion correctly', () => { + // base + let date = new Date(2024, 3, 10, 6, 10, 5, 555); + DateTimeUtil.spinFractionalSeconds(1, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 556).getTime()); + DateTimeUtil.spinFractionalSeconds(-1, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 555).getTime()); + + // delta !== 1 + DateTimeUtil.spinFractionalSeconds(5, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 560).getTime()); + DateTimeUtil.spinFractionalSeconds(-6, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 554).getTime()); + + // without looping over + date = new Date(2024, 3, 10, 6, 10, 5, 999); + DateTimeUtil.spinFractionalSeconds(1, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 999).getTime()); + DateTimeUtil.spinFractionalSeconds(-1000, date, false); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 0).getTime()); + + // with looping over (seconds are not affected) + DateTimeUtil.spinFractionalSeconds(1001, date, true); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 1).getTime()); + DateTimeUtil.spinFractionalSeconds(-5, date, true); + expect(date.getTime()).toEqual(new Date(2024, 3, 10, 6, 10, 5, 996).getTime()); + }); + + it('should spin AM/PM and a/p portion correctly', () => { const currentDate = new Date(2015, 4, 31, 4, 59, 59); const newDate = new Date(2015, 4, 31, 4, 59, 59); // spin from AM to PM @@ -414,6 +459,12 @@ describe(`DateTimeUtil Unit tests`, () => { // spin from PM to AM DateTimeUtil.spinAmPm(currentDate, newDate, 'AM'); expect(currentDate.getHours()).toEqual(4); + + DateTimeUtil.spinAmPm(currentDate, newDate, 'p'); + expect(currentDate.getHours()).toEqual(16); + + DateTimeUtil.spinAmPm(currentDate, newDate, 'a'); + expect(currentDate.getHours()).toEqual(4); }); it('should compare dates correctly', () => { diff --git a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts index d4b1094ce97..ccaf85ad36a 100644 --- a/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts +++ b/projects/igniteui-angular/src/lib/date-common/util/date-time.util.ts @@ -9,8 +9,16 @@ const enum FormatDesc { TwoDigits = '2-digit' } -const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T']; -const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y']; +const TIME_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T', 'a']; +const DATE_CHARS = ['d', 'D', 'M', 'y', 'Y']; + +/** @hidden */ +const enum AmPmValues { + AM = 'AM', + A = 'a', + PM = 'PM', + P = 'p' +} /** @hidden */ const enum DateParts { @@ -56,7 +64,8 @@ export abstract class DateTimeUtil { return null; } - if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) { + if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 + || parts[DatePart.Seconds] > 59 || parts[DatePart.FractionalSeconds] > 999) { return null; } @@ -65,8 +74,11 @@ export abstract class DateTimeUtil { parts[DatePart.Hours] %= 12; } - if (amPm && DateTimeUtil.getCleanVal(inputData, amPm, promptChar).toLowerCase() === 'pm') { - parts[DatePart.Hours] += 12; + if (amPm) { + const cleanVal = DateTimeUtil.getCleanVal(inputData, amPm, promptChar); + if (DateTimeUtil.isPm(cleanVal)) { + parts[DatePart.Hours] += 12; + } } return new Date( @@ -75,7 +87,8 @@ export abstract class DateTimeUtil { parts[DatePart.Date] || 1, parts[DatePart.Hours] || 0, parts[DatePart.Minutes] || 0, - parts[DatePart.Seconds] || 0 + parts[DatePart.Seconds] || 0, + parts[DatePart.FractionalSeconds] || 0 ); } @@ -86,7 +99,7 @@ export abstract class DateTimeUtil { const formatArray = Array.from(format); let currentPart: DatePartInfo = null; let position = 0; - + let lastPartAdded = false; for (let i = 0; i < formatArray.length; i++, position++) { const type = DateTimeUtil.determineDatePart(formatArray[i]); if (currentPart) { @@ -97,8 +110,15 @@ export abstract class DateTimeUtil { } } + if (currentPart.type === DatePart.AmPm && currentPart.format.indexOf('a') !== -1) { + currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart); + } DateTimeUtil.addCurrentPart(currentPart, dateTimeParts); + lastPartAdded = true; position = currentPart.end; + if(i === formatArray.length - 1 && currentPart.type !== type) { + lastPartAdded = false; + } } currentPart = { @@ -110,7 +130,10 @@ export abstract class DateTimeUtil { } // make sure the last member of a format like H:m:s is not omitted - if (!dateTimeParts.filter(p => p.format.includes(currentPart.format)).length) { + if (!lastPartAdded) { + if (currentPart.type === DatePart.AmPm) { + currentPart = DateTimeUtil.simplifyAmPmFormat(currentPart); + } DateTimeUtil.addCurrentPart(currentPart, dateTimeParts); } // formats like "y" or "yyy" are treated like "yyyy" while editing @@ -123,6 +146,13 @@ export abstract class DateTimeUtil { return dateTimeParts; } + /** Simplifies the AmPm part to as many chars as will be displayed */ + private static simplifyAmPmFormat(currentPart: DatePartInfo){ + currentPart.format = currentPart.format.length === 5 ? 'a' : 'aa'; + currentPart.end = currentPart.start + currentPart.format.length; + return { ...currentPart }; + } + public static getPartValue(value: Date, datePartInfo: DatePartInfo, partLength: number): string { let maskedValue; const datePart = datePartInfo.type; @@ -156,8 +186,11 @@ export abstract class DateTimeUtil { case DatePart.Seconds: maskedValue = value.getSeconds(); break; + case DatePart.FractionalSeconds: + maskedValue = value.getMilliseconds(); + break; case DatePart.AmPm: - maskedValue = value.getHours() >= 12 ? 'PM' : 'AM'; + maskedValue = DateTimeUtil.getAmPmValue(partLength, value.getHours() < 12); break; } @@ -168,6 +201,29 @@ export abstract class DateTimeUtil { return maskedValue; } + /** Returns the AmPm part value depending on the part length and a + * conditional expression indicating whether the value is AM or PM. + */ + public static getAmPmValue(partLength: number, isAm: boolean) { + if (isAm) { + return partLength === 1 ? AmPmValues.A : AmPmValues.AM; + } else { + return partLength === 1 ? AmPmValues.P : AmPmValues.PM; + } + } + + /** Returns true if a string value indicates an AM period */ + public static isAm(value: string) { + value = value.toLowerCase(); + return (value === AmPmValues.AM.toLowerCase() || value === AmPmValues.A.toLowerCase()); + } + + /** Returns true if a string value indicates a PM period */ + public static isPm(value: string) { + value = value.toLowerCase(); + return (value === AmPmValues.PM.toLowerCase() || value === AmPmValues.P.toLowerCase()); + } + /** Builds a date-time editor's default input format based on provided locale settings. */ public static getDefaultInputFormat(locale: string): string { locale = locale || DateTimeUtil.DEFAULT_LOCALE; @@ -311,16 +367,28 @@ export abstract class DateTimeUtil { newDate.setSeconds(seconds); } + /** Spins the fractional seconds (milliseconds) portion in a date-time editor. */ + public static spinFractionalSeconds(delta: number, newDate: Date, spinLoop: boolean) { + const maxMs = 999; + const minMs = 0; + let ms = newDate.getMilliseconds() + delta; + if (ms > maxMs) { + ms = spinLoop ? ms % maxMs - 1 : maxMs; + } else if (ms < minMs) { + ms = spinLoop ? maxMs + (ms % maxMs) + 1 : minMs; + } + + newDate.setMilliseconds(ms); + } + /** Spins the AM/PM portion in a date-time editor. */ public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date { - switch (amPmFromMask) { - case 'AM': - newDate = new Date(newDate.setHours(newDate.getHours() + 12)); - break; - case 'PM': - newDate = new Date(newDate.setHours(newDate.getHours() - 12)); - break; + if(DateTimeUtil.isAm(amPmFromMask)) { + newDate = new Date(newDate.setHours(newDate.getHours() + 12)); + } else if(DateTimeUtil.isPm(amPmFromMask)) { + newDate = new Date(newDate.setHours(newDate.getHours() - 12)); } + if (newDate.getDate() !== currentDate.getDate()) { return currentDate; } @@ -517,6 +585,9 @@ export abstract class DateTimeUtil { part.format = part.format.repeat(2); } break; + case DatePart.FractionalSeconds: + part.format = part.format[0].repeat(3); + break; } } @@ -540,8 +611,10 @@ export abstract class DateTimeUtil { case 'm': return DatePart.Minutes; case 's': - case 'S': return DatePart.Seconds; + case 'S': + return DatePart.FractionalSeconds; + case 'a': case 't': case 'T': return DatePart.AmPm; diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/README.md b/projects/igniteui-angular/src/lib/directives/date-time-editor/README.md index c598314c560..4993c98b488 100644 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/README.md +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/README.md @@ -68,7 +68,7 @@ public datePart: typeof DatePart = DatePart; ``` ```html - + keyboard_arrow_up keyboard_arrow_down diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts index ded48622ccf..ea03b98afb9 100644 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts @@ -14,6 +14,7 @@ export enum DatePart { Hours = 'hours', Minutes = 'minutes', Seconds = 'seconds', + FractionalSeconds = 'fractionalSeconds', AmPm = 'ampm', Literal = 'literal' } @@ -34,4 +35,5 @@ export interface DatePartDeltas { hours?: number; minutes?: number; seconds?: number; + fractionalSeconds?: number; } diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts index c283a6eb104..a1e1331ed11 100644 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts @@ -59,11 +59,11 @@ describe('IgxDateTimeEditor', () => { dateTimeEditor.inputFormat = inputFormat; expect(dateTimeEditor.mask).toEqual('00/00/00'); - dateTimeEditor.inputFormat = 'dd-MM-yyyy HH:mm:ss'; - expect(dateTimeEditor.mask).toEqual('00-00-0000 00:00:00'); + dateTimeEditor.inputFormat = 'dd-MM-yyyy HH:mm:ss:SS'; + expect(dateTimeEditor.mask).toEqual('00-00-0000 00:00:00:000'); - dateTimeEditor.inputFormat = 'H:m:s'; - expect(dateTimeEditor.mask).toEqual('00:00:00'); + dateTimeEditor.inputFormat = 'H:m:s:S'; + expect(dateTimeEditor.mask).toEqual('00:00:00:000'); }); }); @@ -222,25 +222,29 @@ describe('IgxDateTimeEditor', () => { describe('Time portions spinning', () => { it('should correctly increment / decrement time portions with passed in DatePart', () => { - inputFormat = 'dd/MM/yyyy HH:mm:ss'; - inputDate = '10/10/2010 12:10:34'; + inputFormat = 'dd/MM/yyyy HH:mm:ss:SS'; + inputDate = '10/10/2010 12:10:34:555'; elementRef = { nativeElement: { value: inputDate } }; initializeDateTimeEditor(); - dateTimeEditor.value = new Date(2010, 11, 10, 12, 10, 34); + dateTimeEditor.value = new Date(2010, 11, 10, 12, 10, 34, 555); spyOnProperty((dateTimeEditor as any), 'inputValue', 'get').and.returnValue(inputDate); const minutes = dateTimeEditor.value.getMinutes(); const seconds = dateTimeEditor.value.getSeconds(); + const ms = dateTimeEditor.value.getMilliseconds(); dateTimeEditor.increment(DatePart.Minutes); expect(dateTimeEditor.value.getMinutes()).toBeGreaterThan(minutes); dateTimeEditor.decrement(DatePart.Seconds); expect(dateTimeEditor.value.getSeconds()).toBeLessThan(seconds); + + dateTimeEditor.increment(DatePart.FractionalSeconds); + expect(dateTimeEditor.value.getMilliseconds()).toBeGreaterThan(ms); }); it('should correctly increment / decrement time portions without passed in DatePart', () => { - inputFormat = 'HH:mm:ss tt'; + inputFormat = 'HH:mm:ss:SS aa'; inputDate = ''; elementRef = { nativeElement: { value: inputDate } }; initializeDateTimeEditor(); @@ -348,6 +352,47 @@ describe('IgxDateTimeEditor', () => { dateTimeEditor.increment(DatePart.AmPm); expect(dateTimeEditor.value).toEqual(new Date(2020, 5, 12, 23, 15, 14)); + + inputFormat = 'dd aa yyyy-MM mm-ss-hh'; + inputDate = '12 AM 2020-06 14-15-11'; + elementRef = { nativeElement: { value: inputDate } }; + initializeDateTimeEditor(); + + dateTimeEditor.inputFormat = inputFormat; + expect(dateTimeEditor.mask).toEqual('00 LL 0000-00 00-00-00'); + + dateTimeEditor.value = new Date(2020, 5, 12, 11, 15, 14); + spyOnProperty((dateTimeEditor as any), 'inputValue', 'get').and.returnValue(inputDate); + + dateTimeEditor.increment(DatePart.AmPm); + expect(dateTimeEditor.value).toEqual(new Date(2020, 5, 12, 23, 15, 14)); + }); + + it('should support AM/PM part formats as Angular\'s DatePipe Period - a, aa, aaa, aaaa & aaaaa', () => { + inputFormat = 'HH:mm:ss '; + elementRef = { nativeElement: { value: inputDate } }; + initializeDateTimeEditor(); + + for (let i = 0; i < 5; i++) { + inputFormat += 'a'; + dateTimeEditor.inputFormat = inputFormat; + const expectedMask = '00:00:00 ' + 'L'.repeat(i === 4 ? 1 : 2); + expect(dateTimeEditor.mask).toEqual(expectedMask); + } + // make sure it works for multiple occurrences of the AmPm part variations at once and at last position + inputFormat = 'a aaa aa aaaaa HH:mm:ss a'; + const expectedMask = 'LL LL LL L 00:00:00 LL'; + dateTimeEditor.inputFormat = inputFormat; + expect(dateTimeEditor.mask).toEqual(expectedMask); + }); + + it('should use \'tt\' format as an alias to a, aa, etc. Period formats', () => { + inputFormat = 'HH:mm:ss tt'; + elementRef = { nativeElement: { value: inputDate } }; + initializeDateTimeEditor(); + const expectedMask = '00:00:00 LL'; + dateTimeEditor.inputFormat = inputFormat; + expect(dateTimeEditor.mask).toEqual(expectedMask); }); }); }); @@ -355,7 +400,7 @@ describe('IgxDateTimeEditor', () => { describe('Integration tests', () => { const dateTimeOptions = { day: '2-digit', month: '2-digit', year: 'numeric', - hour: 'numeric', minute: 'numeric', second: 'numeric' + hour: 'numeric', minute: 'numeric', second: 'numeric', fractionalSecondDigits: 3 }; let fixture; let inputElement: DebugElement; @@ -431,11 +476,11 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('8', inputElement); - expect(inputElement.nativeElement.value).toEqual('8_/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('8_/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); const date = new Date(2000, 0, 8, 0, 0, 0); - let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -444,12 +489,12 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('5', inputElement, 7, 7); - expect(inputElement.nativeElement.value).toEqual('__/__/_5__ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/_5__ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date.setFullYear(2005); date.setDate(1); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -458,19 +503,19 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('3', inputElement, 11, 11); - expect(inputElement.nativeElement.value).toEqual('__/__/____ 3_:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ 3_:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date.setFullYear(2000); date.setHours(3); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); }); it('should not accept invalid date and time parts.', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('63', inputElement); - expect(inputElement.nativeElement.value).toEqual('63/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('63/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); expect(inputElement.nativeElement.value).toEqual(''); @@ -478,7 +523,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('63', inputElement, 3, 3); - expect(inputElement.nativeElement.value).toEqual('__/63/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/63/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); expect(inputElement.nativeElement.value).toEqual(''); @@ -486,7 +531,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('25', inputElement, 11, 11); - expect(inputElement.nativeElement.value).toEqual('__/__/____ 25:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ 25:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); expect(inputElement.nativeElement.value).toEqual(''); @@ -494,7 +539,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('78', inputElement, 14, 14); - expect(inputElement.nativeElement.value).toEqual('__/__/____ __:78:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ __:78:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); expect(inputElement.nativeElement.value).toEqual(''); @@ -502,7 +547,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('78', inputElement, 17, 17); - expect(inputElement.nativeElement.value).toEqual('__/__/____ __:__:78'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ __:__:78:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); expect(inputElement.nativeElement.value).toEqual(''); @@ -511,11 +556,11 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('0000', inputElement, 6, 6); - expect(inputElement.nativeElement.value).toEqual('__/__/0000 __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/0000 __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); const date = new Date(2000, 0, 1, 0, 0, 0); - let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -524,11 +569,11 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('5', inputElement, 6, 6); - expect(inputElement.nativeElement.value).toEqual('__/__/5___ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/5___ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date.setFullYear(2005); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -537,11 +582,11 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('16', inputElement, 6, 6); - expect(inputElement.nativeElement.value).toEqual('__/__/16__ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/16__ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date.setFullYear(2016); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -550,11 +595,11 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('169', inputElement, 6, 6); - expect(inputElement.nativeElement.value).toEqual('__/__/169_ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/169_ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date.setFullYear(169); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/169,/g, '0169'); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/169,/g, '0169').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); }); it('should support different display and input formats.', () => { @@ -563,7 +608,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('9', inputElement); - expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); let date = new Date(2000, 0, 9, 0, 0, 0); @@ -579,7 +624,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('169', inputElement, 6, 6); - expect(inputElement.nativeElement.value).toEqual('__/__/169_ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/169_ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date = new Date(169, 0, 1, 0, 0, 0); @@ -593,7 +638,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('9', inputElement); - expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); let date = new Date(2000, 0, 9, 0, 0, 0); @@ -609,7 +654,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('9', inputElement); - expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); const shortDateOptions = { day: 'numeric', month: 'numeric', year: '2-digit' }; @@ -624,7 +669,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('9', inputElement); - expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__'); + expect(inputElement.nativeElement.value).toEqual('9_/__/____ __:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); const fullDateOptions = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }; @@ -639,7 +684,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('1', inputElement, 11, 11); - expect(inputElement.nativeElement.value).toEqual('__/__/____ 1_:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ 1_:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date = new Date(0, 0, 0, 1, 0, 0); @@ -655,7 +700,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulateTyping('2', inputElement, 11, 11); - expect(inputElement.nativeElement.value).toEqual('__/__/____ 2_:__:__'); + expect(inputElement.nativeElement.value).toEqual('__/__/____ 2_:__:__:___'); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date = new Date(2010, 10, 10, 2, 0, 0); @@ -664,11 +709,11 @@ describe('IgxDateTimeEditor', () => { }); it('should be able to apply custom display format.', fakeAsync(() => { // default format - const date = new Date(2003, 3, 5, 0, 0, 0); - fixture.componentInstance.date = new Date(2003, 3, 5, 0, 0, 0); + const date = new Date(2003, 3, 5, 0, 0, 0, 0); + fixture.componentInstance.date = new Date(2003, 3, 5, 0, 0, 0, 0); fixture.detectChanges(); tick(); - let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + let result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); // custom format @@ -678,7 +723,7 @@ describe('IgxDateTimeEditor', () => { fixture.detectChanges(); UIInteractions.simulateTyping('1', inputElement); date.setDate(15); - result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); @@ -693,7 +738,7 @@ describe('IgxDateTimeEditor', () => { it('should convert dates correctly on paste when different display and input formats are set.', () => { // display format = input format let date = new Date(2020, 10, 10, 10, 10, 10); - let inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + let inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulatePaste(inputDate, inputElement, 0, 19); @@ -752,7 +797,7 @@ describe('IgxDateTimeEditor', () => { fixture.componentInstance.date = date; fixture.detectChanges(); tick(); - const result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + const result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); dateTimeEditorDirective.clear(); @@ -763,7 +808,7 @@ describe('IgxDateTimeEditor', () => { fixture.componentInstance.date = date; fixture.detectChanges(); tick(); - const result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + const result = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(result); const inputHTMLElement = inputElement.nativeElement as HTMLInputElement; @@ -832,7 +877,7 @@ describe('IgxDateTimeEditor', () => { fixture.detectChanges(); let date = new Date(2009, 10, 10, 10, 10, 10); - let inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + let inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); UIInteractions.simulatePaste(inputDate, inputElement, 0, 19); @@ -849,7 +894,7 @@ describe('IgxDateTimeEditor', () => { inputElement.triggerEventHandler('blur', { target: inputElement.nativeElement }); fixture.detectChanges(); date = new Date(2027, 0, 1, 0, 0, 0); - inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, ''); + inputDate = ControlsFunction.formatDate(date, dateTimeOptions, 'en-GB').replace(/,/g, '').replace(/\./g, ':'); expect(inputElement.nativeElement.value).toEqual(inputDate); }); it('should be able to customize prompt char.', () => { @@ -857,7 +902,7 @@ describe('IgxDateTimeEditor', () => { fixture.detectChanges(); inputElement.triggerEventHandler('focus', {}); fixture.detectChanges(); - expect(inputElement.nativeElement.value).toEqual('../../.... ..:..:..'); + expect(inputElement.nativeElement.value).toEqual('../../.... ..:..:..:...'); }); it('should be en/disabled when the input is en/disabled.', fakeAsync(() => { spyOn(dateTimeEditorDirective, 'setDisabledState'); @@ -1048,12 +1093,12 @@ describe('IgxDateTimeEditor', () => { fixture = TestBed.createComponent(IgxDateTimeEditorShadowDomComponent); fixture.detectChanges(); - fixture.componentInstance.dateTimeFormat = 'dd-MM-yyyy hh:mm:ss'; + fixture.componentInstance.dateTimeFormat = 'dd-MM-yyyy hh:mm:ss:SS'; fixture.detectChanges(); inputElement = fixture.debugElement.query(By.css('input')); dateTimeEditorDirective = inputElement.injector.get(IgxDateTimeEditorDirective); - const today = new Date(2022, 5, 12, 14, 35, 12); + const today = new Date(2022, 5, 12, 14, 35, 12, 555); dateTimeEditorDirective.value = today; inputElement.nativeElement.focus(); @@ -1118,6 +1163,16 @@ describe('IgxDateTimeEditor', () => { UIInteractions.triggerEventHandlerKeyDown('ArrowDown', inputElement, false, false, true); fixture.detectChanges(); expect(dateTimeEditorDirective.value.getSeconds()).toEqual(today.getSeconds()); + + dateTimeEditorDirective.nativeElement.setSelectionRange(21, 21); + UIInteractions.triggerEventHandlerKeyDown('ArrowUp', inputElement, false, false, true); + fixture.detectChanges(); + expect(dateTimeEditorDirective.value.getMilliseconds()).toEqual(today.getMilliseconds() + 1); + + dateTimeEditorDirective.nativeElement.setSelectionRange(21, 21); + UIInteractions.triggerEventHandlerKeyDown('ArrowDown', inputElement, false, false, true); + fixture.detectChanges(); + expect(dateTimeEditorDirective.value.getMilliseconds()).toEqual(today.getMilliseconds()); }); }); @@ -1240,7 +1295,7 @@ export class IgxDateTimeEditorBaseTestComponent { export class IgxDateTimeEditorSampleComponent { @ViewChild('igxInputGroup', { static: true }) public igxInputGroup: IgxInputGroupComponent; public date: Date; - public dateTimeFormat = 'dd/MM/yyyy HH:mm:ss'; + public dateTimeFormat = 'dd/MM/yyyy HH:mm:ss:SSS'; public displayFormat: string; public minDate: string | Date; public maxDate: string | Date; @@ -1296,5 +1351,5 @@ class IgxDateTimeEditorFormComponent { imports: [IgxInputGroupComponent, IgxInputDirective, IgxDateTimeEditorDirective] }) export class IgxDateTimeEditorShadowDomComponent { - public dateTimeFormat = 'dd/MM/yyyy hh:mm:ss'; + public dateTimeFormat = 'dd/MM/yyyy hh:mm:ss:SSS'; } diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts index 1958a23510d..f2fad1e650b 100644 --- a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts @@ -227,7 +227,8 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh year: 1, hours: 1, minutes: 1, - seconds: 1 + seconds: 1, + fractionalSeconds: 1 }; private onChangeCallback: (...args: any[]) => void = noop; @@ -267,7 +268,8 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh return this._inputDateParts.some( p => p.type === DatePart.Hours || p.type === DatePart.Minutes - || p.type === DatePart.Seconds); + || p.type === DatePart.Seconds + || p.type === DatePart.FractionalSeconds); } private get dateValue(): Date { @@ -532,9 +534,8 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh this._inputDateParts = DateTimeUtil.parseDateTimeFormat(inputFormat); inputFormat = this._inputDateParts.map(p => p.format).join(''); const mask = (inputFormat || DateTimeUtil.DEFAULT_INPUT_FORMAT) - .replace(new RegExp(/(?=[^t])[\w]/, 'g'), '0'); - this.mask = mask.indexOf('tt') !== -1 ? mask.replace(new RegExp('tt', 'g'), 'LL') : mask; - + .replace(new RegExp(/(?=[^at])[\w]/, 'g'), '0'); + this.mask = mask.replaceAll(/(a{1,2})|tt/g, match => 'L'.repeat(match.length === 1 ? 1 : 2)); const placeholder = this.nativeElement.placeholder; if (!placeholder || oldFormat === placeholder) { this.renderer.setAttribute(this.nativeElement, 'placeholder', inputFormat); @@ -609,6 +610,9 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh case DatePart.Seconds: DateTimeUtil.spinSeconds(delta, newDate, this.spinLoop); break; + case DatePart.FractionalSeconds: + DateTimeUtil.spinFractionalSeconds(delta, newDate, this.spinLoop); + break; case DatePart.AmPm: const formatPart = this._inputDateParts.find(dp => dp.type === DatePart.AmPm); const amPmFromMask = this.inputValue.substring(formatPart.start, formatPart.end); @@ -690,8 +694,12 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh case DatePart.Seconds: maskedValue = this.dateValue.getSeconds(); break; + case DatePart.FractionalSeconds: + partLength = 3; + maskedValue = this.prependValue(this.dateValue.getMilliseconds(), 3, '00'); + break; case DatePart.AmPm: - maskedValue = this.dateValue.getHours() >= 12 ? 'PM' : 'AM'; + maskedValue = DateTimeUtil.getAmPmValue(partLength, this.dateValue.getHours() < 12); break; } diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index af71b88d414..017e6a57839 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -1615,13 +1615,13 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy * @hidden * @internal */ - public defaultTimeFormat = 'hh:mm:ss tt'; + public defaultTimeFormat = 'hh:mm:ss a'; /** * @hidden * @internal */ - public defaultDateTimeFormat = 'dd/MM/yyyy HH:mm:ss tt'; + public defaultDateTimeFormat = 'dd/MM/yyyy HH:mm:ss a'; /** diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts index e4bf2f9138d..0d4348ea44f 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts @@ -43,8 +43,8 @@ const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate'; const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime'; const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium'; const DEFAULT_PIPE_DIGITS_INFO = '1.0-3'; -const DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm:ss tt'; -const DEFAULT_TIME_FORMAT = 'hh:mm:ss tt'; +const DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm:ss a'; +const DEFAULT_TIME_FORMAT = 'hh:mm:ss a'; @Pipe({ name: 'fieldFormatter', diff --git a/projects/igniteui-angular/src/lib/time-picker/README.md b/projects/igniteui-angular/src/lib/time-picker/README.md index e0d4ce52bc8..3babdbe9cdd 100644 --- a/projects/igniteui-angular/src/lib/time-picker/README.md +++ b/projects/igniteui-angular/src/lib/time-picker/README.md @@ -16,12 +16,12 @@ Basic initialization Custom formats for the input field. ```html ``` -If the `inputFormat` is not set, it will default to `hh:mm tt`. The `displayFormat` accepts all supported formats by Angular's `DatePipe`. +If the `inputFormat` is not set, it will default to `hh:mm a`. The `displayFormat` accepts all supported formats by Angular's `DatePipe`. The time picker also supports binding through `ngModel` in case two-way date-binding is needed. ```html diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.common.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.common.ts index 26dc9d68cc9..afe9196ec9a 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.common.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.common.ts @@ -12,7 +12,7 @@ export interface IgxTimePickerBase { secondsList: ElementRef; ampmList: ElementRef; inputFormat: string; - itemsDelta: Pick; + itemsDelta: Pick; spinLoop: boolean; selectedDate: Date; maxDropdownValue: Date; diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.spec.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.spec.ts index bc77c16a322..9d5209a50ea 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.spec.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.spec.ts @@ -740,10 +740,11 @@ describe('IgxTimePicker', () => { })); it('should change date parts correctly and emit valueChange with increment() and decrement() methods', () => { - const date = new Date(2020, 12, 12, 10, 30, 30); + const date = new Date(2020, 12, 12, 10, 30, 30, 999); + timePicker.inputFormat = 'hh:mm:ss:SS a'; timePicker.value = new Date(date); - timePicker.minValue = new Date(2020, 12, 12, 6, 0, 0); - timePicker.maxValue = new Date(2020, 12, 12, 16, 0, 0); + timePicker.minValue = new Date(2020, 12, 12, 6, 0, 0, 0); + timePicker.maxValue = new Date(2020, 12, 12, 16, 0, 0, 0); timePicker.itemsDelta = { hours: 2, minutes: 20, seconds: 15 }; fixture.detectChanges(); spyOn(timePicker.valueChange, 'emit').and.callThrough(); @@ -765,6 +766,12 @@ describe('IgxTimePicker', () => { expect(timePicker.value).toEqual(date); expect(timePicker.valueChange.emit).toHaveBeenCalledTimes(3); expect(timePicker.valueChange.emit).toHaveBeenCalledWith(date); + + timePicker.decrement(DatePart.FractionalSeconds); + date.setMilliseconds(date.getMilliseconds() - timePicker.itemsDelta.fractionalSeconds); + expect(timePicker.value).toEqual(date); + expect(timePicker.valueChange.emit).toHaveBeenCalledTimes(4); + expect(timePicker.valueChange.emit).toHaveBeenCalledWith(date); }); it('should fire vallidationFailed on incrementing time outside min/max range', () => { @@ -792,7 +799,7 @@ describe('IgxTimePicker', () => { }); it('should scroll trough hours/minutes/seconds/AM PM based on default or set itemsDelta', fakeAsync(() => { - timePicker.inputFormat = 'hh:mm:ss tt'; + timePicker.inputFormat = 'hh:mm:ss a'; fixture.detectChanges(); secondsColumn = fixture.debugElement.query(By.css(CSS_CLASS_SECONDSLIST)); @@ -1114,6 +1121,7 @@ describe('IgxTimePicker', () => { expect(timePicker.itemsDelta.hours).toEqual(1); expect(timePicker.itemsDelta.minutes).toEqual(1); expect(timePicker.itemsDelta.seconds).toEqual(1); + expect(timePicker.itemsDelta.fractionalSeconds).toEqual(1); expect(timePicker.disabled).toEqual(false); }); @@ -1407,7 +1415,7 @@ describe('IgxTimePicker', () => { tick(); fixture.detectChanges(); selectedItems = fixture.debugElement.queryAll(By.css(CSS_CLASS_SELECTED_ITEM)); - const selectedAMPM = selectedItems[3].nativeElement.innerText; + let selectedAMPM = selectedItems[3].nativeElement.innerText; expect(selectedAMPM).toEqual(expectedAmPm); item = hourColumn.queryAll(By.directive(IgxTimeItemDirective))[4]; @@ -1433,6 +1441,40 @@ describe('IgxTimePicker', () => { selectedItems = fixture.debugElement.queryAll(By.css(CSS_CLASS_SELECTED_ITEM)); const selectedSecond = selectedItems[2].nativeElement.innerText; expect(selectedSecond).toEqual(expectedSecond); + + timePicker.inputFormat = 'hh:mm:ss a'; + fixture.detectChanges(); + + item = ampmColumn.queryAll(By.directive(IgxTimeItemDirective))[4]; + item.triggerEventHandler('click', UIInteractions.getMouseEvent('click')); + tick(); + fixture.detectChanges(); + selectedItems = fixture.debugElement.queryAll(By.css(CSS_CLASS_SELECTED_ITEM)); + selectedAMPM = selectedItems[3].nativeElement.innerText; + expect(selectedAMPM).toEqual(expectedAmPm); + })); + + it('should set placeholder correctly', fakeAsync(() => { + // no inputFormat set - placeholder equals the default date time input format + let inputEl = fixture.nativeElement.querySelector(CSS_CLASS_INPUT); + expect(inputEl.placeholder).toEqual('hh:mm tt'); + + // no placeholder - set to inputFormat, if it is set + // test with the different a,aa,.. ampm formats + for(let i = 1; i <= 5; i++) { + const format = `hh:mm ${'a'.repeat(i)}`; + timePicker.inputFormat = format; + fixture.detectChanges(); + + inputEl = fixture.nativeElement.querySelector(CSS_CLASS_INPUT); + expect(inputEl.placeholder).toEqual(i === 5 ? 'hh:mm a' : 'hh:mm aa'); + } + + timePicker.placeholder = 'sample placeholder'; + fixture.detectChanges(); + + inputEl = fixture.nativeElement.querySelector(CSS_CLASS_INPUT); + expect(inputEl.placeholder).toEqual('sample placeholder'); })); }); @@ -1754,7 +1796,7 @@ export class IgxTimePickerTestComponent { @ViewChild('picker', { read: IgxTimePickerComponent, static: true }) public timePicker: IgxTimePickerComponent; public mode: PickerInteractionMode = PickerInteractionMode.DropDown; - public date = new Date(2021, 24, 2, 11, 45, 0); + public date = new Date(2021, 24, 2, 11, 45, 0, 0); public minValue; public maxValue; } diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.ts index f2c2355f33e..0dc5df5252c 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.ts @@ -353,7 +353,8 @@ export class IgxTimePickerComponent extends PickerBaseDirective } if (DateTimeUtil.isValidDate(this.value)) { // TODO: Update w/ clear behavior - return this.value.getHours() !== 0 || this.value.getMinutes() !== 0 || this.value.getSeconds() !== 0; + return this.value.getHours() !== 0 || this.value.getMinutes() !== 0 || + this.value.getSeconds() !== 0 || this.value.getMilliseconds() !== 0; } return !!this.dateTimeEditor.value; } @@ -456,7 +457,8 @@ export class IgxTimePickerComponent extends PickerBaseDirective private _resourceStrings = getCurrentResourceStrings(TimePickerResourceStringsEN); private _okButtonLabel = null; private _cancelButtonLabel = null; - private _itemsDelta: Pick = { hours: 1, minutes: 1, seconds: 1 }; + private _itemsDelta: Pick = + { hours: 1, minutes: 1, seconds: 1, fractionalSeconds: 1 }; private _statusChanges$: Subscription; private _ngControl: NgControl = null; @@ -547,7 +549,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective * Defaults to the value from resource strings, `"OK"` for the built-in EN. * * ```html - * + * * ``` */ @Input() @@ -570,7 +572,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective * @remarks * Defaults to the value from resource strings, `"Cancel"` for the built-in EN. * ```html - * + * * ``` */ @Input() @@ -597,11 +599,11 @@ export class IgxTimePickerComponent extends PickerBaseDirective * ``` */ @Input() - public set itemsDelta(value: Pick) { + public set itemsDelta(value: Pick) { Object.assign(this._itemsDelta, value); } - public get itemsDelta(): Pick { + public get itemsDelta(): Pick { return this._itemsDelta; } @@ -654,6 +656,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective hour: '2-digit', minute: '2-digit', second: '2-digit', + fractionalSecondDigits: 3 }); } @@ -665,7 +668,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective const date = this.parseToDate(value); if (date) { this._dateValue = new Date(); - this._dateValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds()); + this._dateValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); this.setSelectedValue(this._dateValue); } else { this.setSelectedValue(null); @@ -825,10 +828,10 @@ export class IgxTimePickerComponent extends PickerBaseDirective if (DateTimeUtil.isValidDate(this.value)) { const oldValue = new Date(this.value); - this.value.setHours(0, 0, 0); + this.value.setHours(0, 0, 0, 0); if (this.value.getTime() !== oldValue.getTime()) { this.emitValueChange(oldValue, this.value); - this._dateValue.setHours(0, 0, 0); + this._dateValue.setHours(0, 0, 0, 0); this.dateTimeEditor.value = new Date(this.value); this.setSelectedValue(this._dateValue); } @@ -935,7 +938,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective } case 'ampmList': { let hour = this._selectedDate.getHours(); - hour = item === 'AM' ? hour - 12 : hour + 12; + hour = DateTimeUtil.isAm(item) ? hour - 12 : hour + 12; date.setHours(hour); date = this.validateDropdownValue(date, true); this.setSelectedValue(date); @@ -1017,7 +1020,8 @@ export class IgxTimePickerComponent extends PickerBaseDirective /** @hidden @internal */ public nextAmPm(delta?: number) { const ampm = this.getPartValue(this._selectedDate, 'ampm'); - if (!delta || (ampm === 'AM' && delta > 0) || (ampm === 'PM' && delta < 0)) { + if (!delta || (DateTimeUtil.isAm(ampm) && delta > 0) + || (DateTimeUtil.isPm(ampm) && delta < 0)) { let hours = this._selectedDate.getHours(); const sign = hours < 12 ? 1 : -1; hours = hours + sign * 12; @@ -1197,9 +1201,9 @@ export class IgxTimePickerComponent extends PickerBaseDirective } private toTwentyFourHourFormat(hour: number, ampm: string): number { - if (ampm === 'PM' && hour < 12) { + if (DateTimeUtil.isPm(ampm) && hour < 12) { hour += 12; - } else if (ampm === 'AM' && hour === 12) { + } else if (DateTimeUtil.isAm(ampm) && hour === 12) { hour = 0; } @@ -1211,7 +1215,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective this.value = newValue ? new Date(newValue) : newValue; } else if (isDate(this.value)) { const date = new Date(this.value); - date.setHours(newValue?.getHours() || 0, newValue?.getMinutes() || 0, newValue?.getSeconds() || 0); + date.setHours(newValue?.getHours() || 0, newValue?.getMinutes() || 0, newValue?.getSeconds() || 0, newValue?.getMilliseconds() || 0); this.value = date; } else { this.value = newValue ? this.toISOString(newValue) : newValue; @@ -1220,7 +1224,7 @@ export class IgxTimePickerComponent extends PickerBaseDirective private updateEditorValue(): void { const date = this.dateTimeEditor.value ? new Date(this.dateTimeEditor.value) : new Date(); - date.setHours(this._selectedDate.getHours(), this._selectedDate.getMinutes(), this._selectedDate.getSeconds()); + date.setHours(this._selectedDate.getHours(), this._selectedDate.getMinutes(), this._selectedDate.getSeconds(), this._selectedDate.getMilliseconds()); this.dateTimeEditor.value = date; } diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.directives.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.directives.ts index b387a6d33d9..cf98925cacb 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.directives.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.directives.ts @@ -262,7 +262,7 @@ export class IgxTimeItemDirective { const secondsPart = inputDateParts.find(element => element.type === 'seconds'); return DateTimeUtil.getPartValue(this.timePicker.selectedDate, secondsPart, secondsPart.format.length) === currentValue; case 'ampmList': - const ampmPart = inputDateParts.find(element => element.format === 'tt'); + const ampmPart = inputDateParts.find(element => element.format.indexOf('a') !== -1 || element.format === 'tt'); return DateTimeUtil.getPartValue(this.timePicker.selectedDate, ampmPart, ampmPart.format.length) === this.value; } } @@ -290,7 +290,7 @@ export class IgxTimeItemDirective { } return '00'; case 'ampmList': - const ampmPart = inputDateParts.find(element => element.format === 'tt'); + const ampmPart = inputDateParts.find(element => element.format.indexOf('a') !== -1 || element.format === 'tt'); return DateTimeUtil.getPartValue(this.timePicker.minDropdownValue, ampmPart, ampmPart.format.length); } } @@ -331,7 +331,7 @@ export class IgxTimeItemDirective { return DateTimeUtil.getPartValue(date, secondsPart, secondsPart.format.length); } case 'ampmList': - const ampmPart = inputDateParts.find(element => element.format === 'tt'); + const ampmPart = inputDateParts.find(element => element.format.indexOf('a') !== -1 || element.format === 'tt'); return DateTimeUtil.getPartValue(this.timePicker.maxDropdownValue, ampmPart, ampmPart.format.length); } } @@ -355,7 +355,7 @@ export class IgxTimeItemDirective { private getHourPart(date: Date): string { const inputDateParts = DateTimeUtil.parseDateTimeFormat(this.timePicker.inputFormat); const hourPart = inputDateParts.find(element => element.type === 'hours'); - const ampmPart = inputDateParts.find(element => element.format === 'tt'); + const ampmPart = inputDateParts.find(element =>element.format.indexOf('a') !== -1 || element.format === 'tt'); const hour = DateTimeUtil.getPartValue(date, hourPart, hourPart.format.length); if (ampmPart) { const ampm = DateTimeUtil.getPartValue(date, ampmPart, ampmPart.format.length); diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts index e79b3358b79..620948deb01 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts @@ -2,6 +2,7 @@ import { Pipe, PipeTransform, Inject } from '@angular/core'; import { DatePipe } from '@angular/common'; import { IGX_TIME_PICKER_COMPONENT, IgxTimePickerBase } from './time-picker.common'; import { DatePart } from '../directives/date-time-editor/public_api'; +import { DateTimeUtil } from '../date-common/util/date-time.util'; const ITEMS_COUNT = 7; @@ -48,8 +49,8 @@ export class TimeItemPipe implements PipeTransform { part = DatePart.Seconds; break; case 'ampm': - list = this.generateAmPm(min, max); const selectedAmPm = this.timePicker.getPartValue(selectedDate, 'ampm'); + list = this.generateAmPm(min, max, selectedAmPm); list = this.scrollListItem(selectedAmPm, list); part = DatePart.AmPm; break; @@ -180,17 +181,17 @@ export class TimeItemPipe implements PipeTransform { return secondsItems; } - private generateAmPm(min: Date, max: Date): any[] { + private generateAmPm(min: Date, max: Date, selectedAmPm: string): any[] { const ampmItems = []; const minHour = min.getHours(); const maxHour = max.getHours(); if (minHour < 12) { - ampmItems.push('AM'); + ampmItems.push(DateTimeUtil.getAmPmValue(selectedAmPm.length, true)); } if (minHour >= 12 || maxHour >= 12) { - ampmItems.push('PM'); + ampmItems.push(DateTimeUtil.getAmPmValue(selectedAmPm.length, false)); } for (let i = 0; i < 5; i++) { diff --git a/src/app/date-time-editor/date-time-editor.sample.html b/src/app/date-time-editor/date-time-editor.sample.html index 765f5b26b04..7d8831ddab2 100644 --- a/src/app/date-time-editor/date-time-editor.sample.html +++ b/src/app/date-time-editor/date-time-editor.sample.html @@ -12,7 +12,7 @@
DateTime editor in a form
+ [igxDateTimeEditor]="'hh:mm tt aaa aaaaa aa a'" [minValue]="minDate" [maxValue]="maxDate" required /> diff --git a/src/app/time-picker/time-picker.sample.ts b/src/app/time-picker/time-picker.sample.ts index 59ffd893246..43c7e5dd5f2 100644 --- a/src/app/time-picker/time-picker.sample.ts +++ b/src/app/time-picker/time-picker.sample.ts @@ -32,7 +32,7 @@ export class TimePickerSampleComponent { public target: IgxInputDirective; public itemsDelta = { hours: 1, minutes: 15, seconds: 20 }; - public format = 'hh:mm:ss tt'; + public format = 'hh:mm:ss:SS a'; public spinLoop = true; public datePart = DatePart.Hours;