Skip to content

Commit 98aaf74

Browse files
devversionjelbourn
authored andcommittedSep 21, 2018
fix: anchor links not scrolling into view (#519)
* Currently if someone visits a link that contains a fragment that refers to an anchor element, the element won't be scrolled into view properly. This is because the `focusOnNavigation` calls `focus()` after the scroll into view. This causes the focused element to be visible in view. * Removes an unnecessary workaround for fixing fragment URLs. Fixes #396
1 parent 8aa0c47 commit 98aaf74

File tree

8 files changed

+41
-55
lines changed

8 files changed

+41
-55
lines changed
 

‎material.angular.io/src/app/pages/component-viewer/component-api.html

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
<span class="cdk-visually-hidden" tabindex="-1" #intialFocusTarget>
1+
<span class="cdk-visually-hidden" tabindex="-1" #initialFocusTarget>
22
API for {{componentViewer.componentDocItem.id}}
33
</span>
4+
45
<doc-viewer
5-
documentUrl="/assets/documents/api/{{componentViewer.componentDocItem.packageName}}-{{componentViewer.componentDocItem.id}}.html"
6-
class="docs-component-view-text-content docs-component-api"
7-
(contentLoaded)="onContentLoaded()"></doc-viewer>
6+
documentUrl="/assets/documents/api/{{componentViewer.componentDocItem.packageName}}-{{componentViewer.componentDocItem.id}}.html"
7+
class="docs-component-view-text-content docs-component-api"
8+
(contentRendered)="scrollToSelectedContentSection()">
9+
</doc-viewer>
10+
811
<table-of-contents #toc
912
*ngIf="showToc | async"
1013
headerSelectors=".docs-api-h3,.docs-api-h4"

‎material.angular.io/src/app/pages/component-viewer/component-examples.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<span class="cdk-visually-hidden" tabindex="-1" #intialFocusTarget>
1+
<span class="cdk-visually-hidden" tabindex="-1" #initialFocusTarget>
22
Examples for {{componentViewer.componentDocItem.id}}
33
</span>
44
<example-viewer
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
<span class="cdk-visually-hidden" tabindex="-1" #intialFocusTarget>
1+
<span class="cdk-visually-hidden" tabindex="-1" #initialFocusTarget>
22
Overview for {{componentViewer.componentDocItem.id}}
33
</span>
44
<doc-viewer
55
documentUrl="/assets/documents/overview/{{componentViewer.componentDocItem.packageName}}-{{componentViewer.componentDocItem.id}}.html"
66
class="docs-component-view-text-content docs-component-overview"
7-
(contentLoaded)="onContentLoaded()">
7+
(contentRendered)="scrollToSelectedContentSection()">
88
</doc-viewer>
99
<table-of-contents #toc container=".mat-drawer-content" *ngIf="showToc | async"></table-of-contents>

‎material.angular.io/src/app/pages/component-viewer/component-viewer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class ComponentViewer implements OnDestroy {
6565
encapsulation: ViewEncapsulation.None,
6666
})
6767
export class ComponentOverview implements OnInit {
68-
@ViewChild('intialFocusTarget') focusTarget: ElementRef;
68+
@ViewChild('initialFocusTarget') focusTarget: ElementRef;
6969
@ViewChild('toc') tableOfContents: TableOfContents;
7070
showToc: Observable<boolean>;
7171

@@ -76,10 +76,10 @@ export class ComponentOverview implements OnInit {
7676

7777
ngOnInit() {
7878
// 100ms timeout is used to allow the page to settle before moving focus for screen readers.
79-
setTimeout(() => this.focusTarget.nativeElement.focus(), 100);
79+
setTimeout(() => this.focusTarget.nativeElement.focus({preventScroll: true}), 100);
8080
}
8181

82-
onContentLoaded() {
82+
scrollToSelectedContentSection() {
8383
if (this.tableOfContents) {
8484
this.tableOfContents.updateScrollPosition();
8585
}

‎material.angular.io/src/app/pages/guide-viewer/guide-viewer.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ <h1>{{guide.name}}</h1>
55
<div class="docs-guide-wrapper">
66
<div class="docs-guide-toc-and-content">
77
<doc-viewer class="docs-guide-content"
8-
(contentLoaded)="toc.updateScrollPosition()"
8+
(contentRendered)="toc.updateScrollPosition()"
99
[documentUrl]="guide.document"></doc-viewer>
1010
<table-of-contents #toc container="guide-viewer"></table-of-contents>
1111
</div>

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer.ts

+12-33
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import {
88
EventEmitter,
99
Injector,
1010
Input,
11+
NgZone,
1112
OnDestroy,
1213
Output,
1314
ViewContainerRef,
1415
} from '@angular/core';
15-
import {Router} from '@angular/router';
1616
import {Subscription} from 'rxjs';
17+
import {take} from 'rxjs/operators';
1718
import {ExampleViewer} from '../example-viewer/example-viewer';
1819
import {HeaderLink} from './header-link';
1920

@@ -31,7 +32,7 @@ export class DocViewer implements OnDestroy {
3132
this._fetchDocument(url);
3233
}
3334

34-
@Output() contentLoaded = new EventEmitter<void>();
35+
@Output() contentRendered = new EventEmitter<void>();
3536

3637
/** The document text. It should not be HTML encoded. */
3738
textContent = '';
@@ -42,7 +43,8 @@ export class DocViewer implements OnDestroy {
4243
private _http: HttpClient,
4344
private _injector: Injector,
4445
private _viewContainerRef: ViewContainerRef,
45-
private _router: Router) {}
46+
private _ngZone: NgZone) {
47+
}
4648

4749
/** Fetch a document by URL. */
4850
private _fetchDocument(url: string) {
@@ -66,8 +68,13 @@ export class DocViewer implements OnDestroy {
6668
this.textContent = this._elementRef.nativeElement.textContent;
6769
this._loadComponents('material-docs-example', ExampleViewer);
6870
this._loadComponents('header-link', HeaderLink);
69-
this._fixFragmentUrls();
70-
this.contentLoaded.next();
71+
72+
// Resolving and creating components dynamically in Angular happens synchronously, but since
73+
// we want to emit the output if the components are actually rendered completely, we wait
74+
// until the Angular zone becomes stable.
75+
this._ngZone.onStable
76+
.pipe(take(1))
77+
.subscribe(() => this.contentRendered.next());
7178
}
7279

7380
/** Show an error that occurred when fetching a document. */
@@ -77,15 +84,6 @@ export class DocViewer implements OnDestroy {
7784
`Failed to load document: ${url}. Error: ${error.statusText}`;
7885
}
7986

80-
releadLiveExamples() {
81-
// When the example viewer is dynamically loaded inside of md-tabs, they somehow end up in
82-
// the wrong place in the DOM after switching tabs. This function is a workaround to
83-
// put the live examples back in the right place.
84-
this._clearLiveExamples();
85-
this._loadComponents('material-docs-example', ExampleViewer);
86-
this._loadComponents('header-link', HeaderLink);
87-
}
88-
8987
/** Instantiate a ExampleViewer for each example. */
9088
private _loadComponents(componentName: string, componentClass: any) {
9189
let exampleElements =
@@ -108,25 +106,6 @@ export class DocViewer implements OnDestroy {
108106
this._portalHosts = [];
109107
}
110108

111-
/**
112-
* A fragment link is a link that references a specific element on the page that should be
113-
* scrolled into the viewport on page load or click.
114-
*
115-
* By default those links refer to the root page of the documentation and the fragment links
116-
* won't work properly. Those links need to be updated to be relative to the current base URL.
117-
*/
118-
private _fixFragmentUrls() {
119-
const baseUrl = this._router.url.split('#')[0];
120-
const anchorElements =
121-
[].slice.call(this._elementRef.nativeElement.querySelectorAll('a')) as HTMLAnchorElement[];
122-
123-
// Update hash links that are referring to the same page and host. Links that are referring
124-
// to a different destination shouldn't be updated. For example the Google Fonts URL.
125-
anchorElements
126-
.filter(anchorEl => anchorEl.hash && anchorEl.host === location.host)
127-
.forEach(anchorEl => anchorEl.href = `${baseUrl}${anchorEl.hash}`);
128-
}
129-
130109
ngOnDestroy() {
131110
this._clearLiveExamples();
132111

‎material.angular.io/src/app/shared/doc-viewer/header-link.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Component, Input, OnInit} from '@angular/core';
1+
import {Component, Input} from '@angular/core';
22
import {Router} from '@angular/router';
33

44
/**
@@ -19,27 +19,31 @@ import {Router} from '@angular/router';
1919
template: `
2020
<a
2121
title="Link to this heading"
22-
[attr.aria-describedby]="example"
23-
class="docs-markdown-a"
2422
aria-label="Link to this heading"
25-
[href]="url">
23+
class="docs-markdown-a"
24+
[attr.aria-describedby]="example"
25+
[href]="_getFragmentUrl()">
2626
<mat-icon>link</mat-icon>
2727
</a>
2828
`
2929
})
30-
export class HeaderLink implements OnInit {
30+
export class HeaderLink {
3131

32+
/**
33+
* Id of the anchor element. Note that is uses "example" because we instantiate the
34+
* header link components through the ComponentPortal.
35+
*/
3236
@Input() example: string;
3337

34-
url: string;
35-
private _rootUrl: string;
38+
/** Base URL that is used to build an absolute fragment URL. */
39+
private _baseUrl: string;
3640

3741
constructor(router: Router) {
38-
this._rootUrl = router.url.split('#')[0];
42+
this._baseUrl = router.url.split('#')[0];
3943
}
4044

41-
ngOnInit(): void {
42-
this.url = `${this._rootUrl}#${this.example}`;
45+
_getFragmentUrl(): string {
46+
return `${this._baseUrl}#${this.example}`;
4347
}
4448

4549
}

‎material.angular.io/src/app/shared/navigation-focus/navigation-focus.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class NavigationFocus implements OnInit {
1313
ngOnInit() {
1414
clearTimeout(lastTimeoutId);
1515
// 100ms timeout is used to allow the page to settle before moving focus for screen readers.
16-
lastTimeoutId = setTimeout(() => this.el.nativeElement.focus(), 100);
16+
lastTimeoutId = setTimeout(() => this.el.nativeElement.focus({preventScroll: true}), 100);
1717
}
1818
}
1919

0 commit comments

Comments
 (0)
Please sign in to comment.