diff --git a/CHANGELOG.md b/CHANGELOG.md index 563f70ac175..b1ed6061abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes for each version of this project will be documented in this file. + +## 7.1.2 +### Features +- `IgxTimePickerComponent`: in addition to the current dialog interaction mode, now the user can select or edit a time value, using an editable masked input with a dropdown. + ## 7.1.1 ### Bug Fixes @@ -14,6 +19,7 @@ All notable changes for each version of this project will be documented in this ### Other * update typedoc-plugin-localization version to 1.4.1 ([#3440](https://github.com/IgniteUI/igniteui-angular/issues/3440)) + ## 7.1.0 ### Features - **New component** `IgxBannerComponent`: @@ -40,6 +46,7 @@ All notable changes for each version of this project will be documented in this - `IgxOverlayService`: - `ElasticPositioningStrategy` added. This strategy positions the element as in **Connected** positioning strategy and resize the element to fit in the view port in case the element is partially getting out of view. + ## 7.0.5 ### Bug Fixes @@ -60,6 +67,7 @@ All notable changes for each version of this project will be documented in this * update typedoc-plugin-localization version to 1.4.1 ([#3440](https://github.com/IgniteUI/igniteui-angular/issues/3440)) * Move all keyboard navigation tests in a separate file ([#2975](https://github.com/IgniteUI/igniteui-angular/issues/2975)) + ## 7.0.4 ### Bug fixes - Fix(igx-grid): revert row editing styles ([#2672](https://github.com/IgniteUI/igniteui-angular/issues/2672)) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-component.scss index b827e7b8c0c..cdabb3b3a8a 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-component.scss @@ -1,90 +1,71 @@ @include b(igx-time-picker) { + @extend %time-picker-display !optional; @include e(header) { - @extend %igx-time-picker__header !optional; + @extend %time-picker__header !optional; } @include e(header-ampm) { - @extend %igx-time-picker__header-ampm !optional; + @extend %time-picker__header-ampm !optional; } @include e(header-hour){ - @extend %igx-time-picker__header-hour !optional; + @extend %time-picker__header-hour !optional; + } + + @include e(main) { + @extend %time-picker__main !optional; } // COLUMN @include e(column) { - @extend %igx-time-picker__column !optional; + @extend %time-picker__column !optional; } @include e(item) { - @extend %igx-time-picker__item !optional; + @extend %time-picker__item !optional; } @include e(item, $mod: selected) { - @extend %igx-time-picker__item--selected !optional; + @extend %time-picker__item--selected !optional; } @include e(item, $m: active) { - @extend %igx-time-picker__item--active !optional; + @extend %time-picker__item--active !optional; } // HOUR @include e(hourList) { - @extend %igx-time-picker__hourList !optional; + @extend %time-picker__hourList !optional; } - // MINUTE @include e(minuteList) { - @extend %igx-time-picker__minuteList !optional; + @extend %time-picker__minuteList !optional; } // AM PM @include e(ampmList) { - @extend %igx-time-picker__ampmList !optional; + @extend %time-picker__ampmList !optional; } @include e(body) { - @extend %igx-time-picker__body !optional; + @extend %time-picker__body !optional; } - .igx-dialog__window { - @extend %time-picker-display !optional; + @include e(buttons) { + @extend %time-picker__buttons !optional; } - @include m(vertical) { - .igx-dialog__window { - @extend %time-picker-display--vertical !optional; - } - - .igx-time-picker__wrapper { - @extend %igx-time-picker__wrapper !optional; - } - - .igx-time-picker__header { - @extend %igx-time-picker__header--vertical !optional; - - &::after { - @extend %igx-time-picker__header--vertical-after !optional; - } - } - - .igx-time-picker__body { - @extend %igx-time-picker__body--vertical !optional; - } - } - - .igx-dialog__window, - .igx-dialog__window-content { - @extend %time-picker-content !optional; + @include m(dropdown) { + @extend %time-picker--dropdown !optional; } - .igx-dialog__window-title { - @extend %time-picker-dialog-title !optional; - } + @include m(vertical) { + @extend %time-picker-display--vertical !optional; - .igx-dialog__window-actions { - @extend %time-picker-dialog-actions !optional; + @include e(header) { + @extend %time-picker__header--vertical !optional; + } } } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-theme.scss index dacd15c91f2..81aaea7aeaa 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/time-picker/_time-picker-theme.scss @@ -92,31 +92,35 @@ $vertical-header-width: rem(168px); - %igx-time-picker__body { + %time-picker-display { display: flex; - padding: 10px 0; - justify-content: center; - background: --var($theme, 'background-color'); + flex-flow: column nowrap; + max-width: 340px; + min-width: 320px; + border-radius: rem(4px); + box-shadow: igx-elevation($elevations, 24); + overflow: hidden; } - %igx-time-picker__body--vertical { - flex: 1 1 auto; + %time-picker-display--vertical { + flex-flow: row nowrap; + min-width: 540px; } - %time-picker-display { - max-width: 340px; - min-width: 320px; - padding: 0; + %time-picker__main { + background: --var($theme, 'background-color'); + flex: 1 1 auto; } - %time-picker-display--vertical { - width: 540px; + %time-picker--dropdown { + min-width: 200px; + box-shadow: igx-elevation($elevations, 3); } - // Take effect only in vertical mode - %igx-time-picker__wrapper { + %time-picker__body { display: flex; - flex-wrap: nowrap; + padding: 10px 0; + justify-content: center; } %time-picker-content { @@ -134,21 +138,21 @@ margin: 0; } - %igx-time-picker__hourList { + %time-picker__hourList { text-align: right; } - %igx-time-picker__minuteList { + %time-picker__minuteList { text-align: center; } - %igx-time-picker__ampmList { + %time-picker__ampmList { display: flex; flex-direction: column; padding-top: 48px; } - %igx-time-picker__column { + %time-picker__column { max-width: 64px; height: 325px; padding: 0; @@ -166,7 +170,7 @@ } } - %igx-time-picker__item { + %time-picker__item { width: 54px; padding: 5px 10px; border-radius: 15px; @@ -185,48 +189,44 @@ } } - %igx-time-picker__item--selected { + %time-picker__item--selected { font-size: rem(24px); color: --var($theme, 'selected-text-color'); } - %igx-time-picker__item--active { + %time-picker__item--active { background: --var($theme, 'active-item-background'); } - %igx-time-picker__header { + %time-picker__header { background: --var($theme, 'header-background'); padding: rem(24px) rem(16px); } - %igx-time-picker__header-ampm { + %time-picker__header-ampm { color: --var($theme, 'header-time-period-color'); } - %igx-time-picker__header--vertical { + %time-picker__header--vertical { width: $vertical-header-width; - position: relative; } - %igx-time-picker__header--vertical-after { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 100%; - height: 100%; - background: --var($theme, 'header-background'); - } - - %igx-time-picker__header-hour { - color: --var($theme, 'header-hour-text-color'); + %time-picker__header-hour { display: flex; + color: --var($theme, 'header-hour-text-color'); } - %igx-time-picker__header-ampm, - %igx-time-picker__header-hour { + %time-picker__header-ampm, + %time-picker__header-hour { margin: 0; } + + %time-picker__buttons { + display: flex; + justify-content: flex-end; + height: rem(52px); + padding: rem(8px); + } } /// Adds typography styles for the igx-calendar component. @@ -246,15 +246,15 @@ $content: map-get($categories, 'content'); @include igx-scope('.igx-typography') { - %igx-time-picker__header-ampm { + %time-picker__header-ampm { @include igx-type-style($type-scale, $time-period); } - %igx-time-picker__header-hour { + %time-picker__header-hour { @include igx-type-style($type-scale, $header-hour); } - %igx-time-picker__column { + %time-picker__column { @include igx-type-style($type-scale, $content); } } diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts index 245deeb6128..6436cd2558d 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, Input, ViewChild, OnInit, ElementRef, Pipe, PipeTransform } from '@angular/core'; +import { Component, Input, ViewChild, ElementRef, Pipe, PipeTransform } from '@angular/core'; import { async, fakeAsync, @@ -68,6 +68,7 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(new Event('focus')); tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('555 55'); @@ -81,6 +82,7 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(new Event('focus')); tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('+359-884 19 08 54'); })); @@ -296,23 +298,22 @@ describe('igxMask', () => { it('Apply display and input pipes on blur and focus.', fakeAsync(() => { const fixture = TestBed.createComponent(PipesMaskComponent); fixture.detectChanges(); + tick(); + fixture.detectChanges(); const input = fixture.componentInstance.input; - input.nativeElement.focus(); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('SSS'); input.nativeElement.dispatchEvent(new Event('blur')); tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('sss'); - - input.nativeElement.dispatchEvent(new Event('focus')); - tick(); - - expect(input.nativeElement.value).toEqual('SSS'); })); it('Apply placehodler when value is not defined.', fakeAsync(() => { @@ -325,13 +326,13 @@ describe('igxMask', () => { expect(input.nativeElement.placeholder).toEqual('hello'); input.nativeElement.dispatchEvent(new Event('focus')); - tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(__) (__)'); expect(input.nativeElement.placeholder).toEqual('hello'); input.nativeElement.dispatchEvent(new Event('blur')); - tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual(''); expect(input.nativeElement.placeholder).toEqual('hello'); diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts index d8fd233fc8e..cf0d61b1ea3 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -12,6 +12,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { KEYS, MaskHelper } from './mask-helper'; +import { isIE } from '../../core/utils'; const noop = () => { }; @@ -170,6 +171,8 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ private _valOnPaste; + private _stopPropagation: boolean; + /** *@hidden */ @@ -249,6 +252,11 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ @HostListener('input', ['$event']) public onInputChanged(event): void { + if (isIE() && this._stopPropagation) { + this._stopPropagation = false; + return; + } + if (this._paste) { this._paste = false; @@ -283,6 +291,9 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { @HostListener('focus', ['$event.target.value']) public onFocus(value) { if (this.focusedValuePipe) { + if (isIE()) { + this._stopPropagation = true; + } this.value = this.focusedValuePipe.transform(value); } else { this.value = this.maskHelper.parseValueByMaskOnInit(this.value, this._maskOptions); @@ -323,8 +334,9 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._maskOptions.promptChar = this.promptChar.substring(0, 1); } - if (value) { - this.value = this.maskHelper.parseValueByMaskOnInit(value, this._maskOptions); + this.value = value ? this.maskHelper.parseValueByMaskOnInit(value, this._maskOptions) : ''; + if (this.displayValuePipe) { + this.value = this.displayValuePipe.transform(this.value); } this.dataValue = this.includeLiterals ? this.value : value; diff --git a/projects/igniteui-angular/src/lib/time-picker/README.md b/projects/igniteui-angular/src/lib/time-picker/README.md index 578f3beea97..e3320be44ab 100644 --- a/projects/igniteui-angular/src/lib/time-picker/README.md +++ b/projects/igniteui-angular/src/lib/time-picker/README.md @@ -58,6 +58,15 @@ The TimePicker input group could be retemplated. ``` +The TimePicker supports another interaction mode - an editable masked input and a dropdown. The user can enter or edit the time value inside the text input or select a vlaue from a dropdown, that will be applied on the text input. +```typescript +mode = InteractionMode.dropdown; +``` + +```html + + +``` # API @@ -83,6 +92,8 @@ List of time-flags: "mm": minutes field with leading zero "tt": 2 characters of string which represents AM/PM field | | `isSpinLoop` | boolean | Determines the spin behavior. By default `isSpinLoop` is set to true. | +| `mode` | InteractionMode | Determines the interaction mode - a dialog picker or a dropdown with editable masked input. Default is dialog picker.| +| `promptChar` | string | Sets the character used to prompt the user for input. The default is a dash. Only applicable for dropdown mode. ### Outputs | Name | Description | @@ -102,4 +113,4 @@ List of time-flags: | `ampmInView` | | `string[]` | Returns an array of the ampm currently in view. | | `scrollHourIntoView` | `(item: string)` | `void` | Scrolls a hour item into view. | | `scrollMinuteIntoView` | `(item: string)` | `void` | Scrolls a minute item into view. | -| `scrollAmPmIntoView` | `(item: string)` | `void` | Scrolls a period item into view. | \ No newline at end of file +| `scrollAmPmIntoView` | `(item: string)` | `void` | Scrolls a period item into view. | 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 a44fcbe06f2..e6ec19b6ddf 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,6 +12,11 @@ export interface IgxTimePickerBase { selectedHour: string; selectedMinute: string; selectedAmPm: string; + format: string; + promptChar: string; + cleared: boolean; + collapsed: boolean; + mode: TimePickerInteractionMode; nextHour(); prevHour(); nextMinute(); @@ -23,4 +28,14 @@ export interface IgxTimePickerBase { scrollHourIntoView(item: string): void; scrollMinuteIntoView(item: string): void; scrollAmPmIntoView(item: string): void; + hideOverlay(): void; + parseMask(preserveAmPm?: boolean): string; +} + +/** + * Defines the posible values of the igxTimePicker's time selection mode. + */ +export enum TimePickerInteractionMode { + dialog, + dropdown } diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html index 20da254c3a0..388b20af257 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html @@ -1,3 +1,27 @@ + + + + + access_time + + + + clear + + + @@ -8,30 +32,35 @@ - - - - -
-
-
{{ selectedAmPm }}
-

- {{ selectedHour }}:{{ selectedMinute }} -

-
-
-
- {{ hour }} -
-
- {{ minute }} -
-
- {{ ampm }} -
-
+ +
+
+
{{ selectedAmPm }}
+

+ {{ selectedHour }}:{{ selectedMinute }} +

+
+
+
+
+ {{ hour }}
- - - +
+ {{ minute }} +
+
+ {{ ampm }} +
+
+
+ + +
+
+
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 5ac97e1b10d..0b9c0c4ab43 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 @@ -2,13 +2,13 @@ import { Component, ViewChild } from '@angular/core'; import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxInputDirective } from '../directives/input/input.directive'; import { IgxTimePickerComponent, IgxTimePickerModule } from './time-picker.component'; -import { UIInteractions, wait } from '../test-utils/ui-interactions.spec'; +import { UIInteractions } from '../test-utils/ui-interactions.spec'; import { IgxInputGroupModule } from '../input-group'; - import { configureTestSuite } from '../test-utils/configure-suite'; +import { TimePickerInteractionMode } from './time-picker.common'; describe('IgxTimePicker', () => { configureTestSuite(); @@ -23,20 +23,26 @@ describe('IgxTimePicker', () => { IgxTimePickerWithAMPMLeadingZerosTimeComponent, IgxTimePickerWithSpinLoopFalseValueComponent, IgxTimePickerWithItemsDeltaValueComponent, - IgxTimePickerRetemplatedComponent + IgxTimePickerRetemplatedComponent, + IgxTimePickerDropDownComponent, + IgxTimePickerDropDownSingleHourComponent, + IgxTimePickerDropDownNoValueComponent ], - imports: [IgxTimePickerModule, FormsModule, BrowserAnimationsModule, IgxInputGroupModule] - }) - .compileComponents(); + imports: [ + IgxTimePickerModule, + FormsModule, + NoopAnimationsModule, + IgxInputGroupModule + ] + }).compileComponents(); })); afterEach(async(() => { UIInteractions.clearOverlay(); })); - it('Initialize a TimePicker component', fakeAsync(() => { + it('Initialize a TimePicker component', (() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; @@ -55,18 +61,16 @@ describe('IgxTimePicker', () => { expect(domTimePicker.id).toBe('customTimePicker'); })); - it('@Input properties', fakeAsync(() => { + it('@Input properties', (() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; expect(timePicker.value).toEqual(new Date(2017, 7, 7, 3, 24)); })); - it('TimePicker DOM input value', fakeAsync(() => { + it('TimePicker DOM input value', (() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const currentTime = new Date(2017, 7, 7, 3, 24); @@ -80,8 +84,8 @@ describe('IgxTimePicker', () => { it('Dialog header value', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPmTimeComponent); - tick(); fixture.detectChanges(); + const dom = fixture.debugElement; // get time-picker value @@ -91,7 +95,7 @@ describe('IgxTimePicker', () => { const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); // get time from dialog header @@ -104,13 +108,12 @@ describe('IgxTimePicker', () => { it('Dialog selected element position', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPmTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const expectedColumnElements = 7; @@ -132,7 +135,6 @@ describe('IgxTimePicker', () => { it('TimePicker open event', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; @@ -143,21 +145,21 @@ describe('IgxTimePicker', () => { spyOn(timePicker.onOpen, 'emit'); target.nativeElement.dispatchEvent(new Event('click', { bubbles: true })); - tick(100); + tick(); expect(timePicker.onOpen.emit).toHaveBeenCalled(); })); it('TimePicker Validation Failed event', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithMInMaxTimeValueComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); // select time difference (-3, -3, 'AM') @@ -166,42 +168,40 @@ describe('IgxTimePicker', () => { const selectHour = hourColumn.children[middlePos - 3]; const minutesColumn = dom.query(By.css('.igx-time-picker__minuteList')); const selectMinutes = minutesColumn.children[middlePos - 3]; - // selectHour.triggerEventHandler('click', {}); + UIInteractions.clickElement(selectHour); - tick(100); fixture.detectChanges(); - // selectMinutes.triggerEventHandler('click', {}); + UIInteractions.clickElement(selectMinutes); - tick(100); fixture.detectChanges(); + const AMPMColumn = dom.query(By.css('.igx-time-picker__ampmList')); const selectAMPM = findByInnerText(AMPMColumn.children, 'AM'); - // selectAMPM.triggerEventHandler('click', {}); + UIInteractions.clickElement(selectAMPM); - tick(100); fixture.detectChanges(); const OkButton = dom.queryAll(By.css('.igx-button--flat'))[1]; spyOn(timePicker.onValidationFailed, 'emit'); + OkButton.triggerEventHandler('click', {}); + tick(); fixture.detectChanges(); - tick(100); expect(timePicker.onValidationFailed.emit).toHaveBeenCalled(); })); it('TimePicker cancel button', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPmTimeComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; const dom = fixture.debugElement; const initialTime = dom.query(By.directive(IgxInputDirective)).nativeElement.value; - const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); // select time difference (2, -3, 'AM') @@ -212,45 +212,41 @@ describe('IgxTimePicker', () => { const selectMinutes = minutesColumn.children[middlePos - 3]; UIInteractions.clickElement(selectHour); - tick(100); fixture.detectChanges(); UIInteractions.clickElement(selectMinutes); - tick(100); fixture.detectChanges(); const AMPMColumn = dom.query(By.css('.igx-time-picker__ampmList')); const selectAMPM = findByInnerText(AMPMColumn.children, 'AM'); UIInteractions.clickElement(selectAMPM); - tick(100); fixture.detectChanges(); + const cancelButton = dom.queryAll(By.css('.igx-button--flat'))[0]; + spyOn(timePicker.onValueChanged, 'emit'); - const cancelButton = dom.queryAll(By.css('.igx-button--flat'))[0]; UIInteractions.clickElement(cancelButton); - tick(100); + tick(); fixture.detectChanges(); const selectedTime = dom.query(By.directive(IgxInputDirective)).nativeElement.value; expect(initialTime).toEqual(selectedTime); expect(timePicker.onValueChanged.emit).not.toHaveBeenCalled(); - })); it('TimePicker ValueChanged event', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); const timePicker = fixture.componentInstance.timePicker; const dom = fixture.debugElement; - const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); @@ -267,19 +263,20 @@ describe('IgxTimePicker', () => { UIInteractions.clickElement(selectHour); fixture.detectChanges(); - tick(100); + UIInteractions.clickElement(selectMinutes); fixture.detectChanges(); - tick(100); + UIInteractions.clickElement(selectAMPM); fixture.detectChanges(); - tick(100); const OkButton = dom.queryAll(By.css('.igx-button--flat'))[1]; + spyOn(timePicker.onValueChanged, 'emit'); + UIInteractions.clickElement(OkButton); + tick(); fixture.detectChanges(); - tick(100); expect(timePicker.onValueChanged.emit).toHaveBeenCalled(); @@ -291,14 +288,13 @@ describe('IgxTimePicker', () => { it('TimePicker UP Down Keyboard navigation', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; - const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn: any = dom.query(By.css('.igx-time-picker__hourList')); @@ -309,47 +305,41 @@ describe('IgxTimePicker', () => { // move arrows several times with hour column let args = { key: 'ArrowUp', bubbles: true }; hourColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); + args = { key: 'ArrowDown', bubbles: true }; hourColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); + args = { key: 'ArrowUp', bubbles: true }; hourColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); minuteColumn.nativeElement.focus(); - tick(100); fixture.detectChanges(); // move arrows several times with minute column args = { key: 'ArrowDown', bubbles: true }; minuteColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); + args = { key: 'ArrowUp', bubbles: true }; minuteColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); + args = { key: 'ArrowDown', bubbles: true }; minuteColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); - tick(100); AMPMColumn.nativeElement.focus(); // move arrows several times with ampm column args = { key: 'ArrowUp', bubbles: true }; AMPMColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); + args = { key: 'ArrowDown', bubbles: true }; AMPMColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - - tick(100); fixture.detectChanges(); // get time from dialog header @@ -359,7 +349,6 @@ describe('IgxTimePicker', () => { args = { key: 'Enter', bubbles: true }; hourColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); const valueFromInput = dom.query(By.directive(IgxInputDirective)).nativeElement.value; @@ -369,7 +358,6 @@ describe('IgxTimePicker', () => { it('TimePicker Left Right Keyboard navigation', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; @@ -378,50 +366,41 @@ describe('IgxTimePicker', () => { let args = { key: 'ArrowRight', bubbles: true }; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(100); fixture.detectChanges(); const hourColumn: any = dom.query(By.css('.igx-time-picker__hourList')); hourColumn.nativeElement.focus(); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__hourList'); document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__minuteList'); args = { key: 'ArrowLeft', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); args = { key: 'ArrowRight', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); args = { key: 'ArrowUp', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(document.activeElement.children[3].innerHTML.trim()).toBe('23'); args = { key: 'ArrowRight', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); args = { key: 'ArrowDown', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(document.activeElement.children[3].innerHTML.trim()).toBe('PM'); args = { key: 'ArrowLeft', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__minuteList'); @@ -433,7 +412,6 @@ describe('IgxTimePicker', () => { args = { key: 'Escape', bubbles: true }; document.activeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); const selectedTime = dom.query(By.directive(IgxInputDirective)).nativeElement.value; @@ -442,11 +420,11 @@ describe('IgxTimePicker', () => { it('TimePicker Mouse Over', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); tick(100); fixture.detectChanges(); @@ -457,34 +435,30 @@ describe('IgxTimePicker', () => { const AMPMColumn: any = dom.query(By.css('.igx-time-picker__ampmList')); hourColumn.triggerEventHandler('focus', {}); - tick(); fixture.detectChanges(); hourColumn.triggerEventHandler('mouseover', {}); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__hourList'); minuteColumn.triggerEventHandler('mouseover', {}); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__minuteList'); AMPMColumn.triggerEventHandler('mouseover', {}); - tick(100); fixture.detectChanges(); expect(document.activeElement.classList).toContain('igx-time-picker__ampmList'); })); it('TimePicker Mouse Wheel', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); // const timePicker = fixture.componentInstance.timePicker; @@ -496,75 +470,71 @@ describe('IgxTimePicker', () => { // focus hours hourColumn.nativeElement.focus(); - tick(100); fixture.detectChanges(); hourColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); event = new WheelEvent('wheel', { deltaX: 0, deltaY: -100 }); hourColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel up and expect the selected element to be 2 expect(hourColumn.nativeElement.children[3].innerText).toBe('2'); event = new WheelEvent('wheel', { deltaX: 0, deltaY: 100 }); hourColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel down and expect the selected element to be 3 again expect(hourColumn.nativeElement.children[3].innerText).toBe('3'); // focus minutes minuteColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); event = new WheelEvent('wheel', { deltaX: 0, deltaY: -100 }); minuteColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel up and expect the selected element to be 23 expect(minuteColumn.nativeElement.children[3].innerText).toBe('23'); event = new WheelEvent('wheel', { deltaX: 0, deltaY: 100 }); minuteColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel down and expect the selected element to be 24 again expect(minuteColumn.nativeElement.children[3].innerText).toBe('24'); // focus ampm AMPMColumn.nativeElement.focus(); - tick(100); fixture.detectChanges(); event = new WheelEvent('wheel', { deltaX: 0, deltaY: 100 }); AMPMColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel down and expect the selected element to be PM expect(AMPMColumn.nativeElement.children[3].innerText).toBe('PM'); event = new WheelEvent('wheel', { deltaX: 0, deltaY: -100 }); AMPMColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); + // move the mouse wheel up and expect the selected element to be AM again expect(AMPMColumn.nativeElement.children[3].innerText).toBe('AM'); })); it('TimePicker Pan Move', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); // const timePicker = fixture.componentInstance.timePicker; @@ -579,89 +549,85 @@ describe('IgxTimePicker', () => { // focus hours hourColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); hourColumn.triggerEventHandler('panmove', event); - tick(100); fixture.detectChanges(); hourColumn.triggerEventHandler('panmove', eventDown); - tick(100); fixture.detectChanges(); + // swipe up and expect the selected element to be 4 expect(hourColumn.nativeElement.children[3].innerText).toBe('4'); hourColumn.triggerEventHandler('panmove', eventUp); - tick(100); fixture.detectChanges(); + // swipe down and expect the selected element to be 3 again expect(hourColumn.nativeElement.children[3].innerText).toBe('3'); // focus minutes minuteColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); minuteColumn.triggerEventHandler('panmove', eventDown); - tick(100); fixture.detectChanges(); + // swipe up and expect the selected element to be 25 expect(minuteColumn.nativeElement.children[3].innerText).toBe('25'); minuteColumn.triggerEventHandler('panmove', eventUp); - tick(100); fixture.detectChanges(); + // swipe down and expect the selected element to be 24 again expect(minuteColumn.nativeElement.children[3].innerText).toBe('24'); // focus ampm AMPMColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); AMPMColumn.triggerEventHandler('panmove', eventDown); - tick(100); fixture.detectChanges(); + // swipe up and expect the selected element to be PM expect(AMPMColumn.nativeElement.children[3].innerText).toBe('PM'); AMPMColumn.triggerEventHandler('panmove', eventUp); - tick(100); fixture.detectChanges(); + // move the swipe up and expect the selected element to be AM again expect(AMPMColumn.nativeElement.children[3].innerText).toBe('AM'); })); it('TimePicker 24 hour format', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWith24HTimeComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); - const AMPMColumn: any = dom.query(By.css('.igx-time-picker__ampmList')); - expect(AMPMColumn.children.length).toBe(0); - const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); const selectHour = hourColumn.children[3]; + const AMPMColumn: any = dom.query(By.css('.igx-time-picker__ampmList')); + + expect(AMPMColumn.children.length).toBe(0); expect(selectHour.nativeElement.innerText).toBe('00'); })); it('TimePicker Items in view', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithAMPMLeadingZerosTimeComponent); - tick(); fixture.detectChanges(); - const timePicker = fixture.componentInstance.timePicker; + const timePicker = fixture.componentInstance.timePicker; const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hoursInview = timePicker.hoursInView(); @@ -675,15 +641,14 @@ describe('IgxTimePicker', () => { it('TimePicker scroll to end', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithSpinLoopFalseValueComponent); - tick(); fixture.detectChanges(); const initialTime = fixture.componentInstance.dateValue; - const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn: any = dom.query(By.css('.igx-time-picker__hourList')); @@ -691,7 +656,6 @@ describe('IgxTimePicker', () => { const AMPMColumn: any = dom.query(By.css('.igx-time-picker__ampmList')); hourColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); spyOn(console, 'error'); @@ -699,23 +663,18 @@ describe('IgxTimePicker', () => { const event = new WheelEvent('wheel', { deltaX: 0, deltaY: -100 }); hourColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); minuteColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); minuteColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); AMPMColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); AMPMColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); // check console for error @@ -737,12 +696,13 @@ describe('IgxTimePicker', () => { it('TimePicker check isSpinLoop with Items Delta', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithItemsDeltaValueComponent); - tick(); fixture.detectChanges(); + const dom = fixture.debugElement; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn: any = dom.query(By.css('.igx-time-picker__hourList')); @@ -754,27 +714,21 @@ describe('IgxTimePicker', () => { // check scrolling each element hourColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); hourColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); minuteColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); minuteColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); AMPMColumn.nativeElement.focus(); - tick(); fixture.detectChanges(); AMPMColumn.triggerEventHandler('wheel', event); - tick(100); fixture.detectChanges(); const timeFromPopupHeader: any = fixture.debugElement.query(By.css('.igx-time-picker__header')).nativeElement.children; @@ -787,28 +741,26 @@ describe('IgxTimePicker', () => { it('TimePicker with not valid element arrow up', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); const validDate = new Date(2017, 7, 7, 4, 27); - + const dom = fixture.debugElement; const timePicker = fixture.componentInstance.timePicker; + timePicker.value = validDate; - const dom = fixture.debugElement; const notValidHour = '700'; timePicker.selectedHour = notValidHour; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn: any = dom.query(By.css('.igx-time-picker__hourList')); const args = { key: 'ArrowUp', bubbles: true }; hourColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(hourColumn.nativeElement.children[3].innerText).toEqual('03'); @@ -816,27 +768,26 @@ describe('IgxTimePicker', () => { it('TimePicker with not valid element arrow down', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); + const dom = fixture.debugElement; const timePicker = fixture.componentInstance.timePicker; const validDate = new Date(2017, 7, 7, 4, 27); + timePicker.value = validDate; - const dom = fixture.debugElement; const notValidValue = '700'; timePicker.selectedMinute = notValidValue; const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const minuteColumn: any = dom.query(By.css('.igx-time-picker__minuteList')); const args = { key: 'ArrowDown', bubbles: true }; minuteColumn.nativeElement.dispatchEvent(new KeyboardEvent('keydown', args)); - tick(100); fixture.detectChanges(); expect(minuteColumn.nativeElement.children[3].innerText).toEqual('28'); @@ -844,28 +795,26 @@ describe('IgxTimePicker', () => { it('TimePicker vertical', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); + const dom = fixture.debugElement; - fixture.componentInstance.isVertical = true; + fixture.componentInstance.timePicker.vertical = true; + fixture.detectChanges(); const timePickerTarget = dom.query(By.directive(IgxInputDirective)); UIInteractions.clickElement(timePickerTarget); - tick(200); + tick(); fixture.detectChanges(); - expect(dom.query(By.css('.igx-time-picker--vertical'))).not.toBeNull(); - - const dialog = dom.query(By.css('.igx-dialog__window')).nativeElement; + const timePickerVertical = dom.query(By.css('.igx-time-picker--vertical')); - expect(dialog.offsetWidth).toBeGreaterThan(dialog.offsetHeight); - tick(); + expect(timePickerVertical).not.toBeNull(); + expect(timePickerVertical.nativeElement.offsetWidth).toBeGreaterThan(timePickerVertical.nativeElement.offsetHeight); })); it('TimePicker with retemplated input group (icon removed)', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerRetemplatedComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; @@ -876,36 +825,34 @@ describe('IgxTimePicker', () => { // https://github.com/IgniteUI/igniteui-angular/issues/2470 it('TimePicker always use date from value', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerWithPassedTimeComponent); - tick(); fixture.detectChanges(); - const dom = fixture.debugElement; + const dom = fixture.debugElement; const initialValue = (fixture.componentInstance.timePicker.value); const initialDate = getDateStringFromDateObject(initialValue); const initialTime = initialValue.getHours() + ':' + initialValue.getMinutes(); - const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); const selectHour = hourColumn.children[5]; - const minutesColumn = dom.query(By.css('.igx-time-picker__minuteList')); const selectMinutes = minutesColumn.children[2]; UIInteractions.clickElement(selectHour); fixture.detectChanges(); - tick(100); + UIInteractions.clickElement(selectMinutes); fixture.detectChanges(); - tick(100); const OkButton = dom.queryAll(By.css('.igx-button--flat'))[1]; + UIInteractions.clickElement(OkButton); + tick(); fixture.detectChanges(); - tick(100); const changedValue = (fixture.componentInstance.timePicker.value); const changedDate = getDateStringFromDateObject(changedValue); @@ -914,34 +861,104 @@ describe('IgxTimePicker', () => { expect(initialDate).toEqual(changedDate); expect(initialTime).not.toEqual(changedTime); expect(changedTime).toEqual('5:23'); - })); it('TimePicker default selected value in dialog', fakeAsync(() => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); - tick(); fixture.detectChanges(); const dom = fixture.debugElement; - const timePickerTarget = dom.query(By.directive(IgxInputDirective)); + UIInteractions.clickElement(timePickerTarget); - tick(100); + tick(); fixture.detectChanges(); const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); const selectHour = hourColumn.children[3]; + expect(selectHour.nativeElement.innerText).toBe('04'); const minuteColumn = dom.query(By.css('.igx-time-picker__minuteList')); const selectMinute = minuteColumn.children[3]; + expect(selectMinute.nativeElement.innerText).toBe('00'); const AMPMColumn: any = dom.query(By.css('.igx-time-picker__ampmList')); const selectAMPM = AMPMColumn.children[3]; + expect(selectAMPM.nativeElement.innerText).toBe('AM'); })); + it('should be able to apply different formats (dropdown mode)', (() => { + const fixture = TestBed.createComponent(IgxTimePickerDropDownSingleHourComponent); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.directive(IgxInputDirective)); + + input.nativeElement.dispatchEvent(new Event('focus')); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('04:05'); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('4:5'); + + fixture.componentInstance.timePicker.format = 'h:m tt'; + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('4:5 AM'); + + input.nativeElement.dispatchEvent(new Event('focus')); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('04:05 AM'); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('4:5 AM'); + })); + + it('should correct spin (arrow buttons) on empty value (dropdown mode)', (() => { + const fixture = TestBed.createComponent(IgxTimePickerDropDownNoValueComponent); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.directive(IgxInputDirective)); + + expect(input.nativeElement.value).toBe('', 'Initial focus AM failed'); + + // press arrow down + input.nativeElement.dispatchEvent(new Event('focus')); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('--:-- AM', 'Initial focus AM failed'); + + // test hours + input.nativeElement.setSelectionRange(1, 1); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + input.nativeElement.setSelectionRange(1, 1); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('01:00 AM', 'Hours spin failed'); + + // test minutes + input.nativeElement.setSelectionRange(3, 3); + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('01:59 AM', 'MouseWheel Down on minutes failed'); + })); + describe('EditorProvider', () => { it('Should return correct edit element', () => { const fixture = TestBed.createComponent(IgxTimePickerTestComponent); @@ -953,6 +970,448 @@ describe('IgxTimePicker', () => { expect(instance.getEditElement()).toBe(editElement); }); }); + + describe('DropDown Mode', () => { + configureTestSuite(); + let fixture; + let timePicker; + let dom; + let input; + beforeEach( + async(() => { + fixture = TestBed.createComponent(IgxTimePickerDropDownComponent); + fixture.detectChanges(); + + timePicker = fixture.componentInstance.timePicker; + dom = fixture.debugElement; + input = dom.query(By.directive(IgxInputDirective)); + }) + ); + + afterEach(async(() => { + UIInteractions.clearOverlay(); + })); + + it('should initialize a timePicker with dropdown', () => { + expect(timePicker).toBeDefined(); + }); + + it('should accept specific time in the input', (() => { + fixture.detectChanges(); + const customValue = '12:01 AM'; + + spyOn(timePicker.onValueChanged, 'emit'); + + UIInteractions.sendInput(input, customValue); + fixture.detectChanges(); + + expect(timePicker.onValueChanged.emit).toHaveBeenCalled(); + expect(input.nativeElement.value).toEqual(customValue); + })); + + it('should increase and decrease hours/minutes/AMPM, where the caret is, using arrows and mousewheel', (() => { + fixture.detectChanges(); + + // initial input value is 05:45 PM + input.nativeElement.value = '05:45 PM'; + timePicker.itemsDelta = { hours: 1, minutes: 1 }; + + // focus the input, position the caret at the hours + input.nativeElement.focus(); + input.nativeElement.setSelectionRange(1, 1); + + // press arrow down + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('04:45 PM', 'ArrowDown on hours failed'); + + // position caret at the hours + input.nativeElement.setSelectionRange(1, 1); + fixture.detectChanges(); + + // mousewheel up + UIInteractions.simulateWheelEvent(input.nativeElement, 0, -10); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('05:45 PM', 'MouseWheel Up on hours dailed'); + + // test minutes + // position caret at the minutes and mousewheel down + input.nativeElement.setSelectionRange(3, 3); + UIInteractions.simulateWheelEvent(input.nativeElement, 0, 10); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('05:44 PM', 'MouseWheel Down on minutes failed'); + + input.nativeElement.setSelectionRange(3, 3); + fixture.detectChanges(); + // press arrow up + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('05:45 PM', 'ArrowUp on minutes failed'); + + // test AMPM + // position caret at AMPM and arrow down + input.nativeElement.setSelectionRange(7, 7); + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('05:45 AM', 'ArrowDown on AMPM failed'); + + input.nativeElement.setSelectionRange(7, 7); + fixture.detectChanges(); + // mousewheel up + UIInteractions.simulateWheelEvent(input.nativeElement, 0, -10); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('05:45 PM', 'MouseWheel Up on AMPM failed'); + + // test full hours + input.nativeElement.setSelectionRange(0, 0); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('06:45 PM', 'MouseWheel Up on AMPM failed'); + })); + + it('should open the dropdown when click on the clock icon', fakeAsync(() => { + fixture.detectChanges(); + + const iconTime = dom.queryAll(By.css('.igx-icon'))[0]; + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + const dropDown = dom.query(By.css('.igx-time-picker--dropdown')); + expect(dropDown.properties.hidden).toBeFalsy(); + })); + + it('should reset value on clear button click', (() => { + fixture.detectChanges(); + + const clearTime = dom.queryAll(By.css('.igx-icon'))[1]; + + UIInteractions.clickElement(clearTime); + fixture.detectChanges(); + + expect(input.nativeElement.innerText).toEqual(''); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(input.nativeElement.placeholder).toBe('hh:mm tt'); + })); + + it('should break on spinloop with min and max value on arrow down', (() => { + fixture.detectChanges(); + + const customValue = '07:07 AM'; + + UIInteractions.sendInput(input, customValue); + fixture.detectChanges(); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + timePicker.isSpinLoop = false; + timePicker.minValue = customValue; + timePicker.maxValue = '08:07 AM'; + timePicker.itemsDelta = { hours: 1, minutes: 1 }; + + input.nativeElement.focus(); + input.nativeElement.setSelectionRange(0, 0); + fixture.detectChanges(); + + // spin hours + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe(customValue, 'SpinLoop did not stop on hours'); + + input.nativeElement.setSelectionRange(5, 5); + fixture.detectChanges(); + + // spin minutes + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + expect(input.nativeElement.value).toBe(customValue, 'SpinLoop did not stop on minutes'); + })); + + it('should break on spinloop with min and max value on arrow up', (() => { + fixture.detectChanges(); + + const customValue = '08:07 AM'; + + UIInteractions.sendInput(input, customValue); + fixture.detectChanges(); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + timePicker.isSpinLoop = false; + timePicker.minValue = '07:07 AM'; + timePicker.maxValue = customValue; + timePicker.itemsDelta = { hours: 1, minutes: 1 }; + fixture.detectChanges(); + + input.nativeElement.focus(); + input.nativeElement.setSelectionRange(2, 2); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe(customValue, 'SpinLoop did not stop on hours'); + + input.nativeElement.setSelectionRange(5, 5); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe(customValue, 'SpinLoop did not stop on minutes'); + + input.nativeElement.setSelectionRange(7, 7); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe(customValue, 'SpinLoop did not stop on AMPM'); + })); + + it('should spinloop on correct time after max or min values', (() => { + fixture.detectChanges(); + + const customValue = '08:05 AM'; + + UIInteractions.sendInput(input, customValue); + fixture.detectChanges(); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + timePicker.isSpinLoop = true; + timePicker.minValue = '08:05 AM'; + timePicker.maxValue = '11:07 AM'; + timePicker.itemsDelta = { hours: 1, minutes: 1 }; + fixture.detectChanges(); + + input.nativeElement.focus(); + input.nativeElement.setSelectionRange(1, 1); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('11:05 AM', 'SpinLoop Down wrong time'); + + // set a new value which is the max value + UIInteractions.sendInput(input, '11:03 AM'); + fixture.detectChanges(); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + // should skip one hour because of the minutes + input.nativeElement.focus(); + input.nativeElement.setSelectionRange(2, 2); + fixture.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', input.nativeElement, true); + fixture.detectChanges(); + + expect(input.nativeElement.value).toBe('09:03 AM', 'SpinLoop Up wrong time'); + })); + + it('should open the dropdown with Alt + arrow down', fakeAsync(() => { + fixture.detectChanges(); + const dropDown = dom.query(By.css('.igx-time-picker--dropdown')); + + // should open dropdown on alt + arrow down + input.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', altKey: true })); + tick(); + fixture.detectChanges(); + + expect(dropDown.properties.hidden).toBeFalsy(); + })); + + it('should open the dropdown with SpaceBar', fakeAsync(() => { + fixture.detectChanges(); + const dropDown = dom.query(By.css('.igx-time-picker--dropdown')); + + // should open dropdown on alt + arrow down + UIInteractions.triggerKeyDownEvtUponElem('SpaceBar', input.nativeElement, true); + tick(); + fixture.detectChanges(); + + expect(dropDown.properties.hidden).toBeFalsy(); + })); + + it('should prevent interaction when disabled', (() => { + fixture.detectChanges(); + + let styles = window.getComputedStyle(input.nativeElement); + // normal text color + expect(styles.color).toBe('rgba(0, 0, 0, 0.87)'); + + timePicker.disabled = true; + fixture.detectChanges(); + + styles = window.getComputedStyle(input.nativeElement); + + // disabled text color + expect(styles.color).toBe('rgba(0, 0, 0, 0.38)'); + expect(dom.query(By.css('.igx-input-group--disabled'))).toBeDefined(); + })); + + it('should trigger onValidationFailed event when setting invalid time.', (() => { + fixture.detectChanges(); + + UIInteractions.sendInput(input, '77:77 TT'); + + spyOn(timePicker.onValidationFailed, 'emit'); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(timePicker.onValidationFailed.emit).toHaveBeenCalled(); + })); + + it('should scroll on dropdown opened and accept value when focust lost', fakeAsync(() => { + fixture.detectChanges(); + + timePicker.itemsDelta = { hours: 1, minutes: 5 }; + + const initVal = fixture.componentInstance.date; + const iconTime = dom.queryAll(By.css('.igx-icon'))[0]; + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); + const minuteColumn = dom.query(By.css('.igx-time-picker__minuteList')); + const AMPMColumn = dom.query(By.css('.igx-time-picker__ampmList')); + + UIInteractions.simulateWheelEvent(hourColumn.nativeElement, 0, -10); + fixture.detectChanges(); + + let childCount = hourColumn.children.length; + expect(hourColumn.children[0].nativeElement.innerText).toBe('01'); + expect(hourColumn.children[childCount - 1].nativeElement.innerText).toBe('07'); + + UIInteractions.simulateWheelEvent(minuteColumn.nativeElement, 0, 10); + fixture.detectChanges(); + + childCount = minuteColumn.children.length; + expect(minuteColumn.children[0].nativeElement.innerText).toBe('35'); + expect(minuteColumn.children[childCount - 1].nativeElement.innerText).toBe('05'); + + UIInteractions.simulateWheelEvent(AMPMColumn.nativeElement, 0, -10); + fixture.detectChanges(); + + expect(AMPMColumn.children[0].nativeElement.innerText).toBe('AM'); + expect(AMPMColumn.children[1].nativeElement.innerText).toBe('PM'); + + // expect input value to be changed + expect(input.nativeElement.value).toBe('04:50 AM'); + // expect the timePicker date not to be changed + expect(fixture.componentInstance.date).toBe(initVal); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(fixture.componentInstance.date).toEqual(new Date(2018, 10, 27, 4, 50, 0, 0)); + })); + + it('should not accept invalid value from dropdown when min is set', fakeAsync(() => { + fixture.detectChanges(); + + timePicker.isSpinLoop = true; + timePicker.minValue = '05:45 PM'; + timePicker.maxValue = '06:45 PM'; + timePicker.itemsDelta = { hours: 1, minutes: 1 }; + + const initVal = fixture.componentInstance.date; + const iconTime = dom.queryAll(By.css('.igx-icon'))[0]; + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + const hourColumn = dom.query(By.css('.igx-time-picker__hourList')); + const minuteColumn = dom.query(By.css('.igx-time-picker__minuteList')); + const AMPMColumn = dom.query(By.css('.igx-time-picker__ampmList')); + + UIInteractions.simulateWheelEvent(hourColumn.nativeElement, 0, -10); + fixture.detectChanges(); + + UIInteractions.simulateWheelEvent(minuteColumn.nativeElement, 0, -10); + fixture.detectChanges(); + + UIInteractions.simulateWheelEvent(AMPMColumn.nativeElement, 0, -10); + fixture.detectChanges(); + + spyOn(timePicker.onValidationFailed, 'emit'); + + input.nativeElement.dispatchEvent(new Event('blur')); + fixture.detectChanges(); + + expect(timePicker.onValidationFailed.emit).toHaveBeenCalled(); + + // initial value should not be changed + expect(fixture.componentInstance.date).toEqual(initVal); + })); + + it('should be able to change the mode at runtime', fakeAsync(() => { + fixture.detectChanges(); + + const iconTime = dom.queryAll(By.css('.igx-icon'))[0]; + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + expect(dom.query(By.css('.igx-time-picker--dropdown'))).toBeDefined(); + + fixture.componentInstance.timePicker.mode = TimePickerInteractionMode.dialog; + fixture.detectChanges(); + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + expect(dom.query(By.css('.igx-time-picker--dropdown'))).toBeNull(); + })); + + it('should fire events onOpen and onClose for dropdown mode.', fakeAsync(() => { + fixture.detectChanges(); + + const iconTime = dom.queryAll(By.css('.igx-icon'))[0]; + + spyOn(timePicker.onOpen, 'emit'); + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + expect(timePicker.onOpen.emit).toHaveBeenCalled(); + + spyOn(timePicker.onClose, 'emit'); + + UIInteractions.clickElement(iconTime); + tick(); + fixture.detectChanges(); + + expect(timePicker.onOpen.emit).toHaveBeenCalled(); + })); + }); }); @Component({ @@ -1060,7 +1519,56 @@ export class IgxTimePickerWithItemsDeltaValueComponent { ` }) -export class IgxTimePickerRetemplatedComponent {} +export class IgxTimePickerRetemplatedComponent { } + +@Component({ + template: ` + + + ` +}) +export class IgxTimePickerDropDownComponent { + itemsDelta = { hours: 1, minutes: 5 }; + format = 'hh:mm tt'; + isSpinLoop = true; + isVertical = true; + mode = TimePickerInteractionMode; + date = new Date(2018, 10, 27, 17, 45, 0, 0); + + @ViewChild(IgxTimePickerComponent) public timePicker: IgxTimePickerComponent; +} +@Component({ + template: ` + + + ` +}) +export class IgxTimePickerDropDownSingleHourComponent { + customDate = new Date(2018, 10, 27, 4, 5); + mode = TimePickerInteractionMode.dropdown; + + @ViewChild(IgxTimePickerComponent) public timePicker: IgxTimePickerComponent; +} +@Component({ + template: ` + + + ` +}) +export class IgxTimePickerDropDownNoValueComponent { + mode = TimePickerInteractionMode.dropdown; + + @ViewChild(IgxTimePickerComponent) public timePicker: IgxTimePickerComponent; +} // helper functions function findByInnerText(collection, searchText) { 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 43933104046..e3af01ee839 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 @@ -14,16 +14,14 @@ import { Output, TemplateRef, ViewChild, - AfterViewInit, - DoCheck, + Inject, ContentChild, Injectable } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { HAMMER_GESTURE_CONFIG, HammerGestureConfig } from '@angular/platform-browser'; -import { IgxDialogComponent, IgxDialogModule } from '../dialog/dialog.component'; import { IgxIconModule } from '../icon/index'; -import { IgxInputGroupModule } from '../input-group/input-group.component'; +import { IgxInputGroupModule, IgxInputGroupComponent } from '../input-group/input-group.component'; import { IgxInputDirective } from '../directives/input/input.directive'; import { IgxAmPmItemDirective, @@ -32,13 +30,28 @@ import { IgxMinuteItemDirective, IgxTimePickerTemplateDirective } from './time-picker.directives'; -import { Subscription } from 'rxjs'; +import { Subject } from 'rxjs'; import { EditorProvider } from '../core/edit-provider'; -import { IgxTimePickerBase, IGX_TIME_PICKER_COMPONENT } from './time-picker.common'; +import { IgxTimePickerBase, IGX_TIME_PICKER_COMPONENT, TimePickerInteractionMode } from './time-picker.common'; +import { IgxOverlayService } from '../services/overlay/overlay'; +import { NoOpScrollStrategy } from '../services/overlay/scroll'; +import { ConnectedPositioningStrategy } from '../services/overlay/position'; +import { HorizontalAlignment, VerticalAlignment, PositionSettings, OverlaySettings } from '../services/overlay/utilities'; +import { takeUntil, filter } from 'rxjs/operators'; +import { IgxButtonModule } from '../directives/button/button.directive'; +import { IgxMaskModule } from '../directives/mask/mask.directive'; +import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; +import { TimeDisplayFormatPipe, TimeInputFormatPipe } from './time-picker.pipes'; import { ITimePickerResourceStrings, TimePickerResourceStringsEN } from '../core/i18n/time-picker-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; +import { KEYS } from '../core/utils'; let NEXT_ID = 0; + +const HOURS_POS = [0, 1, 2]; +const MINUTES_POS = [3, 4, 5]; +const AMPM_POS = [6, 7, 8]; + @Injectable() export class TimePickerHammerConfig extends HammerGestureConfig { public overrides = { @@ -74,7 +87,6 @@ export interface IgxTimePickerValidationFailedEventArgs { } ], selector: 'igx-time-picker', - styles: [':host {display: block;}'], templateUrl: 'time-picker.component.html' }) export class IgxTimePickerComponent implements @@ -82,14 +94,7 @@ export class IgxTimePickerComponent implements ControlValueAccessor, EditorProvider, OnInit, - OnDestroy, - DoCheck, - AfterViewInit { - - private _value: Date; - private _resourceStrings = CurrentResourceStrings.TimePickerResStrings; - private _okButtonLabel = null; - private _cancelButtonLabel = null; + OnDestroy { /** * An @Input property that sets the value of the `id` attribute. @@ -112,8 +117,21 @@ export class IgxTimePickerComponent implements @Input() set value(value: Date) { if (this._isValueValid(value)) { + const oldVal = this._value; + this._value = value; this._onChangeCallback(value); + + const dispVal = this._formatTime(this.value, this.format); + if (this.mode === TimePickerInteractionMode.dropdown && this._displayValue !== dispVal) { + this.displayValue = dispVal; + } + + const args: IgxTimePickerValueChangedEventArgs = { + oldValue: oldVal, + newValue: value + }; + this.onValueChanged.emit(args); } else { const args: IgxTimePickerValidationFailedEventArgs = { timePicker: this, @@ -268,7 +286,49 @@ export class IgxTimePickerComponent implements * ``` */ @Input() - public format = 'hh:mm tt'; + get format() { + return this._format || 'hh:mm tt'; + } + + set format(formatValue: string) { + this._format = formatValue; + this.mask = this._format.indexOf('tt') !== -1 ? '00:00 LL' : '00:00'; + + if (this.displayValue) { + this.displayValue = this._formatTime(this.value, this._format); + } + } + + /** + * Sets the character used to prompt the user for input. + * Default value is "'-'". + * ```html + * + * ``` + * @memberof IgxTimePickerComponent + */ + @Input() + public promptChar = '-'; + + /** + * An @Input property that allows you to switch the interaction mode between + * a dialog picker or dropdown with editable masked input. + * Deafult is dialog picker. + *```html + *public mode = TimePickerInteractionMode.dropdown; + * //.. + * + *``` + * @memberof IgxTimePickerComponent + */ + @Input() + public mode = TimePickerInteractionMode.dialog; + + /** + *@hidden + */ + @Input() + public outlet: IgxOverlayOutletDirective | ElementRef; /** * Emitted when selection is made. The event contains the selected value. Returns {`oldValue`: `Date`, `newValue`: `Date`}. @@ -371,8 +431,26 @@ export class IgxTimePickerComponent implements /** * @hidden */ - @ViewChild(IgxDialogComponent) - private _alert: IgxDialogComponent; + @ViewChild('container') + public container: ElementRef; + + /** + * @hidden + */ + @ViewChild('input', { read: ElementRef }) + private input: ElementRef; + + /** + * @hidden + */ + @ViewChild('group', { read: IgxInputGroupComponent }) + private group: IgxInputGroupComponent; + + /** + * @hidden + */ + @ViewChild('dropdownInputTemplate', { read: TemplateRef }) + private dropdownInputTemplate: TemplateRef; /** * @hidden @@ -387,12 +465,34 @@ export class IgxTimePickerComponent implements */ public _ampmItems = []; - private _isHourListLoop = this.isSpinLoop; - private _isMinuteListLoop = this.isSpinLoop; - - private _hourView = []; - private _minuteView = []; - private _ampmView = []; + /** + * @hidden + */ + public mask: string; + /** + * @hidden + */ + public cleared = false; + /** + * @hidden + */ + public isNotEmpty = false; + /** + * @hidden + */ + public collapsed = true; + /** + * @hidden + */ + public displayFormat = new TimeDisplayFormatPipe(this); + /** + * @hidden + */ + public inputFormat = new TimeInputFormatPipe(this); + /** + * @hidden + */ + public interactMode = TimePickerInteractionMode; /** * @hidden @@ -407,11 +507,47 @@ export class IgxTimePickerComponent implements */ public selectedAmPm: string; + private _value: Date; + private _resourceStrings = CurrentResourceStrings.TimePickerResStrings; + private _okButtonLabel = null; + private _cancelButtonLabel = null; + private _format: string; + private _displayValue: string; + + private _isHourListLoop = this.isSpinLoop; + private _isMinuteListLoop = this.isSpinLoop; + + private _hourView = []; + private _minuteView = []; + private _ampmView = []; + + private _overlayId: string; + private _dateFromModel: Date; + private _destroy$ = new Subject(); + private _positionSettings: PositionSettings; + private _dropDownOverlaySettings: OverlaySettings; + private _dialogOverlaySettings: OverlaySettings; + private _prevSelectedHour: string; private _prevSelectedMinute: string; private _prevSelectedAmPm: string; - protected dialogClosed = new Subscription(); + private _onTouchedCallback: () => void = () => { }; + private _onChangeCallback: (_: Date) => void = () => { }; + + /** + * @hidden + */ + get displayValue(): string { + if (this._displayValue === undefined) { + return this._formatTime(this.value, this.format); + } + return this._displayValue; + } + + set displayValue(value: string) { + this._displayValue = value; + } /** * Returns the current time formatted as string using the `format` option. @@ -454,64 +590,97 @@ export class IgxTimePickerComponent implements } /** - * opens the dialog. - * ```html - * - * ``` - * ```typescript - * @ViewChild('tp', { read: IgxTimePickerComponent }) tp: IgxTimePickerComponent; - * tp.openDialog(); - * ``` + * @hidden */ - public openDialog(timePicker: IgxTimePickerComponent = this): void { - if (this.value) { - const foramttedTime = this._formatTime(this.value, this.format); - const sections = foramttedTime.split(/[\s:]+/); - - this.selectedHour = sections[0]; - this.selectedMinute = sections[1]; + get showClearButton(): boolean { + return (this.displayValue && this.displayValue !== this.parseMask(false)) || this.isNotEmpty; + } - if (this._ampmItems !== null) { - this.selectedAmPm = sections[2]; - } + /** + * @hidden + */ + get dropDownWidth(): any { + if (this.group) { + return this.group.element.nativeElement.getBoundingClientRect().width; } + } - if (this.selectedHour === undefined) { - this.selectedHour = `${this._hourItems[3]}`; + /** + * @hidden + */ + get validMinuteEntries(): any[] { + const minuteEntries = []; + for (let i = 0; i < 60; i++) { + minuteEntries.push(i); } - if (this.selectedMinute === undefined) { - this.selectedMinute = '0'; + return minuteEntries; + } + + /** + * @hidden + */ + get validHourEntries(): any[] { + const hourEntries = []; + const index = this.format.indexOf('h') !== -1 ? 13 : 24; + for (let i = 0; i < index; i++) { + hourEntries.push(i); } - if (this.selectedAmPm === undefined && this._ampmItems !== null) { - this.selectedAmPm = this._ampmItems[3]; + return hourEntries; + } + + /** + * Gets the input group template. + * ```typescript + * let template = this.template(); + * ``` + * @memberof IgxTimePickerComponent + */ + get template(): TemplateRef { + if (this.timePickerTemplateDirective) { + return this.timePickerTemplateDirective.template; } + return this.mode === TimePickerInteractionMode.dialog ? this.defaultTimePickerTemplate : this.dropdownInputTemplate; + } - this._prevSelectedHour = this.selectedHour; - this._prevSelectedMinute = this.selectedMinute; - this._prevSelectedAmPm = this.selectedAmPm; + /** + * Gets the context passed to the input group template. + * @memberof IgxTimePickerComponent + */ + get context() { + return { + value: this.value, + displayTime: this.displayTime, + displayValue: this.displayValue, + openDialog: () => { this.openDialog(); } + }; + } - this._alert.open(); - this._onTouchedCallback(); + constructor(@Inject(IgxOverlayService) private overlayService: IgxOverlayService) { - this._updateHourView(0, 7); - this._updateMinuteView(0, 7); - this._updateAmPmView(0, 7); + this.overlayService.onClosed.pipe( + filter(event => event.id === this._overlayId), + takeUntil(this._destroy$)).subscribe(() => { - if (this.selectedHour) { - this.scrollHourIntoView(this.selectedHour); - } - if (this.selectedMinute) { - this.scrollMinuteIntoView(this.selectedMinute); - } - if (this.selectedAmPm) { - this.scrollAmPmIntoView(this.selectedAmPm); - } + this.collapsed = true; - setTimeout(() => { - this.hourList.nativeElement.focus(); - }); + if (this._input) { + this._input.nativeElement.focus(); + } + + if (this.mode === TimePickerInteractionMode.dropdown) { + this._onDropDownClosed(); + } - this.onOpen.emit(this); + this.onClose.emit(this); + }); + + this.overlayService.onOpened.pipe( + filter(event => event.id === this._overlayId), + takeUntil(this._destroy$)).subscribe(() => { + + this.collapsed = false; + this.onOpen.emit(this); + }); } /** @@ -523,69 +692,44 @@ export class IgxTimePickerComponent implements if (this.format.indexOf('tt') !== -1) { this._generateAmPm(); } - } - /** - * @hidden - */ - public ngAfterViewInit(): void { - this.dialogClosed = this._alert.toggleRef.onClosed.pipe().subscribe((ev) => this.handleDialogCloseAction()); - } + this._positionSettings = { + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Bottom + }; - /** - * @hidden - */ - public ngOnDestroy(): void { - this.dialogClosed.unsubscribe(); - } + this._dropDownOverlaySettings = { + modal: false, + closeOnOutsideClick: true, + scrollStrategy: new NoOpScrollStrategy(), + positionStrategy: new ConnectedPositioningStrategy(this._positionSettings) + }; - // XXX - temporary fix related with issue #1660 - public ngDoCheck(): void { - if (this.vertical && this._alert) { - this._alert.toggleRef.element.classList.remove('igx-time-picker'); - this._alert.toggleRef.element.classList.add('igx-time-picker--vertical'); - } else if (!this.vertical && this._alert) { - this._alert.toggleRef.element.classList.add('igx-time-picker'); - this._alert.toggleRef.element.classList.remove('igx-time-picker--vertical'); - } + this._dialogOverlaySettings = {}; } /** * @hidden */ - public handleDialogCloseAction() { - if (this._input) { - this._input.nativeElement.focus(); + public ngOnDestroy(): void { + if (this._overlayId) { + this.hideOverlay(); } - this.onClose.emit(this); - } - /** - * @hidden - */ - public writeValue(value: Date) { - this.value = value; + this._destroy$.next(true); + this._destroy$.complete(); } /** * @hidden */ - public registerOnChange(fn: (_: Date) => void) { this._onChangeCallback = fn; } - - /** - * @hidden - */ - public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } - - /** @hidden */ - getEditElement() { - return this._input.nativeElement; + @HostListener('keydown.spacebar', ['$event']) + @HostListener('keydown.space', ['$event']) + public onKeydownSpace(event) { + this.openDialog(); + event.preventDefault(); } - private _onTouchedCallback: () => void = () => { }; - - private _onChangeCallback: (_: Date) => void = () => { }; - private _scrollItemIntoView(item: string, items: any[], selectedItem: string, isListLoop: boolean, viewType: string): any { let itemIntoView; if (items) { @@ -830,7 +974,7 @@ export class IgxTimePickerComponent implements } private _convertMinMaxValue(value: string): Date { - const date = this.value ? new Date(this.value) : new Date(); + const date = this.value ? new Date(this.value) : this._dateFromModel ? new Date(this._dateFromModel) : new Date(); const sections = value.split(/[\s:]+/); date.setHours(parseInt(sections[0], 10)); @@ -839,7 +983,7 @@ export class IgxTimePickerComponent implements if (sections[2] && sections[2] === 'PM' && sections[0] !== '12') { date.setHours(date.getHours() + 12); } - if (sections[2] && sections[2] && sections[0] === '12') { + if (sections[0] === '12' && sections[2] && sections[2] === 'AM') { date.setHours(0); } @@ -856,6 +1000,202 @@ export class IgxTimePickerComponent implements } } + private _isEntryValid(val: string): boolean { + const sections = val.split(/[\s:]+/); + const re = new RegExp(this.promptChar, 'g'); + + const hour = parseInt(sections[0].replace(re, ''), 10); + const minutes = parseInt(sections[1].replace(re, ''), 10); + + return this.validHourEntries.indexOf(hour) !== -1 && this.validMinuteEntries.indexOf(minutes) !== -1; + } + + private _getCursorPosition(): number { + return this.input.nativeElement.selectionStart; + } + + private _setCursorPosition(start: number, end: number = start): void { + this.input.nativeElement.setSelectionRange(start, end); + } + + private _updateEditableInput(): void { + if (this.mode === TimePickerInteractionMode.dropdown) { + this.displayValue = this._formatTime(this._getSelectedTime(), this.format); + } + } + + private _spinHours(currentVal: Date, minVal: Date, maxVal: Date, hDelta: number, sign: number): Date { + const oldVal = new Date(currentVal); + + currentVal.setMinutes(sign * hDelta); + if (currentVal.getDate() !== oldVal.getDate() && this.isSpinLoop) { + currentVal.setDate(oldVal.getDate()); + } + + let minutes = currentVal.getMinutes(); + if (currentVal.getTime() > maxVal.getTime()) { + if (this.isSpinLoop) { + minutes = minutes < minVal.getMinutes() ? 60 + minutes : minutes; + minVal.setMinutes(sign * minutes); + return minVal; + } else { + return oldVal; + } + } else if (currentVal.getTime() < minVal.getTime()) { + if (this.isSpinLoop) { + minutes = minutes <= maxVal.getMinutes() ? minutes : minutes - 60; + maxVal.setMinutes(minutes); + return maxVal; + } else { + return oldVal; + } + } else { + return currentVal; + } + } + + private _spinMinutes(currentVal: Date, mDelta: number, sign: number) { + let minutes = currentVal.getMinutes() + (sign * mDelta); + + if (minutes < 0 || minutes >= 60) { + minutes = this.isSpinLoop ? minutes - (sign * 60) : currentVal.getMinutes(); + } + + currentVal.setMinutes(minutes); + return currentVal; + } + + private _onDropDownClosed(): void { + const oldValue = this.value; + const newVal = this._convertMinMaxValue(this.displayValue); + + if (this._isValueValid(newVal)) { + if (!this.value || oldValue.getTime() !== newVal.getTime()) { + this.value = newVal; + } + } else { + this.displayValue = this.inputFormat.transform(this._formatTime(oldValue, this.format)); + + const args: IgxTimePickerValidationFailedEventArgs = { + timePicker: this, + currentValue: newVal, + setThroughUI: true + }; + this.onValidationFailed.emit(args); + } + } + + /** + * @hidden + */ + getEditElement() { + return this._input.nativeElement; + } + + /** + * @hidden + */ + public writeValue(value: Date) { + // use this flag to make sure that min/maxValue are checked (in _convertMinMaxValue() method) + // against the real value when initializing the component and value is bound via ngModel + this._dateFromModel = value; + + this.value = value; + + if (this.mode === TimePickerInteractionMode.dropdown) { + this.displayValue = this._formatTime(this.value, this.format); + } + } + + /** + * @hidden + */ + public registerOnChange(fn: (_: Date) => void) { this._onChangeCallback = fn; } + + /** + * @hidden + */ + public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } + + /** + * opens the dialog. + * ```html + * + * ``` + * ```typescript + * @ViewChild('tp', { read: IgxTimePickerComponent }) tp: IgxTimePickerComponent; + * tp.openDialog(); + * ``` + */ + public openDialog(timePicker: IgxTimePickerComponent = this): void { + if (this.mode === TimePickerInteractionMode.dialog) { + this.collapsed = false; + if (this.outlet) { + this._dialogOverlaySettings.outlet = this.outlet; + } + this._overlayId = this.overlayService.show(this.container, this._dialogOverlaySettings); + } + + if (this.mode === TimePickerInteractionMode.dropdown) { + if (this.collapsed) { + this.collapsed = false; + if (this.outlet) { + this._dropDownOverlaySettings.outlet = this.outlet; + } + this._dropDownOverlaySettings.positionStrategy.settings.target = this.group.element.nativeElement; + this._overlayId = this.overlayService.show(this.container, this._dropDownOverlaySettings); + } else { + this._onDropDownClosed(); + } + } + + if (this.value) { + const foramttedTime = this._formatTime(this.value, this.format); + const sections = foramttedTime.split(/[\s:]+/); + + this.selectedHour = sections[0]; + this.selectedMinute = sections[1]; + + if (this._ampmItems !== null) { + this.selectedAmPm = sections[2]; + } + } + + if (this.selectedHour === undefined) { + this.selectedHour = `${this._hourItems[3]}`; + } + if (this.selectedMinute === undefined) { + this.selectedMinute = '0'; + } + if (this.selectedAmPm === undefined && this._ampmItems !== null) { + this.selectedAmPm = this._ampmItems[3]; + } + + this._prevSelectedHour = this.selectedHour; + this._prevSelectedMinute = this.selectedMinute; + this._prevSelectedAmPm = this.selectedAmPm; + + this._onTouchedCallback(); + + this._updateHourView(0, 7); + this._updateMinuteView(0, 7); + this._updateAmPmView(0, 7); + + if (this.selectedHour) { + this.scrollHourIntoView(this.selectedHour); + } + if (this.selectedMinute) { + this.scrollMinuteIntoView(this.selectedMinute); + } + if (this.selectedAmPm) { + this.scrollAmPmIntoView(this.selectedAmPm); + } + + requestAnimationFrame(() => { + this.hourList.nativeElement.focus(); + }); + } + /** * Scrolls a hour item into view. * ```typescript @@ -873,6 +1213,7 @@ export class IgxTimePickerComponent implements if (hourIntoView) { this._hourView = hourIntoView.view; this.selectedHour = hourIntoView.selectedItem; + this._updateEditableInput(); } } @@ -893,6 +1234,7 @@ export class IgxTimePickerComponent implements if (minuteIntoView) { this._minuteView = minuteIntoView.view; this.selectedMinute = minuteIntoView.selectedItem; + this._updateEditableInput(); } } @@ -913,6 +1255,7 @@ export class IgxTimePickerComponent implements if (ampmIntoView) { this._ampmView = ampmIntoView.view; this.selectedAmPm = ampmIntoView.selectedItem; + this._updateEditableInput(); } } @@ -923,6 +1266,8 @@ export class IgxTimePickerComponent implements const nextHour = this._nextItem(this._hourItems, this.selectedHour, this._isHourListLoop, 'hour'); this._hourView = nextHour.view; this.selectedHour = nextHour.selectedItem; + + this._updateEditableInput(); } /** @@ -932,6 +1277,8 @@ export class IgxTimePickerComponent implements const prevHour = this._prevItem(this._hourItems, this.selectedHour, this._isHourListLoop, 'hour'); this._hourView = prevHour.view; this.selectedHour = prevHour.selectedItem; + + this._updateEditableInput(); } /** @@ -941,6 +1288,8 @@ export class IgxTimePickerComponent implements const nextMinute = this._nextItem(this._minuteItems, this.selectedMinute, this._isMinuteListLoop, 'minute'); this._minuteView = nextMinute.view; this.selectedMinute = nextMinute.selectedItem; + + this._updateEditableInput(); } /** @@ -950,6 +1299,8 @@ export class IgxTimePickerComponent implements const prevMinute = this._prevItem(this._minuteItems, this.selectedMinute, this._isMinuteListLoop, 'minute'); this._minuteView = prevMinute.view; this.selectedMinute = prevMinute.selectedItem; + + this._updateEditableInput(); } /** @@ -961,6 +1312,8 @@ export class IgxTimePickerComponent implements if (selectedIndex + 1 < this._ampmItems.length - 3) { this._updateAmPmView(selectedIndex - 2, selectedIndex + 5); this.selectedAmPm = this._ampmItems[selectedIndex + 1]; + + this._updateEditableInput(); } } @@ -973,6 +1326,8 @@ export class IgxTimePickerComponent implements if (selectedIndex > 3) { this._updateAmPmView(selectedIndex - 4, selectedIndex + 3); this.selectedAmPm = this._ampmItems[selectedIndex - 1]; + + this._updateEditableInput(); } } @@ -985,20 +1340,15 @@ export class IgxTimePickerComponent implements * ``` */ public okButtonClick(): boolean { - if (this._isValueValid(this._getSelectedTime())) { - this._alert.close(); - const oldValue = this.value; - this.value = this._getSelectedTime(); - const args: IgxTimePickerValueChangedEventArgs = { - oldValue, - newValue: this.value - }; - this.onValueChanged.emit(args); + const time = this._getSelectedTime(); + if (this._isValueValid(time)) { + this.hideOverlay(); + this.value = time; return true; } else { const args: IgxTimePickerValidationFailedEventArgs = { timePicker: this, - currentValue: this._getSelectedTime(), + currentValue: time, setThroughUI: true }; this.onValidationFailed.emit(args); @@ -1015,22 +1365,13 @@ export class IgxTimePickerComponent implements * ``` */ public cancelButtonClick(): void { - this._alert.close(); + this.hideOverlay(); + this.selectedHour = this._prevSelectedHour; this.selectedMinute = this._prevSelectedMinute; this.selectedAmPm = this._prevSelectedAmPm; } - /** - * @hidden - */ - @HostListener('keydown.spacebar', ['$event']) - @HostListener('keydown.space', ['$event']) - public onKeydownSpace(event) { - this.openDialog(); - event.preventDefault(); - } - /** * Returns an array of the hours currently in view. *```html @@ -1074,29 +1415,196 @@ export class IgxTimePickerComponent implements } /** - * Gets the input group template. - * ```typescript - * let template = this.template(); - * ``` - * @memberof IgxTimePickerComponent + * @hidden */ - get template(): TemplateRef { - if (this.timePickerTemplateDirective) { - return this.timePickerTemplateDirective.template; + public hideOverlay(): void { + this.overlayService.hide(this._overlayId); + } + + /** + * @hidden + */ + public parseMask(preserveAmPm = true): string { + const prompts = this.promptChar + this.promptChar; + const amPm = preserveAmPm ? 'AM' : prompts; + + return this.format.indexOf('tt') !== -1 ? `${prompts}:${prompts} ${amPm}` : `${prompts}:${prompts}`; + } + + /** + * @hidden + */ + public clear(): void { + if (this.collapsed) { + this.cleared = true; + this.isNotEmpty = false; + + const oldVal = new Date(this.value); + + this.displayValue = ''; + this.value.setHours(0, 0); + + if (oldVal.getTime() !== this.value.getTime()) { + const args: IgxTimePickerValueChangedEventArgs = { + oldValue: oldVal, + newValue: this.value + }; + this.onValueChanged.emit(args); + } } - return this.defaultTimePickerTemplate; } /** - * Gets the context passed to the input group template. - * @memberof IgxTimePickerComponent + * @hidden */ - get context() { - return { - value: this.value, - displayTime: this.displayTime, - openDialog: () => { this.openDialog(); } - }; + public onKeydown(event): void { + switch (event.key) { + case KEYS.UP_ARROW: + case KEYS.UP_ARROW_IE: + this.spinOnEdit(event); + break; + case KEYS.DOWN_ARROW: + case KEYS.DOWN_ARROW_IE: + if (event.altKey) { + this.openDialog(); + } else { + this.spinOnEdit(event); + } + break; + default: + return; + } + } + + /** + * @hidden + */ + public onInput(event): void { + const val = event.target.value; + const oldVal = new Date(this.value); + + this.isNotEmpty = val !== this.parseMask(false); + + // handle cases where all empty positions (promts) are filled and we want to update + // timepicker own value property if it is a valid Date + if (val.indexOf(this.promptChar) === -1) { + if (this._isEntryValid(val)) { + const newVal = this._convertMinMaxValue(val); + if (oldVal.getTime() !== newVal.getTime()) { + this.value = newVal; + } + } else { + const args: IgxTimePickerValidationFailedEventArgs = { + timePicker: this, + currentValue: val, + setThroughUI: false + }; + this.onValidationFailed.emit(args); + } + // handle cases where the user deletes the display value (when pressing backspace or delete) + } else if (!this.value || !val || val === this.parseMask(false)) { + this.isNotEmpty = false; + + this.value.setHours(0, 0); + this.displayValue = val; + + if (oldVal.getTime() !== this.value.getTime()) { + const args: IgxTimePickerValueChangedEventArgs = { + oldValue: oldVal, + newValue: this.value + }; + this.onValueChanged.emit(args); + } + } + } + + /** + * @hidden + */ + public onFocus(event): void { + this.isNotEmpty = event.target.value !== this.parseMask(false); + } + + /** + * @hidden + */ + public onBlur(event): void { + const value = event.target.value; + + this.isNotEmpty = value !== ''; + this.displayValue = value; + + if (value && value !== this.parseMask()) { + if (this._isEntryValid(value)) { + const newVal = this._convertMinMaxValue(value); + if (!this.value || this.value.getTime() !== newVal.getTime()) { + this.value = newVal; + } + } else { + const args: IgxTimePickerValidationFailedEventArgs = { + timePicker: this, + currentValue: value, + setThroughUI: false + }; + this.onValidationFailed.emit(args); + } + } + } + + /** + * @hidden + */ + public spinOnEdit(event): void { + event.preventDefault(); + + let sign: number; + let displayVal: string; + const currentVal = new Date(this.value); + const min = this.minValue ? this._convertMinMaxValue(this.minValue) : this._convertMinMaxValue('00:00'); + const max = this.maxValue ? this._convertMinMaxValue(this.maxValue) : this._convertMinMaxValue('24:00'); + + const cursor = this._getCursorPosition(); + + if (event.key) { + const key = event.key; + sign = key === KEYS.DOWN_ARROW || key === KEYS.DOWN_ARROW_IE ? -1 : 1; + } + + if (event.deltaY) { + sign = event.deltaY < 0 ? 1 : -1; + } + + if (!this.displayValue) { + this.value = min; + displayVal = this._formatTime(this.value, this.format); + } else { + const hDelta = this.itemsDelta.hours * 60 + (sign * this.value.getMinutes()); + const mDelta = this.itemsDelta.minutes; + const sections = this.displayValue.split(/[\s:]+/); + + if (HOURS_POS.indexOf(cursor) !== -1) { + this.value = this._spinHours(currentVal, min, max, hDelta, sign); + } + + if (MINUTES_POS.indexOf(cursor) !== -1) { + this.value = this._spinMinutes(currentVal, mDelta, sign); + } + + if (AMPM_POS.indexOf(cursor) !== -1 && this.format.indexOf('tt') !== -1) { + sign = sections[2] && sections[2] === 'AM' ? 1 : -1; + currentVal.setHours(currentVal.getHours() + (sign * 12)); + + this.value = currentVal; + } + + displayVal = this._formatTime(this.value, this.format); + } + + this.displayValue = this.inputFormat.transform(displayVal); + + requestAnimationFrame(() => { + this._setCursorPosition(cursor); + }); } } @@ -1110,17 +1618,22 @@ export class IgxTimePickerComponent implements IgxItemListDirective, IgxMinuteItemDirective, IgxAmPmItemDirective, - IgxTimePickerTemplateDirective + IgxTimePickerTemplateDirective, + TimeDisplayFormatPipe, + TimeInputFormatPipe ], exports: [ IgxTimePickerComponent, - IgxTimePickerTemplateDirective + IgxTimePickerTemplateDirective, + TimeDisplayFormatPipe, + TimeInputFormatPipe ], imports: [ CommonModule, IgxInputGroupModule, - IgxDialogModule, - IgxIconModule + IgxIconModule, + IgxButtonModule, + IgxMaskModule ], providers: [] }) 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 a8fd2530652..d3eb385534c 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 @@ -6,17 +6,13 @@ import { Directive, ElementRef, - EventEmitter, - forwardRef, - Host, HostBinding, HostListener, Inject, Input, - Output, TemplateRef } from '@angular/core'; -import { IGX_TIME_PICKER_COMPONENT, IgxTimePickerBase } from './time-picker.common'; +import { IGX_TIME_PICKER_COMPONENT, IgxTimePickerBase, TimePickerInteractionMode } from './time-picker.common'; /** @hidden */ @Directive({ @@ -159,6 +155,10 @@ export class IgxItemListDirective { public onKeydownEnter(event: KeyboardEvent) { event.preventDefault(); + if (this.timePicker.mode === TimePickerInteractionMode.dropdown) { + this.timePicker.hideOverlay(); + return; + } this.timePicker.okButtonClick(); } @@ -343,3 +343,4 @@ export class IgxAmPmItemDirective { export class IgxTimePickerTemplateDirective { constructor(public template: TemplateRef) {} } + 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 new file mode 100644 index 00000000000..bd19cda0bba --- /dev/null +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts @@ -0,0 +1,99 @@ +import { Pipe, PipeTransform, Inject} from '@angular/core'; +import { IGX_TIME_PICKER_COMPONENT, IgxTimePickerBase } from './time-picker.common'; + + +/** + * Formats `IgxTimePickerComponent` display value according to the `format` property, + * when the input element loses focus. + **/ +@Pipe({ name: 'displayFormat'}) +export class TimeDisplayFormatPipe implements PipeTransform { + + constructor(@Inject(IGX_TIME_PICKER_COMPONENT) private timePicker: IgxTimePickerBase) { } + + transform(value: any): string { + + const maskAmPM = this.timePicker.parseMask(); + const mask = this.timePicker.parseMask(false); + if (!value || value === mask || value === maskAmPM) { + return ''; + } + + const sections = value.split(/[\s:]+/); + + let hour = sections[0]; + let minutes = sections[1]; + let amPM = sections[2]; + + const format = this.timePicker.format; + const prompt = this.timePicker.promptChar; + const regExp = new RegExp(this.timePicker.promptChar, 'g'); + + if (format.indexOf('hh') !== -1 || format.indexOf('HH') !== -1 && hour.indexOf(prompt) !== -1) { + hour = hour === prompt + prompt ? '00' : hour.replace(regExp, '0'); + } + + if (format.indexOf('mm') !== -1 && minutes.indexOf(prompt) !== -1) { + minutes = minutes === prompt + prompt ? '00' : minutes.replace(regExp, '0'); + } + + if (format.indexOf('hh') === -1 && format.indexOf('HH') === -1) { + hour = hour.indexOf(prompt) !== -1 ? hour.replace(regExp, '') : hour; + const hourVal = parseInt(hour, 10); + hour = !hourVal ? '0' : hourVal < 10 && hourVal !== 0 ? hour.replace('0', '') : hour; + } + + if (format.indexOf('mm') === -1) { + minutes = minutes.indexOf(prompt) !== -1 ? minutes.replace(regExp, '') : minutes; + const minutesVal = parseInt(minutes, 10); + minutes = !minutesVal ? '0' : minutesVal < 10 && minutesVal !== 0 ? minutes.replace('0', '') : minutes; + } + + if (format.indexOf('tt') !== -1 && (amPM !== 'AM' || amPM !== 'PM')) { + amPM = amPM.indexOf('p') !== -1 || amPM.indexOf('P') !== -1 ? 'PM' : 'AM'; + } + + return amPM ? `${hour}:${minutes} ${amPM}` : `${hour}:${minutes}`; + } +} + +/** + * Formats `IgxTimePickerComponent` display value according to the `format` property, + * when the input element gets focus. + **/ +@Pipe({ name: 'inputFormat' }) +export class TimeInputFormatPipe implements PipeTransform { + + constructor(@Inject(IGX_TIME_PICKER_COMPONENT) private timePicker: IgxTimePickerBase) { } + + transform(value: any): string { + const prompt = this.timePicker.promptChar; + const regExp = new RegExp(prompt, 'g'); + + let mask: string; + if (this.timePicker.cleared) { + this.timePicker.cleared = false; + mask = this.timePicker.parseMask(false); + } else { + mask = this.timePicker.parseMask(); + } + + if (!value || value === mask) { + return mask; + } + + const sections = value.split(/[\s:]+/); + + let hour = sections[0].replace(regExp, ''); + let minutes = sections[1].replace(regExp, ''); + const amPM = sections[2]; + + const leadZeroHour = (parseInt(hour, 10) < 10 && !hour.startsWith('0')) || hour === '0'; + const leadZeroMinutes = (parseInt(minutes, 10) < 10 && !minutes.startsWith('0')) || minutes === '0'; + + hour = leadZeroHour ? '0' + hour : hour; + minutes = leadZeroMinutes ? '0' + minutes : minutes; + + return amPM ? `${hour}:${minutes} ${amPM}` : `${hour}:${minutes}`; + } +} diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 43eab793084..25e716ae5ec 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -1,3 +1,4 @@ + /* * Public API Surface of igniteui-angular */ @@ -88,4 +89,6 @@ export * from './lib/core/density'; export { CancelableEventArgs } from './lib/core/utils'; export { changei18n, getCurrentResourceStrings, IResourceStrings } from './lib/core/i18n/resources'; export { IGridResourceStrings } from './lib/core/i18n/grid-resources'; +export { TimePickerInteractionMode } from './lib/time-picker/time-picker.common'; export { ITimePickerResourceStrings } from './lib/core/i18n/time-picker-resources'; + diff --git a/src/app/mask/mask.sample.html b/src/app/mask/mask.sample.html index 7bd3f200402..4501f3628e7 100644 --- a/src/app/mask/mask.sample.html +++ b/src/app/mask/mask.sample.html @@ -26,5 +26,21 @@

Personal Data

+
+
+

Mask Using Pipes

+
+ + + + model value: {{value}} + +
+
+
diff --git a/src/app/mask/mask.sample.ts b/src/app/mask/mask.sample.ts index 2a818815e3a..26c2d467a0e 100644 --- a/src/app/mask/mask.sample.ts +++ b/src/app/mask/mask.sample.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Pipe, PipeTransform } from '@angular/core'; interface IPerson { name: string; @@ -15,6 +15,12 @@ interface IPerson { export class MaskSampleComponent { person: IPerson; + value = '1255'; + mask = '##.##'; + placeholder = '-##.## %'; + displayFormat = new DisplayFormatPipe(); + inputFormat = new InputFormatPipe(); + constructor() { this.person = { birthday: null, @@ -52,3 +58,48 @@ export class MaskSampleComponent { } } +@Pipe({ name: 'displayFormat' }) +export class DisplayFormatPipe implements PipeTransform { + transform(value: any): string { + let val = value; + + if (val === '__.__') { + val = ''; + } + + if (val && val.indexOf('_') !== -1) { + val = val.replace(new RegExp('_', 'g'), '0'); + } + + if (val && val.indexOf('%') === -1) { + val += ' %'; + } + + if (val && val.indexOf('-') === -1) { + val = val.substring(0, 0) + '-' + val.substring(0); + } + + return val; + } +} + +@Pipe({ name: 'inputFormat' }) +export class InputFormatPipe implements PipeTransform { + transform(value: any): string { + let val = value; + + if (!val) { + val = '__.__'; + } + + if (val.indexOf(' %') !== -1) { + val = val.replace(new RegExp(' %', 'g'), ''); + } + + if (val.indexOf('-') !== -1) { + val = val.replace(new RegExp('-', 'g'), ''); + } + + return val; + } +} diff --git a/src/app/time-picker/time-picker.sample.html b/src/app/time-picker/time-picker.sample.html index 9be8fb7f3a5..ad9473f92b5 100644 --- a/src/app/time-picker/time-picker.sample.html +++ b/src/app/time-picker/time-picker.sample.html @@ -4,29 +4,44 @@
-

Default Time Picker.

-

Detailed description to be added.

+

Time Picker with Dropdown

+
{{showDate()}}
+
+ + +
+
+
+

Horizontal Time Picker

+

AM/PM Time format

+

-

Default Time Picker.

-

Detailed description to be added.

+

Vertical Time Picker

+

Vertical dialog

-

24h Time format.

-

Detailed description to be added.

+

24h Time Picker

+

24h Time Format

-

Templated time picker.

-

Time picker with templated input group.

+

Templated Time Picker

+

Time picker with templated input group

diff --git a/src/app/time-picker/time-picker.sample.ts b/src/app/time-picker/time-picker.sample.ts index 6e79e77d1a2..fd71b49a5fb 100644 --- a/src/app/time-picker/time-picker.sample.ts +++ b/src/app/time-picker/time-picker.sample.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; +import { IgxTimePickerComponent, TimePickerInteractionMode } from 'igniteui-angular'; @Component({ selector: 'app-time-picker-sample', @@ -6,4 +7,29 @@ import { Component } from '@angular/core'; templateUrl: 'time-picker.sample.html' }) export class TimePickerSampleComponent { + max = "19:00"; + min = "09:00"; + + itemsDelta = { hours: 1, minutes: 5 }; + format="hh:mm tt"; + isSpinLoop = true; + isVertical = true; + mode = TimePickerInteractionMode; + + date = new Date(2018, 10, 27, 17, 45, 0, 0); + + showDate() { + return this.date ? this.date.toLocaleString() : 'Value is null.'; + } + + valueChanged(event) { + console.log(event); + } + + validationFailed(event) { + console.log(event); + } + + @ViewChild('tp', { read: IgxTimePickerComponent }) + public tp: IgxTimePickerComponent; }