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:
parent
90b93da743
commit
fc7e77934f
|
@ -18,11 +18,11 @@
|
|||
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
|
||||
<mat-icon svgIcon="menu"></mat-icon>
|
||||
</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 *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" [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>
|
||||
<div class="toolbar-external-icons-container">
|
||||
<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 [ngClass]="{'collapsed': !isSideBySide}" #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 [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
|
||||
<mat-sidenav [ngClass]="{'collapsed': !dockSideNav}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
|
||||
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
|
||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="dockSideNav"></aio-nav-menu>
|
||||
|
||||
<div class="doc-version">
|
||||
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>
|
||||
|
|
|
@ -25,11 +25,9 @@ import { first, mapTo } from 'rxjs/operators';
|
|||
import { MockLocationService } from 'testing/location.service';
|
||||
import { MockLogger } from 'testing/logger.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';
|
||||
|
||||
const sideBySideBreakPoint = 992;
|
||||
const hideToCBreakPoint = 800;
|
||||
const startedDelay = 100;
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
@ -58,7 +56,7 @@ describe('AppComponent', () => {
|
|||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
component.onResize(sideBySideBreakPoint + 1); // wide by default
|
||||
component.onResize(showTopMenuWidth + 1); // wide by default
|
||||
|
||||
const de = fixture.debugElement;
|
||||
const docViewerDe = de.query(By.css('aio-doc-viewer'));
|
||||
|
@ -99,7 +97,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
it('should be false on narrow screens', () => {
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
|
||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
@ -112,7 +110,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
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[]);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
|
@ -127,37 +125,47 @@ describe('AppComponent', () => {
|
|||
it('should be false when toc is empty', () => {
|
||||
tocService.tocList.next([]);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when toc is not empty unless the screen is narrow', () => {
|
||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSideBySide', () => {
|
||||
describe('showTopMenu', () => {
|
||||
it('should be updated on resize', () => {
|
||||
component.onResize(sideBySideBreakPoint - 1);
|
||||
expect(component.isSideBySide).toBe(false);
|
||||
component.onResize(showTopMenuWidth - 1);
|
||||
expect(component.showTopMenu).toBe(false);
|
||||
|
||||
component.onResize(sideBySideBreakPoint + 1);
|
||||
expect(component.isSideBySide).toBe(true);
|
||||
component.onResize(showTopMenuWidth + 1);
|
||||
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();
|
||||
};
|
||||
|
||||
describe('when side-by-side (wide)', () => {
|
||||
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side
|
||||
describe('when view is wide', () => {
|
||||
beforeEach(() => resizeTo(dockSideNavWidth + 1)); // wide view
|
||||
|
||||
it('should open when navigating to a guide page (guide/pipes)', () => {
|
||||
navigateTo('guide/pipes');
|
||||
|
@ -232,8 +240,8 @@ describe('AppComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when NOT side-by-side (narrow)', () => {
|
||||
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side
|
||||
describe('when view is narrow', () => {
|
||||
beforeEach(() => resizeTo(dockSideNavWidth - 1)); // narrow view
|
||||
|
||||
it('should be closed when navigating to a guide page (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 nonSidenavDocs = ['features', 'about'];
|
||||
|
||||
sidenavDocs.forEach(doc => {
|
||||
it(`should open when on a sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
nonSidenavDocs.forEach(doc => {
|
||||
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -317,33 +325,33 @@ describe('AppComponent', () => {
|
|||
describe('when manually opened', () => {
|
||||
sidenavDocs.forEach(doc => {
|
||||
it(`should remain opened when on a sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
toggleSidenav();
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
nonSidenavDocs.forEach(doc => {
|
||||
it(`should close when on a non-sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
toggleSidenav();
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(showTopMenuWidth + 1);
|
||||
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 nonSidenavDocs = ['features', 'about'];
|
||||
|
||||
|
@ -352,7 +360,7 @@ describe('AppComponent', () => {
|
|||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -362,7 +370,7 @@ describe('AppComponent', () => {
|
|||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -376,7 +384,7 @@ describe('AppComponent', () => {
|
|||
async function setupSelectorForTesting(mode?: string) {
|
||||
createTestingModule('a/b', mode);
|
||||
await initializeTest();
|
||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
||||
component.onResize(dockSideNavWidth + 1); // wide view
|
||||
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
|
||||
selectComponent = selectElement.componentInstance;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
|||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
const sideNavView = 'SideNav';
|
||||
export const showTopMenuWidth = 992;
|
||||
export const dockSideNavWidth = 992;
|
||||
export const showFloatingTocWidth = 800;
|
||||
|
||||
@Component({
|
||||
selector: 'aio-shell',
|
||||
|
@ -57,18 +60,17 @@ export class AppComponent implements OnInit {
|
|||
isStarting = true;
|
||||
isTransitioning = true;
|
||||
isFetching = false;
|
||||
isSideBySide = false;
|
||||
showTopMenu = false;
|
||||
dockSideNav = false;
|
||||
private isFetchingTimeout: any;
|
||||
private isSideNavDoc = false;
|
||||
|
||||
private sideBySideWidth = 992;
|
||||
sideNavNodes: NavigationNode[];
|
||||
topMenuNodes: NavigationNode[];
|
||||
topMenuNarrowNodes: NavigationNode[];
|
||||
|
||||
hasFloatingToc = false;
|
||||
private showFloatingToc = new BehaviorSubject(false);
|
||||
private showFloatingTocWidth = 800;
|
||||
tocMaxHeight: string;
|
||||
private tocMaxHeightOffset = 0;
|
||||
|
||||
|
@ -76,8 +78,8 @@ export class AppComponent implements OnInit {
|
|||
|
||||
private currentUrl: string;
|
||||
|
||||
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
||||
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
||||
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
|
||||
get mode() { return this.dockSideNav && (this.isSideNavDoc || this.showTopMenu) ? 'side' : 'over'; }
|
||||
|
||||
// Search related properties
|
||||
showSearchResults = false;
|
||||
|
@ -239,13 +241,14 @@ export class AppComponent implements OnInit {
|
|||
|
||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||
onResize(width: number) {
|
||||
this.isSideBySide = width >= this.sideBySideWidth;
|
||||
this.showFloatingToc.next(width > this.showFloatingTocWidth);
|
||||
this.showTopMenu = width >= showTopMenuWidth;
|
||||
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
|
||||
// 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.)
|
||||
this.sidenav.toggle(false);
|
||||
}
|
||||
|
@ -338,7 +341,7 @@ export class AppComponent implements OnInit {
|
|||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// VARIABLES
|
||||
$hamburgerShownMaxWidth: 991px;
|
||||
$hamburgerHiddenMinWidth: $hamburgerShownMaxWidth + 1;
|
||||
$showTopMenuWidth: 992px;
|
||||
$hideTopMenuWidth: $showTopMenuWidth - 1;
|
||||
$hamburgerShownMargin: 0 8px 0 0;
|
||||
$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-start mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
@media (min-width: $hamburgerHiddenMinWidth) {
|
||||
@media (min-width: $showTopMenuWidth) {
|
||||
.hamburger.mat-button {
|
||||
// Hamburger shown on non-marketing pages even on large screens.
|
||||
margin: $hamburgerShownMargin;
|
||||
|
@ -68,7 +68,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||
margin: $hamburgerShownMargin;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: $hamburgerHiddenMinWidth) {
|
||||
@media (min-width: $showTopMenuWidth) {
|
||||
// Hamburger hidden by default on large screens.
|
||||
// (Will be shown per doc.)
|
||||
margin: $hamburgerHiddenMargin;
|
||||
|
@ -106,7 +106,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $hamburgerShownMaxWidth) {
|
||||
@media screen and (max-width: $hideTopMenuWidth) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||
top: 12px;
|
||||
height: 40px;
|
||||
|
||||
@media (max-width: $hamburgerShownMaxWidth) {
|
||||
@media (max-width: $hideTopMenuWidth) {
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue