diff --git a/src/app/pages/component-viewer/component-api.html b/src/app/pages/component-viewer/component-api.html index 3de0bac87..b601b0661 100644 --- a/src/app/pages/component-viewer/component-api.html +++ b/src/app/pages/component-viewer/component-api.html @@ -4,7 +4,5 @@ - + (contentLoaded)="focusInitialTarget(); toc.createLinksAndScroll()"> + diff --git a/src/app/pages/component-viewer/component-overview.html b/src/app/pages/component-viewer/component-overview.html index e56dcb1b5..41c2fe171 100644 --- a/src/app/pages/component-viewer/component-overview.html +++ b/src/app/pages/component-viewer/component-overview.html @@ -4,6 +4,6 @@ + (contentLoaded)="focusInitialTarget(); toc.createLinksAndScroll()"> - + diff --git a/src/app/pages/component-viewer/component-viewer.ts b/src/app/pages/component-viewer/component-viewer.ts index cd033cd16..bce1ea518 100644 --- a/src/app/pages/component-viewer/component-viewer.ts +++ b/src/app/pages/component-viewer/component-viewer.ts @@ -1,5 +1,5 @@ import {CommonModule} from '@angular/common'; -import {Component, ElementRef, NgModule, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; +import {Component, ElementRef, NgModule, ViewChild, ViewEncapsulation} from '@angular/core'; import {MatTabsModule} from '@angular/material'; import {ActivatedRoute, Params, Router, RouterModule} from '@angular/router'; import 'rxjs/add/operator/map'; @@ -50,14 +50,13 @@ export class ComponentViewer { templateUrl: './component-overview.html', encapsulation: ViewEncapsulation.None, }) -export class ComponentOverview implements OnInit { +export class ComponentOverview { @ViewChild('intialFocusTarget') focusTarget: ElementRef; constructor(public componentViewer: ComponentViewer) {} - ngOnInit() { - // 100ms timeout is used to allow the page to settle before moving focus for screen readers. - setTimeout(() => this.focusTarget.nativeElement.focus(), 100); + focusInitialTarget() { + this.focusTarget.nativeElement.focus(); } } diff --git a/src/app/pages/guide-viewer/guide-viewer.html b/src/app/pages/guide-viewer/guide-viewer.html index 0e18c7c75..0bf6b64a4 100644 --- a/src/app/pages/guide-viewer/guide-viewer.html +++ b/src/app/pages/guide-viewer/guide-viewer.html @@ -4,8 +4,8 @@

{{guide.name}}

-
diff --git a/src/app/shared/table-of-contents/table-of-contents.html b/src/app/shared/table-of-contents/table-of-contents.html index 2b24b07ea..2420723d3 100644 --- a/src/app/shared/table-of-contents/table-of-contents.html +++ b/src/app/shared/table-of-contents/table-of-contents.html @@ -1,10 +1,10 @@ -
+
Contents
diff --git a/src/app/shared/table-of-contents/table-of-contents.module.ts b/src/app/shared/table-of-contents/table-of-contents.module.ts index b28ec7804..6e596ac31 100644 --- a/src/app/shared/table-of-contents/table-of-contents.module.ts +++ b/src/app/shared/table-of-contents/table-of-contents.module.ts @@ -1,10 +1,11 @@ import {CommonModule} from '@angular/common'; +import {ScrollDispatchModule} from '@angular/cdk/scrolling'; import {NgModule} from '@angular/core'; import {TableOfContents} from './table-of-contents'; import {RouterModule} from '@angular/router'; @NgModule({ - imports: [CommonModule, RouterModule], + imports: [CommonModule, RouterModule, ScrollDispatchModule], declarations: [TableOfContents], exports: [TableOfContents], entryComponents: [TableOfContents], diff --git a/src/app/shared/table-of-contents/table-of-contents.spec.ts b/src/app/shared/table-of-contents/table-of-contents.spec.ts index 40b4de43e..53e05495d 100644 --- a/src/app/shared/table-of-contents/table-of-contents.spec.ts +++ b/src/app/shared/table-of-contents/table-of-contents.spec.ts @@ -39,13 +39,12 @@ describe('TableOfContents', () => { }); it('should have header and links', () => { - component.links = [ + component._links = [ { type: 'h2', id: 'test', name: 'test', top: 0, - active: false } ]; diff --git a/src/app/shared/table-of-contents/table-of-contents.ts b/src/app/shared/table-of-contents/table-of-contents.ts index 9735aeed2..4df35b888 100644 --- a/src/app/shared/table-of-contents/table-of-contents.ts +++ b/src/app/shared/table-of-contents/table-of-contents.ts @@ -1,11 +1,10 @@ -import {Component, ElementRef, Inject, Input, OnInit} from '@angular/core'; +import {Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit} from '@angular/core'; import {DOCUMENT} from '@angular/platform-browser'; import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/takeUntil'; -import {Observable} from 'rxjs/Observable'; +import {ScrollDispatcher, CdkScrollable} from '@angular/cdk/scrolling'; import {Subject} from 'rxjs/Subject'; +import {filter} from 'rxjs/operators/filter'; +import {takeUntil} from 'rxjs/operators/takeUntil'; interface Link { /* id of the section*/ @@ -14,9 +13,6 @@ interface Link { /* header type h3/h4 */ type: string; - /* If the anchor is in view of the page */ - active: boolean; - /* name of the anchor */ name: string; @@ -27,14 +23,14 @@ interface Link { @Component({ selector: 'table-of-contents', styleUrls: ['./table-of-contents.scss'], - templateUrl: './table-of-contents.html' + templateUrl: './table-of-contents.html', }) -export class TableOfContents implements OnInit { +export class TableOfContents implements OnDestroy, OnInit { - @Input() links: Link[] = []; - @Input() container: string; @Input() headerSelectors = '.docs-markdown-h3,.docs-markdown-h4'; + _links: Link[] = []; + _activeLinkIndex: number; _rootUrl: string; private _scrollContainer: any; private _destroyed = new Subject(); @@ -43,92 +39,82 @@ export class TableOfContents implements OnInit { constructor(private _router: Router, private _route: ActivatedRoute, private _element: ElementRef, + private _scrollDispatcher: ScrollDispatcher, + private _ngZone: NgZone, @Inject(DOCUMENT) private _document: Document) { - this._router.events.takeUntil(this._destroyed).subscribe((event) => { - if (event instanceof NavigationEnd) { + // Create new links and save root url at the end of navigation + this._router.events.pipe( + filter(event => event instanceof NavigationEnd), + takeUntil(this._destroyed), + ) + .subscribe(event => { const rootUrl = _router.url.split('#')[0]; if (rootUrl !== this._rootUrl) { - this.links = this.createLinks(); + this._links = this.createLinks(); this._rootUrl = rootUrl; } - } - }); - - this._route.fragment.takeUntil(this._destroyed).subscribe(fragment => { - this._urlFragment = fragment; - - const target = document.getElementById(this._urlFragment); - if (target) { - target.scrollIntoView(); - } - }); + }); + + // Scroll to section when the fragment changes + this._route.fragment + .pipe(takeUntil(this._destroyed)) + .subscribe(fragment => { + this._urlFragment = fragment; + this.scrollFragmentIntoView(); + }); } - ngOnInit(): void { - // On init, the sidenav content element doesn't yet exist, so it's not possible - // to subscribe to its scroll event until next tick (when it does exist). - Promise.resolve().then(() => { - this._scrollContainer = this.container ? - this._document.querySelectorAll(this.container)[0] : window; - - Observable.fromEvent(this._scrollContainer, 'scroll') - .takeUntil(this._destroyed) - .debounceTime(10) - .subscribe(() => this.onScroll()); - }); + ngOnInit() { + // Update active link after scroll events + this._scrollDispatcher.scrolled() + .pipe(takeUntil(this._destroyed)) + .subscribe(scrollable => + this._ngZone.run(() => { + this.updateScrollContainer(scrollable); + this.setActiveLink(); + })); } ngOnDestroy(): void { this._destroyed.next(); + this._destroyed.complete(); } - updateScrollPosition(): void { - this.links = this.createLinks(); + createLinksAndScroll(): void { + this._links = this.createLinks(); + this.scrollFragmentIntoView(); + } + /** Find the target from the url fragment and scroll it into view. */ + private scrollFragmentIntoView(): void { const target = document.getElementById(this._urlFragment); if (target) { target.scrollIntoView(); } } - /** Gets the scroll offset of the scroll container */ - private getScrollOffset(): number { - const {top} = this._element.nativeElement.getBoundingClientRect(); - if (typeof this._scrollContainer.scrollTop !== 'undefined') { - return this._scrollContainer.scrollTop + top; - } else if (typeof this._scrollContainer.pageYOffset !== 'undefined') { - return this._scrollContainer.pageYOffset + top; - } - } - + /** Gets links generated from header selectors. */ private createLinks(): Link[] { - const links = []; const headers = Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; - if (headers.length) { - for (const header of headers) { - // remove the 'link' icon name from the inner text - const name = header.innerText.trim().replace(/^link/, ''); - const {top} = header.getBoundingClientRect(); - links.push({ - name, - type: header.tagName.toLowerCase(), - top: top, - id: header.id, - active: false - }); - } - } - - return links; + return headers.map(header => { + // remove the 'link' icon name from the inner text + const name = header.innerText.trim().replace(/^link/, ''); + const {top} = header.getBoundingClientRect(); + return { + name, + top, + type: header.tagName.toLowerCase(), + id: header.id + }; + }); } - private onScroll(): void { - for (let i = 0; i < this.links.length; i++) { - this.links[i].active = this.isLinkActive(this.links[i], this.links[i + 1]); - } + private setActiveLink(): void { + this._activeLinkIndex = this._links + .findIndex((link, i) => this.isLinkActive(link, this._links[i + 1])); } private isLinkActive(currentLink: any, nextLink: any): boolean { @@ -138,4 +124,24 @@ export class TableOfContents implements OnInit { return scrollOffset >= currentLink.top && !(nextLink && nextLink.top < scrollOffset); } + /** Gets the scroll offset of the scroll container */ + private getScrollOffset(): number { + const {top} = this._element.nativeElement.getBoundingClientRect(); + if (this._scrollContainer.scrollTop != null) { + return this._scrollContainer.scrollTop + top; + } + + if (this._scrollContainer.pageYOffset != null) { + return this._scrollContainer.pageYOffset + top; + } + + return 0; + } + + private updateScrollContainer(scrollable: CdkScrollable | void): void { + this._scrollContainer = scrollable ? + scrollable.getElementRef().nativeElement : + window; + } + }