feat(docs-infra): change navigation in resources page (#34756)

https://angular.io/resources needs to be sturctured to be able to navigate to all resources with improved user experience. A lone scroll bar in this page will not help the reader a great deal in exploring the resources

Fixes #33526

PR Close #34756
This commit is contained in:
ajitsinghkaler 2020-01-30 21:33:43 +05:30 committed by Misko Hevery
parent 7431206247
commit e672b1f2ac
4 changed files with 113 additions and 60 deletions

View File

@ -1,28 +1,28 @@
<div class="resources-container"> <div class="resources-container">
<div class="l-flex--column"> <div class="flex-center group-buttons">
<div class="showcase" *ngFor="let category of categories"> <a *ngFor="let category of categories"
<header class="c-resource-header"> [class.selected]="category.id == selectedCategory.id"
<a class="h-anchor-offset" id="{{category.id}}"></a> class="button mat-button filter-button"
<h2>{{category.title}}</h2> (click)="selectCategory(category.id)"
</header> (keyup.enter)="selectCategory(category.id)">{{category.title}}</a>
</div>
<div class="l-flex--column align-items-center">
<div class="shadow-1 showcase">
<div *ngFor="let subCategory of selectedCategory?.subCategories">
<a class="h-anchor-offset" id="{{subCategory.id}}"></a>
<h3 class="subcategory-title">{{subCategory.title}}</h3>
<div class="shadow-1"> <div *ngFor="let resource of subCategory.resources">
<div *ngFor="let subCategory of category.subCategories"> <div class="c-resource" *ngIf="resource.rev">
<a class="h-anchor-offset" id="{{subCategory.id}}"></a> <a class="l-flex--column resource-row-link" target="_blank" [href]="resource.url">
<h3 class="subcategory-title">{{subCategory.title}}</h3> <div>
<h4>{{resource.title}}</h4>
<div *ngFor="let resource of subCategory.resources"> <p class="resource-description">{{resource.desc || 'No Description'}}</p>
<div class="c-resource" *ngIf="resource.rev">
<a class="l-flex--column resource-row-link" target="_blank" [href]="resource.url">
<div>
<h4>{{resource.title}}</h4>
<p class="resource-description">{{resource.desc || 'No Description'}}</p>
</div>
</a>
</div> </div>
</div> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

@ -1,78 +1,115 @@
import { ReflectiveInjector } from '@angular/core'; import { ReflectiveInjector } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ResourceListComponent } from './resource-list.component'; import { ResourceListComponent } from './resource-list.component';
import { ResourceService } from './resource.service'; import { ResourceService } from './resource.service';
import { LocationService } from 'app/shared/location.service';
import { Category } from './resource.model'; import { Category } from './resource.model';
// Testing the component class behaviors, independent of its template // Testing the component class behaviors, independent of its template
// Let e2e tests verify how it displays. // Let e2e tests verify how it displays.
describe('ResourceListComponent', () => { describe('ResourceListComponent', () => {
let component: ResourceListComponent;
let injector: ReflectiveInjector; let injector: ReflectiveInjector;
let location: TestPlatformLocation; let resourceService: TestResourceService;
let locationService: TestLocationService;
let categories: Category[];
beforeEach(() => { beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([ injector = ReflectiveInjector.resolveAndCreate([
ResourceListComponent, ResourceListComponent,
{provide: PlatformLocation, useClass: TestPlatformLocation }, {provide: ResourceService, useClass: TestResourceService },
{provide: ResourceService, useClass: TestResourceService } {provide: LocationService, useClass: TestLocationService }
]); ]);
location = injector.get(PlatformLocation); locationService = injector.get(LocationService);
resourceService = injector.get(ResourceService);
categories = resourceService.testCategories;
}); });
it('should set the location w/o leading slashes', () => { it('should select the first category when no query string', () => {
location.pathname = '////resources'; component = getComponent();
const component = getComponent(); expect(component.selectedCategory).toBe(categories[0]);
expect(component.location).toBe('resources');
}); });
it('href(id) should return the expected href', () => { it('should select the first category when query string w/o "category" property', () => {
location.pathname = '////resources'; locationService.searchResult = { foo: 'development' };
const component = getComponent(); component = getComponent();
expect(component.href({id: 'foo'})).toBe('resources#foo'); expect(component.selectedCategory).toBe(categories[0]);
}); });
it('should set scroll position to zero when no target element', () => { it('should select the first category when query category not found', () => {
const component = getComponent(); locationService.searchResult = { category: 'foo' };
component.onScroll(undefined); component = getComponent();
expect(component.scrollPos).toBe(0); expect(component.selectedCategory).toBe(categories[0]);
}); });
it('should set scroll position to element.scrollTop when that is defined', () => { it('should select the education category when query category is "education"', () => {
const component = getComponent(); locationService.searchResult = { category: 'education' };
component.onScroll({scrollTop: 42}); component = getComponent();
expect(component.scrollPos).toBe(42); expect(component.selectedCategory).toBe(categories[1]);
}); });
it('should set scroll position to element.body.scrollTop when that is defined', () => { it('should select the education category when query category is "EDUCATION" (case insensitive)', () => {
const component = getComponent(); locationService.searchResult = { category: 'EDUCATION' };
component.onScroll({body: {scrollTop: 42}}); component = getComponent();
expect(component.scrollPos).toBe(42); expect(component.selectedCategory).toBe(categories[1]);
}); });
it('should set scroll position to 0 when no target.body.scrollTop defined', () => { it('should set the query to the "education" category when user selects "education"', () => {
const component = getComponent(); component = getComponent();
component.onScroll({body: {}}); component.selectCategory('education');
expect(component.scrollPos).toBe(0); expect(locationService.searchResult['category']).toBe('education');
});
it('should set the query to the first category when user selects unknown name', () => {
component = getComponent();
component.selectCategory('education'); // a legit group that isn't the first
component.selectCategory('foo'); // not a legit group name
expect(locationService.searchResult['category']).toBe('development');
}); });
//// Test Helpers //// //// Test Helpers ////
function getComponent(): ResourceListComponent { return injector.get(ResourceListComponent); } function getComponent(): ResourceListComponent {
const comp = injector.get(ResourceListComponent);
class TestPlatformLocation { comp.ngOnInit();
pathname = 'resources'; return comp;
} }
class TestResourceService { class TestResourceService {
categories = of(getTestData); testCategories = getTestData();
categories = of(this.testCategories);
}
interface SearchResult { [index: string]: string; }
class TestLocationService {
searchResult: SearchResult = {};
search = jasmine.createSpy('search').and.callFake(() => this.searchResult);
setSearch = jasmine.createSpy('setSearch')
.and.callFake((_label: string, result: SearchResult) => {
this.searchResult = result;
});
} }
function getTestData(): Category[] { function getTestData(): Category[] {
return []; // Not interested in the data in these tests return [
// Not interested in the sub-categories data in these tests
{
id: 'development',
title: 'Development',
order: 0,
subCategories: []
},
{
id: 'education',
title: 'Education',
order: 1,
subCategories: []
},
];
} }
}); });

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Category } from './resource.model'; import { Category } from './resource.model';
import { ResourceService } from './resource.service'; import { ResourceService } from './resource.service';
import { LocationService } from 'app/shared/location.service';
/* tslint:disable:template-accessibility-elements-content */ /* tslint:disable:template-accessibility-elements-content */
@Component({ @Component({
@ -11,16 +12,27 @@ import { ResourceService } from './resource.service';
export class ResourceListComponent implements OnInit { export class ResourceListComponent implements OnInit {
categories: Category[]; categories: Category[];
location: string; selectedCategory: Category;
constructor( constructor(
private resourceService: ResourceService) { private resourceService: ResourceService,
this.location = location.pathname.replace(/^\/+/, ''); private locationService: LocationService) {
} }
ngOnInit() { ngOnInit() {
const category = this.locationService.search()['category'] || '';
// Not using async pipe because cats appear twice in template // Not using async pipe because cats appear twice in template
// No need to unsubscribe because categories observable completes. // No need to unsubscribe because categories observable completes.
this.resourceService.categories.subscribe(cats => this.categories = cats); this.resourceService.categories.subscribe(cats => {
this.categories = cats;
this.selectCategory(category);
});
}
selectCategory(id: string) {
id = id.toLowerCase();
this.selectedCategory =
this.categories.find(category => category.id.toLowerCase() === id) || this.categories[0];
this.locationService.setSearch('', {category: this.selectedCategory.id});
} }
} }

View File

@ -169,6 +169,10 @@ aio-resource-list {
flex-direction: column; flex-direction: column;
} }
.align-items-center{
align-items: center;
}
.c-resource-header { .c-resource-header {
margin-bottom: 16px; margin-bottom: 16px;
} }