diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index 9101510190..733800c29d 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -4,12 +4,14 @@ - - - + + + - + + + + @@ -17,7 +19,7 @@ - + @@ -28,7 +30,13 @@ - + + diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 8667b95d4c..3565321946 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -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): Promise { - 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): Promise { + 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(); @@ -1037,8 +1099,8 @@ describe('AppComponent', () => { locationService.urlSubject.next('c/d'); // The URL changes. 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. + tick(SHOW_DELAY - 1); // `onDocReady()` is triggered (for the last doc), + triggerDocViewerEvent('docReady'); // before the progress bar is shown. tick(1); fixture.detectChanges(); diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index dd7d170cd5..f95b0a2fdf 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -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; @@ -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() { diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts index e894e325f3..f0f2e7f3bc 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts @@ -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); }); diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts index cdb1809a98..cfb84e88ba 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -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$; diff --git a/aio/src/styles/1-layouts/_top-menu.scss b/aio/src/styles/1-layouts/_top-menu.scss index 03d80a9225..fdfb4e5f54 100644 --- a/aio/src/styles/1-layouts/_top-menu.scss +++ b/aio/src/styles/1-layouts/_top-menu.scss @@ -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; + background-color: $blue; - @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 {