forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(overlay): add scroll blocking strategy
Adds the `BlockScrollStrategy` which, when activated, will prevent the user from scrolling. For now it is only used in the dialog. Relates to angular#4093.
- Loading branch information
Showing
6 changed files
with
210 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
src/lib/core/overlay/scroll/block-scroll-strategy.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import {inject, TestBed, async} from '@angular/core/testing'; | ||
import {ComponentPortal, OverlayModule, BlockScrollStrategy, Platform} from '../../core'; | ||
import {ViewportRuler} from '../position/viewport-ruler'; | ||
|
||
|
||
describe('BlockScrollStrategy', () => { | ||
let platform = new Platform(); | ||
let strategy: BlockScrollStrategy; | ||
let viewport: ViewportRuler; | ||
let forceScrollElement: HTMLElement; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ imports: [OverlayModule] }).compileComponents(); | ||
})); | ||
|
||
beforeEach(inject([ViewportRuler], (_viewportRuler: ViewportRuler) => { | ||
strategy = new BlockScrollStrategy(_viewportRuler); | ||
viewport = _viewportRuler; | ||
forceScrollElement = document.createElement('div'); | ||
document.body.appendChild(forceScrollElement); | ||
forceScrollElement.style.width = '100px'; | ||
forceScrollElement.style.height = '3000px'; | ||
})); | ||
|
||
afterEach(() => { | ||
document.body.removeChild(forceScrollElement); | ||
setScrollPosition(0, 0); | ||
}); | ||
|
||
it('should toggle scroll blocking along the y axis', skipUnreliableBrowser(() => { | ||
forceScrollElement.style.height = '3000px'; | ||
|
||
setScrollPosition(0, 100); | ||
expect(viewport.getViewportScrollPosition().top).toBe(100, | ||
'Expected viewport to be scrollable initially.'); | ||
|
||
strategy.enable(); | ||
expect(document.documentElement.style.top).toBe('-100px', | ||
'Expected <html> element to be offset by the previous scroll amount along the y axis.'); | ||
|
||
setScrollPosition(0, 300); | ||
expect(viewport.getViewportScrollPosition().top).toBe(100, | ||
'Expected the viewport not to scroll.'); | ||
|
||
strategy.disable(); | ||
expect(viewport.getViewportScrollPosition().top).toBe(100, | ||
'Expected old scroll position to have bee restored after disabling.'); | ||
|
||
setScrollPosition(0, 300); | ||
expect(viewport.getViewportScrollPosition().top).toBe(300, | ||
'Expected user to be able to scroll after disabling.'); | ||
})); | ||
|
||
|
||
it('should toggle scroll blocking along the x axis', skipUnreliableBrowser(() => { | ||
forceScrollElement.style.width = '3000px'; | ||
|
||
setScrollPosition(100, 0); | ||
expect(viewport.getViewportScrollPosition().left).toBe(100, | ||
'Expected viewport to be scrollable initially.'); | ||
|
||
strategy.enable(); | ||
expect(document.documentElement.style.left).toBe('-100px', | ||
'Expected <html> element to be offset by the previous scroll amount along the x axis.'); | ||
|
||
setScrollPosition(300, 0); | ||
expect(viewport.getViewportScrollPosition().left).toBe(100, | ||
'Expected the viewport not to scroll.'); | ||
|
||
strategy.disable(); | ||
expect(viewport.getViewportScrollPosition().left).toBe(100, | ||
'Expected old scroll position to have bee restored after disabling.'); | ||
|
||
setScrollPosition(300, 0); | ||
expect(viewport.getViewportScrollPosition().left).toBe(300, | ||
'Expected user to be able to scroll after disabling.'); | ||
})); | ||
|
||
|
||
it('should toggle the `cdk-global-scrollblock` class', skipUnreliableBrowser(() => { | ||
forceScrollElement.style.height = '3000px'; | ||
|
||
expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); | ||
|
||
strategy.enable(); | ||
expect(document.documentElement.classList).toContain('cdk-global-scrollblock'); | ||
|
||
strategy.disable(); | ||
expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); | ||
})); | ||
|
||
it('should restore any previously-set inline styles', skipUnreliableBrowser(() => { | ||
const root = document.documentElement; | ||
|
||
forceScrollElement.style.height = '3000px'; | ||
root.style.top = '13px'; | ||
root.style.left = '37px'; | ||
|
||
strategy.enable(); | ||
|
||
expect(root.style.top).not.toBe('13px'); | ||
expect(root.style.left).not.toBe('37px'); | ||
|
||
strategy.disable(); | ||
|
||
expect(root.style.top).toBe('13px'); | ||
expect(root.style.left).toBe('37px'); | ||
})); | ||
|
||
it(`should't do anything if the page isn't scrollable`, skipUnreliableBrowser(() => { | ||
forceScrollElement.style.display = 'none'; | ||
strategy.enable(); | ||
expect(document.documentElement.classList).not.toContain('cdk-global-scrollblock'); | ||
})); | ||
|
||
|
||
// In the iOS simulator (BrowserStack & SauceLabs), adding content to the | ||
// body causes karma's iframe for the test to stretch to fit that content, | ||
// in addition to not allowing us to set the scroll position programmatically. | ||
// This renders the tests unusable and since we can't really do anything about it, | ||
// we have to skip them on iOS. | ||
function skipUnreliableBrowser(spec: Function) { | ||
return () => { | ||
if (!platform.IOS) { spec(); } | ||
}; | ||
} | ||
|
||
function setScrollPosition(x: number, y: number) { | ||
window.scroll(x, y); | ||
viewport._cacheViewportGeometry(); | ||
} | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import {ScrollStrategy} from './scroll-strategy'; | ||
import {ViewportRuler} from '../position/viewport-ruler'; | ||
|
||
/** | ||
* Strategy that will prevent the user from scrolling while the overlay is visible. | ||
*/ | ||
export class BlockScrollStrategy implements ScrollStrategy { | ||
private _prevHTMLStyles = { top: null, left: null }; | ||
private _previousScrollPosition: { top: number, left: number }; | ||
private _isEnabled = false; | ||
|
||
constructor(private _viewportRuler: ViewportRuler) { } | ||
|
||
attach() { } | ||
|
||
enable() { | ||
if (this._canBeEnabled()) { | ||
const root = document.documentElement; | ||
|
||
this._previousScrollPosition = this._viewportRuler.getViewportScrollPosition(); | ||
|
||
// Cache the previous inline styles in case the user had set them. | ||
this._prevHTMLStyles.left = root.style.left; | ||
this._prevHTMLStyles.top = root.style.top; | ||
|
||
// Note: we're using the `html` node, instead of the `body`, because the `body` may | ||
// have the user agent margin, whereas the `html` is guaranteed not to have one. | ||
root.style.left = `${-this._previousScrollPosition.left}px`; | ||
root.style.top = `${-this._previousScrollPosition.top}px`; | ||
root.classList.add('cdk-global-scrollblock'); | ||
this._isEnabled = true; | ||
} | ||
} | ||
|
||
disable() { | ||
if (this._isEnabled) { | ||
this._isEnabled = false; | ||
document.documentElement.style.left = this._prevHTMLStyles.left; | ||
document.documentElement.style.top = this._prevHTMLStyles.top; | ||
document.documentElement.classList.remove('cdk-global-scrollblock'); | ||
window.scroll(this._previousScrollPosition.left, this._previousScrollPosition.top); | ||
} | ||
} | ||
|
||
private _canBeEnabled(): boolean { | ||
// Since the scroll strategies can't be singletons, we have to use a global CSS class | ||
// (`cdk-global-scrollblock`) to make sure that we don't try to disable global | ||
// scrolling multiple times. | ||
const isBlockedAlready = | ||
document.documentElement.classList.contains('cdk-global-scrollblock') || this._isEnabled; | ||
|
||
if (!isBlockedAlready) { | ||
const body = document.body; | ||
const viewport = this._viewportRuler.getViewportRect(); | ||
return body.scrollHeight > viewport.height || body.scrollWidth > viewport.width; | ||
} | ||
|
||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters