Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(menu): highlight when active anchors #1034

Merged
merged 13 commits into from
Mar 31, 2019
18 changes: 14 additions & 4 deletions src/framework/theme/components/menu/menu.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Location } from '@angular/common';
import { Params } from '@angular/router';
import { Observable, BehaviorSubject, ReplaySubject, Subject } from 'rxjs';
import { share } from 'rxjs/operators';
import { isUrlPathContain, isUrlPathEqual } from './url-matching-helpers';
import { isFragmentContain, isFragmentEqual, isUrlPathContain, isUrlPathEqual } from './url-matching-helpers';

export interface NbMenuBag { tag: string; item: NbMenuItem }

Expand Down Expand Up @@ -376,8 +376,18 @@ export class NbMenuInternalService {

private isSelectedInUrl(item: NbMenuItem): boolean {
const exact: boolean = item.pathMatch === 'full';
return exact
? isUrlPathEqual(this.location.path(), item.link)
: isUrlPathContain(this.location.path(), item.link);
const link: string = item.link;

const isSelectedInPath = exact
? isUrlPathEqual(this.location.path(), link)
: isUrlPathContain(this.location.path(), link);

if (isSelectedInPath && item.fragment != null) {
return exact
? isFragmentEqual(this.location.path(), item.fragment)
: isFragmentContain(this.location.path(), item.fragment);
}

return isSelectedInPath;
}
}
304 changes: 299 additions & 5 deletions src/framework/theme/components/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import { Component, Input, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { Router, Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
import { NbMenuModule } from './menu.module';
import { NbMenuBag, NbMenuItem, NbMenuService } from './menu.service';
import { NbMenuBag, NbMenuInternalService, NbMenuItem, NbMenuService } from './menu.service';
import { NbThemeModule } from '../../theme.module';
import { isUrlPathContain, isUrlPathEqual } from './url-matching-helpers';
import {
getFragmentPartOfUrl, isFragmentContain,
isFragmentEqual,
isUrlPathContain,
isUrlPathEqual,
} from './url-matching-helpers';
import { pairwise, take } from 'rxjs/operators';
import { NbMenuComponent } from './menu.component';

@Component({ template: '' })
export class NoopComponent {}

@Component({
template: `<nb-menu [items]="items" [tag]="menuTag"></nb-menu>`,
Expand All @@ -40,15 +48,15 @@ export class DoubleMenusTestComponent {
@ViewChildren(NbMenuComponent) menuComponent: QueryList<NbMenuComponent>;
}

function createTestBed() {
function createTestBed(routes: Routes = []) {
TestBed.configureTestingModule({
imports: [
NbThemeModule.forRoot(),
NbMenuModule.forRoot(),
RouterTestingModule.withRoutes([]),
RouterTestingModule.withRoutes(routes),
NoopAnimationsModule,
],
declarations: [SingleMenuTestComponent, DoubleMenusTestComponent],
declarations: [SingleMenuTestComponent, DoubleMenusTestComponent, NoopComponent],
providers: [NbMenuService],
});
}
Expand All @@ -75,6 +83,11 @@ function createDoubleMenuComponent( firstMenuItems, firstMenuTag, secondMenuItem
return { fixture, menuService };
}

function createMenuItems(items: Partial<NbMenuItem>[], menuInternaleService: NbMenuInternalService): NbMenuItem[] {
menuInternaleService.prepareItems(items as NbMenuItem[]);
return items as NbMenuItem[];
}

describe('NbMenuItem', () => {

it('should set tag attribute for menu services', () => {
Expand Down Expand Up @@ -243,6 +256,242 @@ describe('menu services', () => {

});

describe('NbMenuInternalService', () => {
let router: Router;
let menuInternalService: NbMenuInternalService;

beforeEach(() => {
const routes = [
{ path: 'menu-1', component: NoopComponent },
{ path: 'menu-1/2', component: NoopComponent },
{
path: 'menu-2',
component: NoopComponent,
children: [{ path: 'menu-2-level-2', component: NoopComponent }],
},
];
createTestBed(routes);
router = TestBed.get(Router);
menuInternalService = TestBed.get(NbMenuInternalService);
});

describe('selectFromUrl pathMatch full', () => {

it('should select menu item with matching path', (done) => {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});

it('should select menu item with matching path and fragment', (done) => {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});

it('should select child menu item and its parent', (done) => {
const items: Partial<NbMenuItem>[] = [{
link: '/menu-2',
children: [{ link: '/menu-2/menu-2-level-2' }] as NbMenuItem[],
}];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const parentMenuItem: NbMenuItem = menuItems[0];
const childMenuItem: NbMenuItem = parentMenuItem.children[0];

expect(parentMenuItem.selected).toBeFalsy();
expect(childMenuItem.selected).toBeFalsy();

router.navigate([childMenuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(parentMenuItem.selected).toEqual(true);
expect(childMenuItem.selected).toEqual(true);
done();
});
});

it('should select child menu item with fragment', (done) => {
const items: Partial<NbMenuItem>[] = [{
link: '/menu-2',
children: [{ link: '/menu-2/menu-2-level-2', fragment: '22' }] as NbMenuItem[],
}];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const parentMenuItem: NbMenuItem = menuItems[0];
const childMenuItem: NbMenuItem = parentMenuItem.children[0];

expect(parentMenuItem.selected).toBeFalsy();
expect(childMenuItem.selected).toBeFalsy();

router.navigate([childMenuItem.link], { fragment: childMenuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(parentMenuItem.selected).toEqual(true);
expect(childMenuItem.selected).toEqual(true);
done();
});
});

it('should not select menu item with matching path if fragment doesn\'t match', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link], { fragment: menuItem.fragment + 'random-fragment' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});

it('should not select menu item with matching fragment if path doesn\'t match', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

const url = menuItem.link + '/2';
router.navigate([url], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});

it('should not select menu item with fragment if no fragment in url', (done) => {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});

it('should not select menu item if path not matches fully', (done) => {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

const url = menuItem.link + '/2';
router.navigate([url], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});

it('should not select menu item if path and fragment not matches fully', (done) => {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link], { fragment: menuItem.fragment + '1' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
});

describe('selectFromUrl pathMatch prefix', () => {

it('should select menu item if url contains menu link', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

const url = menuItem.link + '/2';
router.navigate([url])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});

it('should select menu item if url contains menu link and fragment', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link], { fragment: menuItem.fragment + '1' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});

it('should not select menu item if url contains link without fragment', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});

it('should not select menu item if url contains fragment without link', function(done) {
const items: Partial<NbMenuItem>[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];

expect(menuItem.selected).toBeFalsy();

router.navigate(['menu-2'], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
});
});

describe('menu URL helpers', () => {

it('isUrlPathContain should work by url segments', () => {
Expand Down Expand Up @@ -271,4 +520,49 @@ describe('menu URL helpers', () => {
expect(isUrlPathEqual('/a/b/c?a=1;b=2&c=3', '/a/b/c')).toBeTruthy();
});

it('getFragmentPartOfUrl should return empty string for path without fragment', () => {
expect(getFragmentPartOfUrl('/a/b')).toBeFalsy();
expect(getFragmentPartOfUrl('/a/b/c?a=1;b=2&c=3')).toBeFalsy();
});

it('getFragmentPartOfUrl should return fragment part when it presented', () => {
expect(getFragmentPartOfUrl('/a/b#f')).toEqual('f');
expect(getFragmentPartOfUrl('/a/b/c?a=1;b=2&c=3#fragment')).toEqual('fragment');
});

it('isFragmentEqual should return false for path without fragments', () => {
expect(isFragmentEqual('/a/b', 'fragment')).toBeFalsy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3', 'fragment')).toBeFalsy();
});

it('isFragmentEqual should return false for path with different fragments', () => {
expect(isFragmentEqual('/a/b#f', 'fragment')).toBeFalsy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3#f', 'fragment')).toBeFalsy();
});

it('isFragmentEqual should return true for path with same fragments', () => {
expect(isFragmentEqual('/a/b#fragment', 'fragment')).toBeTruthy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3#fragment', 'fragment')).toBeTruthy();
});

it('isFragmentContain should return true for url with exact fragment', () => {
expect(isFragmentContain('/a/b#1', '1')).toBeTruthy();
expect(isFragmentContain('/#2', '2')).toBeTruthy();
});

it('isFragmentContain should return true for url containing fragments', () => {
expect(isFragmentContain('/a/b#12', '1')).toBeTruthy();
expect(isFragmentContain('/a/b?a=1;b=2&c=3#21', '1')).toBeTruthy();
});

it('isFragmentContain should return false for url without fragment', () => {
expect(isFragmentContain('/a1/b', '1')).toBeFalsy();
expect(isFragmentContain('/a1/b?a=1;b=2&c=3', '1')).toBeFalsy();
});

it('isFragmentContain should return false for url with different fragment', () => {
expect(isFragmentContain('/a1/b#222', '1')).toBeFalsy();
expect(isFragmentContain('/a1/b?a=1;b=2&c=3#222', '1')).toBeFalsy();
});

});
13 changes: 13 additions & 0 deletions src/framework/theme/components/menu/url-matching-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ export function isUrlPathContain(path, link) {
export function getPathPartOfUrl(url): string {
return url.match(/.*?(?=[?;#]|$)/)[0];
}

export function getFragmentPartOfUrl(url: string): string {
const matched = url.match(/#(.+)/);
return matched ? matched[1] : '';
}

export function isFragmentEqual(path: string, fragment: string): boolean {
return getFragmentPartOfUrl(path) === fragment;
}

export function isFragmentContain(path: string, fragment: string): boolean {
return getFragmentPartOfUrl(path).includes(fragment);
}