refactor(docs-infra): decouple showing the top-menu in top-bar from showing the sidenav (#37938)

As part of angular.io's responsive layout, the following rules are
applied:
- On wide screens, a menu is shown in the top-bar and the sidenav is
  shown side-by-side with the docs content.
- On narrow screens, the top-menu is moved from the top-bar to the
  sidenav and the sidenav is closed by default and floats over the
  content when manually opened.

Previously, the break-points at which the top-menu was shown in the
top-bar and the sidenav was shown side-by-side with the content were the
same (using a single variable).

This commit decouples the two break-points to make it possible to use
different values in the future.

PR Close #37938
This commit is contained in:
George Kalpakas 2020-07-06 19:10:23 +03:00 committed by Alex Rickabaugh
parent 90b93da743
commit fc7e77934f
4 changed files with 66 additions and 55 deletions

View File

@ -18,11 +18,11 @@
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu"> <button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon> <mat-icon svgIcon="menu"></mat-icon>
</button> </button>
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide"> <a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home"> <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"> <img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a> </a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu> <aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box> <aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
<div class="toolbar-external-icons-container"> <div class="toolbar-external-icons-container">
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter"> <a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
@ -39,9 +39,9 @@
<mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main"> <mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
<mat-sidenav [ngClass]="{'collapsed': !isSideBySide}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()"> <mat-sidenav [ngClass]="{'collapsed': !dockSideNav}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu> <aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu> <aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="dockSideNav"></aio-nav-menu>
<div class="doc-version"> <div class="doc-version">
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select> <aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>

View File

@ -25,11 +25,9 @@ import { first, mapTo } from 'rxjs/operators';
import { MockLocationService } from 'testing/location.service'; import { MockLocationService } from 'testing/location.service';
import { MockLogger } from 'testing/logger.service'; import { MockLogger } from 'testing/logger.service';
import { MockSearchService } from 'testing/search.service'; import { MockSearchService } from 'testing/search.service';
import { AppComponent } from './app.component'; import { AppComponent, dockSideNavWidth, showFloatingTocWidth, showTopMenuWidth } from './app.component';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
const sideBySideBreakPoint = 992;
const hideToCBreakPoint = 800;
const startedDelay = 100; const startedDelay = 100;
describe('AppComponent', () => { describe('AppComponent', () => {
@ -58,7 +56,7 @@ describe('AppComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
component.onResize(sideBySideBreakPoint + 1); // wide by default component.onResize(showTopMenuWidth + 1); // wide by default
const de = fixture.debugElement; const de = fixture.debugElement;
const docViewerDe = de.query(By.css('aio-doc-viewer')); const docViewerDe = de.query(By.css('aio-doc-viewer'));
@ -99,7 +97,7 @@ describe('AppComponent', () => {
}); });
it('should be false on narrow screens', () => { it('should be false on narrow screens', () => {
component.onResize(hideToCBreakPoint - 1); component.onResize(showFloatingTocWidth - 1);
tocService.tocList.next([{}, {}, {}] as TocItem[]); tocService.tocList.next([{}, {}, {}] as TocItem[]);
expect(component.hasFloatingToc).toBe(false); expect(component.hasFloatingToc).toBe(false);
@ -112,7 +110,7 @@ describe('AppComponent', () => {
}); });
it('should be true on wide screens unless the toc is empty', () => { it('should be true on wide screens unless the toc is empty', () => {
component.onResize(hideToCBreakPoint + 1); component.onResize(showFloatingTocWidth + 1);
tocService.tocList.next([{}, {}, {}] as TocItem[]); tocService.tocList.next([{}, {}, {}] as TocItem[]);
expect(component.hasFloatingToc).toBe(true); expect(component.hasFloatingToc).toBe(true);
@ -127,37 +125,47 @@ describe('AppComponent', () => {
it('should be false when toc is empty', () => { it('should be false when toc is empty', () => {
tocService.tocList.next([]); tocService.tocList.next([]);
component.onResize(hideToCBreakPoint + 1); component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(false); expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint - 1); component.onResize(showFloatingTocWidth - 1);
expect(component.hasFloatingToc).toBe(false); expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint + 1); component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(false); expect(component.hasFloatingToc).toBe(false);
}); });
it('should be true when toc is not empty unless the screen is narrow', () => { it('should be true when toc is not empty unless the screen is narrow', () => {
tocService.tocList.next([{}, {}, {}] as TocItem[]); tocService.tocList.next([{}, {}, {}] as TocItem[]);
component.onResize(hideToCBreakPoint + 1); component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(true); expect(component.hasFloatingToc).toBe(true);
component.onResize(hideToCBreakPoint - 1); component.onResize(showFloatingTocWidth - 1);
expect(component.hasFloatingToc).toBe(false); expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint + 1); component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(true); expect(component.hasFloatingToc).toBe(true);
}); });
}); });
describe('isSideBySide', () => { describe('showTopMenu', () => {
it('should be updated on resize', () => { it('should be updated on resize', () => {
component.onResize(sideBySideBreakPoint - 1); component.onResize(showTopMenuWidth - 1);
expect(component.isSideBySide).toBe(false); expect(component.showTopMenu).toBe(false);
component.onResize(sideBySideBreakPoint + 1); component.onResize(showTopMenuWidth + 1);
expect(component.isSideBySide).toBe(true); expect(component.showTopMenu).toBe(true);
});
});
describe('dockSideNav', () => {
it('should be updated on resize', () => {
component.onResize(dockSideNavWidth - 1);
expect(component.dockSideNav).toBe(false);
component.onResize(dockSideNavWidth + 1);
expect(component.dockSideNav).toBe(true);
}); });
}); });
@ -185,8 +193,8 @@ describe('AppComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}; };
describe('when side-by-side (wide)', () => { describe('when view is wide', () => {
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side beforeEach(() => resizeTo(dockSideNavWidth + 1)); // wide view
it('should open when navigating to a guide page (guide/pipes)', () => { it('should open when navigating to a guide page (guide/pipes)', () => {
navigateTo('guide/pipes'); navigateTo('guide/pipes');
@ -232,8 +240,8 @@ describe('AppComponent', () => {
}); });
}); });
describe('when NOT side-by-side (narrow)', () => { describe('when view is narrow', () => {
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side beforeEach(() => resizeTo(dockSideNavWidth - 1)); // narrow view
it('should be closed when navigating to a guide page (guide/pipes)', () => { it('should be closed when navigating to a guide page (guide/pipes)', () => {
navigateTo('guide/pipes'); navigateTo('guide/pipes');
@ -286,30 +294,30 @@ describe('AppComponent', () => {
}); });
}); });
describe('when changing side-by-side (narrow --> wide)', () => { describe('when changing from narrow to wide view', () => {
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes']; const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
const nonSidenavDocs = ['features', 'about']; const nonSidenavDocs = ['features', 'about'];
sidenavDocs.forEach(doc => { sidenavDocs.forEach(doc => {
it(`should open when on a sidenav doc (${doc})`, () => { it(`should open when on a sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
navigateTo(doc); navigateTo(doc);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint + 1); resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(true); expect(sidenav.opened).toBe(true);
}); });
}); });
nonSidenavDocs.forEach(doc => { nonSidenavDocs.forEach(doc => {
it(`should remain closed when on a non-sidenav doc (${doc})`, () => { it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
navigateTo(doc); navigateTo(doc);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint + 1); resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
}); });
}); });
@ -317,33 +325,33 @@ describe('AppComponent', () => {
describe('when manually opened', () => { describe('when manually opened', () => {
sidenavDocs.forEach(doc => { sidenavDocs.forEach(doc => {
it(`should remain opened when on a sidenav doc (${doc})`, () => { it(`should remain opened when on a sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
navigateTo(doc); navigateTo(doc);
toggleSidenav(); toggleSidenav();
expect(sidenav.opened).toBe(true); expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint + 1); resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(true); expect(sidenav.opened).toBe(true);
}); });
}); });
nonSidenavDocs.forEach(doc => { nonSidenavDocs.forEach(doc => {
it(`should close when on a non-sidenav doc (${doc})`, () => { it(`should close when on a non-sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
navigateTo(doc); navigateTo(doc);
toggleSidenav(); toggleSidenav();
expect(sidenav.opened).toBe(true); expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint + 1); resizeTo(showTopMenuWidth + 1);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
}); });
}); });
}); });
}); });
describe('when changing side-by-side (wide --> narrow)', () => { describe('when changing from wide to narrow view', () => {
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes']; const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
const nonSidenavDocs = ['features', 'about']; const nonSidenavDocs = ['features', 'about'];
@ -352,7 +360,7 @@ describe('AppComponent', () => {
navigateTo(doc); navigateTo(doc);
expect(sidenav.opened).toBe(true); expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
}); });
}); });
@ -362,7 +370,7 @@ describe('AppComponent', () => {
navigateTo(doc); navigateTo(doc);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint - 1); resizeTo(dockSideNavWidth - 1);
expect(sidenav.opened).toBe(false); expect(sidenav.opened).toBe(false);
}); });
}); });
@ -376,7 +384,7 @@ describe('AppComponent', () => {
async function setupSelectorForTesting(mode?: string) { async function setupSelectorForTesting(mode?: string) {
createTestingModule('a/b', mode); createTestingModule('a/b', mode);
await initializeTest(); await initializeTest();
component.onResize(sideBySideBreakPoint + 1); // side-by-side component.onResize(dockSideNavWidth + 1); // wide view
selectElement = fixture.debugElement.query(By.directive(SelectComponent)); selectElement = fixture.debugElement.query(By.directive(SelectComponent));
selectComponent = selectElement.componentInstance; selectComponent = selectElement.componentInstance;
} }

View File

@ -14,6 +14,9 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
const sideNavView = 'SideNav'; const sideNavView = 'SideNav';
export const showTopMenuWidth = 992;
export const dockSideNavWidth = 992;
export const showFloatingTocWidth = 800;
@Component({ @Component({
selector: 'aio-shell', selector: 'aio-shell',
@ -57,18 +60,17 @@ export class AppComponent implements OnInit {
isStarting = true; isStarting = true;
isTransitioning = true; isTransitioning = true;
isFetching = false; isFetching = false;
isSideBySide = false; showTopMenu = false;
dockSideNav = false;
private isFetchingTimeout: any; private isFetchingTimeout: any;
private isSideNavDoc = false; private isSideNavDoc = false;
private sideBySideWidth = 992;
sideNavNodes: NavigationNode[]; sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[]; topMenuNodes: NavigationNode[];
topMenuNarrowNodes: NavigationNode[]; topMenuNarrowNodes: NavigationNode[];
hasFloatingToc = false; hasFloatingToc = false;
private showFloatingToc = new BehaviorSubject(false); private showFloatingToc = new BehaviorSubject(false);
private showFloatingTocWidth = 800;
tocMaxHeight: string; tocMaxHeight: string;
private tocMaxHeightOffset = 0; private tocMaxHeightOffset = 0;
@ -76,8 +78,8 @@ export class AppComponent implements OnInit {
private currentUrl: string; private currentUrl: string;
get isOpened() { return this.isSideBySide && this.isSideNavDoc; } get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
get mode() { return this.isSideBySide ? 'side' : 'over'; } get mode() { return this.dockSideNav && (this.isSideNavDoc || this.showTopMenu) ? 'side' : 'over'; }
// Search related properties // Search related properties
showSearchResults = false; showSearchResults = false;
@ -239,13 +241,14 @@ export class AppComponent implements OnInit {
@HostListener('window:resize', ['$event.target.innerWidth']) @HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width: number) { onResize(width: number) {
this.isSideBySide = width >= this.sideBySideWidth; this.showTopMenu = width >= showTopMenuWidth;
this.showFloatingToc.next(width > this.showFloatingTocWidth); this.dockSideNav = width >= dockSideNavWidth;
this.showFloatingToc.next(width > showFloatingTocWidth);
if (this.isSideBySide && !this.isSideNavDoc) { if (this.showTopMenu && !this.isSideNavDoc) {
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu // If this is a non-sidenav doc and the screen is wide enough so that we can display menu
// items in the top-bar, ensure the sidenav is closed. // items in the top-bar, ensure the sidenav is closed.
// (This condition can only be met when the resize event changes the value of `isSideBySide` // (This condition can only be met when the resize event changes the value of `showTopMenu`
// from `false` to `true` while on a non-sidenav doc.) // from `false` to `true` while on a non-sidenav doc.)
this.sidenav.toggle(false); this.sidenav.toggle(false);
} }
@ -338,7 +341,7 @@ export class AppComponent implements OnInit {
} }
// May be open or closed when wide; always closed when narrow. // May be open or closed when wide; always closed when narrow.
this.sidenav.toggle(this.isSideBySide && openSideNav); this.sidenav.toggle(this.dockSideNav && openSideNav);
} }
// Dynamically change height of table of contents container // Dynamically change height of table of contents container

View File

@ -1,6 +1,6 @@
// VARIABLES // VARIABLES
$hamburgerShownMaxWidth: 991px; $showTopMenuWidth: 992px;
$hamburgerHiddenMinWidth: $hamburgerShownMaxWidth + 1; $hideTopMenuWidth: $showTopMenuWidth - 1;
$hamburgerShownMargin: 0 8px 0 0; $hamburgerShownMargin: 0 8px 0 0;
$hamburgerHiddenMargin: 0 16px 0 -64px; $hamburgerHiddenMargin: 0 16px 0 -64px;
@ -54,7 +54,7 @@ aio-shell.folder-docs mat-toolbar.mat-toolbar,
aio-shell.folder-guide mat-toolbar.mat-toolbar, aio-shell.folder-guide mat-toolbar.mat-toolbar,
aio-shell.folder-start mat-toolbar.mat-toolbar, aio-shell.folder-start mat-toolbar.mat-toolbar,
aio-shell.folder-tutorial mat-toolbar.mat-toolbar { aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
@media (min-width: $hamburgerHiddenMinWidth) { @media (min-width: $showTopMenuWidth) {
.hamburger.mat-button { .hamburger.mat-button {
// Hamburger shown on non-marketing pages even on large screens. // Hamburger shown on non-marketing pages even on large screens.
margin: $hamburgerShownMargin; margin: $hamburgerShownMargin;
@ -68,7 +68,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
margin: $hamburgerShownMargin; margin: $hamburgerShownMargin;
padding: 0; padding: 0;
@media (min-width: $hamburgerHiddenMinWidth) { @media (min-width: $showTopMenuWidth) {
// Hamburger hidden by default on large screens. // Hamburger hidden by default on large screens.
// (Will be shown per doc.) // (Will be shown per doc.)
margin: $hamburgerHiddenMargin; margin: $hamburgerHiddenMargin;
@ -106,7 +106,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
outline-offset: 4px; outline-offset: 4px;
} }
@media screen and (max-width: $hamburgerShownMaxWidth) { @media screen and (max-width: $hideTopMenuWidth) {
padding: 4px 0; padding: 4px 0;
} }
@ -120,7 +120,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
top: 12px; top: 12px;
height: 40px; height: 40px;
@media (max-width: $hamburgerShownMaxWidth) { @media (max-width: $hideTopMenuWidth) {
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);
} }