feat(aio): add progress bar

closes #16110
This commit is contained in:
Ward Bell 2017-05-19 01:52:37 -07:00 committed by Pete Bacon Darwin
parent f5b2ce0206
commit 368169dc15
5 changed files with 657 additions and 488 deletions

View File

@ -1,3 +1,7 @@
<div *ngIf="isFetching" class="progress-bar-container">
<md-progress-bar mode="indeterminate" color="warn"></md-progress-bar>
</div>
<md-toolbar color="primary" class="app-toolbar">
<button class="hamburger" md-button
(click)="sidenav.toggle()" title="Docs menu">

View File

@ -3,6 +3,7 @@ import { async, inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angu
import { Title } from '@angular/platform-browser';
import { APP_BASE_HREF } from '@angular/common';
import { Http } from '@angular/http';
import { MdProgressBar } from '@angular/material';
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@ -36,18 +37,24 @@ describe('AppComponent', () => {
let locationService: MockLocationService;
let sidenav: HTMLElement;
beforeEach(() => {
createTestingModule('a/b');
const initializeTest = () => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
fixture.detectChanges();
component.onResize(1033); // wide by default
docViewer = fixture.debugElement.query(By.css('aio-doc-viewer')).nativeElement;
hamburger = fixture.debugElement.query(By.css('.hamburger')).nativeElement;
locationService = fixture.debugElement.injector.get(LocationService) as any;
sidenav = fixture.debugElement.query(By.css('md-sidenav')).nativeElement;
};
describe('with proper DocViewer', () => {
beforeEach(() => {
createTestingModule('a/b');
initializeTest();
});
it('should create', () => {
@ -403,30 +410,6 @@ describe('AppComponent', () => {
}));
});
describe('initial rendering', () => {
beforeEach(() => {
createTestingModule('a/b');
// Remove the DocViewer for this test and hide the missing component message
TestBed.overrideModule(AppModule, {
remove: { declarations: [DocViewerComponent] },
add: { schemas: [NO_ERRORS_SCHEMA] }
});
});
it('should initially add the starting class until the first document is rendered', () => {
fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.componentInstance.isStarting).toBe(true);
expect(fixture.debugElement.query(By.css('md-sidenav-container')).classes['starting']).toBe(true);
fixture.debugElement.query(By.css('aio-doc-viewer')).triggerEventHandler('docRendered', {});
fixture.detectChanges();
expect(fixture.componentInstance.isStarting).toBe(false);
expect(fixture.debugElement.query(By.css('md-sidenav-container')).classes['starting']).toBe(false);
});
});
describe('click intercepting', () => {
it('should intercept clicks on anchors and call `location.handleAnchorClick()`',
inject([LocationService], (location: LocationService) => {
@ -581,6 +564,151 @@ describe('AppComponent', () => {
});
describe('with mocked DocViewer', () => {
const getDocViewer = () => fixture.debugElement.query(By.css('aio-doc-viewer'));
const triggerDocRendered = () => getDocViewer().triggerEventHandler('docRendered', {});
beforeEach(() => {
createTestingModule('a/b');
// Remove the DocViewer for this test and hide the missing component message
TestBed.overrideModule(AppModule, {
remove: { declarations: [DocViewerComponent] },
add: { schemas: [NO_ERRORS_SCHEMA] }
});
});
describe('initial rendering', () => {
it('should initially add the starting class until the first document is rendered', fakeAsync(() => {
const getSidenavContainer = () => fixture.debugElement.query(By.css('md-sidenav-container'));
initializeTest();
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
triggerDocRendered();
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
tick(499);
fixture.detectChanges();
expect(component.isStarting).toBe(true);
expect(getSidenavContainer().classes['starting']).toBe(true);
tick(2);
fixture.detectChanges();
expect(component.isStarting).toBe(false);
expect(getSidenavContainer().classes['starting']).toBe(false);
}));
});
describe('progress bar', () => {
const SHOW_DELAY = 200;
const HIDE_DELAY = 500;
const getProgressBar = () => fixture.debugElement.query(By.directive(MdProgressBar));
const initializeAndCompleteNavigation = () => {
initializeTest();
triggerDocRendered();
tick(HIDE_DELAY);
};
it('should initially be hidden', () => {
initializeTest();
expect(getProgressBar()).toBeFalsy();
});
it('should be shown (after a delay) when the path changes', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('c/d');
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
tick(SHOW_DELAY - 1);
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
tick(1);
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
}));
it('should not be shown when the URL changes but the path remains the same', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('a/b');
tick(SHOW_DELAY);
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
}));
it('should not be shown if the doc is rendered quickly', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY - 1);
triggerDocRendered();
tick(1);
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
}));
it('should be shown if rendering the doc takes too long', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY);
triggerDocRendered();
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
}));
it('should be hidden (after a delay) once the doc is rendered', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('c/d');
tick(SHOW_DELAY);
triggerDocRendered();
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
tick(HIDE_DELAY - 1);
fixture.detectChanges();
expect(getProgressBar()).toBeTruthy();
tick(1);
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
}));
it('should only take the latest request into account', fakeAsync(() => {
initializeAndCompleteNavigation();
locationService.urlSubject.next('c/d'); // The URL changes.
locationService.urlSubject.next('e/f'); // The URL changes again before `onDocRendered()`.
tick(SHOW_DELAY - 1); // `onDocRendered()` is triggered (for the last doc),
triggerDocRendered(); // before the progress bar is shown.
tick(1);
fixture.detectChanges();
expect(getProgressBar()).toBeFalsy();
tick(HIDE_DELAY); // Fire the remaining timer or `fakeAsync()` complains.
}));
});
});
});
//// test helpers ////
function createTestingModule(initialUrl: string) {

View File

@ -54,8 +54,10 @@ export class AppComponent implements OnInit {
@HostBinding('class')
hostClasses = '';
isFetching = false;
isStarting = true;
isSideBySide = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
private previousNavView: string;
@ -122,6 +124,10 @@ export class AppComponent implements OnInit {
} else {
// don't scroll; leave that to `onDocRendered`
this.currentPath = path;
// Start progress bar if doc not rendered within brief time
clearTimeout(this.isFetchingTimeout);
this.isFetchingTimeout = setTimeout(() => this.isFetching = true, 200);
}
});
@ -168,10 +174,16 @@ export class AppComponent implements OnInit {
}
onDocRendered() {
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
clearTimeout(this.isFetchingTimeout);
// Scroll 500ms after the doc-viewer has finished rendering the new doc
// The delay is to allow time for async layout to complete
setTimeout(() => this.autoScroll(), 500);
setTimeout(() => {
this.autoScroll();
this.isStarting = false;
this.isFetching = false;
}, 500);
}
onDocVersionChange(versionIndex: number) {

View File

@ -5,8 +5,17 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { MdToolbarModule, MdButtonModule, MdIconModule, MdInputModule, MdSidenavModule, MdTabsModule, Platform,
MdIconRegistry } from '@angular/material';
import {
MdButtonModule,
MdIconModule,
MdIconRegistry,
MdInputModule,
MdProgressBarModule,
MdSidenavModule,
MdTabsModule,
MdToolbarModule,
Platform
} from '@angular/material';
// Temporary fix for MdSidenavModule issue:
// crashes with "missing first" operator when SideNav.mode is "over"
@ -67,9 +76,10 @@ export const svgIconProviders = [
MdButtonModule,
MdIconModule,
MdInputModule,
MdToolbarModule,
MdProgressBarModule,
MdSidenavModule,
MdTabsModule,
MdToolbarModule,
SwUpdatesModule
],
declarations: [

View File

@ -1,3 +1,12 @@
.progress-bar-container {
height: 2px;
overflow: hidden;
position: fixed;
top: 64px;
width: 100vw;
z-index: 5;
}
.sidenav-content {
padding: 1rem 3rem 3rem;
margin: auto;
@ -12,9 +21,15 @@
aio-menu {
display: none;
}
.progress-bar-container {
top: 56px;
}
.sidenav-content {
min-height: 450px;
}
}
.sidenav-container {