refactor(common): use getElementById in ViewportScroller.scrollToAnchor (#30143)

This commit uses getElementById and getElementsByName when an anchor scroll happens,
to avoid escaping the anchor and wrapping the code in a try/catch block.

Related to #28960

PR Close #30143
This commit is contained in:
Alessandro 2020-07-16 10:58:17 +02:00 committed by Andrew Kushnir
parent 702958e968
commit 354e66efad
3 changed files with 51 additions and 56 deletions

View File

@ -49,9 +49,9 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 2289, "runtime-es2015": 2289,
"main-es2015": 221897, "main-es2015": 221939,
"polyfills-es2015": 36938, "polyfills-es2015": 36723,
"5-es2015": 779 "5-es2015": 781
} }
} }
}, },

View File

@ -111,26 +111,10 @@ export class BrowserViewportScroller implements ViewportScroller {
*/ */
scrollToAnchor(anchor: string): void { scrollToAnchor(anchor: string): void {
if (this.supportScrollRestoration()) { if (this.supportScrollRestoration()) {
// Escape anything passed to `querySelector` as it can throw errors and stop the application const elSelected =
// from working if invalid values are passed. this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
if (this.window.CSS && this.window.CSS.escape) { if (elSelected) {
anchor = this.window.CSS.escape(anchor); this.scrollToElement(elSelected);
} else {
anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
}
try {
const elSelectedById = this.document.querySelector(`#${anchor}`);
if (elSelectedById) {
this.scrollToElement(elSelectedById);
return;
}
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
if (elSelectedByName) {
this.scrollToElement(elSelectedByName);
return;
}
} catch (e) {
this.errorHandler.handleError(e);
} }
} }
} }

View File

@ -6,21 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller'; import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
{ describe('BrowserViewportScroller', () => {
describe('BrowserViewportScroller', () => {
let scroller: ViewportScroller; let scroller: ViewportScroller;
let documentSpy: any; let documentSpy: any;
let windowSpy: any; let windowSpy: any;
@ -29,11 +18,11 @@ import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scrolle
windowSpy = jasmine.createSpyObj('window', ['history']); windowSpy = jasmine.createSpyObj('window', ['history']);
windowSpy.scrollTo = 1; windowSpy.scrollTo = 1;
windowSpy.history.scrollRestoration = 'auto'; windowSpy.history.scrollRestoration = 'auto';
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!); scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
}); });
describe('setHistoryScrollRestoration', () => {
it('should not crash when scrollRestoration is not writable', () => { it('should not crash when scrollRestoration is not writable', () => {
Object.defineProperty(windowSpy.history, 'scrollRestoration', { Object.defineProperty(windowSpy.history, 'scrollRestoration', {
value: 'auto', value: 'auto',
@ -41,15 +30,37 @@ import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scrolle
}); });
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow(); expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
}); });
});
it('escapes invalid characters selectors', () => { describe('scrollToAnchor', () => {
const invalidSelectorChars = `"' :.[],=`; const anchor = 'anchor';
// Double escaped to make sure we match the actual value passed to `querySelector` const el = document.createElement('a');
const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
scroller.scrollToAnchor(`specials=${invalidSelectorChars}`); it('should only call getElementById when an element is found by id', () => {
expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`); documentSpy.getElementById.and.returnValue(el);
expect(documentSpy.querySelector) spyOn<any>(scroller, 'scrollToElement');
.toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`); scroller.scrollToAnchor(anchor);
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
expect(documentSpy.getElementsByName).not.toHaveBeenCalled();
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
});
it('should call getElementById and getElementsByName when an element is found by name', () => {
documentSpy.getElementsByName.and.returnValue([el]);
spyOn<any>(scroller, 'scrollToElement');
scroller.scrollToAnchor(anchor);
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
});
it('should not call scrollToElement when an element is not found by its id or its name', () => {
documentSpy.getElementsByName.and.returnValue([]);
spyOn<any>(scroller, 'scrollToElement');
scroller.scrollToAnchor(anchor);
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
expect((scroller as any).scrollToElement).not.toHaveBeenCalled();
}); });
}); });
} });