Skip to content

Commit 354e66e

Browse files
aleesaanAndrewKushnir
authored andcommitted
refactor(common): use getElementById in ViewportScroller.scrollToAnchor (angular#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 angular#28960 PR Close angular#30143
1 parent 702958e commit 354e66e

File tree

3 files changed

+51
-56
lines changed

3 files changed

+51
-56
lines changed

goldens/size-tracking/integration-payloads.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@
4949
"master": {
5050
"uncompressed": {
5151
"runtime-es2015": 2289,
52-
"main-es2015": 221897,
53-
"polyfills-es2015": 36938,
54-
"5-es2015": 779
52+
"main-es2015": 221939,
53+
"polyfills-es2015": 36723,
54+
"5-es2015": 781
5555
}
5656
}
5757
},
@@ -66,4 +66,4 @@
6666
}
6767
}
6868
}
69-
}
69+
}

packages/common/src/viewport_scroller.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -111,26 +111,10 @@ export class BrowserViewportScroller implements ViewportScroller {
111111
*/
112112
scrollToAnchor(anchor: string): void {
113113
if (this.supportScrollRestoration()) {
114-
// Escape anything passed to `querySelector` as it can throw errors and stop the application
115-
// from working if invalid values are passed.
116-
if (this.window.CSS && this.window.CSS.escape) {
117-
anchor = this.window.CSS.escape(anchor);
118-
} else {
119-
anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
120-
}
121-
try {
122-
const elSelectedById = this.document.querySelector(`#${anchor}`);
123-
if (elSelectedById) {
124-
this.scrollToElement(elSelectedById);
125-
return;
126-
}
127-
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
128-
if (elSelectedByName) {
129-
this.scrollToElement(elSelectedByName);
130-
return;
131-
}
132-
} catch (e) {
133-
this.errorHandler.handleError(e);
114+
const elSelected =
115+
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
116+
if (elSelected) {
117+
this.scrollToElement(elSelected);
134118
}
135119
}
136120
}

packages/common/test/viewport_scroller_spec.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,61 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
10-
11-
/**
12-
* @license
13-
* Copyright Google LLC All Rights Reserved.
14-
*
15-
* Use of this source code is governed by an MIT-style license that can be
16-
* found in the LICENSE file at https://angular.io/license
17-
*/
18-
199
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
2010
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
2111

22-
{
23-
describe('BrowserViewportScroller', () => {
24-
let scroller: ViewportScroller;
25-
let documentSpy: any;
26-
let windowSpy: any;
12+
describe('BrowserViewportScroller', () => {
13+
let scroller: ViewportScroller;
14+
let documentSpy: any;
15+
let windowSpy: any;
2716

28-
beforeEach(() => {
29-
windowSpy = jasmine.createSpyObj('window', ['history']);
30-
windowSpy.scrollTo = 1;
31-
windowSpy.history.scrollRestoration = 'auto';
32-
33-
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
34-
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
35-
});
17+
beforeEach(() => {
18+
windowSpy = jasmine.createSpyObj('window', ['history']);
19+
windowSpy.scrollTo = 1;
20+
windowSpy.history.scrollRestoration = 'auto';
21+
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
22+
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
23+
});
3624

25+
describe('setHistoryScrollRestoration', () => {
3726
it('should not crash when scrollRestoration is not writable', () => {
3827
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
3928
value: 'auto',
4029
configurable: true,
4130
});
4231
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
4332
});
33+
});
34+
35+
describe('scrollToAnchor', () => {
36+
const anchor = 'anchor';
37+
const el = document.createElement('a');
38+
39+
it('should only call getElementById when an element is found by id', () => {
40+
documentSpy.getElementById.and.returnValue(el);
41+
spyOn<any>(scroller, 'scrollToElement');
42+
scroller.scrollToAnchor(anchor);
43+
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
44+
expect(documentSpy.getElementsByName).not.toHaveBeenCalled();
45+
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
46+
});
47+
48+
it('should call getElementById and getElementsByName when an element is found by name', () => {
49+
documentSpy.getElementsByName.and.returnValue([el]);
50+
spyOn<any>(scroller, 'scrollToElement');
51+
scroller.scrollToAnchor(anchor);
52+
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
53+
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
54+
expect((scroller as any).scrollToElement).toHaveBeenCalledWith(el);
55+
});
4456

45-
it('escapes invalid characters selectors', () => {
46-
const invalidSelectorChars = `"' :.[],=`;
47-
// Double escaped to make sure we match the actual value passed to `querySelector`
48-
const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
49-
scroller.scrollToAnchor(`specials=${invalidSelectorChars}`);
50-
expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`);
51-
expect(documentSpy.querySelector)
52-
.toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`);
57+
it('should not call scrollToElement when an element is not found by its id or its name', () => {
58+
documentSpy.getElementsByName.and.returnValue([]);
59+
spyOn<any>(scroller, 'scrollToElement');
60+
scroller.scrollToAnchor(anchor);
61+
expect(documentSpy.getElementById).toHaveBeenCalledWith(anchor);
62+
expect(documentSpy.getElementsByName).toHaveBeenCalledWith(anchor);
63+
expect((scroller as any).scrollToElement).not.toHaveBeenCalled();
5364
});
5465
});
55-
}
66+
});

0 commit comments

Comments
 (0)