From a753bee9a66f8146a6087294f24e4907985b0e95 Mon Sep 17 00:00:00 2001 From: Sergey Andrievskiy Date: Thu, 18 Jan 2018 19:25:41 +0300 Subject: [PATCH] fix(menu): remove hardcoded max-height (#122) Closes #65 --- e2e/menu.e2e-spec.ts | 268 ++++++++++-------- src/framework/theme/components/helpers.ts | 7 + .../components/menu/menu-item.component.html | 6 +- .../theme/components/menu/menu.component.scss | 11 +- .../theme/components/menu/menu.component.ts | 134 ++++++--- .../theme/components/menu/menu.service.ts | 79 ++++-- 6 files changed, 308 insertions(+), 197 deletions(-) diff --git a/e2e/menu.e2e-spec.ts b/e2e/menu.e2e-spec.ts index d0199d02fc..5e1f1020c5 100644 --- a/e2e/menu.e2e-spec.ts +++ b/e2e/menu.e2e-spec.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { browser, element, by } from 'protractor'; +import { browser, element, by, ExpectedConditions as ec } from 'protractor'; import { hasClass } from './e2e-helper'; @@ -24,6 +24,8 @@ const newMenu = by.css('nb-menu ul li:nth-child(5) a'); const addButton = by.css('#addBtn'); const homeButton = by.css('#homeBtn'); +const waitTime = 20 * 1000; + describe('nb-menu', () => { beforeEach((done) => { @@ -94,125 +96,151 @@ describe('nb-menu', () => { }); }); - // it('should be selected - Menu #3.1', () => { - // expect(hasClass(element.all(menu3SubMenu).first(), 'collapsed')).toBeTruthy(); - // - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu31).first().getText() - // .then(val => { - // expect(val).toEqual('Menu #3.1'); - // }); - // - // element.all(menu31).first().click() - // .then(() => { - // expect(browser.getCurrentUrl()).toContain('#/menu/3/1'); - // }); - // }); - // }); - // - // it('should be selected - Menu #3.2', () => { - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu32).first().getText() - // .then(val => { - // expect(val).toEqual('Menu #3.2'); - // }); - // - // element.all(menu32).first().click() - // .then(() => { - // expect(browser.getCurrentUrl()).toContain('#/menu/3/2'); - // }); - // }); - // }); - - // TODO: Fix test - // it('should be expanded - Menu #3.3', () => { - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu33).first().getText() - // .then(val => { - // expect(val).toEqual('Menu #3.3'); - // }); - - // element.all(menu33).first().click() - // .then(() => { - // expect(hasClass(element.all(menu33SubMenu).first(), 'expanded')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/1'); - - // element.all(menu33).first().click() - // .then(() => { - // expect(hasClass(element.all(menu33SubMenu).first(), 'collapsed')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/1'); - // }); - // }); - // }); - // }); - - // it('should be selected - Menu #3.3.1', () => { - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu33).first().click() - // .then(() => { - // element.all(menu331).first().getText() - // .then(val => { - // expect(val).toEqual('Menu #3.3.1'); - // }); - - // element.all(menu331).first().click() - // .then(() => { - // expect(hasClass(element.all(menu331).first(), 'active')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/3/3/1'); - // }); - // }); - // }); - // }); - - // it('should be selected - Menu #3.3.2', () => { - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu33).first().click() - // .then(() => { - // element.all(menu332).first().getText() - // .then(val => { - // expect(val).toEqual('Menu #3.3.2'); - // }); - - // element.all(menu332).first().click() - // .then(() => { - // expect(hasClass(element.all(menu332).first(), 'active')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/3/3/2'); - // }); - // }); - // }); - // }); - - // it('should be selected - Menu #3.3.3', () => { - // element.all(menu3).first().click() - // .then(() => { - // element.all(menu33).first().click() - // .then(() => { - // element.all(menu333).first().getText() - // .then(val => { - // expect(val).toEqual('@nebular/theme'); - // }); - - // element.all(menu333).first().click() - // .then(() => { - // expect(hasClass(element.all(menu333).first(), 'active')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/1'); - // }); - // }); - // }); - // }); - - // it('should be selected - Menu #3.2.2', () => { - // element(homeButton).click() - // .then(() => { - // expect(hasClass(element.all(menu332).first(), 'active')).toBeTruthy(); - // expect(browser.getCurrentUrl()).toContain('#/menu/3/3/2'); - // }); - // }); + it('should be selected - Menu #3.1', () => { + expect(hasClass(element.all(menu3SubMenu).first(), 'collapsed')).toBeTruthy(); + + element.all(menu3).first().click() + .then(() => { + const menu31el = element.all(menu31).first(); + browser.wait(ec.elementToBeClickable(menu31el), waitTime); + + menu31el.getText() + .then(val => { + expect(val).toEqual('Menu #3.1'); + }); + + menu31el.click() + .then(() => { + expect(browser.getCurrentUrl()).toContain('#/menu/3/1'); + }); + }); + }); + + it('should be selected - Menu #3.2', () => { + element.all(menu3).first().click() + .then(() => { + const menu32el = element.all(menu32).first(); + browser.wait(ec.elementToBeClickable(menu32el), waitTime); + + menu32el.getText() + .then(val => { + expect(val).toEqual('Menu #3.2'); + }); + + menu32el.click() + .then(() => { + expect(browser.getCurrentUrl()).toContain('#/menu/3/2'); + }); + }); + }); + + it('should be expanded - Menu #3.3', () => { + element.all(menu3).first().click() + .then(() => { + const menu33el = element.all(menu33).first(); + browser.wait(ec.elementToBeClickable(menu33el), waitTime); + + menu33el.getText() + .then(val => { + expect(val).toEqual('Menu #3.3'); + }); + + menu33el.click() + .then(() => { + expect(hasClass(element.all(menu33SubMenu).first(), 'expanded')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/1'); + + menu33el.click() + .then(() => { + expect(hasClass(element.all(menu33SubMenu).first(), 'collapsed')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/1'); + }); + }); + }); + }); + + it('should be selected - Menu #3.3.1', () => { + element.all(menu3).first().click() + .then(() => { + const menu33el = element.all(menu33).first(); + browser.wait(ec.elementToBeClickable(menu33el), waitTime); + + menu33el.click() + .then(() => { + const menu331el = element.all(menu331).first(); + browser.wait(ec.elementToBeClickable(menu331el), waitTime); + + menu331el.getText() + .then(val => { + expect(val).toEqual('Menu #3.3.1'); + }); + + menu331el.click() + .then(() => { + expect(hasClass(menu331el, 'active')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/3/3/1'); + }); + }); + }); + }); + + it('should be selected - Menu #3.3.2', () => { + element.all(menu3).first().click() + .then(() => { + const menu33el = element.all(menu33).first(); + browser.wait(ec.elementToBeClickable(menu33el), waitTime); + + menu33el.click() + .then(() => { + const menu332el = element.all(menu332).first(); + browser.wait(ec.elementToBeClickable(menu332el), waitTime); + + menu332el.getText() + .then(val => { + expect(val).toEqual('Menu #3.3.2'); + }); + + menu332el.click() + .then(() => { + expect(hasClass(menu332el, 'active')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/3/3/2'); + }); + }); + }); + }); + + it('should be selected - Menu #3.3.3', () => { + element.all(menu3).first().click() + .then(() => { + const menu33el = element.all(menu33).first(); + browser.wait(ec.elementToBeClickable(menu33el), waitTime); + + menu33el.click() + .then(() => { + const menu333el = element.all(menu333).first(); + browser.wait(ec.elementToBeClickable(menu333el), waitTime); + + menu333el.getText() + .then(val => { + expect(val).toEqual('@nebular/theme'); + }); + + menu333el.click() + .then(() => { + expect(hasClass(menu333el, 'active')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/1'); + }); + }); + }); + }); + + it('should be selected - Menu #3.2.2 (navigate home)', () => { + element(homeButton).click() + .then(() => { + expect(hasClass(element.all(menu332).first(), 'active')).toBeTruthy(); + expect(browser.getCurrentUrl()).toContain('#/menu/3/3/2'); + }); + }); it('should add new menu item', () => { element(addButton).click() diff --git a/src/framework/theme/components/helpers.ts b/src/framework/theme/components/helpers.ts index 6caf0bf60b..8f1d0edc25 100644 --- a/src/framework/theme/components/helpers.ts +++ b/src/framework/theme/components/helpers.ts @@ -13,3 +13,10 @@ export function convertToBoolProperty(val: any): boolean { return !!val; } + +export function getElementHeight (el) { + const style = window.getComputedStyle(el); + const marginTop = parseInt(style.getPropertyValue('margin-top'), 10); + const marginBottom = parseInt(style.getPropertyValue('margin-bottom'), 10); + return el.offsetHeight + marginTop + marginBottom; +} diff --git a/src/framework/theme/components/menu/menu-item.component.html b/src/framework/theme/components/menu/menu-item.component.html index 4cd81119f8..c122bff4a1 100644 --- a/src/framework/theme/components/menu/menu-item.component.html +++ b/src/framework/theme/components/menu/menu-item.component.html @@ -8,8 +8,7 @@ [attr.target]="menuItem.target" [attr.title]="menuItem.title" [class.active]="menuItem.selected" - (mouseenter)="onHoverItem(menuItem)" - (click)="onSelectItem(menuItem)"> + (mouseenter)="onHoverItem(menuItem)"> {{ menuItem.title }} @@ -47,7 +46,8 @@ `, }) -export class NbMenuComponent implements OnInit, OnDestroy { +export class NbMenuComponent implements OnInit, AfterViewInit, OnDestroy { @HostBinding('class.inverse') inverseValue: boolean; /** @@ -146,43 +199,50 @@ export class NbMenuComponent implements OnInit, OnDestroy { .onAddItem() .pipe( takeWhile(() => this.alive), + filter((data: { tag: string; items: NbMenuItem[] }) => this.compareTag(data.tag)), ) - .subscribe((data: { tag: string; items: NbMenuItem[] }) => { - if (this.compareTag(data.tag)) { - this.items.push(...data.items); + .subscribe(data => this.onAddItem(data)); - this.menuInternalService.prepareItems(this.items); - } - }); - - this.menuInternalService.onNavigateHome() + this.menuInternalService + .onNavigateHome() .pipe( takeWhile(() => this.alive), + filter((data: { tag: string; items: NbMenuItem[] }) => this.compareTag(data.tag)), ) - .subscribe((data: { tag: string }) => { - if (this.compareTag(data.tag)) { - this.navigateHome(); - } - }); + .subscribe(() => this.navigateHome()); this.menuInternalService .onGetSelectedItem() .pipe( takeWhile(() => this.alive), - filter((data: any) => !data.tag || data.tag === this.tag), + filter((data: { tag: string; listener: BehaviorSubject }) => this.compareTag(data.tag)), ) - .subscribe((data: { tag: string; listener: BehaviorSubject<{ tag: string; item: NbMenuItem }> }) => { + .subscribe((data: { tag: string; listener: BehaviorSubject }) => { data.listener.next({ tag: this.tag, item: this.getSelectedItem(this.items) }); }); - this.router.events.subscribe(event => { - if (event instanceof NavigationEnd) { - this.menuInternalService.prepareItems(this.items); - } - }); + this.router.events + .pipe( + takeWhile(() => this.alive), + filter(event => event instanceof NavigationEnd), + ) + .subscribe(() => { + this.menuInternalService.resetItems(this.items); + this.menuInternalService.updateSelection(this.items, this.tag, this.autoCollapseValue) + }); + this.items.push(...this.menuInternalService.getItems()); + } + + ngAfterViewInit() { + setTimeout(() => this.menuInternalService.updateSelection(this.items, this.tag)); + } + + onAddItem(data: { tag: string; items: NbMenuItem[] }) { + this.items.push(...data.items); this.menuInternalService.prepareItems(this.items); + this.menuInternalService.updateSelection(this.items, this.tag, this.autoCollapseValue); } onHoverItem(item: NbMenuItem) { @@ -191,7 +251,7 @@ export class NbMenuComponent implements OnInit, OnDestroy { onToggleSubMenu(item: NbMenuItem) { if (this.autoCollapseValue) { - this.menuInternalService.collapseAll(this.items, item); + this.menuInternalService.collapseAll(this.items, this.tag, item); } item.expanded = !item.expanded; this.menuInternalService.submenuToggle(item, this.tag); @@ -200,8 +260,7 @@ export class NbMenuComponent implements OnInit, OnDestroy { // TODO: is not fired on page reload onSelectItem(item: NbMenuItem) { this.menuInternalService.resetItems(this.items); - item.selected = true; - this.menuInternalService.itemSelect(item, this.tag); + this.menuInternalService.selectItem(item, this.tag); } onItemClick(item: NbMenuItem) { @@ -216,9 +275,6 @@ export class NbMenuComponent implements OnInit, OnDestroy { const homeItem = this.getHomeItem(this.items); if (homeItem) { - this.menuInternalService.resetItems(this.items); - homeItem.selected = true; - if (homeItem.link) { this.router.navigate([homeItem.link]); } @@ -230,16 +286,16 @@ export class NbMenuComponent implements OnInit, OnDestroy { } private getHomeItem(items: NbMenuItem[]): NbMenuItem { - let home = null; - items.forEach((item: NbMenuItem) => { + for (const item of items) { if (item.home) { - home = item; + return item; } - if (item.home && item.children && item.children.length > 0) { - home = this.getHomeItem(item.children); + + const homeItem = item.children && this.getHomeItem(item.children); + if (homeItem) { + return homeItem; } - }); - return home; + } } private compareTag(tag: string) { diff --git a/src/framework/theme/components/menu/menu.service.ts b/src/framework/theme/components/menu/menu.service.ts index afb2c797b6..e253073762 100644 --- a/src/framework/theme/components/menu/menu.service.ts +++ b/src/framework/theme/components/menu/menu.service.ts @@ -58,6 +58,11 @@ export abstract class NbMenuItem { * @type {List} */ children?: NbMenuItem[]; + /** + * Children items height + * @type {number} + */ + subMenuHeight?: number = 0; /** * HTML Link target * @type {string} @@ -158,15 +163,21 @@ export class NbMenuInternalService { prepareItems(items: NbMenuItem[]) { items.forEach(i => this.setParent(i)); - items.forEach(i => this.prepareItem(i)); + } + + updateSelection(items: NbMenuItem[], tag: string, collapseOther: boolean = false) { + if (collapseOther) { + this.collapseAll(items, tag); + } + items.forEach(item => this.selectItemByUrl(item, tag)); } resetItems(items: NbMenuItem[]) { items.forEach(i => this.resetItem(i)); } - collapseAll(items: NbMenuItem[], except?: NbMenuItem) { - items.forEach(i => this.collapseItem(i, except)); + collapseAll(items: NbMenuItem[], tag: string, except?: NbMenuItem) { + items.forEach(i => this.collapseItem(i, tag, except)); } onAddItem(): Observable<{ tag: string; items: NbMenuItem[] }> { @@ -205,15 +216,18 @@ export class NbMenuInternalService { }); } - private collapseItem(item: NbMenuItem, except?: NbMenuItem) { + private collapseItem(item: NbMenuItem, tag: string, except?: NbMenuItem) { if (except && item === except) { return; } + + const wasExpanded = item.expanded; item.expanded = false; + if (wasExpanded) { + this.submenuToggle(item); + } - item.children && item.children.forEach(child => { - this.collapseItem(child); - }); + item.children && item.children.forEach(child => this.collapseItem(child, tag)); } private setParent(item: NbMenuItem) { @@ -223,30 +237,45 @@ export class NbMenuInternalService { }); } - private prepareItem(item: NbMenuItem) { - item.selected = false; - - const exact: boolean = item.pathMatch === 'full'; - const location: string = this.location.path(); + selectItem(item: NbMenuItem, tag: string) { + item.selected = true; + this.itemSelect(item, tag); + this.selectParent(item, tag); + } - if ((exact && location === item.link) || (!exact && location.includes(item.link)) - || (exact && item.fragment && location.substr(location.indexOf('#') + 1).includes(item.fragment))) { + private selectParent({ parent: item }: NbMenuItem, tag: string) { + if (!item) { + return; + } - item.selected = true; - this.selectParent(item); + if (!item.expanded) { + item.expanded = true; + this.submenuToggle(item, tag); } - item.children && item.children.forEach(child => { - this.prepareItem(child); - }); + item.selected = true; + this.selectParent(item, tag); } - private selectParent(item: NbMenuItem) { - const parent = item.parent; - if (parent) { - parent.selected = true; - parent.expanded = true; - this.selectParent(parent); + private selectItemByUrl(item: NbMenuItem, tag: string) { + const wasSelected = item.selected; + const isSelected = this.selectedInUrl(item); + if (!wasSelected && isSelected) { + this.selectItem(item, tag); } + if (item.children) { + this.updateSelection(item.children, tag); + } + } + + private selectedInUrl(item: NbMenuItem): boolean { + const exact: boolean = item.pathMatch === 'full'; + const location: string = this.location.path(); + + return ( + (exact && location === item.link) || + (!exact && location.includes(item.link)) || + (exact && item.fragment && location.substr(location.indexOf('#') + 1).includes(item.fragment)) + ); } }