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:
parent
7431206247
commit
e672b1f2ac
|
@ -1,13 +1,14 @@
|
||||||
<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="shadow-1">
|
<div class="l-flex--column align-items-center">
|
||||||
<div *ngFor="let subCategory of category.subCategories">
|
<div class="shadow-1 showcase">
|
||||||
|
<div *ngFor="let subCategory of selectedCategory?.subCategories">
|
||||||
<a class="h-anchor-offset" id="{{subCategory.id}}"></a>
|
<a class="h-anchor-offset" id="{{subCategory.id}}"></a>
|
||||||
<h3 class="subcategory-title">{{subCategory.title}}</h3>
|
<h3 class="subcategory-title">{{subCategory.title}}</h3>
|
||||||
|
|
||||||
|
@ -24,5 +25,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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: []
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue