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:
parent
a0b30e5dfb
commit
2d5623911a
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) : []; }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue