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(docs-infra): use .tooltip and highlight the currently active node in top-bar #33351

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion aio/src/app/app.component.html
Expand Up @@ -22,7 +22,7 @@
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
<div class="toolbar-external-icons-container">
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
Expand Down
90 changes: 67 additions & 23 deletions aio/src/app/layout/top-menu/top-menu.component.spec.ts
@@ -1,42 +1,86 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { BehaviorSubject } from 'rxjs';

import { TopMenuComponent } from './top-menu.component';
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';

describe('TopMenuComponent', () => {
let component: TopMenuComponent;
let fixture: ComponentFixture<TopMenuComponent>;

// Helpers
const getListItems = () => {
const list: HTMLUListElement = fixture.debugElement.nativeElement.querySelector('ul');
return Array.from(list.querySelectorAll('li'));
};
const getSelected = (items: HTMLLIElement[]) =>
items.filter(item => item.classList.contains('selected'));

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TopMenuComponent ],
providers: [
{ provide: NavigationService, useClass: TestNavigationService }
]
declarations: [
TopMenuComponent,
],
});
});

beforeEach(() => {
fixture = TestBed.createComponent(TopMenuComponent);
component = fixture.componentInstance;

component.nodes = [
{url: 'api', title: 'API', tooltip: 'API docs'},
{url: 'features', title: 'Features', tooltip: 'Angular features overview'},
];
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
it('should create an item for each navigation node', () => {
const items = getListItems();
const links = items.map(item => item.querySelector('a'))
.filter((link): link is NonNullable<typeof link> => link !== null);

expect(links.length).toBe(2);
expect(links.map(link => link.pathname)).toEqual(['/api', '/features']);
expect(links.map(link => link.textContent)).toEqual(['API', 'Features']);
expect(links.map(link => link.title)).toEqual(['API docs', 'Angular features overview']);
});
});

//// Test Helpers ////
class TestNavigationService {
navJson = {
TopBar: [
{url: 'api', title: 'API' },
{url: 'features', title: 'Features' }
],
};
it('should mark the currently selected node with `.selected`', () => {
const items = getListItems();
expect(getSelected(items)).toEqual([]);

component.currentNode = {url: 'api', view: 'foo', nodes: []};
fixture.detectChanges();
expect(getSelected(items)).toEqual([items[0]]);

component.currentNode = {url: 'features', view: 'foo', nodes: []};
fixture.detectChanges();
expect(getSelected(items)).toEqual([items[1]]);

navigationViews = new BehaviorSubject<NavigationViews>(this.navJson);
}
component.currentNode = {url: 'something/else', view: 'foo', nodes: []};
fixture.detectChanges();
expect(getSelected(items)).toEqual([]);
});

it('should not mark any node with `.selected` if the current URL is undefined', () => {
component.nodes = [
{url: '', title: 'API', tooltip: 'API docs'},
{url: undefined, title: 'Features', tooltip: 'Angular features overview'},
];
fixture.detectChanges();
const items = getListItems();

component.currentNode = undefined;
fixture.detectChanges();
expect(getSelected(items)).toEqual([]);
});

it('should correctly mark a node with `.selected` even if its URL is empty', () => {
component.nodes = [
{url: '', title: 'API', tooltip: 'API docs'},
{url: undefined, title: 'Features', tooltip: 'Angular features overview'},
];
fixture.detectChanges();
const items = getListItems();

component.currentNode = {url: '', view: 'Empty url', nodes: []};
fixture.detectChanges();
expect(getSelected(items)).toEqual([items[0]]);
});
});
8 changes: 5 additions & 3 deletions aio/src/app/layout/top-menu/top-menu.component.ts
@@ -1,18 +1,20 @@
import { Component, Input } from '@angular/core';
import { NavigationNode } from 'app/navigation/navigation.service';
import { CurrentNode, NavigationNode } from 'app/navigation/navigation.service';

@Component({
selector: 'aio-top-menu',
template: `
<ul role="navigation">
<li *ngFor="let node of nodes">
<a class="nav-link" [href]="node.url" [title]="node.title">
<li *ngFor="let node of nodes" [ngClass]="{selected: node.url === currentUrl}">
<a class="nav-link" [href]="node.url" [title]="node.tooltip">
<span class="nav-link-inner">{{ node.title }}</span>
</a>
</li>
</ul>`
})
export class TopMenuComponent {
@Input() nodes: NavigationNode[];
@Input() currentNode: CurrentNode | undefined;

get currentUrl(): string | null { return this.currentNode ? this.currentNode.url : null; }
}
50 changes: 28 additions & 22 deletions aio/src/styles/1-layouts/_top-menu.scss
Expand Up @@ -149,41 +149,47 @@ aio-top-menu {
&:focus {
outline: none;
}
}

a.nav-link {
margin: 0 4px;
padding: 0px;
cursor: pointer;

.nav-link-inner {
padding: 8px 16px;
a.nav-link {
margin: 0 4px;
padding: 0px;
cursor: pointer;

&:hover {
background: rgba($white, 0.15);
.nav-link-inner {
border-radius: 4px;
padding: 8px 16px;

&:hover {
background: rgba($white, 0.15);
}
}
}

&:focus {
outline: none;
&:focus {
outline: none;

.nav-link-inner {
background: rgba($white, 0.15);
border-radius: 1px;
box-shadow: 0 0 1px 2px $focus-outline-ondark;
.nav-link-inner {
background: rgba($white, 0.15);
border-radius: 1px;
box-shadow: 0 0 1px 2px $focus-outline-ondark;
}
}

&:active {
.nav-link-inner {
background: rgba($white, 0.15);
}
}
}

&:active {
.nav-link-inner {
background: rgba($white, 0.15);
border-radius: 4px;
&.selected {
a.nav-link {
.nav-link-inner {
background: rgba($white, 0.15);
}
}
}
}
}

}

// SEARCH BOX
Expand Down