fix(common): ensure scrollRestoration is writable (#30630)
Some specialised browsers that do not support scroll restoration (e.g. some web crawlers) do not allow `scrollRestoration` to be writable. We already sniff the browser to see if it has the `window.scrollTo` method, so now we also check whether `window.history.scrollRestoration` is writable too. Fixes #30629 PR Close #30630
This commit is contained in:
parent
8227b56f9e
commit
bb88c9fa3d
|
@ -80,6 +80,26 @@ describe('ScrollService', () => {
|
||||||
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
expect(updateScrollPositionInHistorySpy).toHaveBeenCalledTimes(1);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should not support `manual` scrollRestoration when it is not writable', () => {
|
||||||
|
const original = Object.getOwnPropertyDescriptor(window.history, 'scrollRestoration');
|
||||||
|
try {
|
||||||
|
Object.defineProperty(window.history, 'scrollRestoration', {
|
||||||
|
value: 'auto',
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
scrollService = createScrollService(
|
||||||
|
document, platformLocation as PlatformLocation, viewportScrollerStub, location);
|
||||||
|
|
||||||
|
expect(scrollService.supportManualScrollRestoration).toBe(false);
|
||||||
|
} finally {
|
||||||
|
if (original !== undefined) {
|
||||||
|
Object.defineProperty(window.history, 'scrollRestoration', original);
|
||||||
|
} else {
|
||||||
|
delete window.history.scrollRestoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should set `scrollRestoration` to `manual` if supported', () => {
|
it('should set `scrollRestoration` to `manual` if supported', () => {
|
||||||
if (scrollService.supportManualScrollRestoration) {
|
if (scrollService.supportManualScrollRestoration) {
|
||||||
expect(window.history.scrollRestoration).toBe('manual');
|
expect(window.history.scrollRestoration).toBe('manual');
|
||||||
|
|
|
@ -24,8 +24,7 @@ export class ScrollService implements OnDestroy {
|
||||||
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 = !!window && ('scrollTo' in window) &&
|
supportManualScrollRestoration: boolean = !!window && ('scrollTo' in window) &&
|
||||||
('scrollX' in window) && ('scrollY' in window) && !!history &&
|
('scrollX' in window) && ('scrollY' in window) && isScrollRestorationWritable();
|
||||||
('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
|
||||||
|
@ -243,3 +242,20 @@ export class ScrollService implements OnDestroy {
|
||||||
return decodeURIComponent(this.platformLocation.hash.replace(/^#/, ''));
|
return decodeURIComponent(this.platformLocation.hash.replace(/^#/, ''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to check whether we can write to `history.scrollRestoration`
|
||||||
|
*
|
||||||
|
* We do this by checking the property descriptor of the property, but
|
||||||
|
* it might actually be defined on the `history` prototype not the instance.
|
||||||
|
*
|
||||||
|
* In this context "writable" means either than the property is a `writable`
|
||||||
|
* data file or a property that has a setter.
|
||||||
|
*/
|
||||||
|
function isScrollRestorationWritable() {
|
||||||
|
const scrollRestorationDescriptor =
|
||||||
|
Object.getOwnPropertyDescriptor(history, 'scrollRestoration') ||
|
||||||
|
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'scrollRestoration');
|
||||||
|
return scrollRestorationDescriptor !== undefined &&
|
||||||
|
!!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
|
||||||
|
}
|
||||||
|
|
|
@ -21,9 +21,9 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3097,
|
"runtime-es2015": 3097,
|
||||||
"main-es2015": 429885,
|
"main-es2015": 430239,
|
||||||
"polyfills-es2015": 52195
|
"polyfills-es2015": 52195
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,13 +165,25 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||||
*/
|
*/
|
||||||
private supportScrollRestoration(): boolean {
|
private supportScrollRestoration(): boolean {
|
||||||
try {
|
try {
|
||||||
return !!this.window && !!this.window.scrollTo;
|
if (!this.window || !this.window.scrollTo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The `scrollRestoration` property could be on the `history` instance or its prototype.
|
||||||
|
const scrollRestorationDescriptor = getScrollRestorationProperty(this.window.history) ||
|
||||||
|
getScrollRestorationProperty(Object.getPrototypeOf(this.window.history));
|
||||||
|
// We can write to the `scrollRestoration` property if it is a writable data field or it has a
|
||||||
|
// setter function.
|
||||||
|
return !!scrollRestorationDescriptor &&
|
||||||
|
!!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
||||||
|
return Object.getOwnPropertyDescriptor(obj, 'scrollRestoration');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an empty implementation of the viewport scroller. This will
|
* Provides an empty implementation of the viewport scroller. This will
|
||||||
|
|
|
@ -23,10 +23,25 @@ import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scrolle
|
||||||
describe('BrowserViewportScroller', () => {
|
describe('BrowserViewportScroller', () => {
|
||||||
let scroller: ViewportScroller;
|
let scroller: ViewportScroller;
|
||||||
let documentSpy: any;
|
let documentSpy: any;
|
||||||
|
let windowSpy: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
windowSpy = jasmine.createSpyObj('window', ['history']);
|
||||||
|
windowSpy.scrollTo = 1;
|
||||||
|
windowSpy.history.scrollRestoration = 'auto';
|
||||||
|
|
||||||
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
|
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
|
||||||
scroller = new BrowserViewportScroller(documentSpy, {scrollTo: 1}, null!);
|
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not crash when scrollRestoration is not writable', () => {
|
||||||
|
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
|
||||||
|
value: 'auto',
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('escapes invalid characters selectors', () => {
|
it('escapes invalid characters selectors', () => {
|
||||||
const invalidSelectorChars = `"' :.[],=`;
|
const invalidSelectorChars = `"' :.[],=`;
|
||||||
// Double escaped to make sure we match the actual value passed to `querySelector`
|
// Double escaped to make sure we match the actual value passed to `querySelector`
|
||||||
|
|
Loading…
Reference in New Issue