style(docs-infra): reformat ScrollService file (#30630)

Pre-empting code formatting changes when the
code is updated in a subsequent commit.

PR Close #30630
This commit is contained in:
Pete Bacon Darwin 2020-07-14 17:13:22 +01:00 committed by Andrew Kushnir
parent 1609815743
commit 8227b56f9e
2 changed files with 140 additions and 146 deletions

View File

@ -1,10 +1,10 @@
import { ReflectiveInjector } from '@angular/core'; import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
import { Location, LocationStrategy, PlatformLocation, ViewportScroller } from '@angular/common'; import {DOCUMENT} from '@angular/common';
import { DOCUMENT } from '@angular/common'; import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
import { MockLocationStrategy, SpyLocation } from '@angular/common/testing'; import {ReflectiveInjector} from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing'; import {fakeAsync, tick} from '@angular/core/testing';
import { ScrollService, topMargin } from './scroll.service'; import {ScrollService, topMargin} from './scroll.service';
describe('ScrollService', () => { describe('ScrollService', () => {
const scrollServiceInstances: ScrollService[] = []; const scrollServiceInstances: ScrollService[] = [];
@ -32,27 +32,25 @@ describe('ScrollService', () => {
} }
class MockElement { class MockElement {
getBoundingClientRect = jasmine.createSpy('Element getBoundingClientRect') getBoundingClientRect =
.and.returnValue({top: 0}); jasmine.createSpy('Element getBoundingClientRect').and.returnValue({top: 0});
scrollIntoView = jasmine.createSpy('Element scrollIntoView'); scrollIntoView = jasmine.createSpy('Element scrollIntoView');
} }
const viewportScrollerStub = jasmine.createSpyObj( const viewportScrollerStub =
'viewportScroller', jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
['getScrollPosition', 'scrollToPosition']);
beforeEach(() => { beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([ injector = ReflectiveInjector.resolveAndCreate([
{ {
provide: ScrollService, provide: ScrollService,
useFactory: createScrollService, useFactory: createScrollService,
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location], deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
}, },
{ provide: Location, useClass: SpyLocation }, {provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
{ provide: DOCUMENT, useClass: MockDocument }, {provide: PlatformLocation, useClass: MockPlatformLocation},
{ provide: PlatformLocation, useClass: MockPlatformLocation }, {provide: ViewportScroller, useValue: viewportScrollerStub},
{ provide: ViewportScroller, useValue: viewportScrollerStub }, {provide: LocationStrategy, useClass: MockLocationStrategy}
{ provide: LocationStrategy, useClass: MockLocationStrategy }
]); ]);
platformLocation = injector.get(PlatformLocation); platformLocation = injector.get(PlatformLocation);
document = injector.get(DOCUMENT); document = injector.get(DOCUMENT);
@ -68,18 +66,19 @@ describe('ScrollService', () => {
}); });
it('should debounce `updateScrollPositonInHistory()`', fakeAsync(() => { it('should debounce `updateScrollPositonInHistory()`', fakeAsync(() => {
const updateScrollPositionInHistorySpy = spyOn(scrollService, 'updateScrollPositionInHistory'); const updateScrollPositionInHistorySpy =
spyOn(scrollService, 'updateScrollPositionInHistory');
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(249); tick(249);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(249); tick(249);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(249); tick(249);
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled(); expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
tick(1); tick(1);
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1); expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
})); }));
it('should set `scrollRestoration` to `manual` if supported', () => { it('should set `scrollRestoration` to `manual` if supported', () => {
if (scrollService.supportManualScrollRestoration) { if (scrollService.supportManualScrollRestoration) {
@ -96,7 +95,9 @@ describe('ScrollService', () => {
try { try {
// Simulate `window.sessionStorage` being inaccessible, when cookies are disabled. // Simulate `window.sessionStorage` being inaccessible, when cookies are disabled.
Object.defineProperty(window, 'sessionStorage', { Object.defineProperty(window, 'sessionStorage', {
get() { throw new Error('The operation is insecure'); }, get() {
throw new Error('The operation is insecure');
},
}); });
const platformLoc = platformLocation as PlatformLocation; const platformLoc = platformLocation as PlatformLocation;
@ -198,8 +199,7 @@ describe('ScrollService', () => {
platformLocation.hash = ''; platformLocation.hash = '';
const topOfPage = new MockElement(); const topOfPage = new MockElement();
document.getElementById.and document.getElementById.and.callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
.callFake((id: string) => id === 'top-of-page' ? topOfPage : null);
scrollService.scroll(); scrollService.scroll();
expect(topOfPage.scrollIntoView).toHaveBeenCalled(); expect(topOfPage.scrollIntoView).toHaveBeenCalled();
@ -227,7 +227,7 @@ describe('ScrollService', () => {
it('should scroll to the element whose id matches the hash with encoded characters', () => { it('should scroll to the element whose id matches the hash with encoded characters', () => {
const element = new MockElement(); const element = new MockElement();
platformLocation.hash = '%F0%9F%91%8D'; // 👍 platformLocation.hash = '%F0%9F%91%8D'; // 👍
document.getElementById.and.returnValue(element); document.getElementById.and.returnValue(element);
scrollService.scroll(); scrollService.scroll();
@ -289,8 +289,7 @@ describe('ScrollService', () => {
it('should scroll to top', () => { it('should scroll to top', () => {
const topOfPageElement = new MockElement() as any as Element; const topOfPageElement = new MockElement() as any as Element;
document.getElementById.and.callFake( document.getElementById.and.callFake(
(id: string) => id === 'top-of-page' ? topOfPageElement : null (id: string) => id === 'top-of-page' ? topOfPageElement : null);
);
scrollService.scrollToTop(); scrollService.scrollToTop();
expect(topOfPageElement.scrollIntoView).toHaveBeenCalled(); expect(topOfPageElement.scrollIntoView).toHaveBeenCalled();
@ -312,58 +311,55 @@ describe('ScrollService', () => {
describe('#needToFixScrollPosition', async () => { describe('#needToFixScrollPosition', async () => {
it('should return true when popState event was fired after a back navigation if the browser supports ' + it('should return true when popState event was fired after a back navigation if the browser supports ' +
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => { 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
() => {
if (scrollService.supportManualScrollRestoration) {
location.go('/initial-url1');
// We simulate a scroll down
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
location.go('/initial-url2');
location.back();
if (scrollService.supportManualScrollRestoration) { expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
location.go('/initial-url1'); expect(scrollService.needToFixScrollPosition()).toBe(true);
// We simulate a scroll down } else {
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]}); location.go('/initial-url1');
location.go('/initial-url2'); location.go('/initial-url2');
location.back(); location.back();
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]); expect(scrollService.poppedStateScrollPosition).toBe(null);
expect(scrollService.needToFixScrollPosition()).toBe(true); expect(scrollService.needToFixScrollPosition()).toBe(false);
} else { }
location.go('/initial-url1'); });
location.go('/initial-url2');
location.back();
expect(scrollService.poppedStateScrollPosition).toBe(null);
expect(scrollService.needToFixScrollPosition()).toBe(false);
}
});
it('should return true when popState event was fired after a forward navigation if the browser supports ' + it('should return true when popState event was fired after a forward navigation if the browser supports ' +
'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false', () => { 'scrollRestoration`. Otherwise, needToFixScrollPosition() returns false',
() => {
if (scrollService.supportManualScrollRestoration) {
location.go('/initial-url1');
location.go('/initial-url2');
// We simulate a scroll down
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
if (scrollService.supportManualScrollRestoration) { location.back();
location.go('/initial-url1'); scrollService.poppedStateScrollPosition = [0, 0];
location.go('/initial-url2'); location.forward();
// We simulate a scroll down
location.replaceState('/initial-url1', 'hack', {scrollPosition: [2000, 0]});
location.back(); expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]);
scrollService.poppedStateScrollPosition = [0, 0]; expect(scrollService.needToFixScrollPosition()).toBe(true);
location.forward(); } else {
location.go('/initial-url1');
location.go('/initial-url2');
location.back();
location.forward();
expect(scrollService.poppedStateScrollPosition).toEqual([2000, 0]); expect(scrollService.poppedStateScrollPosition).toBe(null);
expect(scrollService.needToFixScrollPosition()).toBe(true); expect(scrollService.needToFixScrollPosition()).toBe(false);
} else { }
location.go('/initial-url1'); });
location.go('/initial-url2');
location.back();
location.forward();
expect(scrollService.poppedStateScrollPosition).toBe(null);
expect(scrollService.needToFixScrollPosition()).toBe(false);
}
});
}); });
describe('#scrollAfterRender', async () => { describe('#scrollAfterRender', async () => {
let scrollSpy: jasmine.Spy; let scrollSpy: jasmine.Spy;
let scrollToTopSpy: jasmine.Spy; let scrollToTopSpy: jasmine.Spy;
let needToFixScrollPositionSpy: jasmine.Spy; let needToFixScrollPositionSpy: jasmine.Spy;
@ -383,69 +379,69 @@ describe('ScrollService', () => {
it('should call `scroll` when we navigate to a location with anchor', fakeAsync(() => { it('should call `scroll` when we navigate to a location with anchor', fakeAsync(() => {
needToFixScrollPositionSpy.and.returnValue(false); needToFixScrollPositionSpy.and.returnValue(false);
getStoredScrollPositionSpy.and.returnValue(null); getStoredScrollPositionSpy.and.returnValue(null);
isLocationWithHashSpy.and.returnValue(true); isLocationWithHashSpy.and.returnValue(true);
scrollService.scrollAfterRender(scrollDelay); scrollService.scrollAfterRender(scrollDelay);
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollSpy).not.toHaveBeenCalled();
tick(scrollDelay); tick(scrollDelay);
expect(scrollSpy).toHaveBeenCalled(); expect(scrollSpy).toHaveBeenCalled();
})); }));
it('should call `scrollToTop` when we navigate to a location without anchor', fakeAsync(() => { it('should call `scrollToTop` when we navigate to a location without anchor', fakeAsync(() => {
needToFixScrollPositionSpy.and.returnValue(false); needToFixScrollPositionSpy.and.returnValue(false);
getStoredScrollPositionSpy.and.returnValue(null); getStoredScrollPositionSpy.and.returnValue(null);
isLocationWithHashSpy.and.returnValue(false); isLocationWithHashSpy.and.returnValue(false);
scrollService.scrollAfterRender(scrollDelay); scrollService.scrollAfterRender(scrollDelay);
expect(scrollToTopSpy).toHaveBeenCalled(); expect(scrollToTopSpy).toHaveBeenCalled();
tick(scrollDelay); tick(scrollDelay);
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollSpy).not.toHaveBeenCalled();
})); }));
it('should call `viewportScroller.scrollToPosition` when we reload a page', fakeAsync(() => { it('should call `viewportScroller.scrollToPosition` when we reload a page', fakeAsync(() => {
getStoredScrollPositionSpy.and.returnValue([0, 1000]); getStoredScrollPositionSpy.and.returnValue([0, 1000]);
scrollService.scrollAfterRender(scrollDelay); scrollService.scrollAfterRender(scrollDelay);
expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled(); expect(viewportScrollerStub.scrollToPosition).toHaveBeenCalled();
expect(getStoredScrollPositionSpy).toHaveBeenCalled(); expect(getStoredScrollPositionSpy).toHaveBeenCalled();
})); }));
it('should call `scrollToPosition` after a popState', fakeAsync(() => { it('should call `scrollToPosition` after a popState', fakeAsync(() => {
needToFixScrollPositionSpy.and.returnValue(true); needToFixScrollPositionSpy.and.returnValue(true);
getStoredScrollPositionSpy.and.returnValue(null); getStoredScrollPositionSpy.and.returnValue(null);
scrollService.scrollAfterRender(scrollDelay); scrollService.scrollAfterRender(scrollDelay);
expect(scrollToPosition).toHaveBeenCalled(); expect(scrollToPosition).toHaveBeenCalled();
tick(scrollDelay); tick(scrollDelay);
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollSpy).not.toHaveBeenCalled();
expect(scrollToTopSpy).not.toHaveBeenCalled(); expect(scrollToTopSpy).not.toHaveBeenCalled();
})); }));
}); });
describe('once destroyed', () => { describe('once destroyed', () => {
it('should stop updating scroll position', fakeAsync(() => { it('should stop updating scroll position', fakeAsync(() => {
const updateScrollPositionInHistorySpy = const updateScrollPositionInHistorySpy =
spyOn(scrollService, 'updateScrollPositionInHistory'); spyOn(scrollService, 'updateScrollPositionInHistory');
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(250); tick(250);
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1); expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(250); tick(250);
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2); expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(2);
updateScrollPositionInHistorySpy.calls.reset(); updateScrollPositionInHistorySpy.calls.reset();
scrollService.ngOnDestroy(); scrollService.ngOnDestroy();
window.dispatchEvent(new Event('scroll')); window.dispatchEvent(new Event('scroll'));
tick(250); tick(250);
expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled(); expect(updateScrollPositionInHistorySpy).not.toHaveBeenCalled();
})); }));
it('should stop updating the stored location href', () => { it('should stop updating the stored location href', () => {
const updateScrollLocationHrefSpy = spyOn(scrollService, 'updateScrollLocationHref'); const updateScrollLocationHrefSpy = spyOn(scrollService, 'updateScrollLocationHref');

View File

@ -1,7 +1,7 @@
import { DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller } from '@angular/common'; import {DOCUMENT, Location, PlatformLocation, PopStateEvent, ViewportScroller} from '@angular/common';
import { Injectable, Inject, OnDestroy } from '@angular/core'; import {Inject, Injectable, OnDestroy} from '@angular/core';
import { fromEvent, Subject } from 'rxjs'; import {fromEvent, Subject} from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators'; import {debounceTime, takeUntil} from 'rxjs/operators';
type ScrollPosition = [number, number]; type ScrollPosition = [number, number];
interface ScrollPositionPopStateEvent extends PopStateEvent { interface ScrollPositionPopStateEvent extends PopStateEvent {
@ -15,18 +15,17 @@ export const topMargin = 16;
*/ */
@Injectable() @Injectable()
export class ScrollService implements OnDestroy { export class ScrollService implements OnDestroy {
private _topOffset: number|null;
private _topOffset: number | null;
private _topOfPageElement: Element; private _topOfPageElement: Element;
private onDestroy = new Subject<void>(); private onDestroy = new Subject<void>();
private storage: Storage; private storage: Storage;
// The scroll position which has to be restored, after a `popstate` event. // The scroll position which has to be restored, after a `popstate` event.
poppedStateScrollPosition: ScrollPosition | null = null; poppedStateScrollPosition: ScrollPosition|null = null;
// Whether the browser supports the necessary features for manual scroll restoration. // Whether the browser supports the necessary features for manual scroll restoration.
supportManualScrollRestoration: boolean = supportManualScrollRestoration: boolean = !!window && ('scrollTo' in window) &&
!!window && ('scrollTo' in window) && ('scrollX' in window) && ('scrollY' in window) && ('scrollX' in window) && ('scrollY' in window) && !!history &&
!!history && ('scrollRestoration' in history); ('scrollRestoration' in history);
// Offset from the top of the document to bottom of any static elements // Offset from the top of the document to bottom of any static elements
// at the top (e.g. toolbar) + some margin // at the top (e.g. toolbar) + some margin
@ -46,10 +45,8 @@ export class ScrollService implements OnDestroy {
} }
constructor( constructor(
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any, private platformLocation: PlatformLocation,
private platformLocation: PlatformLocation, private viewportScroller: ViewportScroller, private location: Location) {
private viewportScroller: ViewportScroller,
private location: Location) {
try { try {
this.storage = window.sessionStorage; this.storage = window.sessionStorage;
} catch { } catch {
@ -118,9 +115,7 @@ export class ScrollService implements OnDestroy {
*/ */
scroll() { scroll() {
const hash = this.getCurrentHash(); const hash = this.getCurrentHash();
const element: HTMLElement = hash const element: HTMLElement = hash ? this.document.getElementById(hash) : this.topOfPageElement;
? this.document.getElementById(hash)
: this.topOfPageElement;
this.scrollToElement(element); this.scrollToElement(element);
} }
@ -132,8 +127,8 @@ export class ScrollService implements OnDestroy {
} }
/** /**
* When we load a document, we have to scroll to the correct position depending on whether this is a new location, * When we load a document, we have to scroll to the correct position depending on whether this is
* a back/forward in the history, or a refresh * a new location, a back/forward in the history, or a refresh
* @param delay before we scroll to the good position * @param delay before we scroll to the good position
*/ */
scrollAfterRender(delay: number) { scrollAfterRender(delay: number) {
@ -208,19 +203,22 @@ export class ScrollService implements OnDestroy {
updateScrollPositionInHistory() { updateScrollPositionInHistory() {
if (this.supportManualScrollRestoration) { if (this.supportManualScrollRestoration) {
const currentScrollPosition = this.viewportScroller.getScrollPosition(); const currentScrollPosition = this.viewportScroller.getScrollPosition();
this.location.replaceState(this.location.path(true), undefined, {scrollPosition: currentScrollPosition}); this.location.replaceState(
this.location.path(true), undefined, {scrollPosition: currentScrollPosition});
this.storage.setItem('scrollPosition', currentScrollPosition.join(',')); this.storage.setItem('scrollPosition', currentScrollPosition.join(','));
} }
} }
getStoredScrollLocationHref(): string | null { getStoredScrollLocationHref(): string|null {
const href = this.storage.getItem('scrollLocationHref'); const href = this.storage.getItem('scrollLocationHref');
return href || null; return href || null;
} }
getStoredScrollPosition(): ScrollPosition | null { getStoredScrollPosition(): ScrollPosition|null {
const position = this.storage.getItem('scrollPosition'); const position = this.storage.getItem('scrollPosition');
if (!position) { return null; } if (!position) {
return null;
}
const [x, y] = position.split(','); const [x, y] = position.split(',');
return [+x, +y]; return [+x, +y];