fix(aio): improve transitions between pages
- Avoid unnecessary animations, style transitions, repositioning on initial rendering. - Better handle transitioning from/to Home page (which is the only page with transparent top-menu). - Better coordinate sidenav and hamburger animations with page transitions. - Improve fade-in/out animations. Fixes #20996
This commit is contained in:
parent
2986e25abb
commit
8ceffd8b48
|
@ -4,12 +4,14 @@
|
|||
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
|
||||
</div>
|
||||
|
||||
<mat-toolbar color="primary" class="app-toolbar">
|
||||
<button class="hamburger" [class.starting]="isStarting" mat-button
|
||||
(click)="sidenav.toggle()" title="Docs menu">
|
||||
<mat-icon [ngClass]="{'sidenav-open': !isSideBySide }" svgIcon="menu"></mat-icon>
|
||||
<mat-toolbar color="primary" class="app-toolbar" [class.transitioning]="isTransitioning">
|
||||
<button mat-button class="hamburger" (click)="sidenav.toggle()" title="Docs menu">
|
||||
<mat-icon svgIcon="menu"></mat-icon>
|
||||
</button>
|
||||
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
||||
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide">
|
||||
<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-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||
</mat-toolbar>
|
||||
|
@ -28,7 +30,13 @@
|
|||
|
||||
<section class="sidenav-content" [id]="pageId" role="content">
|
||||
<aio-mode-banner [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner>
|
||||
<aio-doc-viewer [doc]="currentDocument" (docReady)="onDocReady()" (docRemoved)="onDocRemoved()" (docInserted)="onDocInserted()"></aio-doc-viewer>
|
||||
<aio-doc-viewer [class.no-animations]="isStarting"
|
||||
[doc]="currentDocument"
|
||||
(docReady)="onDocReady()"
|
||||
(docRemoved)="onDocRemoved()"
|
||||
(docInserted)="onDocInserted()"
|
||||
(docRendered)="onDocRendered()">
|
||||
</aio-doc-viewer>
|
||||
<aio-dt [on]="dtOn" [(doc)]="currentDocument"></aio-dt>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -154,34 +154,35 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('SideNav when side-by-side (wide)', () => {
|
||||
const navigateTo = (path: string) => {
|
||||
locationService.go(path);
|
||||
component.updateSideNav();
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
||||
});
|
||||
|
||||
it('should open when nav to a guide page (guide/pipes)', () => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/pipes');
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
|
||||
it('should open when nav to an api page', () => {
|
||||
locationService.go('api/a/b/c/d');
|
||||
fixture.detectChanges();
|
||||
navigateTo('api/a/b/c/d');
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
|
||||
it('should be closed when nav to a marketing page (features)', () => {
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
navigateTo('features');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
describe('when manually closed', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/pipes');
|
||||
hamburger.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@ -191,56 +192,53 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
it('should stay closed when nav from one guide page to another', () => {
|
||||
locationService.go('guide/bags');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/bags');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should stay closed when nav from a guide page to api page', () => {
|
||||
locationService.go('api');
|
||||
fixture.detectChanges();
|
||||
navigateTo('api');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should reopen when nav to market page and back to guide page', () => {
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
locationService.go('guide/bags');
|
||||
fixture.detectChanges();
|
||||
navigateTo('features');
|
||||
navigateTo('guide/bags');
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SideNav when NOT side-by-side (narrow)', () => {
|
||||
const navigateTo = (path: string) => {
|
||||
locationService.go(path);
|
||||
component.updateSideNav();
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
component.onResize(sideBySideBreakPoint - 1); // NOT side-by-side
|
||||
});
|
||||
|
||||
it('should be closed when nav to a guide page (guide/pipes)', () => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/pipes');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should be closed when nav to an api page', () => {
|
||||
locationService.go('api/a/b/c/d');
|
||||
fixture.detectChanges();
|
||||
navigateTo('api/a/b/c/d');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should be closed when nav to a marketing page (features)', () => {
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
navigateTo('features');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
describe('when manually opened', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/pipes');
|
||||
hamburger.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@ -257,20 +255,17 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
it('should close when nav to another guide page', () => {
|
||||
locationService.go('guide/bags');
|
||||
fixture.detectChanges();
|
||||
navigateTo('guide/bags');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should close when nav to api page', () => {
|
||||
locationService.go('api');
|
||||
fixture.detectChanges();
|
||||
navigateTo('api');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
it('should close again when nav to market page', () => {
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
navigateTo('features');
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -325,101 +320,6 @@ describe('AppComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('pageId', () => {
|
||||
|
||||
it('should set the id of the doc viewer container based on the current doc', () => {
|
||||
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
|
||||
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
expect(component.pageId).toEqual('guide-pipes');
|
||||
expect(container.properties['id']).toEqual('guide-pipes');
|
||||
|
||||
locationService.go('news');
|
||||
fixture.detectChanges();
|
||||
expect(component.pageId).toEqual('news');
|
||||
expect(container.properties['id']).toEqual('news');
|
||||
|
||||
locationService.go('');
|
||||
fixture.detectChanges();
|
||||
expect(component.pageId).toEqual('home');
|
||||
expect(container.properties['id']).toEqual('home');
|
||||
});
|
||||
|
||||
it('should not be affected by changes to the query', () => {
|
||||
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
|
||||
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
|
||||
locationService.go('guide/other?search=http');
|
||||
fixture.detectChanges();
|
||||
expect(component.pageId).toEqual('guide-other');
|
||||
expect(container.properties['id']).toEqual('guide-other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hostClasses', () => {
|
||||
|
||||
it('should set the css classes of the host container based on the current doc and navigation view', () => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
|
||||
checkHostClass('page', 'guide-pipes');
|
||||
checkHostClass('folder', 'guide');
|
||||
checkHostClass('view', 'SideNav');
|
||||
|
||||
locationService.go('features');
|
||||
fixture.detectChanges();
|
||||
checkHostClass('page', 'features');
|
||||
checkHostClass('folder', 'features');
|
||||
checkHostClass('view', 'TopBar');
|
||||
|
||||
locationService.go('');
|
||||
fixture.detectChanges();
|
||||
checkHostClass('page', 'home');
|
||||
checkHostClass('folder', 'home');
|
||||
checkHostClass('view', '');
|
||||
});
|
||||
|
||||
it('should set the css class of the host container based on the open/closed state of the side nav', async () => {
|
||||
locationService.go('guide/pipes');
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'open');
|
||||
|
||||
sidenav.close();
|
||||
await waitForEmit(sidenav.onClose);
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'closed');
|
||||
|
||||
sidenav.open();
|
||||
await waitForEmit(sidenav.onOpen);
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'open');
|
||||
|
||||
function waitForEmit(emitter: Observable<void>): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
emitter.subscribe(resolve);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should set the css class of the host container based on the initial deployment mode', () => {
|
||||
createTestingModule('a/b', 'archive');
|
||||
initializeTest();
|
||||
checkHostClass('mode', 'archive');
|
||||
});
|
||||
|
||||
function checkHostClass(type, value) {
|
||||
const host = fixture.debugElement;
|
||||
const classes = host.properties['className'];
|
||||
const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
|
||||
expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
|
||||
expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
describe('currentDocument', () => {
|
||||
it('should display a guide page (guide/pipes)', () => {
|
||||
locationService.go('guide/pipes');
|
||||
|
@ -895,7 +795,9 @@ describe('AppComponent', () => {
|
|||
|
||||
describe('with mocked DocViewer', () => {
|
||||
const getDocViewer = () => fixture.debugElement.query(By.css('aio-doc-viewer'));
|
||||
const triggerDocReady = () => getDocViewer().triggerEventHandler('docReady', undefined);
|
||||
const triggerDocViewerEvent =
|
||||
(evt: 'docReady' | 'docRemoved' | 'docInserted' | 'docRendered') =>
|
||||
getDocViewer().triggerEventHandler(evt, undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingModule('a/b');
|
||||
|
@ -907,7 +809,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('initial rendering', () => {
|
||||
it('should initially add the starting class until the first document is ready', fakeAsync(() => {
|
||||
it('should initially add the starting class until a document is rendered', () => {
|
||||
const getSidenavContainer = () => fixture.debugElement.query(By.css('mat-sidenav-container'));
|
||||
|
||||
initializeTest();
|
||||
|
@ -915,21 +817,181 @@ describe('AppComponent', () => {
|
|||
expect(component.isStarting).toBe(true);
|
||||
expect(getSidenavContainer().classes['starting']).toBe(true);
|
||||
|
||||
triggerDocReady();
|
||||
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);
|
||||
triggerDocViewerEvent('docRendered');
|
||||
fixture.detectChanges();
|
||||
expect(component.isStarting).toBe(false);
|
||||
expect(getSidenavContainer().classes['starting']).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should initially disable animations on the DocViewer for the first rendering', () => {
|
||||
initializeTest();
|
||||
|
||||
expect(component.isStarting).toBe(true);
|
||||
expect(docViewer.classList.contains('no-animations')).toBe(true);
|
||||
|
||||
triggerDocViewerEvent('docRendered');
|
||||
fixture.detectChanges();
|
||||
expect(component.isStarting).toBe(false);
|
||||
expect(docViewer.classList.contains('no-animations')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subsequent rendering', () => {
|
||||
beforeEach(jasmine.clock().install);
|
||||
afterEach(jasmine.clock().uninstall);
|
||||
|
||||
it('should set the transitioning class on `.app-toolbar` while a document is being rendered', () => {
|
||||
const getToolbar = () => fixture.debugElement.query(By.css('.app-toolbar'));
|
||||
|
||||
initializeTest();
|
||||
|
||||
// Initially, `isTransitoning` is true.
|
||||
expect(component.isTransitioning).toBe(true);
|
||||
expect(getToolbar().classes['transitioning']).toBe(true);
|
||||
|
||||
triggerDocViewerEvent('docRendered');
|
||||
fixture.detectChanges();
|
||||
expect(component.isTransitioning).toBe(false);
|
||||
expect(getToolbar().classes['transitioning']).toBe(false);
|
||||
|
||||
// While a document is being rendered, `isTransitoning` is set to true.
|
||||
triggerDocViewerEvent('docReady');
|
||||
fixture.detectChanges();
|
||||
expect(component.isTransitioning).toBe(true);
|
||||
expect(getToolbar().classes['transitioning']).toBe(true);
|
||||
|
||||
triggerDocViewerEvent('docRendered');
|
||||
fixture.detectChanges();
|
||||
expect(component.isTransitioning).toBe(false);
|
||||
expect(getToolbar().classes['transitioning']).toBe(false);
|
||||
});
|
||||
|
||||
it('should update the sidenav state as soon as a new document is inserted', () => {
|
||||
initializeTest();
|
||||
const updateSideNavSpy = spyOn(component, 'updateSideNav');
|
||||
|
||||
triggerDocViewerEvent('docInserted');
|
||||
jasmine.clock().tick(0);
|
||||
expect(updateSideNavSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
triggerDocViewerEvent('docInserted');
|
||||
jasmine.clock().tick(0);
|
||||
expect(updateSideNavSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pageId', () => {
|
||||
const navigateTo = (path: string) => {
|
||||
locationService.go(path);
|
||||
triggerDocViewerEvent('docInserted');
|
||||
jasmine.clock().tick(0);
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(jasmine.clock().install);
|
||||
afterEach(jasmine.clock().uninstall);
|
||||
|
||||
it('should set the id of the doc viewer container based on the current doc', () => {
|
||||
initializeTest();
|
||||
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
|
||||
|
||||
navigateTo('guide/pipes');
|
||||
expect(component.pageId).toEqual('guide-pipes');
|
||||
expect(container.properties['id']).toEqual('guide-pipes');
|
||||
|
||||
navigateTo('news');
|
||||
expect(component.pageId).toEqual('news');
|
||||
expect(container.properties['id']).toEqual('news');
|
||||
|
||||
navigateTo('');
|
||||
expect(component.pageId).toEqual('home');
|
||||
expect(container.properties['id']).toEqual('home');
|
||||
});
|
||||
|
||||
it('should not be affected by changes to the query', () => {
|
||||
initializeTest();
|
||||
const container = fixture.debugElement.query(By.css('section.sidenav-content'));
|
||||
|
||||
navigateTo('guide/pipes');
|
||||
navigateTo('guide/other?search=http');
|
||||
|
||||
expect(component.pageId).toEqual('guide-other');
|
||||
expect(container.properties['id']).toEqual('guide-other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hostClasses', () => {
|
||||
const triggerUpdateHostClasses = () => {
|
||||
triggerDocViewerEvent('docInserted');
|
||||
jasmine.clock().tick(0);
|
||||
fixture.detectChanges();
|
||||
};
|
||||
const navigateTo = (path: string) => {
|
||||
locationService.go(path);
|
||||
triggerUpdateHostClasses();
|
||||
};
|
||||
|
||||
beforeEach(jasmine.clock().install);
|
||||
afterEach(jasmine.clock().uninstall);
|
||||
|
||||
it('should set the css classes of the host container based on the current doc and navigation view', () => {
|
||||
initializeTest();
|
||||
|
||||
navigateTo('guide/pipes');
|
||||
checkHostClass('page', 'guide-pipes');
|
||||
checkHostClass('folder', 'guide');
|
||||
checkHostClass('view', 'SideNav');
|
||||
|
||||
navigateTo('features');
|
||||
checkHostClass('page', 'features');
|
||||
checkHostClass('folder', 'features');
|
||||
checkHostClass('view', 'TopBar');
|
||||
|
||||
navigateTo('');
|
||||
checkHostClass('page', 'home');
|
||||
checkHostClass('folder', 'home');
|
||||
checkHostClass('view', '');
|
||||
});
|
||||
|
||||
it('should set the css class of the host container based on the open/closed state of the side nav', async () => {
|
||||
initializeTest();
|
||||
|
||||
navigateTo('guide/pipes');
|
||||
checkHostClass('sidenav', 'open');
|
||||
|
||||
sidenav.close();
|
||||
await waitForEmit(sidenav.onClose);
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'closed');
|
||||
|
||||
sidenav.open();
|
||||
await waitForEmit(sidenav.onOpen);
|
||||
fixture.detectChanges();
|
||||
checkHostClass('sidenav', 'open');
|
||||
|
||||
function waitForEmit(emitter: Observable<void>): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
emitter.subscribe(resolve);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should set the css class of the host container based on the initial deployment mode', () => {
|
||||
createTestingModule('a/b', 'archive');
|
||||
initializeTest();
|
||||
|
||||
triggerUpdateHostClasses();
|
||||
checkHostClass('mode', 'archive');
|
||||
});
|
||||
|
||||
function checkHostClass(type, value) {
|
||||
const host = fixture.debugElement;
|
||||
const classes = host.properties['className'];
|
||||
const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
|
||||
expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
|
||||
expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
describe('progress bar', () => {
|
||||
|
@ -938,7 +1000,7 @@ describe('AppComponent', () => {
|
|||
const getProgressBar = () => fixture.debugElement.query(By.directive(MatProgressBar));
|
||||
const initializeAndCompleteNavigation = () => {
|
||||
initializeTest();
|
||||
triggerDocReady();
|
||||
triggerDocViewerEvent('docReady');
|
||||
tick(HIDE_DELAY);
|
||||
};
|
||||
|
||||
|
@ -975,7 +1037,7 @@ describe('AppComponent', () => {
|
|||
it('should not be shown when re-navigating to the empty path', fakeAsync(() => {
|
||||
initializeAndCompleteNavigation();
|
||||
locationService.urlSubject.next('');
|
||||
triggerDocReady();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
locationService.urlSubject.next('');
|
||||
|
||||
|
@ -991,7 +1053,7 @@ describe('AppComponent', () => {
|
|||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY - 1);
|
||||
triggerDocReady();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
tick(1);
|
||||
fixture.detectChanges();
|
||||
|
@ -1005,7 +1067,7 @@ describe('AppComponent', () => {
|
|||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY);
|
||||
triggerDocReady();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getProgressBar()).toBeTruthy();
|
||||
|
@ -1018,7 +1080,7 @@ describe('AppComponent', () => {
|
|||
locationService.urlSubject.next('c/d');
|
||||
|
||||
tick(SHOW_DELAY);
|
||||
triggerDocReady();
|
||||
triggerDocViewerEvent('docReady');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getProgressBar()).toBeTruthy();
|
||||
|
@ -1038,7 +1100,7 @@ describe('AppComponent', () => {
|
|||
locationService.urlSubject.next('e/f'); // The URL changes again before `onDocReady()`.
|
||||
|
||||
tick(SHOW_DELAY - 1); // `onDocReady()` is triggered (for the last doc),
|
||||
triggerDocReady(); // before the progress bar is shown.
|
||||
triggerDocViewerEvent('docReady'); // before the progress bar is shown.
|
||||
|
||||
tick(1);
|
||||
fixture.detectChanges();
|
||||
|
|
|
@ -4,7 +4,6 @@ import { MatSidenav } from '@angular/material/sidenav';
|
|||
|
||||
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { DocumentService, DocumentContents } from 'app/documents/document.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
|
@ -16,6 +15,7 @@ import { TocService } from 'app/shared/toc.service';
|
|||
import { Observable } from 'rxjs/Observable';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
import 'rxjs/add/operator/first';
|
||||
|
||||
const sideNavView = 'SideNav';
|
||||
|
||||
|
@ -58,6 +58,7 @@ export class AppComponent implements OnInit {
|
|||
|
||||
isFetching = false;
|
||||
isStarting = true;
|
||||
isTransitioning = true;
|
||||
isSideBySide = false;
|
||||
private isFetchingTimeout: any;
|
||||
private isSideNavDoc = false;
|
||||
|
@ -75,18 +76,9 @@ export class AppComponent implements OnInit {
|
|||
|
||||
versionInfo: VersionInfo;
|
||||
|
||||
get homeImageUrl() {
|
||||
return this.isSideBySide ?
|
||||
'assets/images/logos/angular/logo-nav@2x.png' :
|
||||
'assets/images/logos/angular/shield-large.svg';
|
||||
}
|
||||
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
||||
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
||||
|
||||
// Need the doc-viewer element for scrolling the contents
|
||||
@ViewChild(DocViewerComponent, { read: ElementRef })
|
||||
docViewer: ElementRef;
|
||||
|
||||
// Search related properties
|
||||
showSearchResults = false;
|
||||
searchResults: Observable<SearchResults>;
|
||||
|
@ -120,12 +112,13 @@ export class AppComponent implements OnInit {
|
|||
|
||||
/* No need to unsubscribe because this root component never dies */
|
||||
|
||||
this.documentService.currentDocument.subscribe(doc => {
|
||||
this.currentDocument = doc;
|
||||
this.setPageId(doc.id);
|
||||
this.setFolderId(doc.id);
|
||||
this.updateHostClasses();
|
||||
});
|
||||
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
|
||||
// Generally, we want to delay updating the host classes for the new document, until after the
|
||||
// leaving document has been removed (to avoid having the styles for the new document applied
|
||||
// prematurely).
|
||||
// On the first document, though, (when we know there is no previous document), we want to
|
||||
// ensure the styles are applied as soon as possible to avoid flicker.
|
||||
this.documentService.currentDocument.first().subscribe(doc => this.updateHostClassesForDoc(doc));
|
||||
|
||||
this.locationService.currentPath.subscribe(path => {
|
||||
// Redirect to docs if we are in not in stable mode and are not hitting a docs page
|
||||
|
@ -146,21 +139,7 @@ export class AppComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
|
||||
this.navigationService.currentNodes.subscribe(currentNodes => {
|
||||
this.currentNodes = currentNodes;
|
||||
|
||||
// Preserve current sidenav open state by default
|
||||
let openSideNav = this.sidenav.opened;
|
||||
const isSideNavDoc = !!currentNodes[sideNavView];
|
||||
|
||||
if (this.isSideNavDoc !== isSideNavDoc) {
|
||||
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
|
||||
// Open if changed to a sidenav doc; close if changed to a marketing doc.
|
||||
openSideNav = this.isSideNavDoc = isSideNavDoc;
|
||||
}
|
||||
// May be open or closed when wide; always closed when narrow
|
||||
this.sideNavToggle(this.isSideBySide ? openSideNav : false);
|
||||
});
|
||||
this.navigationService.currentNodes.subscribe(currentNodes => this.currentNodes = currentNodes);
|
||||
|
||||
// Compute the version picker list from the current version and the versions in the navigation map
|
||||
combineLatest(
|
||||
|
@ -204,14 +183,14 @@ export class AppComponent implements OnInit {
|
|||
}
|
||||
|
||||
onDocReady() {
|
||||
// About to transition to new view.
|
||||
this.isTransitioning = true;
|
||||
|
||||
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
|
||||
clearTimeout(this.isFetchingTimeout);
|
||||
|
||||
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
|
||||
setTimeout(() => {
|
||||
this.isStarting = false;
|
||||
this.isFetching = false;
|
||||
}, 500);
|
||||
setTimeout(() => this.isFetching = false, 500);
|
||||
}
|
||||
|
||||
onDocRemoved() {
|
||||
|
@ -221,11 +200,25 @@ export class AppComponent implements OnInit {
|
|||
}
|
||||
|
||||
onDocInserted() {
|
||||
// TODO: Find a better way to avoid `ExpressionChangedAfterItHasBeenChecked` error.
|
||||
setTimeout(() => {
|
||||
// Update the SideNav state (if necessary).
|
||||
this.updateSideNav();
|
||||
|
||||
// Update the host classes to match the new document.
|
||||
this.updateHostClassesForDoc(this.currentDocument);
|
||||
});
|
||||
|
||||
// Scroll 500ms after the new document has been inserted into the doc-viewer.
|
||||
// The delay is to allow time for async layout to complete.
|
||||
setTimeout(() => this.autoScroll(), 500);
|
||||
}
|
||||
|
||||
onDocRendered() {
|
||||
this.isStarting = false;
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
onDocVersionChange(versionIndex: number) {
|
||||
const version = this.docVersions[versionIndex];
|
||||
if (version.url) {
|
||||
|
@ -290,6 +283,27 @@ export class AppComponent implements OnInit {
|
|||
this.hostClasses = `${mode} ${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`;
|
||||
}
|
||||
|
||||
updateHostClassesForDoc(doc: DocumentContents) {
|
||||
this.setPageId(doc.id);
|
||||
this.setFolderId(doc.id);
|
||||
this.updateHostClasses();
|
||||
}
|
||||
|
||||
updateSideNav() {
|
||||
// Preserve current sidenav open state by default.
|
||||
let openSideNav = this.sidenav.opened;
|
||||
const isSideNavDoc = !!this.currentNodes[sideNavView];
|
||||
|
||||
if (this.isSideNavDoc !== isSideNavDoc) {
|
||||
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
|
||||
// Open if changed to a sidenav doc; close if changed to a marketing doc.
|
||||
openSideNav = this.isSideNavDoc = isSideNavDoc;
|
||||
}
|
||||
|
||||
// May be open or closed when wide; always closed when narrow.
|
||||
this.sideNavToggle(this.isSideBySide && openSideNav);
|
||||
}
|
||||
|
||||
// Dynamically change height of table of contents container
|
||||
@HostListener('window:scroll')
|
||||
onScroll() {
|
||||
|
|
|
@ -368,7 +368,7 @@ describe('DocViewerComponent', () => {
|
|||
});
|
||||
|
||||
it('should display nothing if the document has no contents', async () => {
|
||||
docViewer.currViewContainer.innerHTML = 'Test';
|
||||
await doRender('Test');
|
||||
expect(docViewerEl.textContent).toBe('Test');
|
||||
|
||||
await doRender('');
|
||||
|
@ -647,6 +647,8 @@ describe('DocViewerComponent', () => {
|
|||
oldCurrViewContainer.innerHTML = 'Current view';
|
||||
oldNextViewContainer.innerHTML = 'Next view';
|
||||
|
||||
docViewerEl.appendChild(oldCurrViewContainer);
|
||||
|
||||
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
|
||||
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
|
||||
});
|
||||
|
|
|
@ -80,8 +80,6 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||
|
||||
if (this.hostElement.firstElementChild) {
|
||||
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
|
||||
} else {
|
||||
this.hostElement.appendChild(this.currViewContainer);
|
||||
}
|
||||
|
||||
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
|
||||
|
@ -188,7 +186,7 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||
return 1000 * seconds;
|
||||
};
|
||||
const animateProp =
|
||||
(elem: HTMLElement, prop: string, from: string, to: string, duration = 333) => {
|
||||
(elem: HTMLElement, prop: string, from: string, to: string, duration = 200) => {
|
||||
const animationsDisabled = !DocViewerComponent.animationsEnabled
|
||||
|| this.hostElement.classList.contains(NO_ANIMATIONS);
|
||||
|
||||
|
@ -206,8 +204,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||
.switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$);
|
||||
};
|
||||
|
||||
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.25');
|
||||
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.25', '1');
|
||||
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1');
|
||||
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.1', '1');
|
||||
|
||||
let done$ = this.void$;
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// VARIABLES
|
||||
$hamburgerShownMargin: 0;
|
||||
$hamburgerHiddenMargin: 0 24px 0 -88px;
|
||||
|
||||
|
||||
// DOCS PAGE / STANDARD: TOPNAV TOOLBAR FIXED
|
||||
mat-toolbar.mat-toolbar {
|
||||
position: fixed;
|
||||
|
@ -16,11 +21,13 @@ mat-toolbar.mat-toolbar {
|
|||
|
||||
// HOME PAGE OVERRIDE: TOPNAV TOOLBAR
|
||||
aio-shell.page-home mat-toolbar.mat-toolbar {
|
||||
background-color: transparent;
|
||||
transition: background-color .2s linear .3s;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
background-color: $blue;
|
||||
|
||||
@media (min-width: 481px) {
|
||||
&:not(.transitioning) {
|
||||
background-color: transparent;
|
||||
transition: background-color .2s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,10 +43,18 @@ aio-shell.page-resources mat-toolbar.mat-toolbar {
|
|||
@media (min-width: 481px) {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DOCS PAGES OVERRIDE: HAMBURGER
|
||||
aio-shell.folder-api mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-docs mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-guide mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
@media (min-width: 992px) {
|
||||
button.hamburger {
|
||||
margin: 0 24px 0 -88px;
|
||||
.hamburger.mat-button {
|
||||
// Hamburger shown on non-marketing pages on large screens.
|
||||
margin: $hamburgerShownMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,16 +63,16 @@ aio-shell.page-resources mat-toolbar.mat-toolbar {
|
|||
// HAMBURGER BUTTON
|
||||
.hamburger.mat-button {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
margin: $hamburgerShownMargin;
|
||||
padding: 0;
|
||||
transition-duration: .4s;
|
||||
transition-property: color, margin;
|
||||
transition-timing-function: cubic-bezier(.25, .8, .25, 1);
|
||||
|
||||
&.starting {
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-color, color;
|
||||
transition-timing-function: ease-in-out;
|
||||
@media (min-width: 992px) {
|
||||
// Hamburger hidden by default on large screens.
|
||||
// (Will be shown per doc.)
|
||||
margin: $hamburgerHiddenMargin;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
Loading…
Reference in New Issue