fix(aio): sidebar folder state after select item

Closes #17245 and #17253

When the user selects a doc item in the side nav:
1) expand folder(s) leading to the selected doc item
2) on a wide display, keep other already expanded folders open
3) on narrow (mobile) display, collapse other expanded folders

Used to do (3) when wide. Issue #17245 asks for (2).

That logic was bypassed for selected node when we allowed headers to have content
because that unintentionally expanded the header’s folder when selected.
Because the selected node is no longer a header with content, removing this exclusion
also means that folders are expanded/collapsed with above logic even for API pages.
This commit is contained in:
Ward Bell 2017-06-08 15:46:32 -07:00 committed by Pete Bacon Darwin
parent a0b30e5dfb
commit 2d5623911a
6 changed files with 187 additions and 148 deletions

View File

@ -27,9 +27,7 @@ describe('site App', function() {
// Show the menu // Show the menu
page.docsMenuLink.click(); page.docsMenuLink.click();
// Open the tutorial header // Tutorial folder should still be expanded because this test runs in wide mode
page.getNavItem(/tutorial/i).click();
// Navigate to the tutorial introduction via a link in the sidenav // Navigate to the tutorial introduction via a link in the sidenav
page.getNavItem(/introduction/i).click(); page.getNavItem(/introduction/i).click();
expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i); expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i);

View File

@ -18,8 +18,8 @@
<md-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main"> <md-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
<md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()"> <md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode" (open)="updateHostClasses()" (close)="updateHostClasses()">
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow"></aio-nav-menu> <aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" ></aio-nav-menu> <aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
<div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}"> <div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}">
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="docVersions && docVersions[0]"></aio-select> <aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="docVersions && docVersions[0]"></aio-select>

View File

@ -20,7 +20,7 @@
</button> </button>
<div class="heading-children" [ngClass]="classes"> <div class="heading-children" [ngClass]="classes">
<aio-nav-item *ngFor="let node of node.children" [level]="level + 1" <aio-nav-item *ngFor="let node of node.children" [level]="level + 1" [isWide]="isWide"
[node]="node" [selectedNodes]="selectedNodes"></aio-nav-item> [node]="node" [selectedNodes]="selectedNodes"></aio-nav-item>
</div> </div>
</div> </div>

View File

@ -1,13 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SimpleChange, SimpleChanges } from '@angular/core'; import { By } from '@angular/platform-browser';
import { SimpleChange, SimpleChanges, NO_ERRORS_SCHEMA } from '@angular/core';
import { NavItemComponent } from './nav-item.component'; import { NavItemComponent } from './nav-item.component';
import { NavigationNode } from 'app/navigation/navigation.model'; import { NavigationNode } from 'app/navigation/navigation.model';
// Testing the component class behaviors, independent of its template describe('NavItemComponent', () => {
// No dependencies. Just new it and test :)
// Let e2e tests verify how it displays. // Testing the component class behaviors, independent of its template
describe('NavItemComponent (class-only)', () => { // No dependencies. Just new it and test :)
// Let e2e tests verify how it displays.
describe('(class-only)', () => {
let component: NavItemComponent; let component: NavItemComponent;
@ -42,9 +45,8 @@ describe('NavItemComponent (class-only)', () => {
it('with selected node', () => { it('with selected node', () => {
initialize(selectedNodes[0]); initialize(selectedNodes[0]);
expect(component.classes).toEqual( expect(component.classes).toEqual(
// selecting the current node has no effect on expanded state, // selected node should be expanded even if is a header.
// even if current node is a header. { 'level-1': true, collapsed: false, expanded: true, selected: true }
{ 'level-1': true, collapsed: true, expanded: false, selected: true}
); );
}); });
@ -52,14 +54,14 @@ describe('NavItemComponent (class-only)', () => {
initialize(selectedNodes[1]); initialize(selectedNodes[1]);
expect(component.classes).toEqual( expect(component.classes).toEqual(
// ancestor is a header and should be expanded // ancestor is a header and should be expanded
{ 'level-1': true, collapsed: false, expanded: true, selected: true} { 'level-1': true, collapsed: false, expanded: true, selected: true }
); );
}); });
it('with other than a selected node or ancestor', () => { it('with other than a selected node or ancestor', () => {
initialize({ title: 'x' }); initialize({ title: 'x' });
expect(component.classes).toEqual( expect(component.classes).toEqual(
{ 'level-1': true, collapsed: true, expanded: false, selected: false} { 'level-1': true, collapsed: true, expanded: false, selected: false }
); );
}); });
}); });
@ -69,16 +71,24 @@ describe('NavItemComponent (class-only)', () => {
// this node won't be the selected node when ngOnChanges() called // this node won't be the selected node when ngOnChanges() called
beforeEach(() => component.node = { title: 'x' }); beforeEach(() => component.node = { title: 'x' });
it('should collapse if previously expanded', () => { it('should de-select if previously selected', () => {
component.isSelected = true;
onChanges();
expect(component.isSelected).toBe(false, 'becomes de-selected');
});
it('should collapse if previously expanded in narrow mode', () => {
component.isWide = false;
component.isExpanded = true; component.isExpanded = true;
onChanges(); onChanges();
expect(component.isExpanded).toBe(false, 'becomes collapsed'); expect(component.isExpanded).toBe(false, 'becomes collapsed');
}); });
it('should de-select if previously selected', () => { it('should remain expanded in wide mode', () => {
component.isSelected = true; component.isWide = true;
component.isExpanded = true;
onChanges(); onChanges();
expect(component.isSelected).toBe(false, 'becomes de-selected'); expect(component.isExpanded).toBe(true, 'remains expanded');
}); });
}); });
@ -93,10 +103,10 @@ describe('NavItemComponent (class-only)', () => {
expect(component.isSelected).toBe(true, 'becomes selected'); expect(component.isSelected).toBe(true, 'becomes selected');
}); });
it('should leave the expanded/collapsed state untouched', () => { it('should expand the current node or keep it expanded', () => {
component.isExpanded = false; component.isExpanded = false;
onChanges(); onChanges();
expect(component.isExpanded).toBe(false, 'remains false'); expect(component.isExpanded).toBe(true, 'becomes true');
component.isExpanded = true; component.isExpanded = true;
onChanges(); onChanges();
@ -156,4 +166,31 @@ describe('NavItemComponent (class-only)', () => {
expect(setClassesSpy).toHaveBeenCalled(); expect(setClassesSpy).toHaveBeenCalled();
}); });
}); });
});
describe('(via TestBed)', () => {
it('should pass the `isWide` property to all child nav-items', () => {
TestBed.configureTestingModule({
declarations: [NavItemComponent],
schemas: [NO_ERRORS_SCHEMA]
});
const fixture = TestBed.createComponent(NavItemComponent);
fixture.componentInstance.node = {
title: 'x',
children: [{ title: 'a' }, { title: 'b' }]
};
fixture.componentInstance.isWide = true;
fixture.detectChanges();
let children = fixture.debugElement.queryAll(By.directive(NavItemComponent));
expect(children.length).toEqual(2);
children.forEach(child => expect(child.componentInstance.isWide).toBe(true));
fixture.componentInstance.isWide = false;
fixture.detectChanges();
children = fixture.debugElement.queryAll(By.directive(NavItemComponent));
expect(children.length).toEqual(2);
children.forEach(child => expect(child.componentInstance.isWide).toBe(false));
});
});
}); });

View File

@ -6,20 +6,23 @@ import { NavigationNode } from 'app/navigation/navigation.model';
templateUrl: 'nav-item.component.html', templateUrl: 'nav-item.component.html',
}) })
export class NavItemComponent implements OnChanges { export class NavItemComponent implements OnChanges {
@Input() selectedNodes: NavigationNode[]; @Input() isWide = false;
@Input() node: NavigationNode;
@Input() level = 1; @Input() level = 1;
@Input() node: NavigationNode;
@Input() selectedNodes: NavigationNode[];
isExpanded = false; isExpanded = false;
isSelected = false; isSelected = false;
classes: {[index: string]: boolean }; classes: {[index: string]: boolean };
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['selectedNodes'] || changes['node']) { if (changes['selectedNodes'] || changes['node'] || changes['isWide']) {
if (this.selectedNodes) { if (this.selectedNodes) {
const ix = this.selectedNodes.indexOf(this.node); const ix = this.selectedNodes.indexOf(this.node);
this.isSelected = ix !== -1; this.isSelected = ix !== -1; // this node is the selected node or its ancestor
if (ix !== 0) { this.isExpanded = this.isSelected; } this.isExpanded = this.isSelected || // expand if selected or ...
// preserve expanded state when display is wide; collapse in mobile.
(this.isWide && this.isExpanded);
} else { } else {
this.isSelected = false; this.isSelected = false;
} }

View File

@ -4,11 +4,12 @@ import { CurrentNode, NavigationNode } from 'app/navigation/navigation.service';
@Component({ @Component({
selector: 'aio-nav-menu', selector: 'aio-nav-menu',
template: ` template: `
<aio-nav-item *ngFor="let node of filteredNodes" [node]="node" [selectedNodes]="currentNode?.nodes"> <aio-nav-item *ngFor="let node of filteredNodes" [node]="node" [selectedNodes]="currentNode?.nodes" [isWide]="isWide">
</aio-nav-item>` </aio-nav-item>`
}) })
export class NavMenuComponent { export class NavMenuComponent {
@Input() currentNode: CurrentNode; @Input() currentNode: CurrentNode;
@Input() nodes: NavigationNode[] ; @Input() isWide = false;
@Input() nodes: NavigationNode[];
get filteredNodes() { return this.nodes ? this.nodes.filter(n => !n.hidden) : []; } get filteredNodes() { return this.nodes ? this.nodes.filter(n => !n.hidden) : []; }
} }