From 3f987a8cda4aa0c5ffe42671f8b3493efabb958a Mon Sep 17 00:00:00 2001 From: Peter Kornuishin Date: Tue, 3 Jul 2018 19:26:16 +0300 Subject: [PATCH 01/11] feat(navbar): collapse optimisation --- src/lib-dev/navbar/module.ts | 2 +- src/lib-dev/navbar/styles.scss | 4 + src/lib-dev/navbar/template.html | 4 +- src/lib/core/utils/utils.ts | 17 +++ src/lib/navbar/_navbar-base.scss | 5 + src/lib/navbar/navbar.component.spec.ts | 53 +++++--- src/lib/navbar/navbar.component.ts | 157 +++++++++++++++++------- 7 files changed, 176 insertions(+), 66 deletions(-) diff --git a/src/lib-dev/navbar/module.ts b/src/lib-dev/navbar/module.ts index 6a0775677..c09846de4 100644 --- a/src/lib-dev/navbar/module.ts +++ b/src/lib-dev/navbar/module.ts @@ -34,7 +34,7 @@ export class NavbarDemoComponent { } collapsedNavbarWidthChange() { - this.navbar.collapse(); + this.navbar.updateCollapsed(); } onItemClick(event: MouseEvent) { diff --git a/src/lib-dev/navbar/styles.scss b/src/lib-dev/navbar/styles.scss index ca308d7a1..1f7484e90 100644 --- a/src/lib-dev/navbar/styles.scss +++ b/src/lib-dev/navbar/styles.scss @@ -16,3 +16,7 @@ mc-navbar.small-navbar > .mc-navbar { [mc-dropdown] > [mc-icon^='mc-angle'] { margin-left: 8px; } + +.logo { + margin-top: 5px; +} diff --git a/src/lib-dev/navbar/template.html b/src/lib-dev/navbar/template.html index 2ea712a17..cfc28698a 100644 --- a/src/lib-dev/navbar/template.html +++ b/src/lib-dev/navbar/template.html @@ -7,7 +7,7 @@

Common example

- + Brand @@ -72,7 +72,7 @@

Collapse example

- + Brand diff --git a/src/lib/core/utils/utils.ts b/src/lib/core/utils/utils.ts index da1ee539c..1828eab1c 100644 --- a/src/lib/core/utils/utils.ts +++ b/src/lib/core/utils/utils.ts @@ -8,3 +8,20 @@ export function toBoolean(value: any): boolean { export function isNotNil(value: any): boolean { return value !== 'undefined' && value !== null; } + +export function debounce(f: (...args) => any, ms: number) { + let timer: any; // at-loader expects Timer type here but there should be number by spec + + return (...args) => { + const onComplete = () => { + f.apply(this, args); + timer = null; + }; + + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(onComplete, ms); + }; +} diff --git a/src/lib/navbar/_navbar-base.scss b/src/lib/navbar/_navbar-base.scss index c549c9a9f..a5771ed0d 100644 --- a/src/lib/navbar/_navbar-base.scss +++ b/src/lib/navbar/_navbar-base.scss @@ -5,6 +5,7 @@ $mc-navbar-height: 48px; $mc-navbar-item-h-padding: 16px; $mc-navbar-brand-h-padding: 12px; $mc-navbar-title-icon-space: 8px; +$mc-navbar-icon-min-width: 15px; %mc-navbar-container-base { height: 100%; @@ -32,6 +33,10 @@ $mc-navbar-title-icon-space: 8px; margin-left: $mc-navbar-title-icon-space; } + [mc-icon] { + min-width: 15px; + } + mc-navbar-title:not(.mc-navbar-collapsed-title) + [mc-icon] { margin-left: $mc-navbar-title-icon-space; } diff --git a/src/lib/navbar/navbar.component.spec.ts b/src/lib/navbar/navbar.component.spec.ts index 979dd1087..e1c4ceab1 100644 --- a/src/lib/navbar/navbar.component.spec.ts +++ b/src/lib/navbar/navbar.component.spec.ts @@ -3,54 +3,75 @@ import { fakeAsync, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { McNavbarModule, McNavbar } from './index'; +import { McIconModule } from './../icon/icon.module'; +const FONT_RENDER_TIMEOUT_MS = 10; + describe('McNavbar', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [McNavbarModule], + imports: [McNavbarModule, McIconModule], declarations: [TestApp] }); TestBed.compileComponents(); })); - it('should be collapsed on init stage', () => { + it('should be collapsed on init stage', (done) => { const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); - const collapsedElements = fixture.debugElement.queryAll(By.css('.mc-navbar-collapsed-title')); + // Note: setTimeout - please see the issue about font rendering time + setTimeout(() => { + fixture.detectChanges(); + const collapsedElements = fixture.debugElement.queryAll(By.css('.mc-navbar-collapsed-title')); - expect(collapsedElements.length).toBeGreaterThan(0); + expect(collapsedElements.length).toBeGreaterThan(0); + done(); + }, FONT_RENDER_TIMEOUT_MS); }); - it('collapsed elements should have title', () => { + it('collapsed elements should have title', (done) => { const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); - const items = fixture.debugElement.queryAll(By.css('mc-navbar-item')); - const collapsedElements = items.filter((item) => - item.nativeElement.querySelectorAll('.mc-navbar-collapsed-title').length > 0); + // Note: setTimeout - please see the issue about font rendering time + setTimeout(() => { + fixture.detectChanges(); + + const items = fixture.debugElement.queryAll(By.css('mc-navbar-item')); + const collapsedElements = items.filter((item) => + item.nativeElement.querySelectorAll('.mc-navbar-collapsed-title').length > 0); - const hasTitle = collapsedElements.reduce((acc, el) => acc && el.nativeElement.hasAttribute('title'), true); + const hasTitle = collapsedElements.reduce((acc, el) => acc && el.nativeElement.hasAttribute('title'), true); - expect(hasTitle).toBeTruthy(); + expect(hasTitle).toBeTruthy(); + done(); + }, FONT_RENDER_TIMEOUT_MS); }); - it('collapsed elements should have specific title if defined', () => { + it('collapsed elements should have specific title if defined', (done) => { const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); - const items = fixture.debugElement.queryAll(By.css('mc-navbar-item')); - const collapsedElements = items.filter((item) => - item.nativeElement.querySelectorAll('.mc-navbar-collapsed-title').length > 0); + // Note: setTimeout - please see the issue about font rendering time + setTimeout(() => { + fixture.detectChanges(); + + const items = fixture.debugElement.queryAll(By.css('mc-navbar-item')); + const collapsedElements = items.filter((item) => + item.nativeElement.querySelectorAll('.mc-navbar-collapsed-title').length > 0); + + const elementWithCustomTitle = collapsedElements[collapsedElements.length - 1]; - const elementWithCustomTitle = collapsedElements[collapsedElements.length - 1]; + expect(elementWithCustomTitle.nativeElement.getAttribute('title')).toBe('customTitle'); - expect(elementWithCustomTitle.nativeElement.getAttribute('title')).toBe('customTitle'); + done(); + }, FONT_RENDER_TIMEOUT_MS); }); it('items should allow click if not disable', () => { diff --git a/src/lib/navbar/navbar.component.ts b/src/lib/navbar/navbar.component.ts index f1450d926..1e2f6d20a 100644 --- a/src/lib/navbar/navbar.component.ts +++ b/src/lib/navbar/navbar.component.ts @@ -1,5 +1,5 @@ import { - AfterViewInit, +AfterViewInit, Component, Directive, ElementRef, HostBinding, @@ -10,9 +10,10 @@ import { } from '@angular/core'; import { FocusMonitor } from '@ptsecurity/cdk/a11y'; -import { CanDisable, mixinDisabled } from '@ptsecurity/mosaic/core'; +import { CanDisable, debounce, mixinDisabled } from '@ptsecurity/mosaic/core'; +const COLLAPSED_CLASS: string = 'mc-navbar-collapsed-title'; const MC_DROPDOWN = 'mc-dropdown'; const MC_ICON = 'mc-icon'; const MC_NAVBAR = 'mc-navbar'; @@ -57,7 +58,7 @@ export const _McNavbarMixinBase = mixinDisabled(McNavbarItemBase); @Component({ selector: MC_NAVBAR_ITEM, template: ` - + @@ -127,22 +128,79 @@ export class McNavbarContainer { } } +class CachedCollapsedItemWidth { + collapsed: boolean = false; + + constructor( + public element: HTMLElement, + public width: number + ) {} +} + +class CachedItemWidth { + get canCollapse(): boolean { + return this.itemsForCollapse.length > 0; + } + + private get title(): string { + const calculatedTitle = this.element.getAttribute('calculatedTitle'); + + return calculatedTitle + ? decodeURI(calculatedTitle) + : (this.itemsForCollapse.length > 0 ? this.itemsForCollapse[0].element.innerText : ''); + } + + constructor( + public element: HTMLElement, + public width: number, + public itemsForCollapse: CachedCollapsedItemWidth[] = [] + ) {} + + setCollapsed(collapsed: boolean): number { + if (this.itemsForCollapse.length > 0) { + if (collapsed) { + this.element.setAttribute('title', this.title); + } else { + this.element.removeAttribute('title'); + } + } + + let res = 0; + + for (const subItem of this.itemsForCollapse) { + res += subItem.width; + + if (collapsed) { + subItem.element.classList.add(COLLAPSED_CLASS); + } else { + subItem.element.classList.remove(COLLAPSED_CLASS); + } + + subItem.collapsed = collapsed; + } + + return res; + } +} + + @Component({ selector: MC_NAVBAR, template: ` -