refactor(aio): use the SelectMenuComponent for all select menus
The API filters and the docs version switcher now use the SelectMenuComponent. Fixes #16367 and #17055
This commit is contained in:
parent
c9b930dd82
commit
bb46f54ad7
|
@ -22,9 +22,7 @@
|
||||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" ></aio-nav-menu>
|
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" ></aio-nav-menu>
|
||||||
|
|
||||||
<div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}">
|
<div class="doc-version" title="Angular docs version {{currentDocVersion?.title}}">
|
||||||
<select (change)="onDocVersionChange($event.target.selectedIndex)">
|
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="docVersions && docVersions[0]"></aio-select>
|
||||||
<option *ngFor="let version of docVersions" [value]="version.title">{{version.title}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</md-sidenav>
|
</md-sidenav>
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { ScrollService } from 'app/shared/scroll.service';
|
||||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||||
import { SearchService } from 'app/search/search.service';
|
import { SearchService } from 'app/search/search.service';
|
||||||
|
import { SelectComponent, Option } from 'app/shared/select/select.component';
|
||||||
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
||||||
import { TocComponent } from 'app/embedded/toc/toc.component';
|
import { TocComponent } from 'app/embedded/toc/toc.component';
|
||||||
import { MdSidenav } from '@angular/material';
|
import { MdSidenav } from '@angular/material';
|
||||||
|
@ -221,26 +222,28 @@ describe('AppComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SideNav version selector', () => {
|
describe('SideNav version selector', () => {
|
||||||
|
let selectElement: DebugElement;
|
||||||
|
let selectComponent: SelectComponent;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
||||||
|
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
|
||||||
|
selectComponent = selectElement.componentInstance;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pick first (current) version by default', () => {
|
it('should pick first (current) version by default', () => {
|
||||||
const versionSelector = sidenav.querySelector('select');
|
expect(selectComponent.selected.title).toEqual(component.versionInfo.raw);
|
||||||
expect(versionSelector.value).toEqual(component.versionInfo.raw);
|
|
||||||
expect(versionSelector.selectedIndex).toEqual(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Older docs versions have an href
|
// Older docs versions have an href
|
||||||
it('should navigate when change to a version with an href', () => {
|
it('should navigate when change to a version with an href', () => {
|
||||||
component.onDocVersionChange(1);
|
selectElement.triggerEventHandler('change', { option: component.docVersions[1] as Option, index: 1});
|
||||||
expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url);
|
expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The current docs version should not have an href
|
// The current docs version should not have an href
|
||||||
// This may change when we perfect our docs versioning approach
|
// This may change when we perfect our docs versioning approach
|
||||||
it('should not navigate when change to a version without an href', () => {
|
it('should not navigate when change to a version without an href', () => {
|
||||||
component.onDocVersionChange(0);
|
selectElement.triggerEventHandler('change', { option: component.docVersions[0] as Option, index: 0});
|
||||||
expect(locationService.go).not.toHaveBeenCalled();
|
expect(locationService.go).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,8 @@ import { SearchResultsComponent } from './search/search-results/search-results.c
|
||||||
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
import { SearchBoxComponent } from './search/search-box/search-box.component';
|
||||||
import { TocService } from 'app/shared/toc.service';
|
import { TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
|
||||||
// These are the hardcoded inline svg sources to be used by the `<md-icon>` component
|
// These are the hardcoded inline svg sources to be used by the `<md-icon>` component
|
||||||
export const svgIconProviders = [
|
export const svgIconProviders = [
|
||||||
{
|
{
|
||||||
|
@ -80,7 +82,8 @@ export const svgIconProviders = [
|
||||||
MdSidenavModule,
|
MdSidenavModule,
|
||||||
MdTabsModule,
|
MdTabsModule,
|
||||||
MdToolbarModule,
|
MdToolbarModule,
|
||||||
SwUpdatesModule
|
SwUpdatesModule,
|
||||||
|
SharedModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
|
|
@ -1,26 +1,17 @@
|
||||||
<div class="l-flex-wrap info-banner api-filter">
|
<div class="l-flex-wrap info-banner api-filter">
|
||||||
|
|
||||||
<div class="form-select-menu">
|
<aio-select (change)="setType($event.option)"
|
||||||
<button class="form-select-button has-symbol" (click)="toggleTypeMenu()">
|
[options]="types"
|
||||||
<strong>Type:</strong><span class="symbol {{type.name}}"></span>{{type.title}}
|
[selected]="type"
|
||||||
</button>
|
[showSymbol]="true"
|
||||||
<ul class="form-select-dropdown" *ngIf="showTypeMenu">
|
label="Type:">
|
||||||
<li *ngFor="let t of types" (click)="setType(t)" [class.selected]="t === type">
|
</aio-select>
|
||||||
<span class="symbol {{t.name}}"></span>{{t.title}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-select-menu">
|
<aio-select (change)="setStatus($event.option)"
|
||||||
<button class="form-select-button" (click)="toggleStatusMenu()">
|
[options]="statuses"
|
||||||
<strong>Status:</strong>{{status.title}}
|
[selected]="status"
|
||||||
</button>
|
label="Status:">
|
||||||
<ul class="form-select-dropdown" *ngIf="showStatusMenu">
|
</aio-select>
|
||||||
<li *ngFor="let s of statuses" (click)="setStatus(s)" [class.selected]="s === status">
|
|
||||||
{{s.title}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-search">
|
<div class="form-search">
|
||||||
<input #filter placeholder="Filter" (input)="setQuery($event.target.value)">
|
<input #filter placeholder="Filter" (input)="setQuery($event.target.value)">
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
import { ApiListComponent } from './api-list.component';
|
import { ApiListComponent } from './api-list.component';
|
||||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
|
||||||
describe('ApiListComponent', () => {
|
describe('ApiListComponent', () => {
|
||||||
let component: ApiListComponent;
|
let component: ApiListComponent;
|
||||||
|
@ -12,6 +13,7 @@ describe('ApiListComponent', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ SharedModule ],
|
||||||
declarations: [ ApiListComponent ],
|
declarations: [ ApiListComponent ],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ApiService, useClass: TestApiService },
|
{ provide: ApiService, useClass: TestApiService },
|
||||||
|
@ -75,17 +77,17 @@ describe('ApiListComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('item.show should be true for items with selected status', () => {
|
it('item.show should be true for items with selected status', () => {
|
||||||
component.setStatus({name: 'stable', title: 'Stable'});
|
component.setStatus({value: 'stable', title: 'Stable'});
|
||||||
expectFilteredResult('status: stable', item => item.stability === 'stable');
|
expectFilteredResult('status: stable', item => item.stability === 'stable');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('item.show should be true for items with "security-risk" status when selected', () => {
|
it('item.show should be true for items with "security-risk" status when selected', () => {
|
||||||
component.setStatus({name: 'security-risk', title: 'Security Risk'});
|
component.setStatus({value: 'security-risk', title: 'Security Risk'});
|
||||||
expectFilteredResult('status: security-risk', item => item.securityRisk);
|
expectFilteredResult('status: security-risk', item => item.securityRisk);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('item.show should be true for items of selected type', () => {
|
it('item.show should be true for items of selected type', () => {
|
||||||
component.setType({name: 'class', title: 'Class'});
|
component.setType({value: 'class', title: 'Class'});
|
||||||
expectFilteredResult('type: class', item => item.docType === 'class');
|
expectFilteredResult('type: class', item => item.docType === 'class');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -189,8 +191,8 @@ describe('ApiListComponent', () => {
|
||||||
|
|
||||||
it('should have query, status, and type', () => {
|
it('should have query, status, and type', () => {
|
||||||
component.setQuery('foo');
|
component.setQuery('foo');
|
||||||
component.setStatus({name: 'stable', title: 'Stable'});
|
component.setStatus({value: 'stable', title: 'Stable'});
|
||||||
component.setType({name: 'class', title: 'Class'});
|
component.setType({value: 'class', title: 'Class'});
|
||||||
|
|
||||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||||
expect(search.query).toBe('foo');
|
expect(search.query).toBe('foo');
|
||||||
|
|
|
@ -15,10 +15,7 @@ import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||||
|
|
||||||
interface MenuItem {
|
import { Option } from 'app/shared/select/select.component';
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchCriteria {
|
class SearchCriteria {
|
||||||
query? = '';
|
query? = '';
|
||||||
|
@ -40,29 +37,29 @@ export class ApiListComponent implements OnInit {
|
||||||
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
|
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
|
||||||
private searchCriteria = new SearchCriteria();
|
private searchCriteria = new SearchCriteria();
|
||||||
|
|
||||||
status: MenuItem;
|
status: Option;
|
||||||
type: MenuItem;
|
type: Option;
|
||||||
|
|
||||||
// API types
|
// API types
|
||||||
types: MenuItem[] = [
|
types: Option[] = [
|
||||||
{ name: 'all', title: 'All' },
|
{ value: 'all', title: 'All' },
|
||||||
{ name: 'directive', title: 'Directive' },
|
{ value: 'directive', title: 'Directive' },
|
||||||
{ name: 'pipe', title: 'Pipe'},
|
{ value: 'pipe', title: 'Pipe'},
|
||||||
{ name: 'decorator', title: 'Decorator' },
|
{ value: 'decorator', title: 'Decorator' },
|
||||||
{ name: 'class', title: 'Class' },
|
{ value: 'class', title: 'Class' },
|
||||||
{ name: 'interface', title: 'Interface' },
|
{ value: 'interface', title: 'Interface' },
|
||||||
{ name: 'function', title: 'Function' },
|
{ value: 'function', title: 'Function' },
|
||||||
{ name: 'enum', title: 'Enum' },
|
{ value: 'enum', title: 'Enum' },
|
||||||
{ name: 'type-alias', title: 'Type Alias' },
|
{ value: 'type-alias', title: 'Type Alias' },
|
||||||
{ name: 'const', title: 'Const'}
|
{ value: 'const', title: 'Const'}
|
||||||
];
|
];
|
||||||
|
|
||||||
statuses: MenuItem[] = [
|
statuses: Option[] = [
|
||||||
{ name: 'all', title: 'All' },
|
{ value: 'all', title: 'All' },
|
||||||
{ name: 'stable', title: 'Stable' },
|
{ value: 'stable', title: 'Stable' },
|
||||||
{ name: 'deprecated', title: 'Deprecated' },
|
{ value: 'deprecated', title: 'Deprecated' },
|
||||||
{ name: 'experimental', title: 'Experimental' },
|
{ value: 'experimental', title: 'Experimental' },
|
||||||
{ name: 'security-risk', title: 'Security Risk' }
|
{ value: 'security-risk', title: 'Security Risk' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ViewChild('filter') queryEl: ElementRef;
|
@ViewChild('filter') queryEl: ElementRef;
|
||||||
|
@ -90,16 +87,16 @@ export class ApiListComponent implements OnInit {
|
||||||
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
|
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status: MenuItem) {
|
setStatus(status: Option) {
|
||||||
this.toggleStatusMenu();
|
this.toggleStatusMenu();
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.setSearchCriteria({status: status.name});
|
this.setSearchCriteria({status: status.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
setType(type: MenuItem) {
|
setType(type: Option) {
|
||||||
this.toggleTypeMenu();
|
this.toggleTypeMenu();
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.setSearchCriteria({type: type.name});
|
this.setSearchCriteria({type: type.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleStatusMenu() {
|
toggleStatusMenu() {
|
||||||
|
@ -150,13 +147,13 @@ export class ApiListComponent implements OnInit {
|
||||||
// Hack: can't bind to query because input cursor always forced to end-of-line.
|
// Hack: can't bind to query because input cursor always forced to end-of-line.
|
||||||
this.queryEl.nativeElement.value = q;
|
this.queryEl.nativeElement.value = q;
|
||||||
|
|
||||||
this.status = this.statuses.find(x => x.name === status) || this.statuses[0];
|
this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
|
||||||
this.type = this.types.find(x => x.name === type) || this.types[0];
|
this.type = this.types.find(x => x.value === type) || this.types[0];
|
||||||
|
|
||||||
this.searchCriteria = {
|
this.searchCriteria = {
|
||||||
query: q,
|
query: q,
|
||||||
status: this.status.name,
|
status: this.status.value,
|
||||||
type: this.type.name
|
type: this.type.value
|
||||||
};
|
};
|
||||||
|
|
||||||
this.criteriaSubject.next(this.searchCriteria);
|
this.criteriaSubject.next(this.searchCriteria);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { PrettyPrinter } from './code/pretty-printer.service';
|
||||||
// Reusable components (used inside embedded components)
|
// Reusable components (used inside embedded components)
|
||||||
import { MdIconModule, MdTabsModule } from '@angular/material';
|
import { MdIconModule, MdTabsModule } from '@angular/material';
|
||||||
import { CodeComponent } from './code/code.component';
|
import { CodeComponent } from './code/code.component';
|
||||||
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
|
||||||
// Embedded Components
|
// Embedded Components
|
||||||
import { ApiListComponent } from './api/api-list.component';
|
import { ApiListComponent } from './api/api-list.component';
|
||||||
|
@ -41,7 +42,8 @@ export class EmbeddedComponents {
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MdIconModule,
|
MdIconModule,
|
||||||
MdTabsModule
|
MdTabsModule,
|
||||||
|
SharedModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
embeddedComponents,
|
embeddedComponents,
|
||||||
|
|
|
@ -31,13 +31,13 @@ aio-api-list {
|
||||||
aio-api-list > div {
|
aio-api-list > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 32px auto;
|
margin: 32px auto;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
.form-select-menu, .form-search {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,82 +115,6 @@ $tablet-breakpoint: 800px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SELECT MENU */
|
|
||||||
|
|
||||||
$form-select-width: 200px;
|
|
||||||
|
|
||||||
.form-select-menu {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-select-button {
|
|
||||||
background: $white;
|
|
||||||
box-shadow: 0 2px 2px rgba($black, 0.24), 0 0 2px rgba($black, 0.12);
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid $white;
|
|
||||||
color: $blue-grey-600;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
outline: none;
|
|
||||||
padding: 0 16px;
|
|
||||||
text-align: left;
|
|
||||||
width: $form-select-width - 16px;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-right: 8px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-symbol {
|
|
||||||
.symbol {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-select-dropdown {
|
|
||||||
background: $white;
|
|
||||||
box-shadow: 0 16px 16px rgba($black, 0.24), 0 0 16px rgba($black, 0.12);
|
|
||||||
border-radius: 4px;
|
|
||||||
left: -8px;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 0;
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
width: 200px;
|
|
||||||
z-index: $layer-2;
|
|
||||||
|
|
||||||
li {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 32px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 16px 0 40px;
|
|
||||||
position: relative;
|
|
||||||
transition: all .2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $blue-grey-50;
|
|
||||||
color: $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: $blue-grey-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.symbol {
|
|
||||||
left: 16px;
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
z-index: $layer-5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* API SYMBOLS */
|
/* API SYMBOLS */
|
||||||
|
|
||||||
/* SYMBOL CLASS */
|
/* SYMBOL CLASS */
|
||||||
|
@ -224,8 +148,12 @@ $form-select-width: 200px;
|
||||||
/* API FILTER MENU */
|
/* API FILTER MENU */
|
||||||
|
|
||||||
.api-filter {
|
.api-filter {
|
||||||
.form-select-menu {
|
aio-select {
|
||||||
float: left;
|
width: 200px;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-search {
|
.form-search {
|
||||||
|
|
Loading…
Reference in New Issue