diff --git a/CHANGELOG.md b/CHANGELOG.md index c4de0d651d1..08c2618f50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Ignite UI for Angular Change Log All notable changes for each version of this project will be documented in this file. +## 7.1.2 +### Features +- `igx-circular-bar` and `igx-linear-bar` now feature an indeterminate input property. When this property is set to true the indicator will be continually growing and shrinking along the track. +- `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 +* onSortingDone is not fired when sorting indicator of a header in the group by area is clicked ([#3257](https://github.com/IgniteUI/igniteui-angular/issues/3257)) +* igx-grid isn't displayed properly in IE11 when it is inside an igx-tabs-group ([#3047](https://github.com/IgniteUI/igniteui-angular/issues/3047)) +* Preventing wrap-around for scrollNext and scrollPrev([#3365](https://github.com/IgniteUI/igniteui-angular/issues/3365)) +* IgxTreeGrid does not respect its parent container height ([#3467](https://github.com/IgniteUI/igniteui-angular/issues/3467)) +* Include grid's unpinnedWidth and totalWidth in cell width calculation ([#3465](https://github.com/IgniteUI/igniteui-angular/issues/3465)) + +### Other +* update typedoc-plugin-localization version to 1.4.1 ([#3440](https://github.com/IgniteUI/igniteui-angular/issues/3440)) + ## 7.1.0 ### Features @@ -28,6 +44,28 @@ 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 + +* igx-grid isn't displayed properly in IE11 when it is inside an igx-tabs-group. ([#3047](https://github.com/IgniteUI/igniteui-angular/issues/3047)) +* igx-slider max-value defaults to min-value ([#3418](https://github.com/IgniteUI/igniteui-angular/issues/3418)) +* Inconsistency in scrollNext and scrollPrev ([#3365](https://github.com/IgniteUI/igniteui-angular/issues/3365)) +* The header link in the api docs page should be to the product page ([#3423](https://github.com/IgniteUI/igniteui-angular/issues/3423)) +* Error thrown when edit primaryKey cell in Tree Grid ([#3329](https://github.com/IgniteUI/igniteui-angular/issues/3329)) +* IgxGridHeaderGroupComponent should have preset min width ([#3071](https://github.com/IgniteUI/igniteui-angular/issues/3071)) +* Pressing ESC on a cell in an editable column throws an error ([#3429](https://github.com/IgniteUI/igniteui-angular/issues/3429)) +* Cell foreground is white on hover with the default theme ([#3384](https://github.com/IgniteUI/igniteui-angular/issues/3384)) +* [IE] Grid toolbar's buttons and title are misaligned ([#3371](https://github.com/IgniteUI/igniteui-angular/issues/3371)) +* Dialog window does not hold the focus when opened ([#3199](https://github.com/IgniteUI/igniteui-angular/issues/3199)) +* refactor(themes): don't include contrast colors in the palettes ([#3166](https://github.com/IgniteUI/igniteui-angular/issues/3166)) + +### Other +* 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)) @@ -91,6 +129,44 @@ All notable changes for each version of this project will be documented in this - Updated package dependencies to Angular 7 ([#3000](https://github.com/IgniteUI/igniteui-angular/pull/3000)) - Themes: Add dark schemas and mixins (PR [#3025](https://github.com/IgniteUI/igniteui-angular/pull/3025)) +## 6.2.4 + +### Bug Fixes +* onSortingDone is not fired when sorting indicator of a header in the group by area is clicked ([#3257](https://github.com/IgniteUI/igniteui-angular/issues/3257)) +* igx-grid isn't displayed properly in IE11 when it is inside an igx-tabs-group ([#3047](https://github.com/IgniteUI/igniteui-angular/issues/3047)) +* Preventing wrap-around for scrollNext and scrollPrev([#3365](https://github.com/IgniteUI/igniteui-angular/issues/3365)) +* IgxTreeGrid does not respect its parent container height ([#3467](https://github.com/IgniteUI/igniteui-angular/issues/3467)) +* The header link in the api docs page should be to the product page ([#3423](https://github.com/IgniteUI/igniteui-angular/issues/3423)) +* fix(dialog): dialog gets focus when is opened ([#3276](https://github.com/IgniteUI/igniteui-angular/issues/3276)) +* IgxTreeGrid - Add row editing + transactions to tree grid ([#2908](https://github.com/IgniteUI/igniteui-angular/issues/2908)) +* Regular highlight makes the highlighted text unreadable when the row is selected. ([#1852](https://github.com/IgniteUI/igniteui-angular/issues/1852)) +* Use value instead of ngModel to update editValue for checkbox and calendar in igxCell ([#3224](https://github.com/IgniteUI/igniteui-angular/issues/3224)) +* Disable combo checkbox animations on scroll ([#3300](https://github.com/IgniteUI/igniteui-angular/issues/3300)) +* "Select/Unselect All" checkbox is checked after deleting all rows ([#3068](https://github.com/IgniteUI/igniteui-angular/issues/3068)) +* Fixing column chooser column updating ([#3234](https://github.com/IgniteUI/igniteui-angular/issues/3234)) +* Fix - Combo - Hide Search input when !filterable && !allowCustomValues ([#3315](https://github.com/IgniteUI/igniteui-angular/issues/3315)) +* Add @inheritdoc ([#2943](https://github.com/IgniteUI/igniteui-angular/issues/2943)) +* refactor(displayDensity): Code cleanup in display density base class #3280 +* Calculating updated grid height when rebinding columns ([#3285](https://github.com/IgniteUI/igniteui-angular/issues/3285)) +* Fix - Combo, Drop Down - Fix TAB key navigation ([#3206](https://github.com/IgniteUI/igniteui-angular/issues/3206)) +* Added validation if last column collides with grid's scroll ([#3142](https://github.com/IgniteUI/igniteui-angular/issues/3142)) +* When in the tree grid are pinned columns and scroll horizontal the cells text is over the pinned text ([#3163](https://github.com/IgniteUI/igniteui-angular/issues/3163)) +* refactor(themes): don't include contrast colors in the palettes ([#3166](https://github.com/IgniteUI/igniteui-angular/issues/3166)) + +### Code enhancements +* Fix the logic calculating test results ([#3461](https://github.com/IgniteUI/igniteui-angular/issues/3461)) +* Update typedoc version and localize some shell strings ([#3237](https://github.com/IgniteUI/igniteui-angular/issues/3237)) +* fix(toolbar): including custom content in the show toolbar check ([#2983](https://github.com/IgniteUI/igniteui-angular/issues/2983)) +* docs(toolbar): adding more API docs ([#2983](https://github.com/IgniteUI/igniteui-angular/issues/2983)) + +### Other +* update typedoc-plugin-localization version to 1.4.1 ([#3440](https://github.com/IgniteUI/igniteui-angular/issues/3440)) +* Update contributing document with localization ([#3313](https://github.com/IgniteUI/igniteui-angular/issues/3313)) +* docs(*): add 6.2.3 missing changes and bug fixes to changelog ([#3251](https://github.com/IgniteUI/igniteui-angular/issues/3251)) +* Docs - Expansion Panel - Add comments and README([#3245](https://github.com/IgniteUI/igniteui-angular/issues/3245)) +* Move all keyboard navigation tests in a separate file ([#2975](https://github.com/IgniteUI/igniteui-angular/issues/2975)) + + ## 6.2.3 - `igxGrid` - `resourceStrings` property added, which allows changing/localizing strings for component. If a new instance is set, diff --git a/extras/docs/themes/sassdoc/index.js b/extras/docs/themes/sassdoc/index.js index bc88e6adea5..9c229dda119 100644 --- a/extras/docs/themes/sassdoc/index.js +++ b/extras/docs/themes/sassdoc/index.js @@ -130,23 +130,23 @@ const theme = themeleon(__dirname, function (t) { switch (operator) { case '==': // tslint:disable-next-line:triple-equals - return (v1 == v2) ? options.fn(this) : options.inverse(this); + return (v1 == v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '===': - return (v1 === v2) ? options.fn(this) : options.inverse(this); + return (v1 === v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '<': - return (v1 < v2) ? options.fn(this) : options.inverse(this); + return (v1 < v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '<=': - return (v1 <= v2) ? options.fn(this) : options.inverse(this); + return (v1 <= v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '>': - return (v1 > v2) ? options.fn(this) : options.inverse(this); + return (v1 > v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '>=': - return (v1 >= v2) ? options.fn(this) : options.inverse(this); + return (v1 >= v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '&&': - return (v1 && v2) ? options.fn(this) : options.inverse(this); + return (v1 && v2) ? options.fn(options.data.root) : options.inverse(options.data.root); case '||': - return (v1 || v2) ? options.fn(this) : options.inverse(this); + return (v1 || v2) ? options.fn(options.data.root) : options.inverse(options.data.root); default: - return options.inverse(this); + return options.inverse(options.data.root); } }, localize: (options) => { diff --git a/extras/docs/themes/sassdoc/views/partials/header.hbs b/extras/docs/themes/sassdoc/views/partials/header.hbs index 4ebbba4e909..518cfad1d83 100644 --- a/extras/docs/themes/sassdoc/views/partials/header.hbs +++ b/extras/docs/themes/sassdoc/views/partials/header.hbs @@ -2,8 +2,13 @@ {{#ifCond lang '==' 'jp'}}{{> infraHeadJA }}{{else}}{{> infraHead }}{{/ifCond}}
diff --git a/extras/docs/themes/typedoc/src/partials/header.hbs b/extras/docs/themes/typedoc/src/partials/header.hbs index 78f1147525b..f6751a49ac8 100644 --- a/extras/docs/themes/typedoc/src/partials/header.hbs +++ b/extras/docs/themes/typedoc/src/partials/header.hbs @@ -3,7 +3,11 @@

- {{project.name}} + {{#ifCond project.localization '==' 'jp'}} + {{project.name}} + {{else}} + {{project.name}} + {{/ifCond}} API ver. 7.0.x

diff --git a/projects/igniteui-angular/package.json b/projects/igniteui-angular/package.json index d78eaa88cc7..0eddf69b446 100644 --- a/projects/igniteui-angular/package.json +++ b/projects/igniteui-angular/package.json @@ -68,7 +68,7 @@ "web-animations-js": "^2.3.1" }, "devDependencies": { - "igniteui-cli": "~3.1.0" + "igniteui-cli": "~3.2.0" }, "ng-update": { "migrations": "./migrations/migration-collection.json" diff --git a/projects/igniteui-angular/src/lib/avatar/avatar.component.html b/projects/igniteui-angular/src/lib/avatar/avatar.component.html index b0d47c90bb2..55f188f6fc7 100644 --- a/projects/igniteui-angular/src/lib/avatar/avatar.component.html +++ b/projects/igniteui-angular/src/lib/avatar/avatar.component.html @@ -1,19 +1,17 @@ + + + + -
+
-
- {{initials.substring(0, 2)}} -
+ {{initials.substring(0, 2)}}
- - {{icon}} - + {{icon}} - diff --git a/projects/igniteui-angular/src/lib/avatar/avatar.component.spec.ts b/projects/igniteui-angular/src/lib/avatar/avatar.component.spec.ts index cee2df5eb55..e97f91c67fa 100644 --- a/projects/igniteui-angular/src/lib/avatar/avatar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/avatar/avatar.component.spec.ts @@ -5,12 +5,25 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { IgxIconModule } from '../icon/index'; -import { IgxAvatarComponent, IgxAvatarModule } from './avatar.component'; +import { IgxAvatarComponent, AvatarType, Size } from './avatar.component'; import { configureTestSuite } from '../test-utils/configure-suite'; describe('Avatar', () => { configureTestSuite(); + const baseClass = 'igx-avatar'; + + const classes = { + default: `${baseClass}--default`, + round: `${baseClass}--rounded`, + small: `${baseClass}--small`, + medium: `${baseClass}--medium`, + large: `${baseClass}--large`, + image: `${baseClass}--image`, + initials: `${baseClass}--initials`, + icon: `${baseClass}--icon` + }; + beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ @@ -18,8 +31,7 @@ describe('Avatar', () => { AvatarWithAttribsComponent, IgxAvatarComponent, InitIconAvatarComponent, - InitImageAvatarComponent, - InitAvatarWithAriaComponent + InitImageAvatarComponent ], imports: [IgxIconModule] }) @@ -29,122 +41,183 @@ describe('Avatar', () => { it('Initializes avatar with auto-incremented id', () => { const fixture = TestBed.createComponent(InitAvatarComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; - const domAvatar = fixture.debugElement.query(By.css('igx-avatar')).nativeElement; + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.id).toContain('igx-avatar-'); + expect(hostEl.id).toContain('igx-avatar-'); + + instance.id = 'customAvatar'; + fixture.detectChanges(); + + expect(instance.id).toBe('customAvatar'); + expect(hostEl.id).toBe('customAvatar'); + }); + + it('Initializes square and round avatar', () => { + const fixture = TestBed.createComponent(AvatarWithAttribsComponent); + fixture.detectChanges(); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.roundShape).toBeTruthy(); + expect(hostEl.classList).toContain(classes.round); - expect(avatar.id).toContain('igx-avatar-'); - expect(domAvatar.id).toContain('igx-avatar-'); + instance.roundShape = false; + + fixture.detectChanges(); + expect(instance.roundShape).toBeFalsy(); + expect(hostEl.classList).not.toContain(classes.round); + }); + + it('Can change its size', () => { + const fixture = TestBed.createComponent(AvatarWithAttribsComponent); + fixture.detectChanges(); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.size).toEqual(Size.SMALL); + expect(hostEl.classList).toContain(classes.small); + + instance.size = Size.MEDIUM; + fixture.detectChanges(); + expect(instance.size).toEqual(Size.MEDIUM); + expect(hostEl.classList).not.toContain(classes.medium); - avatar.id = 'customAvatar'; + instance.size = Size.LARGE; fixture.detectChanges(); + expect(instance.size).toEqual(Size.LARGE); + expect(hostEl.classList).not.toContain(classes.large); - expect(avatar.id).toBe('customAvatar'); - expect(domAvatar.id).toBe('customAvatar'); + instance.size = 'nonsense'; + fixture.detectChanges(); + expect(instance.size).toEqual(Size.SMALL); + expect(hostEl.classList).toContain(classes.small); }); - it('Initializes avatar with initials', () => { + it('Initializes default avatar', () => { const fixture = TestBed.createComponent(InitAvatarComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; - expect(fixture.debugElement.query(By.css('.igx-avatar__initials'))).toBeTruthy(); - expect(avatar.roundShape).toEqual(false); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.type).toEqual(AvatarType.DEFAULT); + expect(instance.initials).toBeUndefined(); + expect(instance.src).toBeUndefined(); + expect(instance.icon).toBeUndefined(); + + expect(hostEl.textContent).toEqual('TEST'); + expect(hostEl.classList).toContain(classes.default); }); - it('Initializes round avatar with initials', () => { + + it('Initializes initials avatar', () => { const fixture = TestBed.createComponent(AvatarWithAttribsComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; - expect(avatar.elementRef.nativeElement.classList.contains('igx-avatar--rounded')).toBeTruthy(); - expect(fixture.debugElement.query(By.css('.igx-avatar__initials'))).toBeTruthy(); - expect(avatar.roundShape).toBeTruthy(); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.type).toEqual(AvatarType.INITIALS); + expect(instance.initials).toEqual('ZK'); + expect(hostEl.querySelector('span').textContent).toEqual('ZK'); + expect(hostEl.classList).toContain(classes.initials); }); it('Initializes icon avatar', () => { const fixture = TestBed.createComponent(InitIconAvatarComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; - const spanEl = avatar.elementRef.nativeElement.querySelector('.igx-avatar__icon'); - - expect(avatar.image === undefined).toBeTruthy(); - expect(avatar.src).toBeFalsy(); - expect(spanEl).toBeTruthy(); - expect(avatar.elementRef.nativeElement.classList.contains('igx-avatar--small')).toBeTruthy(); - expect(spanEl.classList.length === 1).toBeTruthy(); - expect(avatar.roundShape).toBeFalsy(); - - // For ARIA - expect(spanEl.getAttribute('aria-roledescription') === 'icon type avatar').toBeTruthy(); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.type).toEqual(AvatarType.ICON); + expect(instance.icon).toBeTruthy(); + expect(hostEl.classList).toContain(classes.icon); }); it('Initializes image avatar', () => { const fixture = TestBed.createComponent(InitImageAvatarComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; - expect(avatar.image).toBeTruthy(); - expect(avatar.image.nativeElement.style.backgroundImage.length !== 0).toBeTruthy(); - expect(avatar.elementRef.nativeElement.classList.contains('igx-avatar--large')).toBeTruthy(); - expect(avatar.image.nativeElement.classList.length === 1).toBeTruthy(); - expect(avatar.roundShape).toBeTruthy(); + expect(instance.type).toEqual(AvatarType.IMAGE); + expect(instance.image).toBeTruthy(); + expect(instance.image.nativeElement.style.backgroundImage).toBeDefined(); - // For ARIA - expect(avatar.image.nativeElement.getAttribute('aria-roledescription') === 'image type avatar').toBeTruthy(); - expect(avatar.roleDescription === 'image type avatar').toBeTruthy(); + expect(instance.image.nativeElement.classList).toContain(`${baseClass}__image`); + expect(hostEl.classList).toContain(classes.image); }); - it('Should set ARIA attributes.', () => { - const fixture = TestBed.createComponent(InitAvatarWithAriaComponent); + it('Sets background and foreground colors', () => { + const fixture = TestBed.createComponent(AvatarWithAttribsComponent); fixture.detectChanges(); - const avatar = fixture.componentInstance.avatar; + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(hostEl.style.background).toEqual(instance.bgColor); + expect(hostEl.style.color).toEqual(instance.color); - expect(avatar.elementRef.nativeElement.querySelector('.igx-avatar__initials') - .getAttribute('aria-roledescription')).toMatch('initials type avatar'); + instance.bgColor = '#000'; + instance.color = '#fff'; + + fixture.detectChanges(); + expect(hostEl.style.background).toEqual('rgb(0, 0, 0)'); + expect(hostEl.style.color).toEqual('rgb(255, 255, 255)'); + }); + + it('Sets ARIA attributes', () => { + const fixture = TestBed.createComponent(InitImageAvatarComponent); + fixture.detectChanges(); + const instance = fixture.componentInstance.avatar; + const hostEl = fixture.debugElement.query(By.css(baseClass)).nativeElement; + + expect(instance.roleDescription).toEqual('image avatar'); + expect(hostEl.getAttribute('role')).toEqual('img'); + expect(hostEl.getAttribute('aria-roledescription')).toEqual('image avatar'); + expect(hostEl.getAttribute('aria-label')).toEqual('avatar'); }); }); @Component({ - template: ` - `}) + template: `TEST` +}) class InitAvatarComponent { @ViewChild(IgxAvatarComponent) public avatar: IgxAvatarComponent; } @Component({ - template: ``}) + template: ` + ` +}) class AvatarWithAttribsComponent { @ViewChild(IgxAvatarComponent) public avatar: IgxAvatarComponent; public initials = 'ZK'; - public bgColor = 'lightblue'; + public color = 'orange'; + public bgColor = 'royalblue'; public roundShape = 'true'; } @Component({ - template: ` - `}) + template: `` +}) class InitIconAvatarComponent { @ViewChild(IgxAvatarComponent) public avatar: IgxAvatarComponent; } @Component({ - template: ` - `}) + template: `` +}) class InitImageAvatarComponent { @ViewChild(IgxAvatarComponent) public avatar: IgxAvatarComponent; // tslint:disable-next-line:max-line-length public source = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxQTEhUUExQWFRUXFRcaFRgXFxcXFBcYFxQXFxcUFxcYHCggGBolHBQUITEhJSkrLi4uFx8zODMsNygtLiwBCgoKDg0OGhAQGi4kHyQsLCwsLCwsLSwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLP/AABEIAK4BIgMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAACAwABBAcFBv/EADUQAAICAgADBQYFBAIDAAAAAAABAgMRIQQxQQYSUaPSE1NhcYHwM5GhseEWI8HxQ9EUInP/xAAaAQEBAQEBAQEAAAAAAAAAAAAAAQIDBAYF/8QAMBEBAAECAggEBgIDAAAAAAAAAAECEQNRBRITFCGhotEEFTFTFkGBsdLwQ2EGM1L/2gAMAwEAAhEDEQA/APp+Gj0Hxis8s66iKORppjyz9/UDNxkc70vvkLpo1k9HiK8pff2wa6l+XL4AL9k8LBdcceJqppx/I9UR/jIGateP/X+xtdOzXTw+dDLqsaQGZ0JFxoxyHxTfQdXHYGNQfUKNeDcoC5V8wAUC5LoFXF5I1sBeBiQfsw3EDJKP7GZ0tt6PSugDGIHnrhsfUaquhsjDJbrQGeFAFlKWcnoRgZ3Xlv5gZK68/IXZwia2up6NcNgSgB5tlWvzM74bK3k9WVWmLjD4AeNbw+G8pvwJGjevljw0ev7DYp1Yf3sDPVRjf7cgb6875ffM2NaWRUoged/42zNbQstPoz1FB5F8XVzesa+/2A8mzh8LWV/r9Aqac7+WH9P2NX34gVWY5Y+/tADPhVjo+XiJv4XC8Ovhs9GMtchPEy1j9gPn/Zvx/REPSwvH9ywHV5NtEdY3n6GfhYZfP/s9Hha88/ECpvGt/Uup/uPvgnjAuqADMJL7/wAmmpJ46/EXClfM1RSxhfwA+K8AnV4k4eIedgV7PHIBxNKexGMv6gCkSMeZojAjrASoBKGB0UCwEyQXdG91Bez0AnuC5o0SQkCokkhkYF93IEgtCc4GpYX0Ed3IF8yOAyDAcwF2CMDLoPmLekAFmurE2rwHTeQMcgFxgw3VlBx/wGloBDpaXiIugnp9eaPRxyEOHj/AHlXR1jHUVVwuNvS/we1GCzyFzq2B53VJL/AriasPL59Pv6np2VpI8/iIN/f6AeY/mQNw+C/NlgauHqWXs9GpIzx4br9/IfDQGxQLpq8eQNd6aNUGArGGMiNSRTq8AG1svu7LpRJgV7RoYq1zM0ZZl+xsrXQCgostoGpAW9gsPuk7rApLREF3cAyYFTkhGdhyr6klXyAvJbQUYlyjoBeNMGIcUVKIFd7oLUQ3F9CopgLsQiS6Gh/EUgFKoCcMI1uIiSyAiKZoS1skaw4VgBGJLIrpz6ju7sGaAVJCt9OnIbbB4yVBa8AF2VcjDxED1E29GTio4eFzYHkSofh+hRqnxiTenzIA1yyHE43X2243a9t193V6Bj7a8bj8br7ur0HLbQ4bxTlLsaNdS0cWh2y43X9/X/zq9Bu4bthxvvvLq9BNvSxPi6I+UuwRQ2uXQ5BZ2v433/l1egOntjxvvvLq9A29LO+4eU/v1dgSx9ALVrJymXbXjMNe28ur9P8A0F3dseNwsXeXV6Cbek37Dynl3db4avqaIy2cjr7Z8b3fxvLq9Av+s+Nx+N5dXoG3pTfsPKeXd2VMiici4ftlxuPxvLq9Af8AWHG++8ur0jeKU8ww8p5d3W69gTk84OVw7Y8Yv+by6vSBHtnxjk17b5f26vSN4pTzDDynl3dWjBsYoJHKF2x4z33l1+kj7Ycb77y6/SN4pPMcLKeXd1axCJo5c+2PG++8ur0iru2PG++6+7q9A3ilY0hh5Ty7utxKkjktfbHjffeXV6Rj7YcZ77y6vSN4pPMMPKeXd1PugSZy2XbDjcfjdPd1egzrtjxvvvLq9A3ikjx+HPynl3derRbjo5NDthxuPxvLq9AT7Y8b77y6vSN4pPMMPKeXd1GxAKODlj7Y8b77y6vSIn2z43P43l1egbeld+w8p5d3W0hLicsj2x43f97y6vQA+2HG5/G8ur0Db0rv2HlP79XW4QHYOP19suN995dXoHf1lxmPxvLq9I3ilN+w8p5d3UrGLjE5RZ2y43P43l1egz/1nxy53eXV6C7elqPGUZT+/V1+4VCRyeXbfjPfeXV6DMu2vGvld5dXoG3pWPF0ZT+/V2C2zWvv72Y7Jdfmcon2245f83l1egzS7Z8c+d3l1egbalqPE0ZS6o6WQ5M+2vG+/wDLq9BZdtC7xTlLwYpbHRWRTX7DaY7PM8MnezNVK/JY/MFfm+nzNVVWDLhVVwLnIingK5Y/wSineWRnhY+mDby/oNui9fMKnxYeMvZHKZ4resCHEcl0IodchImy6Fp/UuUvAXF5Wiu/sJYUZ7BfNBKG8jMIF12MOEzPKYyuQSY4DmhMx9ktGfG8ApMhDAdkNA1v7+Ac2En1JkxUYhTkApZDcQYglyA2iNMCTaBnDIMmUregWwormBNY2MhLmBYsBYKqY2MzOuY5V8gspL/Bhtls9P2ejLdDYhaZY2go1t8vqM9n8xsI65FbmpinWYz0LeqPPskjUOtDO5linL4ohp2s114+uTTFGRS2a6UZlyqaOFqfPqbq44BqgMZl5qpvJd3ND0taESSbQzK5ESWiqr4hN+ArhW09jJy8A5z6pnwFTmPr2JvWwR6i4Nc/vmBdF5G0RwiWAvxHQs6L4iKArnhh8U9Bn5s8UUrCq2NSDciTAfiPwLa2GYkuL+I+vlvmKhHfzH8kCpnsiJjHH5j7DOg3BtUs5CxkVBjYMJJEwJL8h9iwIk/9BqB1vmgbZaFwlhjpRC+kslcmmPVouUNjfZcslamw3Y8ASfUOcFgyS546ESIHFvI1S1kTkkp6wVbM/GWb0YO6bO7kX3OeSu1M2hk9n8CDnJeBDTpeWap4xo21Jv4GaETXRokpXL0qlpDqrDFGQ6u3xwZeWaU4iW8kjMXbLQFb+IatwbYPkPseGZqp5HLeEiOUxxF3y5S0XKvYqTwEgMbHyHN7E98tTTCzB8UFY8lVx0SL3gME2R+/v6F1sbZHIuCwGr8DW9ASlsFvoDNhIg+sOYuOQZ29AluIbAYxz9STJVPIa+SlH+A60DWv4GwjpgmSL5dBVstYKtt2JTyw6RCI0SsEOPMuMtYYWTKZbHuejFVIZHl9/mVJhdthl9rsOd2/oZpL9w3TSc55+hS3zLjHKJLGApdj54ESbwNkt/BguOStwyYZZGmWadB11/H9B1cPiJbHQkeOcSvN9vGh/BT60c6u7RFl6YuIRz2tebfkfgPb6qu60yRr8Acj0Jxq4+bUaC8BP8fVV3SttGim1pCE9h97BicfEzXyDR8+uH1Vd2n2vUCc8gRkU2Tb4ma/D2jvb6qvySEw4y+AokJDbYmafD+jvb6qvyaoXEV2HyEqReSbfEzX4e0b7XVV+TQ789BU5lJi5sRj4mf2X4d0bH8XVV+Se02X3xUBiLOPXmkf4/o72+qru0q3C5Coz5skmKkyRj4mf2Wf8e0bH8XVV+QpWgqxpARCuRrb15s/D+j7f6+qrudGeg5W/AVHkWuRnb4mf2a+HdHe11VfkR4/MpayFPxKfI1t6808g0f7fVV3DnwKUvELAt6Ltq80nQPgPb6qu461hhWPohff0DKehtcTNJ0Fo/2+qruTNbKrTI+ZcUdNrXmx5J4H2+dXcyM2DLZUi1Im1rzXyPwHt86u5Tj8QIa6jLEB3DUYtWbM6F8F7fOruTKD8f0KG5RDW1qZ8n8H/wAc6u5Q6Bm74yqZmqH6FNUXa0y3ICMi4o5Wei62aIMzjq3ozU1T6jbCi8gBIw6Qtg94JoXJiCR94iyVFh4CoEgMssimxYFjy9cyrJYRdKwsvmyf2t78FxgW2ScxcpdPEE8DO9kt8i+7oFkUEdMpvLI46KizTI3IYuQlstSJZbrbFOWAmxNkjUQxVJzYi2QXe8BbNRDNUrKSKGRax8SyzHFXdBehjA5iFkMpEyXMCxFhmVTYEpAymAzpEOVVQXMgpkOlnDWkOBsGJTGxLLNJ7ngOqWTOpDVM5zDvTVxOYUJaFNhpmJh1iTnIODM7mHCRiYbiri0d4TORIMqSJENTN4FCwZCwzA94urdnXs1xmW7DNB5LfgTVa1+Bzl3sDnYZ4vCBdhLXWKrGzmVB7FyKSLbgl+LXCZYiuQffMTDpEiyJTDUhedliGZkeSgO8WmWyXRgSDmxUmahmoUWFjItbCcxMJEhRGyrJld41Zm562gO8VGzWALrDMRxamqLXXkCbKjYD7Y3aXOaosXIXNjZS6iLpnWlwrmxbkQU7PiQ66rza6h9b0Z0x0BUUepr5FxnoGbEqzZmIu6TVaW+DLczJG3P5jFPRzml1jEua7Ao2GRyYUJbLNJGJxehGRUmKU+RcrGctV31uA++C5GedocJGtWzGvfgdCQcZ7yZe+/1GqZJpaprPlPIrvYAUy5sllmq5qsyFCZkUmWrOY1EjEbLJC3YIVhVmxFKziZNVdyF22b+ZmjJh9/KLqWlNpeLHxmTviURMmqa0nuzJWRLkU5l1TXaFIpyExb6lSmNU1zLXnDAQtTySTNavyY1r8TZT2KmLcyKeixTZia7md4WwJyKNxDE1JOYu2RU5AW9DcQ41VAILafiQ62efW/p//9k='; } - -@Component({ - template: ` - `}) -class InitAvatarWithAriaComponent { - @ViewChild(IgxAvatarComponent) public avatar: IgxAvatarComponent; -} diff --git a/projects/igniteui-angular/src/lib/avatar/avatar.component.ts b/projects/igniteui-angular/src/lib/avatar/avatar.component.ts index c706ee638ae..4ee0d2ddcae 100644 --- a/projects/igniteui-angular/src/lib/avatar/avatar.component.ts +++ b/projects/igniteui-angular/src/lib/avatar/avatar.component.ts @@ -19,6 +19,14 @@ export enum Size { MEDIUM = 'medium', LARGE = 'large' } + +export enum AvatarType { + DEFAULT = 'default', + INITIALS = 'initials', + IMAGE = 'image', + ICON = 'icon' +} + /** * **Ignite UI for Angular Avatar** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/avatar.html) @@ -38,7 +46,6 @@ export enum Size { }) export class IgxAvatarComponent implements OnInit, AfterViewInit { - /** * This is a reference to the avatar `image` element in the DOM. * @@ -50,6 +57,11 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { @ViewChild('image') public image: ElementRef; + /** + *@hidden + */ + @ViewChild('defaultTemplate', { read: TemplateRef }) + protected defaultTemplate: TemplateRef; /** *@hidden @@ -62,11 +74,13 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { */ @ViewChild('initialsTemplate', { read: TemplateRef }) protected initialsTemplate: TemplateRef; + /** *@hidden */ @ViewChild('iconTemplate', { read: TemplateRef }) protected iconTemplate: TemplateRef; + /** * Returns the `aria-label` of the avatar. * @@ -77,6 +91,7 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { */ @HostBinding('attr.aria-label') public ariaLabel = 'avatar'; + /** * Returns the `role` attribute of the avatar. * @@ -88,6 +103,7 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { */ @HostBinding('attr.role') public role = 'img'; + /** * Returns the class of the avatar. * @@ -99,6 +115,7 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { */ @HostBinding('class.igx-avatar') public cssClass = 'igx-avatar'; + /** * Returns the type of the avatar. * The avatar can be: `"initials type avatar"`, `"icon type avatar"` or `"image type avatar"`. @@ -109,6 +126,8 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { * * @memberof IgxAvatarComponent */ + + @HostBinding('attr.aria-roledescription') public roleDescription: string; /** @@ -128,6 +147,7 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { @HostBinding('attr.id') @Input() public id = `igx-avatar-${NEXT_ID++}`; + /** * Sets a round shape to the avatar if `roundShape` is `"true"`. * By default the shape of the avatar is a square. @@ -138,6 +158,7 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { * * @memberof IgxAvatarComponent */ + @HostBinding('class.igx-avatar--rounded') @Input() public roundShape = false; @@ -151,6 +172,8 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { * * @memberof IgxAvatarComponent */ + + @HostBinding('style.color') @Input() public color: string; @@ -163,6 +186,8 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { * * @memberof IgxAvatarComponent */ + + @HostBinding('style.background') @Input() public bgColor: string; @@ -237,26 +262,52 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { this._size = 'small'; } } + /** - * Returns the template of the avatar. + * Returns the type of the avatar. * * ```typescript - * let template = this.avatar.template; + * let avatarType = this.avatar.type; * ``` * * @memberof IgxAvatarComponent */ - get template() { + get type(): AvatarType { if (this.src) { - return this.imageTemplate; + return AvatarType.IMAGE; + } + + if (this.icon) { + return AvatarType.ICON; } if (this.initials) { - return this.initialsTemplate; + return AvatarType.INITIALS; } - return this.iconTemplate; + return AvatarType.DEFAULT; + } + /** + * Returns the template of the avatar. + * + * ```typescript + * let template = this.avatar.template; + * ``` + * + * @memberof IgxAvatarComponent + */ + get template(): TemplateRef { + switch (this.type) { + case AvatarType.IMAGE: + return this.imageTemplate; + case AvatarType.INITIALS: + return this.initialsTemplate; + case AvatarType.ICON: + return this.iconTemplate; + default: + return this.defaultTemplate; + } } constructor(public elementRef: ElementRef) { } @@ -272,18 +323,23 @@ export class IgxAvatarComponent implements OnInit, AfterViewInit { *@hidden */ public ngAfterViewInit() { - this.elementRef.nativeElement.classList.add(`igx-avatar--${this._size}`); + this.elementRef.nativeElement.classList + .add(`igx-avatar--${this._size}`, `igx-avatar--${this.type}`); } + /** * @hidden */ - private getRole() { - if (this.initials) { - return 'initials type avatar'; - } else if (this.src) { - return 'image type avatar'; - } else { - return 'icon type avatar'; + private getRole(): string { + switch (this.type) { + case AvatarType.IMAGE: + return 'image avatar'; + case AvatarType.ICON: + return 'icon avatar'; + case AvatarType.INITIALS: + return 'initials avatar'; + default: + return 'custom avatar'; } } diff --git a/projects/igniteui-angular/src/lib/chips/chip.component.ts b/projects/igniteui-angular/src/lib/chips/chip.component.ts index 3c6735b0939..5aceae71a77 100644 --- a/projects/igniteui-angular/src/lib/chips/chip.component.ts +++ b/projects/igniteui-angular/src/lib/chips/chip.component.ts @@ -303,14 +303,7 @@ export class IgxChipComponent extends DisplayDensityBase { */ @HostBinding('attr.class') get hostClass(): string { - const classes = []; - if (this.isCosy()) { - classes.push('igx-chip--cosy'); - } else if (this.isCompact()) { - classes.push('igx-chip--compact'); - } else { - classes.push('igx-chip'); - } + const classes = [this.getComponentDensityClass('igx-chip')]; classes.push(this.disabled ? 'igx-chip--disabled' : ''); // The custom classes should be at the end. classes.push(this.class); @@ -359,13 +352,7 @@ export class IgxChipComponent extends DisplayDensityBase { * @hidden */ public get ghostClass(): string { - if (this.isCosy()) { - return 'igx-chip__ghost--cosy'; - } else if (this.isCompact()) { - return 'igx-chip__ghost--compact'; - } else { - return 'igx-chip__ghost'; - } + return this.getComponentDensityClass('igx-chip__ghost'); } public get chipTabindex() { diff --git a/projects/igniteui-angular/src/lib/chips/chip.spec.ts b/projects/igniteui-angular/src/lib/chips/chip.spec.ts index 892dab515d5..c5db50865d2 100644 --- a/projects/igniteui-angular/src/lib/chips/chip.spec.ts +++ b/projects/igniteui-angular/src/lib/chips/chip.spec.ts @@ -235,8 +235,7 @@ describe('IgxChip', () => { const components = fix.debugElement.queryAll(By.directive(IgxChipComponent)); const firstComponent = components[0]; - const isFirstChipComfortable = firstComponent.componentInstance.isComfortable(); - expect(isFirstChipComfortable).toEqual(true); + expect(firstComponent.componentInstance.displayDensity).toEqual(DisplayDensity.comfortable); // Assert default css class is applied const comfortableComponents = fix.debugElement.queryAll(By.css('.igx-chip')); diff --git a/projects/igniteui-angular/src/lib/core/displayDensity.ts b/projects/igniteui-angular/src/lib/core/displayDensity.ts index 168fa6f07bd..eaf87b58f61 100644 --- a/projects/igniteui-angular/src/lib/core/displayDensity.ts +++ b/projects/igniteui-angular/src/lib/core/displayDensity.ts @@ -43,7 +43,8 @@ export class DisplayDensityBase implements DoCheck { */ @Input() public get displayDensity(): DisplayDensity | string { - return this._displayDensity; + return this._displayDensity || + ((this.displayDensityOptions && this.displayDensityOptions.displayDensity) || DisplayDensity.comfortable); } /** @@ -51,66 +52,53 @@ export class DisplayDensityBase implements DoCheck { */ public set displayDensity(val: DisplayDensity | string) { const currentDisplayDensity = this._displayDensity; - switch (val) { - case 'compact': - this._displayDensity = DisplayDensity.compact; - break; - case 'cosy': - this._displayDensity = DisplayDensity.cosy; - break; - case 'comfortable': - this._displayDensity = DisplayDensity.comfortable; - } + this._displayDensity = val as DisplayDensity; + if (currentDisplayDensity !== this._displayDensity) { const densityChangedArgs: IDensityChangedEventArgs = { oldDensity: currentDisplayDensity, newDensity: this._displayDensity }; + this.onDensityChanged.emit(densityChangedArgs); } } @Output() public onDensityChanged = new EventEmitter(); - protected oldDisplayDensityOptions: IDisplayDensityOptions = { displayDensity: DisplayDensity.comfortable }; - /** - *@hidden - */ - public isCosy(): boolean { - return this._displayDensity === DisplayDensity.cosy || - (!this._displayDensity && this.displayDensityOptions && this.displayDensityOptions.displayDensity === DisplayDensity.cosy); - } + protected oldDisplayDensityOptions: IDisplayDensityOptions = { displayDensity: DisplayDensity.comfortable }; - /** - *@hidden - */ - public isComfortable(): boolean { - return this._displayDensity === DisplayDensity.comfortable || - (!this._displayDensity && (!this.displayDensityOptions || - this.displayDensityOptions.displayDensity === DisplayDensity.comfortable)); - } - /** - *@hidden - */ - public isCompact(): boolean { - return this._displayDensity === DisplayDensity.compact || - (!this._displayDensity && this.displayDensityOptions && this.displayDensityOptions.displayDensity === DisplayDensity.compact); - } constructor(protected displayDensityOptions: IDisplayDensityOptions) { Object.assign(this.oldDisplayDensityOptions, displayDensityOptions); } public ngDoCheck() { - if (this.oldDisplayDensityOptions && this.displayDensityOptions && !this._displayDensity && - this.oldDisplayDensityOptions.displayDensity !== this.displayDensityOptions.displayDensity) { + if (!this._displayDensity && this.displayDensityOptions && + this.oldDisplayDensityOptions.displayDensity !== this.displayDensityOptions.displayDensity) { const densityChangedArgs: IDensityChangedEventArgs = { oldDensity: this.oldDisplayDensityOptions.displayDensity, newDensity: this.displayDensityOptions.displayDensity }; + this.onDensityChanged.emit(densityChangedArgs); this.oldDisplayDensityOptions = Object.assign(this.oldDisplayDensityOptions, this.displayDensityOptions); } } + + /** + * Given a style class of a component/element returns the modified version of it based + * on the current display density. + */ + protected getComponentDensityClass(baseStyleClass: string): string { + switch (this.displayDensity) { + case DisplayDensity.cosy: + return `${baseStyleClass}--${DisplayDensity.cosy}`; + case DisplayDensity.compact: + return `${baseStyleClass}--${DisplayDensity.compact}`; + default: + return baseStyleClass; + } + } } diff --git a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts index ade60e12ed8..6b66e5e35cf 100644 --- a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts +++ b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts @@ -43,6 +43,8 @@ export interface IGridResourceStrings { igx_grid_summary_max?: string; igx_grid_summary_sum?: string; igx_grid_summary_average?: string; + igx_grid_summary_earliest?: string; + igx_grid_summary_latest?: string; } export const GridResourceStringsEN: IGridResourceStrings = { @@ -90,4 +92,6 @@ export const GridResourceStringsEN: IGridResourceStrings = { igx_grid_summary_max: 'Max', igx_grid_summary_sum: 'Sum', igx_grid_summary_average: 'Avg', + igx_grid_summary_earliest: 'Earliest', + igx_grid_summary_latest: 'Latest', }; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-component.scss index 68611e1251b..de393efc9fb 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-component.scss @@ -12,6 +12,11 @@ @extend %igx-avatar-display !optional; + @include e(image) { + @extend %igx-avatar-inner !optional; + @extend %igx-avatar-image !optional; + } + @include m(rounded) { @extend %igx-avatar-display !optional; @extend %igx-avatar--rounded !optional; @@ -19,40 +24,30 @@ @include m(small) { @extend %igx-avatar--small !optional; - - @include e(initials) { - @extend %igx-avatar-initials--small !optional; - } } @include m(medium) { @extend %igx-avatar--medium !optional; - - @include e(initials) { - @extend %igx-avatar-initials--medium !optional; - } } @include m(large) { @extend %igx-avatar--large !optional; - - @include e(initials) { - @extend %igx-avatar-initials--large !optional; - } } - @include e(icon) { - @extend %igx-avatar-inner !optional; + @include m(icon) { @extend %igx-avatar-icon !optional; } - @include e(image) { - @extend %igx-avatar-inner !optional; - @extend %igx-avatar-image !optional; + @include m(initials) { + @extend %igx-avatar-initials !optional; + @extend %igx-avatar-initials--small !optional; } - @include e(initials) { - @extend %igx-avatar-inner !optional; - @extend %igx-avatar-initials !optional; + @include mx(medium, initials) { + @extend %igx-avatar-initials--medium !optional; + } + + @include mx(large, initials) { + @extend %igx-avatar-initials--large !optional; } } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-theme.scss index a83bd05a921..e0c780b1a87 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/avatar/_avatar-theme.scss @@ -67,8 +67,13 @@ $large-size: 88px; %igx-avatar-display { + display: inline-flex; + justify-content: center; + align-items: center; position: relative; user-select: none; + color: --var($theme, 'initials-color'); + background: --var($theme, 'initials-background'); } %igx-avatar--rounded { @@ -94,15 +99,12 @@ } %igx-avatar-inner { - width: inherit; - height: inherit; + width: 100%; + height: 100%; border-radius: inherit; } %igx-avatar-icon { - display: flex; - justify-content: center; - align-items: center; color: --var($theme, 'icon-color'); background: --var($theme, 'icon-background'); } @@ -115,9 +117,6 @@ } %igx-avatar-initials { - display: flex; - justify-content: center; - align-items: center; text-transform: uppercase; color: --var($theme, 'initials-color'); background-color: --var($theme, 'initials-background'); diff --git a/projects/igniteui-angular/src/lib/core/styles/components/drop-down/_drop-down-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/drop-down/_drop-down-theme.scss index 82c97bcbdbe..fc26ead6ea4 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/drop-down/_drop-down-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/drop-down/_drop-down-theme.scss @@ -11,7 +11,7 @@ /// @param {Color} $background-color [null] - The drop-down background color. /// @param {Color} $header-text-color [null] - The drop-down header text color. /// -///@param {Color} $item-text-color [null] - The drop-down text color. +/// @param {Color} $item-text-color [null] - The drop-down text color. /// @param {Color} $hover-item-text-color [null] - The drop-down hover text color. /// /// @param {Color} $hover-item-background [null] - The drop-down hover item background color. diff --git a/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-component.scss index 84bae9236a4..b862f5f675b 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-component.scss @@ -7,47 +7,19 @@ //// /// Linear Progress -@include b(progress-linear) { +@include b(igx-linear-bar) { $this: bem--selector-to-string(&); @include register-component(str-slice($this, 2, -1)); @extend %linear-display !optional; - @include e(bar) { + @include e(base) { @extend %linear-bar !optional; } - @include e(bar-base) { - @extend %linear-bar-base !optional; - } - - @include e(bar-progress) { - @extend %linear-bar-progress !optional; - } - - @include e(bar-progress, $m: default) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--default !optional; - } - - @include e(bar-progress, $m: danger) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--danger !optional; - } - - @include e(bar-progress, $m: warning) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--warning !optional; - } - - @include e(bar-progress, $m: info) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--info !optional; - } - - @include e(bar-progress, $m: success) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--success !optional; + @include e(indicator) { + @extend %linear-indicator !optional; + @extend %linear-indicator--default !optional; } @include e(value) { @@ -80,52 +52,61 @@ @extend %linear-value--hidden !optional; } - @include m(striped) { - @include e(bar-progress) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include m(danger) { + @include e(indicator) { + @extend %linear-indicator--danger !optional; } + } - @include e(bar-progress, $m: default) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include m(warning) { + @include e(indicator) { + @extend %linear-indicator--warning !optional; } + } - @include e(bar-progress, $m: danger) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include m(info) { + @include e(indicator) { + @extend %linear-indicator--info !optional; } + } - @include e(bar-progress, $m: warning) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include m(success) { + @include e(indicator) { + @extend %linear-indicator--success !optional; } + } + + @include m(striped) { + @include e(indicator) { + @extend %linear-indicator--striped !optional; + } + } - @include e(bar-progress, $m: info) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include m(indeterminate) { + @include e(indicator) { + @extend %linear-indicator--indeterminate !optional; } - @include e(bar-progress, $m: success) { - @extend %linear-bar-progress !optional; - @extend %linear-bar-progress--striped !optional; + @include e(value) { + @extend %linear-value !optional; + @extend %linear-value--hidden !optional; } } } /// Circular Progress -@include b(progress-circular) { +@include b(igx-circular-bar) { $this: bem--selector-to-string(&); @include register-component(str-slice($this, 2, -1)); @extend %circular-display !optional; - @include e(innercircle) { - @extend %circular-innercircle !optional; + @include e(inner) { + @extend %circular-inner !optional; } - @include e(circle) { - @extend %circular-circle !optional; + @include e(outer) { + @extend %circular-outer !optional; } @include e(text) { @@ -136,4 +117,16 @@ @extend %circular-text !optional; @extend %circular-text--hidden !optional; } + + @include m(indeterminate) { + @extend %circular-display--indeterminate !optional; + + @include e(outer) { + @extend %circular-outer--indeterminate !optional; + } + + @include e(text) { + @extend %circular-text--hidden !optional; + } + } } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-theme.scss index 368b72ebe93..2b669c0743c 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/progress/_progress-theme.scss @@ -41,7 +41,7 @@ $stripes-color: null, $text-color: null ) { - $name: 'igx-progress-linear'; + $name: 'igx-linear-bar'; $theme: apply-palette(map-get($schema, $name), $palette); @return extend($theme, ( @@ -76,33 +76,26 @@ %linear-display { position: relative; display: flex; - align-items: center; - flex-flow: column nowrap; width: 100%; + flex: 1 1 100%; } %linear-bar { - width: inherit; - height: $bar-height; - overflow: hidden; - } - - %linear-bar-base { position: absolute; width: inherit; - height: inherit; + height: $bar-height; background: --var($theme, 'track-color'); + overflow: hidden; z-index: 0; } - %linear-bar-progress { + %linear-indicator { width: 100%; position: relative; height: inherit; - backface-visibility: hidden; } - %linear-bar-progress--striped { + %linear-indicator--striped { background-image: linear-gradient( -45deg, $stripe-color 25%, @@ -116,23 +109,39 @@ background-size: 40px 40px; } - %linear-bar-progress--default { + %linear-indicator--indeterminate { + @include animation(indeterminate-bar 2s cubic-bezier(0, .085, .68, .53) infinite); + width: 100% !important; + + &::before { + position: absolute; + content: ''; + top: 0; + left: -200%; + width: 100%; + height: inherit; + background-color: inherit; + transform-origin: top right; + } + } + + %linear-indicator--default { background: --var($theme, 'fill-color-default') } - %linear-bar-progress--danger { + %linear-indicator--danger { background-color: --var($theme, 'fill-color-danger'); } - %linear-bar-progress--warning { + %linear-indicator--warning { background-color: --var($theme, 'fill-color-warning'); } - %linear-bar-progress--info { + %linear-indicator--info { background-color: --var($theme, 'fill-color-info'); } - %linear-bar-progress--success { + %linear-indicator--success { background-color: --var($theme, 'fill-color-success'); } @@ -191,7 +200,7 @@ $progress-circle-color: null, $text-color: null ) { - $name: 'igx-progress-circular'; + $name: 'igx-circular-bar'; $theme: apply-palette(map-get($schema, $name), $palette); @return extend($theme, ( @@ -202,12 +211,15 @@ text-color: $text-color )); } - /// @param {Map} $theme - The theme used to style the component. /// @requires {mixin} igx-root-css-vars /// @requires rem +/// @requires {mixin} rotate-center /// @requires --var @mixin igx-progress-circular($theme) { + // Include rotate animation + @include rotate-center(); + @include igx-root-css-vars($theme); // @debug $theme; @@ -216,27 +228,44 @@ $circular-value-fw: 600; %circular-display { - width: 100%; - height: 100%; + display: inline-flex; + flex: 1 1 auto; + + svg { + width: 100%; + height: 100%; + min-width: 24px; + min-height: 24px; + } + } + + %circular-display--indeterminate { + @include animation(rotate-center 1.4s linear infinite); + transform-origin: 50% 50%; } - %circular-innercircle { + %circular-inner { stroke-width: 4; fill: transparent; stroke: --var($theme, 'base-circle-color'); } - %circular-circle { + %circular-outer { fill: transparent; stroke: --var($theme, 'progress-circle-color'); - stroke-width: 6; - stroke-linecap: round; + stroke-width: 4; stroke-dashoffset: 289; stroke-dasharray: 289; transform-origin: 50% 50%; transform: rotate(-90deg); } + %circular-outer--indeterminate { + stroke-dashoffset: 180; + stroke-dasharray: 180; + @include animation(indeterminate-accordion 1.5s cubic-bezier(0, .085, .68, .53) infinite); + } + %circular-text { font-size: $circular-value-fs; font-weight: $circular-value-fw; @@ -247,3 +276,31 @@ visibility: hidden; } } + +@include keyframes('indeterminate-bar') { + 0% { + transform: scaleX(0) translateX(-100%); + transform-origin: left; + } + + 50% { + transform: scaleX(1) translateX(50%); + transform-origin: right; + } + + 100% { + transform: scaleX(0) translateX(200%); + transform-origin: right; + } +} + +@include keyframes('indeterminate-accordion') { + 50% { + stroke-dashoffset: 260; + stroke-dasharray: 289; + } + + 100% { + stroke-dashoffset: -180; + } +} 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/core/styles/themes/schemas/dark/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss index f7b2e4bdf3a..b86c91c73a1 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss @@ -113,8 +113,8 @@ $dark-schema: ( igx-navbar: $_dark-navbar, igx-navdrawer: $_dark-navdrawer, igx-overlay: $_dark-overlay, - igx-progress-linear: $_dark-progress-linear, - igx-progress-circular: $_dark-progress-circular, + igx-linear-bar: $_dark-progress-linear, + igx-circular-bar: $_dark-progress-circular, igx-radio: $_dark-radio, igx-ripple: $_dark-ripple, igx-slider: $_dark-slider, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss index a6a17eff30d..eb49679135c 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss @@ -114,8 +114,8 @@ $light-schema: ( igx-navbar: $_light-navbar, igx-navdrawer: $_light-navdrawer, igx-overlay: $_light-overlay, - igx-progress-linear: $_light-progress-linear, - igx-progress-circular: $_light-progress-circular, + igx-linear-bar: $_light-progress-linear, + igx-circular-bar: $_light-progress-circular, igx-radio: $_light-radio, igx-ripple: $_light-ripple, igx-slider: $_light-slider, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_progress.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_progress.scss index 94deb7defdf..86a8faf8d2b 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_progress.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_progress.scss @@ -19,8 +19,6 @@ /// @see $default-palette $_light-progress-linear: ( - name: 'igx-progress-linear', - track-color: ( igx-color: ('grays', 300) ), @@ -62,8 +60,6 @@ $_light-progress-linear: ( /// @see $default-palette $_light-progress-circular: ( - name: 'igx-progress-circular', - base-circle-color: ( igx-color: ('grays', 300) ), diff --git a/projects/igniteui-angular/src/lib/data-operations/groupby-record.interface.ts b/projects/igniteui-angular/src/lib/data-operations/groupby-record.interface.ts index f291475b806..4d2f2273909 100644 --- a/projects/igniteui-angular/src/lib/data-operations/groupby-record.interface.ts +++ b/projects/igniteui-angular/src/lib/data-operations/groupby-record.interface.ts @@ -1,5 +1,8 @@ import { ISortingExpression } from './sorting-expression.interface'; +/** + * @hidden + */ export class GroupedRecords extends Array {} export interface IGroupByRecord { diff --git a/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.spec.ts index 3db1b6db999..c3873ee3121 100644 --- a/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.spec.ts @@ -1007,6 +1007,31 @@ describe('IgxForOf directive -', () => { } }); }); + describe('no width and height component', () => { + configureTestSuite(); + let fix: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + TestIgxForOfDirective, + NoWidthAndHeightComponent + ], + imports: [IgxForOfModule] + }).compileComponents(); + })); + + it('should use itemSize when no width or height are provided', () => { + fix = TestBed.createComponent(NoWidthAndHeightComponent); + fix.componentRef.hostView.detectChanges(); + fix.detectChanges(); + + const children = fix.componentInstance.childVirtDirs; + const instance = fix.componentInstance; + const expectedElementsLength = (parseInt(instance.width, 10) / instance.itemSize) + 2; + expect(children.length).toEqual(expectedElementsLength); + }); + }); }); class DataGenerator { @@ -1454,3 +1479,46 @@ export class RemoteVirtualizationComponent implements OnInit, AfterViewInit { }); } } + +@Component({ + template: ` +
+ +
{{item.text}}
+
+
+ `, + styles: [`.container { + display: flex; + flex-flow: column; + position: relative; + width: 300px; + height: 300px; + overflow: hidden; + border: 1px solid #000; + }`, `.forOfElement { + flex: 0 0 60px; + border-right: 1px solid #888; + }`] +}) + +export class NoWidthAndHeightComponent { + public items = []; + public width = '300px'; + public itemSize = 60; + public height = '300px'; + + @ViewChildren('child') + public childVirtDirs: QueryList; + + constructor() { + for (let i = 0; i < 100; i++) { + this.items.push({text: i + ''}); + } + } +} diff --git a/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts b/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts index 08d2ff8db3e..b0228f01cf3 100644 --- a/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts @@ -903,7 +903,7 @@ export class IgxForOfDirective implements OnInit, OnChanges, DoCheck, OnDestr size = parseInt(this.igxForItemSize, 10) || 0; this.heightCache.push(size); } else { - size = parseInt(items[i][dimension], 10) || 0; + size = this._getItemSize(items[i], dimension); } totalSize += size; this.sizesCache.push(totalSize); @@ -922,7 +922,7 @@ export class IgxForOfDirective implements OnInit, OnChanges, DoCheck, OnDestr let sum = 0; const dimension = this.igxForScrollOrientation === 'horizontal' ? 'width' : 'height'; - const reducer = (accumulator, currentItem) => accumulator + parseInt(currentItem[dimension], 10); + const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension); const availableSize = parseInt(this.igxForContainerSize, 10); for (i; i < this.igxForOf.length; i++) { let item = this.igxForOf[i]; @@ -931,7 +931,7 @@ export class IgxForOfDirective implements OnInit, OnChanges, DoCheck, OnDestr } const size = dimension === 'height' ? this.heightCache[i] : - parseInt(item[dimension], 10); + this._getItemSize(item, dimension); sum = arr.reduce(reducer, size); if (sum <= availableSize) { arr.push(item); @@ -1108,6 +1108,11 @@ export class IgxForOfDirective implements OnInit, OnChanges, DoCheck, OnDestr this.hScroll.scrollLeft - this.sizesCache[this.state.startIndex] : 0; this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px'; } + + private _getItemSize(item, dimension: string): number { + const hasDimension = (item[dimension] !== null && item[dimension] !== undefined); + return hasDimension ? parseInt(item[dimension], 10) : this.igxForItemSize; + } } export function getTypeNameForDebugging(type: any): string { 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/grids/api.service.ts b/projects/igniteui-angular/src/lib/grids/api.service.ts index 498eeb1b6b0..8b14408d371 100644 --- a/projects/igniteui-angular/src/lib/grids/api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/api.service.ts @@ -599,4 +599,20 @@ export class GridBaseAPIService { const grid = this.get(id); return grid.primaryKey ? rowData[grid.primaryKey] : rowData; } + + public row_deleted_transaction(id: string, rowID: any): boolean { + const grid = this.get(id); + if (!grid) { + return false; + } + if (!grid.transactions.enabled) { + return false; + } + const state = grid.transactions.getState(rowID); + if (state) { + return state.type === TransactionType.DELETE; + } + + return false; + } } diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index 1d7f2a47a17..b839f523eed 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -287,6 +287,7 @@ export class IgxGridCellComponent implements OnInit, AfterViewInit { return; } if (this.column.editable && value) { + this.focused = true; this.gridAPI.set_cell_inEditMode(this.gridID, this); if (this.highlight && this.grid.lastSearchInfo.searchText) { this.highlight.observe(); @@ -382,23 +383,7 @@ export class IgxGridCellComponent implements OnInit, AfterViewInit { @HostBinding('style.max-width') @HostBinding('style.flex-basis') get width() { - const hasVerticalScroll = !this.grid.verticalScrollContainer.dc.instance.notVirtual; - const colWidth = this.column.width; - const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; - - if (colWidth && !isPercentageWidth) { - let cellWidth = this.isLastUnpinned && hasVerticalScroll && - (this.grid.unpinnedWidth - this.grid.totalWidth < 0) ? - parseInt(colWidth, 10) - 18 + '' : colWidth; - - if (typeof cellWidth !== 'string' || cellWidth.endsWith('px') === false) { - cellWidth += 'px'; - } - - return cellWidth; - } else { - return colWidth; - } + return this.column.getCellWidth(); } /** @@ -538,6 +523,10 @@ export class IgxGridCellComponent implements OnInit, AfterViewInit { this.selected = true; if (fireFocus) { this.nativeElement.focus(); + } else { + if (!this.focused) { + this.focused = this.nativeElement === document.activeElement; + } } this.grid.onSelection.emit({ cell: this, event }); } @@ -842,8 +831,8 @@ export class IgxGridCellComponent implements OnInit, AfterViewInit { } public onKeydownExitEditMode(event) { - if (this.column.editable) { - const editableCell = this.gridAPI.get_cell_inEditMode(this.gridID); + const editableCell = this.gridAPI.get_cell_inEditMode(this.gridID); + if (this.column.editable && editableCell) { const args: IGridEditEventArgs = { cellID: editableCell.cellID, rowID: editableCell.cellID.rowID, diff --git a/projects/igniteui-angular/src/lib/grids/column.component.ts b/projects/igniteui-angular/src/lib/grids/column.component.ts index fe7059c031e..0097d33553c 100644 --- a/projects/igniteui-angular/src/lib/grids/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/column.component.ts @@ -1198,6 +1198,31 @@ export class IgxColumnComponent implements AfterContentInit { } } + /** + *@hidden + */ + public getCellWidth() { + const hasVerticalScroll = !this.grid.verticalScrollContainer.dc.instance.notVirtual; + const colWidth = this.width; + const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; + + if (colWidth && !isPercentageWidth) { + const unpinnedColumns = this.grid.unpinnedColumns; + const isLastUnpinned = unpinnedColumns[unpinnedColumns.length - 1] === this; + + let cellWidth = isLastUnpinned && hasVerticalScroll && + (this.grid.unpinnedWidth - this.grid.totalWidth < 0) ? + parseInt(colWidth, 10) - 18 + '' : colWidth; + + if (typeof cellWidth !== 'string' || cellWidth.endsWith('px') === false) { + cellWidth += 'px'; + } + + return cellWidth; + } else { + return colWidth; + } + } } diff --git a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-cell.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-cell.component.ts index 989e390a03d..3c1f972f24f 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering-cell.component.ts @@ -178,6 +178,13 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit, DoC this.expressionsList[0].isSelected = true; } + const index = this.filteringService.unpinnedFilterableColumns.indexOf(this.column); + if (index >= 0 && !this.isColumnRightVisible(index)) { + this.filteringService.scrollToFilterCell(this.filteringService.unpinnedFilterableColumns[index], true); + } else if (index >= 0 && !this.isColumnLeftVisible(index)) { + this.filteringService.scrollToFilterCell(this.filteringService.unpinnedFilterableColumns[index], false); + } + this.filteringService.filteredColumn = this.column; this.filteringService.isFilterRowVisible = true; this.filteringService.selectedExpression = expression; @@ -357,7 +364,7 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit, DoC } } const width = this.filteringService.displayContainerWidth + this.filteringService.displayContainerScrollLeft; - return currentColumnRight <= width && this.isColumnLeftVisible(columnIndex); + return currentColumnRight <= width; } private isColumnLeftVisible(columnIndex: number): boolean { diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts index 74fa509ebb0..7ddb1603afe 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts @@ -57,7 +57,7 @@ import { IgxRowEditActionsDirective } from './grid.rowEdit.directive'; import { IgxGridNavigationService } from './grid-navigation.service'; -import { IDisplayDensityOptions, DisplayDensityToken, DisplayDensityBase } from '../core/displayDensity'; +import { IDisplayDensityOptions, DisplayDensityToken, DisplayDensityBase, DisplayDensity } from '../core/displayDensity'; import { IgxGridRowComponent } from './grid'; import { IgxFilteringService } from './filtering/grid-filtering.service'; import { IgxGridFilteringCellComponent } from './filtering/grid-filtering-cell.component'; @@ -1577,26 +1577,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ @HostBinding('attr.class') get hostClass(): string { - if (this.isCosy()) { - return 'igx-grid--cosy'; - } else if (this.isCompact()) { - return 'igx-grid--compact'; - } else { - return 'igx-grid'; - } + return this.getComponentDensityClass('igx-grid'); } get bannerClass(): string { - let bannerClass = ''; - if (this.isCosy()) { - bannerClass = 'igx-banner--cosy'; - } else if (this.isCompact()) { - bannerClass = 'igx-banner--compact'; - } else { - bannerClass = 'igx-banner'; - } - bannerClass += this.rowEditPositioningStrategy.isTop ? ' igx-banner__border-top' : ' igx-banner__border-bottom'; - return bannerClass; + const position = this.rowEditPositioningStrategy.isTop ? 'igx-banner__border-top' : 'igx-banner__border-bottom'; + return `${this.getComponentDensityClass('igx-banner')} ${position}`; } /** @@ -2288,6 +2274,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.summaryService.clearSummaryCache(); this._pipeTrigger++; this.markForCheck(); + if (this.transactions.getAggregatedChanges(false).length === 0) { + // Needs better check, calling 'transactions.clear()' will also trigger this + if (this.data.length % this.perPage === 0 && this.isLastPage && this.page !== 0) { + this.page--; + } + } }); } @@ -2302,8 +2294,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.initColumns(this.columnList, (col: IgxColumnComponent) => this.onColumnInit.emit(col)); this.columnListDiffer.diff(this.columnList); - this._derivePossibleHeight(); this.markForCheck(); + this._derivePossibleHeight(); this.columnList.changes .pipe(takeUntil(this.destroy$)) @@ -2493,12 +2485,13 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @memberof IgxGridBaseComponent */ get defaultRowHeight(): number { - if (this.isCosy()) { - return 40; - } else if (this.isCompact()) { - return 32; - } else { - return 50; + switch (this.displayDensity) { + case DisplayDensity.cosy: + return 40; + case DisplayDensity.compact: + return 32; + default: + return 50; } } @@ -2509,12 +2502,13 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @memberof IgxGridBaseComponent */ get defaultHeaderGroupMinWidth(): number { - if (this.isCosy()) { - return 32; - } else if (this.isCompact()) { - return 24; - } else { - return 48; + switch (this.displayDensity) { + case DisplayDensity.cosy: + return 32; + case DisplayDensity.compact: + return 24; + default: + return 48; } } @@ -3075,9 +3069,11 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.deleteRowFromData(rowId, index); this._pipeTrigger++; this.cdr.markForCheck(); - + // Data needs to be recalculated if transactions are in place + // If no transactions, `data` will be a reference to the grid getter, otherwise it will be stale + const dataAfterDelete = this.transactions.enabled ? this.dataWithAddedInTransactionRows : data; this.refreshSearch(); - if (data.length % this.perPage === 0 && this.isLastPage && this.page !== 0) { + if (dataAfterDelete.length % this.perPage === 0 && dataAfterDelete.length / this.perPage - 1 < this.page && this.page !== 0) { this.page--; } } @@ -3315,8 +3311,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @hidden */ public refreshGridState(args?) { - this.endEdit(true); - this.summaryService.clearSummaryCache(args); + this.endEdit(true); + this.summaryService.clearSummaryCache(args); } // TODO: We have return values here. Move them to event args ?? @@ -3558,7 +3554,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements if ((this._height && this._height.indexOf('%') === -1) || !this._height) { return; } - if (!this.nativeElement.parentNode.clientHeight) { + if (!this.nativeElement.parentNode || !this.nativeElement.parentNode.clientHeight) { const viewPortHeight = document.documentElement.clientHeight; this._height = this.rowBasedHeight <= viewPortHeight ? null : viewPortHeight.toString(); } else { @@ -3679,7 +3675,14 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements const columnsToSize = visibleChildColumns.length - columnsWithSetWidths.length; const sumExistingWidths = columnsWithSetWidths - .reduce((prev, curr) => prev + parseInt(curr.width, 10), 0); + .reduce((prev, curr) => { + const colWidth = curr.width; + const widthValue = parseInt(colWidth, 10); + const currWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1 ? + widthValue / 100 * computedWidth : + widthValue; + return prev + currWidth; + }, 0); const columnWidth = !Number.isFinite(sumExistingWidths) ? Math.max(computedWidth / columnsToSize, MINIMUM_COLUMN_WIDTH) : @@ -3692,24 +3695,32 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @hidden */ protected calculateGridWidth() { + let width; const computed = this.document.defaultView.getComputedStyle(this.nativeElement); + const el = this.document.getElementById(this.nativeElement.id); if (this._width && this._width.indexOf('%') !== -1) { /* width in %*/ - const width = computed.getPropertyValue('width').indexOf('%') === -1 ? - parseInt(computed.getPropertyValue('width'), 10) : - this.document.getElementById(this.nativeElement.id).offsetWidth; - - if (Number.isFinite(width) && width !== this.calcWidth) { - this.calcWidth = width; - - this.cdr.markForCheck(); - } + width = computed.getPropertyValue('width').indexOf('%') === -1 ? + parseInt(computed.getPropertyValue('width'), 10) : null; } else { - this.calcWidth = parseInt(this._width, 10); + width = parseInt(this._width, 10); + } + + if (!width && el) { + width = el.offsetWidth; } this._derivePossibleWidth(); + + if (!width) { + width = this.columnList.reduce((sum, item) => sum + parseInt((item.width || item.defaultWidth), 10), 0); + } + + if (Number.isFinite(width) && width !== this.calcWidth) { + this.calcWidth = width; + this.cdr.markForCheck(); + } } /** @@ -3795,7 +3806,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements protected _disableMultipleSummaries(expressions) { expressions.forEach((column) => { const columnName = column && column.fieldName ? column.fieldName : column; - this._summaries(columnName, false); }); + this._summaries(columnName, false); + }); } /** @@ -4021,7 +4033,13 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ public selectRows(rowIDs: any[], clearCurrentSelection?: boolean) { let newSelection: Set; - newSelection = this.selection.add_items(this.id, rowIDs, clearCurrentSelection); + let selectableRows = []; + if (this.transactions.enabled) { + selectableRows = rowIDs.filter(e => !this.gridAPI.row_deleted_transaction(this.id, e)); + } else { + selectableRows = rowIDs; + } + newSelection = this.selection.add_items(this.id, selectableRows, clearCurrentSelection); this.triggerRowSelectionChange(newSelection); } @@ -4461,16 +4479,20 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements private checkIfGridIsAdded(node): boolean { if (node === this.nativeElement) { return true; - } else { - for (const childNode of node.childNodes) { - const added = this.checkIfGridIsAdded(childNode); - if (added) { - return true; - } - } + } + + if (!node.childNodes) { return false; } + + for (const childNode of node.childNodes) { + const added = this.checkIfGridIsAdded(childNode); + if (added) { + return true; + } + } + return false; } /** diff --git a/projects/igniteui-angular/src/lib/grids/grid-toolbar.component.ts b/projects/igniteui-angular/src/lib/grids/grid-toolbar.component.ts index daf2ac100c4..4f22ea4b2af 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-toolbar.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-toolbar.component.ts @@ -208,13 +208,7 @@ export class IgxGridToolbarComponent extends DisplayDensityBase { @HostBinding('attr.class') get hostClass(): string { - if (this.isCosy()) { - return 'igx-grid-toolbar--cosy'; - } else if (this.isCompact()) { - return 'igx-grid-toolbar--compact'; - } else { - return 'igx-grid-toolbar'; - } + return this.getComponentDensityClass('igx-grid-toolbar'); } constructor(public gridAPI: GridBaseAPIService, diff --git a/projects/igniteui-angular/src/lib/grids/grid.common.ts b/projects/igniteui-angular/src/lib/grids/grid.common.ts index 419f62a948c..a971122b072 100644 --- a/projects/igniteui-angular/src/lib/grids/grid.common.ts +++ b/projects/igniteui-angular/src/lib/grids/grid.common.ts @@ -177,6 +177,12 @@ export class IgxColumnMovingService { rowID: any }; + public activeElement: { + tag: string, + column: IgxColumnComponent, + rowIndex: number + }; + get column(): IgxColumnComponent { return this._column; } @@ -275,6 +281,20 @@ export class IgxColumnMovingDragDirective extends IgxDragDirective { rowID: currSelection.rowID }; } + // tslint:disable-next-line:no-bitwise + if (document.activeElement.compareDocumentPosition(this.column.grid.nativeElement) & Node.DOCUMENT_POSITION_CONTAINS) { + if (parseInt(document.activeElement.getAttribute('data-visibleIndex'), 10) !== this.column.visibleIndex) { + (document.activeElement as HTMLElement).blur(); + return; + } + this.cms.activeElement = { + tag: document.activeElement.tagName.toLowerCase() === 'igx-grid-summary-cell' ? + document.activeElement.tagName.toLowerCase() : '', + column: this.column, + rowIndex: parseInt(document.activeElement.getAttribute('data-rowindex'), 10) + }; + (document.activeElement as HTMLElement).blur(); + } const args = { source: this.column @@ -528,12 +548,13 @@ export class IgxColumnMovingDropDirective extends IgxDropDirective implements On rowID: this.cms.selection.rowID, columnID: this.column.grid.columnList.toArray().indexOf(this.cms.selection.column) }])); - - const cell = this.column.grid.getCellByKey(this.cms.selection.rowID, this.cms.selection.column.field); - - if (cell) { - cell.nativeElement.focus(); - } + } + if (this.cms.activeElement) { + const gridEl = this.column.grid.nativeElement; + const activeEl = gridEl.querySelector(`${this.cms.activeElement.tag}[data-rowindex="${this.cms.activeElement.rowIndex}"]` + + `[data-visibleIndex="${this.cms.activeElement.column.visibleIndex}"]`); + if (activeEl) { activeEl.focus(); } + this.cms.activeElement = null; } this.column.grid.draggedColumn = null; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 77dea99fe0e..64890eb6d68 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -3,7 +3,7 @@
- +
diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts index 7afd742d3fe..a0f4c543b45 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts @@ -39,7 +39,8 @@ describe('IgxGrid - Summaries', () => { FilteringComponent, ColumnGroupFourLevelTestComponent, SummarieGroupByComponent, - SummarieGroupByWithScrollsComponent + SummarieGroupByWithScrollsComponent, + SummaryColumnsWithSpecificWidthsComponent ], imports: [BrowserAnimationsModule, IgxGridModule.forRoot(), NoopAnimationsModule] }) @@ -251,6 +252,26 @@ describe('IgxGrid - Summaries', () => { } })); + it('Last column summary cell should be aligned according to its data cells', ((() => { + const fixture = TestBed.createComponent(SummaryColumnsWithSpecificWidthsComponent); + fixture.detectChanges(); + + // Get last cell of first data row + const dataRow = fixture.debugElement.queryAll(By.css('igx-grid-row'))[0]; + const lastColumnNormalCell = dataRow.queryAll(By.css('igx-grid-cell'))[4]; + const lastColumnNormalCellRect = (lastColumnNormalCell.nativeElement).getBoundingClientRect(); + + // Get last summary cell of the summary row + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, 0); + const lastColumnSummaryCell = HelperUtils.getSummaryCellByVisibleIndex(summaryRow, 4); + const lastColumnSummaryCellRect = (lastColumnSummaryCell.nativeElement).getBoundingClientRect(); + + expect(lastColumnSummaryCellRect.left).toBe(lastColumnNormalCellRect.left, + 'summary cell and data cell are not left aligned'); + expect(lastColumnSummaryCellRect.right).toBe(lastColumnNormalCellRect.right, + 'summary cell and data cell are not right aligned'); + }))); + describe('', () => { let fix; let grid: IgxGridComponent; @@ -1514,8 +1535,8 @@ class EarliestSummary extends IgxDateSummaryOperand { - + [summaries]="earliest"> + ` }) @@ -1528,3 +1549,27 @@ export class CustomSummariesComponent { public dealsSummaryMinMax = DealsSummaryMinMax; public earliest = EarliestSummary; } + +@Component({ + template: ` + + + + + + + + + + + + + ` +}) +export class SummaryColumnsWithSpecificWidthsComponent { + + @ViewChild('grid1', { read: IgxGridComponent }) + public grid1: IgxGridComponent; + + public data = SampleTestData.foodProductData(); +} diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-toolbar.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-toolbar.spec.ts index ea4b7659df5..4cd132e6ea0 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-toolbar.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-toolbar.spec.ts @@ -425,7 +425,7 @@ describe('IgxGrid - Grid Toolbar', () => { fixture.detectChanges(); const toolbar = getToolbar(fixture).nativeElement; - expect(grid.toolbar.isComfortable()).toBe(true); + expect(grid.toolbar.displayDensity).toEqual(DisplayDensity.comfortable); expect(toolbar.classList[0]).toBe('igx-grid-toolbar'); expect(parseFloat(toolbar.offsetHeight) > 55).toBe(true); @@ -452,7 +452,7 @@ describe('IgxGrid - Grid Toolbar', () => { fixture.detectChanges(); const toolbar = getToolbar(fixture).nativeElement; - expect(grid.toolbar.isComfortable()).toBe(true); + expect(grid.toolbar.displayDensity).toEqual(DisplayDensity.comfortable); expect(toolbar.classList[0]).toBe('igx-grid-toolbar'); grid.displayDensity = DisplayDensity.compact; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts index 621c29b03ed..a88cd5842ba 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts @@ -26,6 +26,7 @@ import { IgxGridCellComponent } from '../cell.component'; import { TransactionType, Transaction } from '../../services'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { DefaultSortingStrategy } from '../../data-operations/sorting-strategy'; +import { IgxTabsModule, IgxTabsComponent } from '../../tabs'; const DEBOUNCETIME = 30; @@ -304,6 +305,7 @@ describe('IgxGrid Component Tests', () => { TestBed.configureTestingModule({ declarations: [ IgxGridDefaultRenderingComponent, + IgxGridColumnPercentageWidthComponent, IgxGridWrappedInContComponent, IgxGridFormattingComponent ], @@ -751,6 +753,16 @@ describe('IgxGrid Component Tests', () => { } }); }); + + it('Should calculate default column width when a column has width in %', () => { + const fix = TestBed.createComponent(IgxGridColumnPercentageWidthComponent); + fix.componentInstance.initColumnsRows(5, 3); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + expect(grid.columns[1].width).toEqual('150'); + expect(grid.columns[1].width).toEqual('150'); + }); }); describe('IgxGrid - API methods', () => { @@ -1793,11 +1805,12 @@ describe('IgxGrid Component Tests', () => { const targetCell = grid.getCellByColumn(0, 'ProductName'); targetCell.onFocus({}); - tick(); + tick(100); fixture.detectChanges(); expect(grid.endRowTransaction).toHaveBeenCalledTimes(1); expect(targetCell.focused).toBeTruthy(); - expect(firstCell.focused).toBeFalsy(); + expect(targetCell.selected).toBeTruthy(); + expect(firstCell.selected).toBeFalsy(); })); }); @@ -2865,6 +2878,105 @@ describe('IgxGrid Component Tests', () => { expect(targetRowElement.classList).toContain('igx-grid__tr--edited', 'row does not contain edited class w/ edits'); expect(targetCellElement.classList).toContain('igx-grid__td--edited', 'cell does not contain edited class w/ edits'); })); + + it('Should change pages when the only item on the last page is a pending added row that gets deleted', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxGridRowEditingTransactionComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + expect(grid.data.length).toEqual(10); + grid.paging = true; + grid.perPage = 5; + fixture.detectChanges(); + tick(); + expect(grid.totalPages).toEqual(2); + grid.addRow({ + ProductID: 123, + ProductName: 'DummyItem', + InStock: true, + UnitsInStock: 1, + OrderDate: new Date() + }); + fixture.detectChanges(); + tick(); + expect(grid.totalPages).toEqual(3); + grid.page = 2; + tick(); + fixture.detectChanges(); + expect(grid.page).toEqual(2); + grid.deleteRowById(123); + tick(); + fixture.detectChanges(); + // This is behaving incorrectly - if there is only 1 transaction and it is an ADD transaction on the last page + // Deleting the ADD transaction on the last page will trigger grid.page-- TWICE + expect(grid.page).toEqual(1); // Should be 1 + expect(grid.totalPages).toEqual(2); + })); + + it('Should change pages when commiting deletes on the last page', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxGridRowEditingTransactionComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + expect(grid.data.length).toEqual(10); + grid.paging = true; + grid.perPage = 5; + fixture.detectChanges(); + tick(); + expect(grid.totalPages).toEqual(2); + grid.page = 1; + tick(); + fixture.detectChanges(); + expect(grid.page).toEqual(1); + for (let i = 0; i < grid.data.length / 2; i++) { + grid.deleteRowById(grid.data.reverse()[i].ProductID); + } + fixture.detectChanges(); + tick(); + expect(grid.page).toEqual(1); + grid.transactions.commit(grid.data); + fixture.detectChanges(); + tick(); + expect(grid.page).toEqual(0); + expect(grid.totalPages).toEqual(1); + })); + + it('Should NOT change pages when deleting a row on the last page', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxGridRowEditingTransactionComponent); + fixture.detectChanges(); + const grid = fixture.componentInstance.grid; + grid.paging = true; + grid.perPage = 5; + fixture.detectChanges(); + tick(); + expect(grid.totalPages).toEqual(2); + expect(grid.data.length).toEqual(10); + grid.page = 1; + tick(); + fixture.detectChanges(); + expect(grid.page).toEqual(1); + grid.deleteRowById(grid.data[grid.data.length - 1].ProductID); + fixture.detectChanges(); + tick(); + expect(grid.page).toEqual(1); + expect(grid.totalPages).toEqual(2); + })); + + it('Should not allow selecting rows that are deleted', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxGridRowEditingTransactionComponent); + fixture.detectChanges(); + const grid = fixture.componentInstance.grid; + grid.rowSelectable = true; + fixture.detectChanges(); + + grid.deleteRowById(2); + grid.deleteRowById(3); + + fixture.detectChanges(); + grid.selectRows([2, 3, 4]); + fixture.detectChanges(); + expect(grid.selectedRows()).toEqual([4]); + })); }); describe('Row Editing - Grouping', () => { @@ -2984,6 +3096,35 @@ describe('IgxGrid Component Tests', () => { })); }); }); + + describe('IgxGrid - Integration with other Igx Controls', () => { + configureTestSuite(); + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + IgxGridInsideIgxTabsComponent + ], + imports: [ + NoopAnimationsModule, IgxGridModule, IgxTabsModule] + }).compileComponents(); + })); + + it('IgxTabs: should initialize a grid with correct width/height', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridInsideIgxTabsComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const tab = fix.componentInstance.tabs; + tab.tabs.toArray()[2].select(); + tick(100); + fix.detectChanges(); + const gridHeader = fix.debugElement.query(By.css('.igx-grid__thead')); + const gridBody = fix.debugElement.query(By.css('.igx-grid__tbody')); + expect(parseInt(window.getComputedStyle(gridHeader.nativeElement).width, 10)).toBe(400); + expect(parseInt(window.getComputedStyle(gridBody.nativeElement).width, 10)).toBe(400); + expect(parseInt(window.getComputedStyle(gridBody.nativeElement).height, 10)).toBe(510); + })); + }); }); @Component({ @@ -3033,7 +3174,7 @@ export class IgxGridTestComponent { public isVerticalScrollbarVisible() { const scrollbar = this.grid.verticalScrollContainer.getVerticalScroll(); - if (scrollbar) { + if (scrollbar && scrollbar.offsetHeight > 0) { return scrollbar.offsetHeight < scrollbar.children[0].offsetHeight; } return false; @@ -3118,6 +3259,20 @@ export class IgxGridDefaultRenderingComponent { } } +@Component({ + template: ` + + + ` +}) +export class IgxGridColumnPercentageWidthComponent extends IgxGridDefaultRenderingComponent { + public initColumns(column) { + if (column.index === 0) { + column.width = '40%'; + } + } +} + @Component({ template: `
@@ -3507,3 +3662,56 @@ export class IgxGridRowEditingWithFeaturesComponent extends DataParent { this.currentSortExpressions = sortExpr; } } + +@Component({ + template: ` +
+ + This is Tab 1 content. + This is Tab 2 content. + + + + + + + +
+ ` +}) +export class IgxGridInsideIgxTabsComponent { + + @ViewChild(IgxGridComponent, { read: IgxGridComponent }) + public grid: IgxGridComponent; + + @ViewChild(IgxTabsComponent, { read: IgxTabsComponent }) + public tabs: IgxTabsComponent; + + public columns = [ + { field: 'id', width: 100}, + { field: '1', width: 100}, + { field: '2', width: 100}, + { field: '3', width: 100} + ]; + + public data = []; + + constructor() { + const data = []; + for (let j = 1; j <= 10; j++) { + const item = {}; + item['id'] = j; + for (let k = 2, len = this.columns.length; k <= len; k++) { + const field = this.columns[k - 1].field; + item[field] = `item${j}-${k}`; + } + data.push(item); + } + this.data = data; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index ece859db917..57ae8b2cea5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -428,13 +428,7 @@ export class IgxGridComponent extends IgxGridBaseComponent implements OnInit, Do * @hidden */ get groupAreaHostClass(): string { - if (this.isCosy()) { - return 'igx-drop-area--cosy'; - } else if (this.isCompact()) { - return 'igx-drop-area--compact'; - } else { - return 'igx-drop-area'; - } + return this.getComponentDensityClass('igx-drop-area'); } /** diff --git a/projects/igniteui-angular/src/lib/grids/row.component.ts b/projects/igniteui-angular/src/lib/grids/row.component.ts index 9ab955de0b8..e382bf1871e 100644 --- a/projects/igniteui-angular/src/lib/grids/row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/row.component.ts @@ -160,7 +160,7 @@ export class IgxRowComponent implements DoCheck /** @hidden */ public get deleted(): boolean { - return this.isRowDeleted(); + return this.gridAPI.row_deleted_transaction(this.gridID, this.rowID); } public get inEditMode(): boolean { @@ -320,13 +320,4 @@ export class IgxRowComponent implements DoCheck const deletedClass = this.deleted ? 'igx-grid__tr--deleted' : ''; return `${this.defaultCssClass} ${indexClass} ${selectedClass} ${editClass} ${dirtyClass} ${deletedClass}`.trim(); } - - protected isRowDeleted(): boolean { - const state: State = this.grid.transactions.getState(this.rowID); - if (state) { - return state.type === TransactionType.DELETE; - } - - return false; - } } diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html index 6b429e970ff..07d845b7205 100644 --- a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html @@ -10,7 +10,7 @@
- {{ summary.label }} + {{ translateSummary(summary) }} {{ columnDatatype === 'number' ? (summary.summaryResult | igxdecimal) : columnDatatype === 'date' ? (summary.summaryResult | igxdate) : (summary.summaryResult) }} diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts index 60119a30d6a..fad89d290d3 100644 --- a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts @@ -145,22 +145,7 @@ export class IgxSummaryCellComponent { @HostBinding('style.max-width') @HostBinding('style.flex-basis') get width() { - const hasVerticalScroll = !this.grid.verticalScrollContainer.dc.instance.notVirtual; - const colWidth = this.column.width; - const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; - - if (colWidth && !isPercentageWidth) { - let cellWidth = this.isLastUnpinned && hasVerticalScroll ? - parseInt(colWidth, 10) - 18 + '' : colWidth; - - if (typeof cellWidth !== 'string' || cellWidth.endsWith('px') === false) { - cellWidth += 'px'; - } - - return cellWidth; - } else { - return colWidth; - } + return this.column.getCellWidth(); } get nativeElement(): any { @@ -193,4 +178,8 @@ export class IgxSummaryCellComponent { 'home', 'end', 'tab', 'space', ' ', 'spacebar'].indexOf(key) !== -1; } + + public translateSummary(summary: IgxSummaryResult): string { + return this.grid.resourceStrings[`igx_grid_summary_${summary.key}`] || summary.label; + } } diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html index 6081768bf16..c40a728dd5e 100644 --- a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html @@ -10,9 +10,9 @@ >
- + - + diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts index a8d49aa99aa..f7378e60751 100644 --- a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts @@ -70,17 +70,6 @@ export class IgxSummaryRowComponent implements DoCheck { return this.element.nativeElement; } - // TO DO: to be refactored when displayDensity refactoring is merged - get gridDensity(): string { - if (this.grid.isCosy()) { - return DisplayDensity.cosy; - } else if (this.grid.isCompact()) { - return DisplayDensity.compact; - } else { - return DisplayDensity.comfortable; - } - } - public getColumnSummaries(columnName) { if (!this.summaries.get(columnName)) { return []; diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts index 4cb76ce2ff2..e0058f34d7e 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts @@ -5,7 +5,7 @@ import { ITreeGridRecord } from './tree-grid.interfaces'; import { IRowToggleEventArgs } from './tree-grid.interfaces'; import { IgxColumnComponent } from '../column.component'; import { first } from 'rxjs/operators'; -import { HierarchicalTransaction, TransactionType } from '../../services'; +import { HierarchicalTransaction, TransactionType, State } from '../../services'; import { mergeObjects } from '../../core/utils'; export class IgxTreeGridAPIService extends GridBaseAPIService { @@ -174,4 +174,26 @@ export class IgxTreeGridAPIService extends GridBaseAPIService { expect(trans.add).toHaveBeenCalledTimes(2); expect(trans.add).toHaveBeenCalledWith(transPasrams, null); })); + + it('Should NOT select deleted rows through API - Hierarchical DS', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + + treeGrid.rowSelectable = true; + tick(); + fix.detectChanges(); + /** Select deleted row */ + treeGrid.deleteRowById(663); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + treeGrid.selectRows([663]); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + /** Select row with deleted parent */ + treeGrid.deleteRowById(147); + tick(); + fix.detectChanges(); + // 147 -> 475 + treeGrid.selectRows([475]); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + })); + + it('Should NOT select deleted rows through API - Flat DS', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + + treeGrid.rowSelectable = true; + tick(); + fix.detectChanges(); + /** Select deleted row */ + treeGrid.deleteRowById(6); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + treeGrid.selectRows([6]); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + /** Select row with deleted parent */ + treeGrid.deleteRowById(10); + tick(); + fix.detectChanges(); + // 10 -> 9 + treeGrid.selectRows([9]); + tick(); + fix.detectChanges(); + expect(treeGrid.selectedRows()).toEqual([]); + })); }); describe('Multi-column header', () => { diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 2fc3a915e4b..a32f9f7e41b 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -1,6 +1,6 @@
- +
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts index f96f54714ad..e81df6fa60c 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts @@ -3,7 +3,6 @@ import { IgxTreeGridComponent } from './tree-grid.component'; import { IgxRowComponent } from '../row.component'; import { ITreeGridRecord } from './tree-grid.interfaces'; import { IgxTreeGridAPIService } from './tree-grid-api.service'; -import { State, TransactionType } from '../../services'; import { GridBaseAPIService } from '../api.service'; import { IgxSelectionAPIService } from '../../core/selection'; @@ -86,27 +85,4 @@ export class IgxTreeGridRowComponent extends IgxRowComponent { + + describe('IgxTreeGrid - default rendering for rows and columns', () => { + configureTestSuite(); + let fix; + let grid: IgxTreeGridComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + IgxTreeGridWrappedInContComponent + ], + imports: [ + NoopAnimationsModule, IgxTreeGridModule] + }).compileComponents(); + })); + + beforeEach(async(() => { + fix = TestBed.createComponent(IgxTreeGridWrappedInContComponent); + grid = fix.componentInstance.treeGrid; + })); + + it('should match width and height of parent container when width/height are set in %', fakeAsync(() => { + fix.componentInstance.outerWidth = 800; + fix.componentInstance.outerHeight = 600; + grid.width = '50%'; + grid.height = '50%'; + tick(); + fix.detectChanges(); + + expect(window.getComputedStyle(grid.nativeElement).height).toMatch('300px'); + expect(window.getComputedStyle(grid.nativeElement).width).toMatch('400px'); + expect(grid.rowList.length).toBeGreaterThan(0); + })); + + it('should render 10 records if height is unset and parent container\'s height is unset', () => { + fix.detectChanges(); + const defaultHeight = fix.debugElement.query(By.css('.igx-grid__tbody')).styles.height; + expect(defaultHeight).not.toBeNull(); + expect(parseInt(defaultHeight, 10)).toBeGreaterThan(400); + expect(fix.componentInstance.isVerticalScrollbarVisible()).toBeTruthy(); + expect(grid.rowList.length).toBeGreaterThanOrEqual(10); + }); + + it('should render 10 records if height is 100% and parent container\'s height is unset', fakeAsync(() => { + grid.height = '600px'; + tick(); + fix.detectChanges(); + const defaultHeight = fix.debugElement.query(By.css('.igx-grid__tbody')).styles.height; + expect(defaultHeight).not.toBeNull(); + expect(parseInt(defaultHeight, 10)).toBeGreaterThan(400); + expect(fix.componentInstance.isVerticalScrollbarVisible()).toBeTruthy(); + expect(grid.rowList.length).toBeGreaterThanOrEqual(10); + })); + + it(`should render all records exactly if height is 100% and parent container\'s height is unset and + there are fewer than 10 records in the data view`, fakeAsync(() => { + grid.height = '100%'; + fix.componentInstance.data = fix.componentInstance.data.slice(0, 1); + tick(); + fix.detectChanges(); + const defaultHeight = fix.debugElement.query(By.css('.igx-grid__tbody')).styles.height; + expect(defaultHeight).not.toBeNull(); + expect(parseInt(defaultHeight, 10)).toBeGreaterThan(200); + expect(fix.componentInstance.isVerticalScrollbarVisible()).toBeFalsy(); + expect(grid.rowList.length).toEqual(6); + })); + + it(`should render 11 records if height is 100% and parent container\'s height is unset and + display density is changed`, fakeAsync(() => { + grid.height = '100%'; + fix.componentInstance.density = DisplayDensity.compact; + tick(); + fix.detectChanges(); + const defaultHeight = fix.debugElement.query(By.css('.igx-grid__tbody')).styles.height; + const defaultHeightNum = parseInt(defaultHeight, 10); + expect(defaultHeight).not.toBeNull(); + expect(defaultHeightNum).toBeGreaterThan(300); + expect(defaultHeightNum).toBeLessThan(330); + expect(fix.componentInstance.isVerticalScrollbarVisible()).toBeTruthy(); + expect(grid.rowList.length).toEqual(11); + })); + }); + +}); diff --git a/projects/igniteui-angular/src/lib/input-group/input-group.component.ts b/projects/igniteui-angular/src/lib/input-group/input-group.component.ts index bb410ff8e51..9f5532f5f5a 100644 --- a/projects/igniteui-angular/src/lib/input-group/input-group.component.ts +++ b/projects/igniteui-angular/src/lib/input-group/input-group.component.ts @@ -222,7 +222,7 @@ export class IgxInputGroupComponent extends DisplayDensityBase implements IgxInp */ @HostBinding('class.igx-input-group--cosy') get isDisplayDensityCosy() { - return this.isCosy(); + return this.displayDensity === DisplayDensity.cosy; } /** @@ -230,7 +230,7 @@ export class IgxInputGroupComponent extends DisplayDensityBase implements IgxInp */ @HostBinding('class.igx-input-group--comfortable') get isDisplayDensityComfortable() { - return this.isComfortable(); + return this.displayDensity === DisplayDensity.comfortable; } /** @@ -238,7 +238,7 @@ export class IgxInputGroupComponent extends DisplayDensityBase implements IgxInp */ @HostBinding('class.igx-input-group--compact') get isDisplayDensityCompact() { - return this.isCompact(); + return this.displayDensity === DisplayDensity.compact; } /** diff --git a/projects/igniteui-angular/src/lib/progressbar/README.md b/projects/igniteui-angular/src/lib/progressbar/README.md index ff4ba52ae08..5b4f153d056 100644 --- a/projects/igniteui-angular/src/lib/progressbar/README.md +++ b/projects/igniteui-angular/src/lib/progressbar/README.md @@ -43,6 +43,7 @@ export class AppModule {} | `textVisibility` | boolean | Set the text to be visible. By default is set to `true`. | | `textTop` | boolean | Set the position that defene is text to be aligned above the progress line. By default is set to `false`. | | `text` | string | Set a custom text that is displayed according defined position. | +| `indeterminate` | boolean | Display the indicator continually growing and shrinking along the track. | ## igx-circular-bar | Name | Type | Description | |:----------|:-------------:|:------| @@ -51,6 +52,7 @@ export class AppModule {} | `value` | number | Set value that indicates the completed bar position. | | `animate` | boolean | animation on progress bar. | | `textVisibility` | boolean | Set the text to be visible. By default is set to `true`. | +| `indeterminate` | boolean | Display the indicator continually growing and shrinking along the track. | ## Common | Name | Description | |:----------|:------| diff --git a/projects/igniteui-angular/src/lib/progressbar/circularbar.component.spec.ts b/projects/igniteui-angular/src/lib/progressbar/circularbar.component.spec.ts index 79cbcb08f94..ca6bd20a09f 100644 --- a/projects/igniteui-angular/src/lib/progressbar/circularbar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/progressbar/circularbar.component.spec.ts @@ -12,6 +12,12 @@ import { Common } from './common.spec'; import { configureTestSuite } from '../test-utils/configure-suite'; +const CIRCULAR_INNER_CLASS = 'igx-circular-bar__inner'; +const CIRCULAR_OUTER_CLASS = 'igx-circular-bar__outer'; +const CIRCULAR_TEXT_CLASS = 'igx-circular-bar__text'; +const CIRCULAR_HIDDEN_TEXT_CLASS = 'igx-circular-bar__text--hidden'; +const CIRCULAR_INDETERMINATE_CLASS = 'igx-circular-bar--indeterminate'; + describe('IgCircularBar', () => { configureTestSuite(); const tickTime = 2000; @@ -26,7 +32,7 @@ describe('IgCircularBar', () => { IgxProgressBarModule ] }) - .compileComponents(); + .compileComponents(); })); it('Initialize circularProgressbar with default values', () => { @@ -35,12 +41,9 @@ describe('IgCircularBar', () => { fixture.detectChanges(); const progress = fixture.componentInstance.circularBar; - const domProgress = fixture.debugElement.query(By.css('igx-circular-bar')).nativeElement; - const value = 0; const defaultMaxValue = 100; expect(progress.id).toContain('igx-circular-bar-'); - expect(domProgress.id).toContain('igx-circular-bar-'); expect(progress.max).toBe(defaultMaxValue); expect(progress.value).toBe(0); }); @@ -277,7 +280,7 @@ describe('IgCircularBar', () => { }); it(`when step value is not divisble to passed value the result returned from the - value getter should be as same as the passed one`, fakeAsync(() => { + value getter should be the same as the passed one`, fakeAsync(() => { const fix = TestBed.createComponent(InitCircularProgressBarComponent); fix.detectChanges(); @@ -308,20 +311,40 @@ describe('IgCircularBar', () => { fixture.detectChanges(); const componentInstance = fixture.componentInstance; - const progressBarElem = fixture.debugElement.nativeElement - .querySelector('.progress-circular'); + const progressBarElem = fixture.debugElement.query(By.css('svg')).nativeElement; + fixture.detectChanges(); expect(progressBarElem.attributes['aria-valuenow'].textContent).toBe('20'); - expect(progressBarElem.children[0].classList.value).toBe('progress-circular__innercircle'); - expect(progressBarElem.children[1].classList.value).toBe('progress-circular__circle'); + expect(progressBarElem.children[0].classList.value).toBe(CIRCULAR_INNER_CLASS); + expect(progressBarElem.children[1].classList.value).toBe(CIRCULAR_OUTER_CLASS); expect(progressBarElem.children[2].children.length).toBe(2); expect(progressBarElem.children[2].children[0].textContent.trim()).toBe('Value is:'); expect(progressBarElem.children[2].children[1].textContent.trim()).toMatch('20'); componentInstance.progressbar.textVisibility = false; fixture.detectChanges(); - expect(progressBarElem.children[2].classList.value).toMatch('progress-circular__text--hidden'); + expect(progressBarElem.children[2].classList.value).toMatch(CIRCULAR_HIDDEN_TEXT_CLASS); + }); + + it('When indeterminate mode is on value should not be updated', () => { + const fix = TestBed.createComponent(InitCircularProgressBarComponent); + fix.detectChanges(); + + const progressbar = fix.componentInstance.circularBar; + expect(progressbar.value).toEqual(0); + + progressbar.indeterminate = true; + progressbar.value = 20; + fix.detectChanges(); + + expect(progressbar.value).toEqual(0); + + progressbar.animate = false; + progressbar.value = 30; + fix.detectChanges(); + + expect(progressbar.value).toEqual(0); }); // UI TESTS @@ -332,8 +355,7 @@ describe('IgCircularBar', () => { fixture.detectChanges(); const componentInstance = fixture.componentInstance; - const progressBarElem = fixture.debugElement.nativeElement - .querySelector('.progress-circular'); + const progressBarElem = fixture.debugElement.query(By.css('svg')).nativeElement; let expectedTextContent = componentInstance.circularBar.value + '%'; tick(tickTime); @@ -342,9 +364,9 @@ describe('IgCircularBar', () => { expect(progressBarElem.attributes['aria-valuenow'].textContent).toBe(componentInstance.value.toString()); expect(progressBarElem.attributes['aria-valuemax'].textContent).toBe(componentInstance.max.toString()); - expect(progressBarElem.children[0].classList.value).toBe('progress-circular__innercircle'); - expect(progressBarElem.children[1].classList.value).toBe('progress-circular__circle'); - expect(progressBarElem.children[2].children[0].classList.value).toBe('progress-circular__text'); + expect(progressBarElem.children[0].classList.value).toBe(CIRCULAR_INNER_CLASS); + expect(progressBarElem.children[1].classList.value).toBe(CIRCULAR_OUTER_CLASS); + expect(progressBarElem.children[2].children[0].classList.value).toBe(CIRCULAR_TEXT_CLASS); expect(progressBarElem.children[2].children[0].textContent.trim()).toMatch(expectedTextContent); componentInstance.circularBar.text = 'No progress'; @@ -356,7 +378,7 @@ describe('IgCircularBar', () => { componentInstance.circularBar.textVisibility = false; fixture.detectChanges(); - expect(progressBarElem.children[2].classList.value).toMatch('progress-circular__text--hidden'); + expect(progressBarElem.children[2].classList.value).toMatch(CIRCULAR_HIDDEN_TEXT_CLASS); })); it('The max representation should respond correctly to passed maximum value', fakeAsync(() => { @@ -364,8 +386,7 @@ describe('IgCircularBar', () => { fixture.detectChanges(); const componentInstance = fixture.componentInstance; - const progressBarElem = fixture.debugElement.nativeElement - .querySelector('.progress-circular'); + const progressBarElem = fixture.debugElement.query(By.css('svg')).nativeElement; tick(tickTime); fixture.detectChanges(); @@ -378,9 +399,6 @@ describe('IgCircularBar', () => { fixture.detectChanges(); expect(progressBarElem.attributes['aria-valuemax'].textContent).toBe(componentInstance.max.toString()); - expect(progressBarElem.children[0].classList.value).toBe('progress-circular__innercircle'); - expect(progressBarElem.children[1].classList.value).toBe('progress-circular__circle'); - expect(progressBarElem.children[2].children[0].classList.value).toBe('progress-circular__text'); })); it('Manipulate progressbar with floating point numbers', fakeAsync(() => { @@ -397,8 +415,8 @@ describe('IgCircularBar', () => { fix.detectChanges(); const progressRepresentation = Common.calcPercentage(val, maxVal); - const progressBarElem = fix.debugElement.query(By.css('.progress-circular')); - const valueInPercent = progressBarElem.query(By.css('.progress-circular__text')).nativeElement; + const progressBarElem = fix.debugElement.query(By.css('svg')); + const valueInPercent = progressBarElem.query(By.css(`.${CIRCULAR_TEXT_CLASS}`)).nativeElement; expect(valueInPercent.textContent.trim()).toBe(`${progressRepresentation}%`); })); @@ -411,15 +429,29 @@ describe('IgCircularBar', () => { const value = 2.55; bar.step = 0.634; bar.max = maxVal; - bar.value = value; + bar.value = value; tick(tickTime + tickTime); // enough time to exceed the progress update. fix.detectChanges(); - const progressBarContainer = fix.debugElement.query(By.css('.progress-circular')).nativeElement; + const progressBarContainer = fix.debugElement.query(By.css('svg')).nativeElement; expect(parseFloat(progressBarContainer.attributes['aria-valuenow'].textContent)).toBe(value); expect(bar.value).toBe(value); })); + + it('When enable indeterminate mode, then the appropriate class should be applied.', () => { + const fix = TestBed.createComponent(InitCircularProgressBarComponent); + fix.detectChanges(); + + const bar = fix.debugElement.nativeElement.querySelector('igx-circular-bar'); + expect(bar.classList.contains(CIRCULAR_INDETERMINATE_CLASS)).toEqual(false); + + const barComponent = fix.componentInstance.circularBar; + barComponent.indeterminate = true; + fix.detectChanges(); + + expect(bar.classList.contains(CIRCULAR_INDETERMINATE_CLASS)).toEqual(true); + }); }); }); @Component({ template: `` }) diff --git a/projects/igniteui-angular/src/lib/progressbar/linearbar.component.spec.ts b/projects/igniteui-angular/src/lib/progressbar/linearbar.component.spec.ts index 83d2e1e8527..26a36eacc35 100644 --- a/projects/igniteui-angular/src/lib/progressbar/linearbar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/progressbar/linearbar.component.spec.ts @@ -11,6 +11,14 @@ import { Common } from './common.spec'; import { configureTestSuite } from '../test-utils/configure-suite'; +const SUCCESS_TYPE_CLASS = 'igx-linear-bar--success'; +const INFO_TYPE_CLASS = 'igx-linear-bar--info'; +const WARNING_TYPE_CLASS = 'igx-linear-bar--warning'; +const DANGER_TYPE_CLASS = 'igx-linear-bar--danger'; +const STRIPED_CLASS = 'igx-linear-bar--striped'; +const LINEAR_BAR_TAG = 'igx-linear-bar'; +const INDETERMINATE_CLASS = 'igx-linear-bar--indeterminate'; + describe('IgLinearBar', () => { configureTestSuite(); const tickTime = 2000; @@ -30,7 +38,7 @@ describe('IgLinearBar', () => { fixture.detectChanges(); const progress = fixture.componentInstance.linearBar; - const domProgress = fixture.debugElement.query(By.css('igx-linear-bar')).nativeElement; + const domProgress = fixture.debugElement.query(By.css(LINEAR_BAR_TAG)).nativeElement; const defaultMaxValue = 100; const defaultValue = 0; const defaultStriped = false; @@ -144,7 +152,7 @@ describe('IgLinearBar', () => { it('Should update value when we try to decrese it', fakeAsync(() => { const fixture = TestBed.createComponent(LinearBarComponent); fixture.detectChanges(); - const progressBar = fixture.componentInstance.linearBar; + const progressBar = fixture.componentInstance.progressbar; let expectedValue = 50; fixture.componentInstance.value = expectedValue; @@ -167,7 +175,7 @@ describe('IgLinearBar', () => { it('Should update value when we try to decrease it (without animation)', () => { const fixture = TestBed.createComponent(LinearBarComponent); - const progressBar = fixture.componentInstance.linearBar; + const progressBar = fixture.componentInstance.progressbar; let expectedValue = 50; fixture.componentInstance.animate = false; @@ -187,24 +195,24 @@ describe('IgLinearBar', () => { const fix = TestBed.createComponent(LinearBarComponent); fix.detectChanges(); - const datepicker = fix.componentInstance.linearBar; + const progressbar = fix.componentInstance.linearBar; const expectedRes = fix.componentInstance.value; tick(tickTime); fix.detectChanges(); - expect(datepicker.value).toEqual(expectedRes); + expect(progressbar.value).toEqual(expectedRes); - datepicker.value = '0345-234'; + progressbar.value = '0345-234'; tick(tickTime); fix.detectChanges(); - expect(datepicker.value).toEqual(expectedRes); + expect(progressbar.value).toEqual(expectedRes); })); it('The update step is 1% of the maximum value, which prevents from slow update with big nums', () => { const fix = TestBed.createComponent(LinearBarComponent); fix.detectChanges(); - const bar = fix.componentInstance.linearBar; + const bar = fix.componentInstance.progressbar; const ONE_PERCENT = 0.01; let expectedValue = bar.max * ONE_PERCENT; expect(bar.step).toBe(expectedValue); @@ -225,7 +233,7 @@ describe('IgLinearBar', () => { fix.detectChanges(); tick(tickTime); - const bar = compInstance.linearBar; + const bar = compInstance.progressbar; const expectedRes = 0; expect(bar.value).toBe(expectedRes); expect(bar.valueInPercent).toBe(expectedRes); @@ -248,7 +256,7 @@ describe('IgLinearBar', () => { compInstance.value = value; fix.detectChanges(); - const bar = compInstance.linearBar; + const bar = compInstance.progressbar; tick(tickTime); expect(bar.value).toBe(max); expect(bar.valueInPercent).toBe(100); @@ -325,6 +333,26 @@ describe('IgLinearBar', () => { expect(bar.valueInPercent).toBe(valueInPercent); })); + it('When indeterminate mode is on value should not be updated', () => { + const fix = TestBed.createComponent(InitLinearProgressBarComponent); + fix.detectChanges(); + + const progressbar = fix.componentInstance.linearBar; + expect(progressbar.value).toEqual(0); + + progressbar.indeterminate = true; + progressbar.value = 30; + fix.detectChanges(); + + expect(progressbar.value).toEqual(0); + + progressbar.animate = false; + progressbar.value = 20; + fix.detectChanges(); + + expect(progressbar.value).toEqual(0); + }); + // UI Tests describe('UI tests linear bar', () => { configureTestSuite(); @@ -333,70 +361,62 @@ describe('IgLinearBar', () => { fixture.detectChanges(); const componentInstance = fixture.componentInstance; - const progressBarContainer = - fixture.debugElement.nativeElement.querySelector('.progress-linear__bar'); - const progressBarElem = progressBarContainer.querySelector('[class*=\'progress-linear__bar-progress\']'); + const linearBar = fixture.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); + const progressIndicator = linearBar.querySelector('.igx-linear-bar__indicator'); tick(tickTime); fixture.detectChanges(); - expect(progressBarElem.style.width).toBe(componentInstance.value + '%'); - expect(progressBarContainer.attributes['aria-valuenow'].textContent).toBe(componentInstance.value.toString()); + expect(progressIndicator.style.width).toBe(componentInstance.value + '%'); + expect(linearBar.attributes['aria-valuenow'].textContent).toBe(componentInstance.value.toString()); })); it('Should change class suffix which would be relevant to the type that has been passed', () => { const fixture = TestBed.createComponent(LinearBarComponent); fixture.detectChanges(); - const progressBarContainer = - fixture.debugElement.nativeElement.querySelector('.progress-linear__bar'); - const progressBarElem = progressBarContainer.querySelector('[class*=\'progress-linear__bar-progress\']'); + const linearBar = + fixture.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); - expect(progressBarElem.classList.contains('progress-linear__bar-progress--default')).toBe(true); + expect(linearBar.classList.length).toEqual(1); + expect(linearBar.classList.contains(LINEAR_BAR_TAG)).toEqual(true); fixture.componentInstance.type = 'info'; fixture.detectChanges(); - expect(progressBarElem.classList.contains('progress-linear__bar-progress--info')).toBe(true); + expect(linearBar.classList.contains(INFO_TYPE_CLASS)).toEqual(true); }); it('Change progressbar style to be striped', () => { const fixture = TestBed.createComponent(LinearBarComponent); fixture.detectChanges(); - const progressElem = fixture.debugElement.nativeElement - .getElementsByClassName('progress-linear')[0]; + const linerBar = fixture.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); - expect(progressElem.classList.contains('progress-linear--striped')).toBe(false); + expect(linerBar.classList.contains(STRIPED_CLASS)).toEqual(false); fixture.componentInstance.striped = true; fixture.detectChanges(); - expect(progressElem.classList.contains('progress-linear--striped')).toBe(true); + expect(linerBar.classList.contains(STRIPED_CLASS)).toEqual(true); }); it('should stay striped when the type has changed', () => { const fixture = TestBed.createComponent(LinearBarComponent); fixture.detectChanges(); - const progressElem = fixture.debugElement.nativeElement - .getElementsByClassName('progress-linear')[0]; - - const progressBarContainer = - fixture.debugElement.nativeElement.querySelector('.progress-linear__bar'); - const progressBarElem = progressBarContainer.querySelector('[class*=\'progress-linear__bar-progress\']'); + const linearBar = fixture.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); fixture.componentInstance.striped = true; fixture.detectChanges(); - expect(progressBarElem.classList.contains('progress-linear__bar-progress--default')).toBe(true); - expect(progressElem.classList.contains('progress-linear--striped')).toBe(true); + expect(linearBar.classList.contains(STRIPED_CLASS)).toEqual(true); fixture.componentInstance.type = 'success'; fixture.detectChanges(); - expect(progressBarElem.classList.contains('progress-linear__bar-progress--success')).toBe(true); - expect(progressElem.classList.contains('progress-linear--striped')).toBe(true); + expect(linearBar.classList.contains(SUCCESS_TYPE_CLASS)).toEqual(true); + expect(linearBar.classList.contains(STRIPED_CLASS)).toEqual(true); }); it('Manipulate progressbar with floating point numbers', fakeAsync(() => { @@ -413,8 +433,9 @@ describe('IgLinearBar', () => { fix.detectChanges(); const progressRepresentation = Common.calcPercentage(val, maxVal); - const getProgressIndicator = fix.debugElement.query(By.css(`[class*='progress-linear__bar-progress']`)); - expect(getProgressIndicator.styles.width).toBe(`${progressRepresentation}%`); + const progressbar = fix.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); + const progressIndicator = progressbar.querySelector('.igx-linear-bar__indicator'); + expect(progressIndicator.style.width).toBe(`${progressRepresentation}%`); })); it('Prevent constant update of progress value when value and max value differ', fakeAsync(() => { @@ -431,10 +452,23 @@ describe('IgLinearBar', () => { tick(tickTime + tickTime); // enough time to exceed the progress update. fix.detectChanges(); - const progressBarContainer = fix.debugElement.query(By.css('.progress-linear__bar')).nativeElement; + const progressBarContainer = fix.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); expect(parseFloat(progressBarContainer.attributes['aria-valuenow'].textContent)).toBe(value); expect(bar.value).toBe(value); })); + + it('When enable indeterminate mode, then the appropriate class should be applied.', () => { + const fix = TestBed.createComponent(LinearBarComponent); + fix.detectChanges(); + + const bar = fix.debugElement.nativeElement.querySelector(LINEAR_BAR_TAG); + expect(bar.classList.contains(INDETERMINATE_CLASS)).toEqual(false); + + fix.componentInstance.progressbar.indeterminate = true; + + fix.detectChanges(); + expect(bar.classList.contains(INDETERMINATE_CLASS)).toEqual(true); + }); }); }); @@ -451,7 +485,6 @@ class InitLinearProgressBarComponent {
` }) class LinearBarComponent { @ViewChild(IgxLinearProgressBarComponent) public progressbar: IgxLinearProgressBarComponent; - @ViewChild('wrapper') public wrapper; @ViewChild('linearBar') public linearBar; public value: string | number = 30; diff --git a/projects/igniteui-angular/src/lib/progressbar/progressbar.component.ts b/projects/igniteui-angular/src/lib/progressbar/progressbar.component.ts index 11d324420fa..bc02e2f8137 100644 --- a/projects/igniteui-angular/src/lib/progressbar/progressbar.component.ts +++ b/projects/igniteui-angular/src/lib/progressbar/progressbar.component.ts @@ -1,17 +1,14 @@ import { CommonModule } from '@angular/common'; import { - AfterViewInit, Component, ElementRef, EventEmitter, HostBinding, Input, NgModule, - OnChanges, Output, Renderer2, ViewChild, - TemplateRef, ContentChild } from '@angular/core'; import { IgxProcessBarTextTemplateDirective } from './progressbar.common'; @@ -25,6 +22,13 @@ export enum IgxTextAlign { END = 'end' } +export enum IgxProgressType { + DANGER = 'danger', + INFO = 'info', + WARNING = 'warning', + SUCCESS = 'success' +} + export interface IChangeProgressEventArgs { previousValue: number; currentValue: number; @@ -110,8 +114,8 @@ export abstract class BaseProgress { this.updateProgress(val); cancelAnimationFrame(this.requestAnimationId); } else if (this.isInLimitRange(progressValue, passedValue, step)) { - this.updateProgress(val); - cancelAnimationFrame(this.requestAnimationId); + this.updateProgress(val); + cancelAnimationFrame(this.requestAnimationId); } else { this.valueInPercent = progressValue; this.requestAnimationId = requestAnimationFrame(() => this.updateProgressSmoothly.call(this, val, step)); @@ -184,77 +188,6 @@ let NEXT_CIRCULAR_ID = 0; }) export class IgxLinearProgressBarComponent extends BaseProgress { - /**An @Input property that sets the value of `id` attribute. If not provided it will be automatically generated. - * ```html - * - * ``` - */ - @HostBinding('attr.id') - @Input() - public id = `igx-linear-bar-${NEXT_LINEAR_ID++}`; - - /** - *Set the position that defines where the text is aligned. - Possible options - `IgxTextAlign.START` (default), `IgxTextAlign.CENTER`, `IgxTextAlign.END`. - *```typescript - *public positionCenter: IgxTextAlign; - *public ngOnInit() { - * this.positionCenter = IgxTextAlign.CENTER; - *} - * //... - *``` - * ```html - * - *``` - */ - @Input() - public textAlign: IgxTextAlign = IgxTextAlign.START; - - /** - *Set the text to be visible. By default it is set to true. - * ```html - * - *``` - */ - @Input() - public textVisibility = true; - - /** - *Set the position that defines if the text should be aligned above the progress line. By default is set to false. - *```html - * - *``` - */ - @Input() - public textTop = false; - - /** - *Set a custom text that is displayed according to the defined position. - * ```html - * - *``` - */ - @Input() - public text: string; - - /** - *Set `IgxLinearProgressBarComponent` to have striped style. By default it is set to false. - *```html - * - *``` - */ - @Input() - public striped = false; - - /** - *Set type of the `IgxLinearProgressBarComponent`. Possible options - `default`, `success`, `info`, `warning`, and `danger`. - *```html - * - *``` - */ - @Input() - public type = 'default'; - /** *Animation on progress `IgxLinearProgressBarComponent`. By default it is set to true. *```html @@ -287,6 +220,7 @@ export class IgxLinearProgressBarComponent extends BaseProgress { * *``` */ + @HostBinding('attr.aria-valuemax') @Input() set max(maxNum: number) { this._max = maxNum; @@ -337,7 +271,109 @@ export class IgxLinearProgressBarComponent extends BaseProgress { this._step = Number(val); } + constructor() { + super(); + } + + @HostBinding('attr.aria-valuemin') + public valueMin = 0; + + @HostBinding('class.igx-linear-bar') + public cssClass = 'igx-linear-bar'; + + /** + *Set `IgxLinearProgressBarComponent` to have striped style. By default it is set to false. + *```html + * + *``` + */ + @HostBinding('class.igx-linear-bar--striped') + @Input() + public striped = false; + + /** + *Set `IgxLinearProgressBarComponent` to have indeterminate. By default it is set to false. + *```html + * + *``` + */ + @HostBinding('class.igx-linear-bar--indeterminate') + @Input() + public indeterminate = false; + + /**An @Input property that sets the value of the `role` attribute. If not provided it will be automatically set to `progressbar`. + * ```html + * + * ``` + */ + @HostBinding('attr.role') + @Input() + public role = 'progressbar'; + + /**An @Input property that sets the value of `id` attribute. If not provided it will be automatically generated. + * ```html + * + * ``` + */ + @HostBinding('attr.id') + @Input() + public id = `igx-linear-bar-${NEXT_LINEAR_ID++}`; + + /** + *Set the position that defines where the text is aligned. + Possible options - `IgxTextAlign.START` (default), `IgxTextAlign.CENTER`, `IgxTextAlign.END`. + *```typescript + *public positionCenter: IgxTextAlign; + *public ngOnInit() { + * this.positionCenter = IgxTextAlign.CENTER; + *} + * //... + *``` + * ```html + * + *``` + */ + @Input() + public textAlign: IgxTextAlign = IgxTextAlign.START; + + /** + *Set the text to be visible. By default it is set to true. + * ```html + * + *``` + */ + @Input() + public textVisibility = true; + /** + *Set the position that defines if the text should be aligned above the progress line. By default is set to false. + *```html + * + *``` + */ + @Input() + public textTop = false; + + /** + *Set a custom text that is displayed according to the defined position. + * ```html + * + *``` + */ + @Input() + public text: string; + + /** + *Set type of the `IgxLinearProgressBarComponent`. Possible options - `default`, `success`, `info`, `warning`, and `danger`. + *```html + * + *``` + */ + + @Input() + public type = 'default'; + + /** *Returns value that indicates the current `IgxLinearProgressBarComponent` position. *```typescript *@ViewChild("MyProgressBar") @@ -348,6 +384,7 @@ export class IgxLinearProgressBarComponent extends BaseProgress { *} *``` */ + @HostBinding('attr.aria-valuenow') @Input() get value(): number { return this._value; @@ -361,7 +398,7 @@ export class IgxLinearProgressBarComponent extends BaseProgress { */ set value(val) { val = Number(val); - if (this._value === val) { + if (this._value === val || this.indeterminate) { return; } @@ -398,8 +435,36 @@ export class IgxLinearProgressBarComponent extends BaseProgress { */ @Output() public onProgressChanged = new EventEmitter(); - constructor() { - super(); + /** + * @hidden + */ + @HostBinding('class.igx-linear-bar--danger') + public get danger() { + return this.type === IgxProgressType.DANGER; + } + + /** + * @hidden + */ + @HostBinding('class.igx-linear-bar--info') + public get info() { + return this.type === IgxProgressType.INFO; + } + + /** + * @hidden + */ + @HostBinding('class.igx-linear-bar--warning') + public get warning() { + return this.type === IgxProgressType.WARNING; + } + + /** + * @hidden + */ + @HostBinding('class.igx-linear-bar--success') + public get success() { + return this.type === IgxProgressType.SUCCESS; } } @@ -412,6 +477,10 @@ export class IgxCircularProgressBarComponent extends BaseProgress { private readonly STROKE_OPACITY_DVIDER = 100; private readonly STROKE_OPACITY_ADDITION = .2; + /** @hidden */ + @HostBinding('class.igx-circular-bar') + public cssClass = 'igx-circular-bar'; + /** *An event, which is triggered after a progress is changed. *```typescript @@ -437,6 +506,16 @@ export class IgxCircularProgressBarComponent extends BaseProgress { @Input() public id = `igx-circular-bar-${NEXT_CIRCULAR_ID++}`; + /** + *An @Input property that sets the value of the `indeterminate` attribute. If not provided it will be automatically set to false. + *```html + * + *``` + */ + @HostBinding('class.igx-circular-bar--indeterminate') + @Input() + public indeterminate = false; + /** *Sets the text visibility. By default it is set to true. *```html @@ -466,7 +545,7 @@ export class IgxCircularProgressBarComponent extends BaseProgress { */ public get context(): any { return { - $implicit: { value: this.value, valueInPercent: this.valueInPercent, max: this.max} + $implicit: { value: this.value, valueInPercent: this.valueInPercent, max: this.max } }; } @@ -582,7 +661,7 @@ export class IgxCircularProgressBarComponent extends BaseProgress { */ set value(val: number) { val = Number(val); - if (this._value === val) { + if (this._value === val || this.indeterminate) { return; } diff --git a/projects/igniteui-angular/src/lib/progressbar/templates/circular-bar.component.html b/projects/igniteui-angular/src/lib/progressbar/templates/circular-bar.component.html index 537895cb1c6..ae9b8f5208d 100644 --- a/projects/igniteui-angular/src/lib/progressbar/templates/circular-bar.component.html +++ b/projects/igniteui-angular/src/lib/progressbar/templates/circular-bar.component.html @@ -1,13 +1,19 @@ - - - - + + + + - {{textContent ? textContent: valueInPercent + '%'}} + {{textContent ? textContent: valueInPercent + '%'}} - \ No newline at end of file + diff --git a/projects/igniteui-angular/src/lib/progressbar/templates/linear-bar.component.html b/projects/igniteui-angular/src/lib/progressbar/templates/linear-bar.component.html index 8409ef2b14c..b3b2b1eb8d3 100644 --- a/projects/igniteui-angular/src/lib/progressbar/templates/linear-bar.component.html +++ b/projects/igniteui-angular/src/lib/progressbar/templates/linear-bar.component.html @@ -1,12 +1,15 @@ -
-
-
-
-
- - {{text ? text : valueInPercent + '%'}} - +
+
+ + + {{text ? text : valueInPercent + '%'}} + diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index b5249922387..f645446d478 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -19,6 +19,7 @@ import { configureTestSuite } from '../../test-utils/configure-suite'; import { IgxTreeGridPrimaryForeignKeyComponent } from '../../test-utils/tree-grid-components.spec'; import { IgxTreeGridModule, IgxTreeGridComponent } from '../../grids/tree-grid'; import { IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; +import { wait } from '../../test-utils/ui-interactions.spec'; describe('Excel Exporter', () => { configureTestSuite(); @@ -449,6 +450,53 @@ describe('Excel Exporter', () => { fix.detectChanges(); await exportAndVerify(treeGrid, options, actualData.treeGridDataExpDepth(0)); }); + + it('should throw an exception when nesting level is greater than 8.', async () => { + const nestedData = SampleTestData.employeePrimaryForeignKeyTreeData(); + for (let i = 1; i < 9; i++) { + nestedData[i - 1].ID = i; + nestedData[i - 1].ParentID = i - 1; + } + nestedData.push({ ID: 9, ParentID: 8, Name: 'Test', JobTitle: '', Age: 49 }); + treeGrid.data = nestedData; + fix.detectChanges(); + await wait(16); + + let error = ''; + try { + exporter.export(treeGrid, options); + await wait(); + } catch (ex) { + error = ex.message; + } + expect(error).toMatch('Can create an outline of up to eight levels!'); + + treeGrid.deleteRowById(9); + fix.detectChanges(); + await wait(16); + + error = ''; + try { + exporter.export(treeGrid, options); + await wait(); + } catch (ex) { + error = ex.message; + } + expect(error).toEqual(''); + + treeGrid.addRow({ ID: 9, ParentID: 8, Name: 'Test', JobTitle: '', Age: 49 }); + fix.detectChanges(); + await wait(16); + + error = ''; + try { + exporter.export(treeGrid, options); + await wait(); + } catch (ex) { + error = ex.message; + } + expect(error).toMatch('Can create an outline of up to eight levels!'); + }); }); function getExportedData(grid, exportOptions: IgxExcelExporterOptions) { diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index b963347131b..d6745a60789 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -1,17 +1,10 @@ import * as JSZip from 'jszip/dist/jszip'; -import { CommonModule } from '@angular/common'; -import { Directive, EventEmitter, Injectable, NgModule, Output } from '@angular/core'; - +import { EventEmitter, Injectable, Output } from '@angular/core'; import { ExcelElementsFactory } from './excel-elements-factory'; import { ExcelFolderTypes } from './excel-enums'; import { IgxExcelExporterOptions } from './excel-exporter-options'; - -import { - IExcelFile, - IExcelFolder -} from './excel-interfaces'; - +import { IExcelFolder } from './excel-interfaces'; import { IgxBaseExporter } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { WorksheetData } from './worksheet-data'; @@ -45,8 +38,6 @@ export interface IExcelExportEndedEventArgs { export class IgxExcelExporterService extends IgxBaseExporter { private static ZIP_OPTIONS = { compression: 'DEFLATE', type: 'base64' }; - private static DATA_URL_PREFIX = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,'; - private _xlsx: JSZip; /** @@ -75,6 +66,16 @@ export class IgxExcelExporterService extends IgxBaseExporter { } protected exportDataImplementation(data: any[], options: IgxExcelExporterOptions): void { + if (this._isTreeGrid) { + let maxLevel = 0; + data.forEach((r) => { + maxLevel = Math.max(maxLevel, r.originalRowData.level); + }); + if (maxLevel > 7) { + throw Error('Can create an outline of up to eight levels!'); + } + } + const worksheetData = new WorksheetData(data, options, this._indexOfLastPinnedColumn, this._sort, this._isTreeGrid); this._xlsx = new JSZip(); diff --git a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts index 42b22eb17f9..1f3d768d405 100644 --- a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts +++ b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts @@ -192,6 +192,7 @@ export abstract class IgxBaseExporter { } private prepareData(grid: any, options: IgxExporterOptionsBase): any[] { + this.flatRecords = []; let rootRecords = grid.rootRecords; this._isTreeGrid = rootRecords !== undefined; diff --git a/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts b/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts index 786dd331a4d..a79ffaa3d57 100644 --- a/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts +++ b/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts @@ -2,6 +2,7 @@ import { HierarchicalTransaction, HierarchicalState, TransactionType } from './t import { Injectable } from '@angular/core'; import { IgxTransactionService } from './igx-transaction'; import { DataUtil } from '../../data-operations/data-util'; +import { cloneValue } from '../../core/utils'; /** @experimental @hidden */ @Injectable() @@ -11,7 +12,7 @@ export class IgxHierarchicalTransactionService { - const value = mergeChanges ? this.mergeValues(state.recordRef, state.value) : state.value; + const value = mergeChanges ? this.mergeValues(state.recordRef, state.value) : cloneValue(state.value); this.clearArraysFromObject(value); result.push({ id: key, path: state.path, newValue: value, type: state.type } as T); }); diff --git a/projects/igniteui-angular/src/lib/slider/slider.component.spec.ts b/projects/igniteui-angular/src/lib/slider/slider.component.spec.ts index b6da2a6bb88..5bbb5ca8616 100644 --- a/projects/igniteui-angular/src/lib/slider/slider.component.spec.ts +++ b/projects/igniteui-angular/src/lib/slider/slider.component.spec.ts @@ -24,7 +24,7 @@ describe('IgxSlider', () => { })); describe('Base tests', () => { - configureTestSuite(); + configureTestSuite(); let fixture: ComponentFixture; let slider: IgxSliderComponent; @@ -152,7 +152,7 @@ describe('IgxSlider', () => { slider.value = 45; fixture.detectChanges(); - expect(fixture.componentInstance.slider.value).toBe(20); + expect(fixture.componentInstance.slider.value).toBe(40); }); it('should not set upper value to outside bounds slider when slider is RANGE', () => { @@ -178,7 +178,7 @@ describe('IgxSlider', () => { fixture.detectChanges(); expect(slider.value.lower).toBe(20); - expect(slider.value.upper).toBe(30); + expect(slider.value.upper).toBe(40); }); it('should not set value upper when is less than lower value when slider is RANGE', () => { @@ -227,7 +227,7 @@ describe('IgxSlider', () => { upper: 30 }; fixture.detectChanges(); - expect(slider.value.lower).toBe(20); + expect(slider.value.lower).toBe(10); expect(slider.value.upper).toBe(30); }); @@ -321,6 +321,28 @@ describe('IgxSlider', () => { expect(Math.round(slider.value as number)).toBe(60); })); + it('Value should remain to the max one if it exceeds.', () => { + const fix = TestBed.createComponent(SliderMinMaxComponent); + fix.detectChanges(); + + const sliderRef = fix.componentInstance.slider; + let expectedVal = 150; + let expectedMax = 300; + + expect(sliderRef.value).toEqual(expectedVal); + expect(sliderRef.maxValue).toEqual(expectedMax); + + expectedVal = 250; + expectedMax = 200; + sliderRef.maxValue = expectedMax; + sliderRef.value = expectedVal; + fix.detectChanges(); + + expect(sliderRef.value).not.toEqual(expectedVal); + expect(sliderRef.value).toEqual(expectedMax); + expect(sliderRef.maxValue).toEqual(expectedMax); + }); + function panRight(element, elementHeight, elementWidth, duration) { const panOptions = { deltaX: elementWidth * 0.6, @@ -628,6 +650,27 @@ describe('IgxSlider', () => { expect((slider.value as IRangeSliderValue).upper).toBe(7); }); + it('Lower and upper bounds should not exceed min and max values', () => { + const fix = TestBed.createComponent(SliderTestComponent); + fix.detectChanges(); + + const componentInst = fix.componentInstance; + const slider = componentInst.slider; + const expectedMinVal = 0; + const expectedMaxVal = 10; + + expect(slider.minValue).toEqual(expectedMinVal); + expect(slider.maxValue).toEqual(expectedMaxVal); + + const expectedLowerBound = -1; + const expectedUpperBound = 11; + slider.lowerBound = expectedLowerBound; + slider.upperBound = expectedUpperBound; + + expect(slider.lowerBound).toEqual(expectedMinVal); + expect(slider.upperBound).toEqual(expectedMaxVal); + }); + describe('EditorProvider', () => { it('Should return correct edit element (single)', () => { const fixture = TestBed.createComponent(SliderInitializeTestComponent); diff --git a/projects/igniteui-angular/src/lib/slider/slider.component.ts b/projects/igniteui-angular/src/lib/slider/slider.component.ts index 50215fa5e64..3f156ccc067 100644 --- a/projects/igniteui-angular/src/lib/slider/slider.component.ts +++ b/projects/igniteui-angular/src/lib/slider/slider.component.ts @@ -158,7 +158,6 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, @ViewChild('thumbTo') private thumbTo: ElementRef; - private _minValue = 0; // Measures & Coordinates private width = 0; @@ -173,6 +172,7 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, // From/upperValue in percent values private hasViewInit = false; private timer; + private _minValue = 0; private _maxValue = 100; private _lowerBound?: number; private _upperBound?: number; @@ -306,7 +306,7 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, return; } - this._lowerBound = value; + this._lowerBound = this.valueInRange(value, this.minValue, this.maxValue); } /** @@ -342,7 +342,7 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, return; } - this._upperBound = value; + this._upperBound = this.valueInRange(value, this.minValue, this.maxValue); } /** @@ -370,9 +370,7 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, *``` */ public set lowerValue(value: number) { - if (value < this.lowerBound || this.upperBound < value) { - return; - } + value = this.valueInRange(value, this.lowerBound, this.upperBound); if (this.isRange && value > this.upperValue) { return; @@ -406,9 +404,7 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, *``` */ public set upperValue(value: number) { - if (value < this.lowerBound || this.upperBound < value) { - return; - } + value = this.valueInRange(value, this.lowerBound, this.upperBound); if (this.isRange && value < this.lowerValue) { return; @@ -725,6 +721,10 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, ); } + private valueInRange(value, min = 0, max = 100) { + return Math.max(Math.min(value, max), min); + } + private invalidateValue() { if (!this.isRange) { if (this.value >= this._lowerBound && this.value <= this._upperBound) { @@ -885,11 +885,11 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, } private setPointerPercent() { - this.pPointer = this.limit(this.toFixed(this.xPointer / this.width)); + this.pPointer = this.valueInRange(this.toFixed(this.xPointer / this.width), this.pMin, this.pMax); } private valueToFraction(value: number) { - return this.limit((value - this.minValue) / (this.maxValue - this.minValue)); + return this.valueInRange((value - this.minValue) / (this.maxValue - this.minValue), this.pMin, this.pMax); } private fractionToValue(fraction: number): number { @@ -899,14 +899,6 @@ export class IgxSliderComponent implements ControlValueAccessor, EditorProvider, return (max - min) * fraction + min; } - private fractionToPercent(fraction: number): number { - return this.toFixed(fraction * 100); - } - - private limit(num: number): number { - return Math.max(this.pMin, Math.min(num, this.pMax)); - } - private updateTrack() { const fromPosition = this.valueToFraction(this.lowerValue); const toPosition = this.valueToFraction(this.upperValue); diff --git a/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts b/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts index b64ff56aca3..82b1bf321b8 100644 --- a/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts @@ -142,7 +142,9 @@ export class ColumnDefinitions { - +
+ +
diff --git a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts index f74d1130cb5..90bee09c0eb 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts @@ -2,7 +2,7 @@ import { Component, ViewChild } from '@angular/core'; import { IgxTreeGridComponent } from '../grids/tree-grid/tree-grid.component'; import { SampleTestData } from './sample-test-data.spec'; import { IgxNumberSummaryOperand, IgxSummaryResult } from '../grids'; -import { IgxTransactionService, IgxHierarchicalTransactionService } from '../../public_api'; +import { IgxTransactionService, IgxHierarchicalTransactionService, DisplayDensity } from '../../public_api'; import { IgxGridTransaction } from '../grids/grid-base.component'; @Component({ @@ -389,3 +389,55 @@ export class IgxTreeGridRowEditingHierarchicalDSTransactionComponent { @ViewChild('treeGrid', { read: IgxTreeGridComponent }) public treeGrid: IgxTreeGridComponent; public paging = false; } + +@Component({ + template: + `
+ + + + + + +
` +}) + +export class IgxTreeGridWrappedInContComponent { + @ViewChild(IgxTreeGridComponent) public treeGrid: IgxTreeGridComponent; + public data = SampleTestData.employeeTreeData(); + + public height = null; + public paging = false; + public pageSize = 5; + public density = DisplayDensity.comfortable; + public outerWidth = 800; + public outerHeight: number; + + public isHorizontalScrollbarVisible() { + const scrollbar = this.treeGrid.parentVirtDir.getHorizontalScroll(); + if (scrollbar) { + return scrollbar.offsetWidth < scrollbar.children[0].offsetWidth; + } + + return false; + } + + public getVerticalScrollHeight() { + const scrollbar = this.treeGrid.verticalScrollContainer.getVerticalScroll(); + if (scrollbar) { + return parseInt(scrollbar.style.height, 10); + } + + return 0; + } + + public isVerticalScrollbarVisible() { + const scrollbar = this.treeGrid.verticalScrollContainer.getVerticalScroll(); + if (scrollbar && scrollbar.offsetHeight > 0) { + return scrollbar.offsetHeight < scrollbar.children[0].offsetHeight; + } + return false; + } + +} 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/avatar/avatar.sample.html b/src/app/avatar/avatar.sample.html index 37c13eba090..df5c74820f2 100644 --- a/src/app/avatar/avatar.sample.html +++ b/src/app/avatar/avatar.sample.html @@ -10,7 +10,7 @@

Circular Avatars

- +
@@ -34,7 +34,7 @@

Circular Avatars

- +
diff --git a/src/app/combo/combo.sample.html b/src/app/combo/combo.sample.html index 13ea53673cc..1302cf063b0 100644 --- a/src/app/combo/combo.sample.html +++ b/src/app/combo/combo.sample.html @@ -64,9 +64,9 @@

Display Density

- - - + + +

Search Input

diff --git a/src/app/combo/combo.sample.ts b/src/app/combo/combo.sample.ts index 8b19ea49b65..b9df9298355 100644 --- a/src/app/combo/combo.sample.ts +++ b/src/app/combo/combo.sample.ts @@ -1,5 +1,5 @@ import { Component, ViewChild, OnInit, TemplateRef } from '@angular/core'; -import { IgxComboComponent } from 'igniteui-angular'; +import { IgxComboComponent, DisplayDensity } from 'igniteui-angular'; import { take } from 'rxjs/operators'; import { NgModule } from '@angular/core'; import { FormGroup, FormControl, Validators, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -44,8 +44,13 @@ export class ComboSampleComponent implements OnInit { @ViewChild('customItemTemplate', {read: TemplateRef}) private customItemTemplate; private initialItemTemplate: TemplateRef = null; - constructor() { + comfortable = DisplayDensity.comfortable; + cosy = DisplayDensity.cosy; + compact = DisplayDensity.compact; + + + constructor() { const division = { 'New England 01': ['Connecticut', 'Maine', 'Massachusetts'], 'New England 02': ['New Hampshire', 'Rhode Island', 'Vermont'], @@ -126,7 +131,7 @@ export class ComboSampleComponent implements OnInit { this.initialItemTemplate = comboTemplate; } - setDensity(density: string) { + setDensity(density: DisplayDensity) { this.igxCombo.displayDensity = density; } } 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/progressbar/progressbar.sample.css b/src/app/progressbar/progressbar.sample.css index 73d5c7c6359..158fc149e17 100644 --- a/src/app/progressbar/progressbar.sample.css +++ b/src/app/progressbar/progressbar.sample.css @@ -5,15 +5,17 @@ .circular-container { display: flex; - width: 84px; - height: 84px; - padding: 16px; + margin: 16px; } -.linear-container + .linear-container{ - padding-top: 16px; +.circular-sample { + width: 54px; + height: 54px; } +.linear-container+.linear-container { + padding-top: 16px; +} .button-container { position: fixed; diff --git a/src/app/progressbar/progressbar.sample.html b/src/app/progressbar/progressbar.sample.html index 5b9c49d566c..290cf992cb0 100644 --- a/src/app/progressbar/progressbar.sample.html +++ b/src/app/progressbar/progressbar.sample.html @@ -6,7 +6,7 @@

Linear progress bar

- +
@@ -29,6 +29,11 @@

Linear progress bar

+ +
+ + +
@@ -63,29 +68,32 @@

Striped linear progress bar

Circular progress indicator

- +
- +
- +
- + +
+
+
- +
- +
- + Value is: {{process.value}} @@ -93,10 +101,10 @@

Circular progress indicator

- +
- +
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; }