fix(aio): autoscroll & add #top-of-page

* Scrolls to hash element or top of page when no hash.
* Scrolls down a bit (80px) to account for top menu overhang.
* No longer scrolls when the hash element is not found.
* Adds `<a id="top-of-page"></a>` which will benefit future efforts to
navigate there from within a page.
This commit is contained in:
Ward Bell 2017-04-17 15:19:15 -07:00 committed by Pete Bacon Darwin
parent ab03852234
commit 800b1b060e
5 changed files with 32 additions and 24 deletions

View File

@ -9,6 +9,7 @@
<span class="fill-remaining-space"></span> <span class="fill-remaining-space"></span>
</md-toolbar> </md-toolbar>
<a id="top-of-page"></a>
<md-sidenav-container class="sidenav-container" role="main"> <md-sidenav-container class="sidenav-container" role="main">
<md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode"> <md-sidenav [ngClass]="{'collapsed': !isSideBySide }" #sidenav class="sidenav" [opened]="isOpened" [mode]="mode">

View File

@ -286,14 +286,14 @@ describe('AppComponent', () => {
const scrollService: AutoScrollService = fixture.debugElement.injector.get(AutoScrollService); const scrollService: AutoScrollService = fixture.debugElement.injector.get(AutoScrollService);
spyOn(scrollService, 'scroll'); spyOn(scrollService, 'scroll');
locationService.go('some/url#fragment'); locationService.go('some/url#fragment');
expect(scrollService.scroll).toHaveBeenCalledWith(jasmine.any(HTMLElement)); expect(scrollService.scroll).toHaveBeenCalledWith();
}); });
it('should be called when a document has been rendered', () => { it('should be called when a document has been rendered', () => {
const scrollService: AutoScrollService = fixture.debugElement.injector.get(AutoScrollService); const scrollService: AutoScrollService = fixture.debugElement.injector.get(AutoScrollService);
spyOn(scrollService, 'scroll'); spyOn(scrollService, 'scroll');
component.onDocRendered(); component.onDocRendered();
expect(scrollService.scroll).toHaveBeenCalledWith(jasmine.any(HTMLElement)); expect(scrollService.scroll).toHaveBeenCalledWith();
}); });
}); });

View File

@ -106,7 +106,7 @@ export class AppComponent implements OnInit {
// Scroll to the anchor in the hash fragment. // Scroll to the anchor in the hash fragment.
autoScroll() { autoScroll() {
this.autoScrollService.scroll(this.docViewer.nativeElement.offsetParent); this.autoScrollService.scroll();
} }
onDocRendered() { onDocRendered() {

View File

@ -3,11 +3,9 @@ import { PlatformLocation } from '@angular/common';
import { DOCUMENT } from '@angular/platform-browser'; import { DOCUMENT } from '@angular/platform-browser';
import { AutoScrollService } from './auto-scroll.service'; import { AutoScrollService } from './auto-scroll.service';
describe('AutoScrollService', () => { describe('AutoScrollService', () => {
let injector: ReflectiveInjector, let injector: ReflectiveInjector,
autoScroll: AutoScrollService, autoScroll: AutoScrollService,
container: HTMLElement,
location: MockPlatformLocation, location: MockPlatformLocation,
document: MockDocument; document: MockDocument;
@ -16,6 +14,7 @@ describe('AutoScrollService', () => {
} }
class MockDocument { class MockDocument {
body = new MockElement();
getElementById = jasmine.createSpy('Document getElementById'); getElementById = jasmine.createSpy('Document getElementById');
} }
@ -23,6 +22,10 @@ describe('AutoScrollService', () => {
scrollIntoView = jasmine.createSpy('Element scrollIntoView'); scrollIntoView = jasmine.createSpy('Element scrollIntoView');
} }
beforeEach(() => {
spyOn(window, 'scrollBy');
});
beforeEach(() => { beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([ injector = ReflectiveInjector.resolveAndCreate([
AutoScrollService, AutoScrollService,
@ -31,34 +34,37 @@ describe('AutoScrollService', () => {
]); ]);
location = injector.get(PlatformLocation); location = injector.get(PlatformLocation);
document = injector.get(DOCUMENT); document = injector.get(DOCUMENT);
container = window.document.createElement('div');
container.scrollTop = 100;
autoScroll = injector.get(AutoScrollService); autoScroll = injector.get(AutoScrollService);
}); });
it('should scroll the container to the top if there is no hash', () => { it('should scroll to the top if there is no hash', () => {
location.hash = ''; location.hash = '';
autoScroll.scroll(container); const topOfPage = new MockElement();
expect(container.scrollTop).toEqual(0); document.getElementById.and
.callFake(id => id === 'top-of-page' ? topOfPage : null);
autoScroll.scroll();
expect(topOfPage.scrollIntoView).toHaveBeenCalled();
}); });
it('should scroll the container to the top if the hash does not match an element id', () => { it('should not scroll if the hash does not match an element id', () => {
location.hash = 'some-id'; location.hash = 'not-found';
document.getElementById.and.returnValue(null); document.getElementById.and.returnValue(null);
autoScroll.scroll(container); autoScroll.scroll();
expect(document.getElementById).toHaveBeenCalledWith('some-id'); expect(document.getElementById).toHaveBeenCalledWith('not-found');
expect(container.scrollTop).toEqual(0); expect(window.scrollBy).not.toHaveBeenCalled();
}); });
it('should scroll the container to the element whose id matches the hash', () => { it('should scroll to the element whose id matches the hash', () => {
const element = new MockElement(); const element = new MockElement();
location.hash = 'some-id'; location.hash = 'some-id';
document.getElementById.and.returnValue(element); document.getElementById.and.returnValue(element);
autoScroll.scroll(container); autoScroll.scroll();
expect(document.getElementById).toHaveBeenCalledWith('some-id'); expect(document.getElementById).toHaveBeenCalledWith('some-id');
expect(element.scrollIntoView).toHaveBeenCalled(); expect(element.scrollIntoView).toHaveBeenCalled();
expect(window.scrollBy).toHaveBeenCalledWith(0, -80);
}); });
}); });

View File

@ -7,22 +7,23 @@ import { DOCUMENT } from '@angular/platform-browser';
*/ */
@Injectable() @Injectable()
export class AutoScrollService { export class AutoScrollService {
constructor( constructor(
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any,
private location: PlatformLocation) { } private location: PlatformLocation) { }
/** /**
* Scroll the contents of the container * Scroll to the element with id extracted from the current location hash fragment
* to the element with id extracted from the current location hash fragment * Scroll to top if no hash
* Don't scroll if hash not found
*/ */
scroll(container: HTMLElement) { scroll() {
const hash = this.getCurrentHash(); const hash = this.getCurrentHash();
const element: HTMLElement = this.document.getElementById(hash); const element: HTMLElement = hash
? this.document.getElementById(hash)
: this.document.getElementById('top-of-page') || this.document.body;
if (element) { if (element) {
element.scrollIntoView(); element.scrollIntoView();
} else { if (window && window.scrollBy) { window.scrollBy(0, -80); }
container.scrollTop = 0;
} }
} }