Skip to content

Commit

Permalink
feat(overlay): add scroll blocking strategy (#4500)
Browse files Browse the repository at this point in the history
Adds the `BlockScrollStrategy` which, when activated, will prevent the user from scrolling. For now it is only used in the dialog.

Relates to #4093.
  • Loading branch information
crisbeto authored and jelbourn committed May 15, 2017
1 parent 0666207 commit 6842046
Show file tree
Hide file tree
Showing 16 changed files with 435 additions and 4 deletions.
134 changes: 134 additions & 0 deletions e2e/components/block-scroll-strategy/block-scroll-strategy.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {browser, Key, element, by} from 'protractor';
import {screenshot} from '../../screenshot';
import {getScrollPosition} from '../../util/query';


describe('scroll blocking', () => {
beforeEach(() => browser.get('/block-scroll-strategy'));
afterEach(() => clickOn('disable'));

it('should not be able to scroll programmatically along the x axis', async (done) => {
scrollPage(0, 100);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
scrollPage(0, 200);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page not to be scrollable.');

clickOn('disable');
scrollPage(0, 300);
expect((await getScrollPosition()).y).toBe(300, 'Exected page to be scrollable again.');

screenshot();
done();
});

it('should not be able to scroll programmatically along the y axis', async (done) => {
scrollPage(100, 0);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
scrollPage(200, 0);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page not to be scrollable.');

clickOn('disable');
scrollPage(300, 0);
expect((await getScrollPosition()).x).toBe(300, 'Exected page to be scrollable again.');

screenshot();
done();
});

it('should not be able to scroll via the keyboard along the y axis', async (done) => {
const body = element(by.tagName('body'));

scrollPage(0, 100);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
await body.sendKeys(Key.ARROW_DOWN);
await body.sendKeys(Key.ARROW_DOWN);
await body.sendKeys(Key.ARROW_DOWN);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page not to be scrollable.');

clickOn('disable');
await body.sendKeys(Key.ARROW_DOWN);
await body.sendKeys(Key.ARROW_DOWN);
await body.sendKeys(Key.ARROW_DOWN);
expect((await getScrollPosition()).y)
.toBeGreaterThan(100, 'Expected the page to be scrollable again.');

screenshot();
done();
});

it('should not be able to scroll via the keyboard along the x axis', async (done) => {
const body = element(by.tagName('body'));

scrollPage(100, 0);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
await body.sendKeys(Key.ARROW_RIGHT);
await body.sendKeys(Key.ARROW_RIGHT);
await body.sendKeys(Key.ARROW_RIGHT);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page not to be scrollable.');

clickOn('disable');
await body.sendKeys(Key.ARROW_RIGHT);
await body.sendKeys(Key.ARROW_RIGHT);
await body.sendKeys(Key.ARROW_RIGHT);
expect((await getScrollPosition()).x)
.toBeGreaterThan(100, 'Expected the page to be scrollable again.');

screenshot();
done();
});

it('should not be able to scroll the page after reaching the end of an element along the y axis',
async (done) => {
const scroller = element(by.id('scroller'));

browser.executeScript(`document.getElementById('scroller').scrollTop = 200;`);
scrollPage(0, 100);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
scroller.sendKeys(Key.ARROW_DOWN);
scroller.sendKeys(Key.ARROW_DOWN);
scroller.sendKeys(Key.ARROW_DOWN);
expect((await getScrollPosition()).y).toBe(100, 'Expected the page not to have scrolled.');

screenshot();
done();
});

it('should not be able to scroll the page after reaching the end of an element along the x axis',
async (done) => {
const scroller = element(by.id('scroller'));

browser.executeScript(`document.getElementById('scroller').scrollLeft = 200;`);
scrollPage(100, 0);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page to be scrollable.');

clickOn('enable');
scroller.sendKeys(Key.ARROW_RIGHT);
scroller.sendKeys(Key.ARROW_RIGHT);
scroller.sendKeys(Key.ARROW_RIGHT);
expect((await getScrollPosition()).x).toBe(100, 'Expected the page not to have scrolled.');

screenshot();
done();
});
});

// Clicks on a button programmatically. Note that we can't use Protractor's `.click`, because
// it performs a real click, which will scroll the button into view.
function clickOn(id: string) {
browser.executeScript(`document.getElementById('${id}').click()`);
}

// Scrolls the page to the specified coordinates.
function scrollPage(x: number, y: number) {
return browser.executeScript(`window.scrollTo(${x}, ${y});`);
}
1 change: 1 addition & 0 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"inlineSources": true,
"lib": ["es2015"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
Expand Down
2 changes: 1 addition & 1 deletion e2e/util/asserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function expectFocusOn(element: FinderResult, expected = true): void {
}

/**
* Asserts that an element has a certan location.
* Asserts that an element has a certain location.
*/
export function expectLocation(element: FinderResult, {x, y}: Point): void {
getElement(element).getLocation().then((location: Point) => {
Expand Down
18 changes: 18 additions & 0 deletions e2e/util/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {ElementFinder, by, element, ProtractorBy, browser} from 'protractor';
import {Point} from './actions';

/**
* Normalizes either turning a selector into an
Expand All @@ -15,4 +16,21 @@ export function waitForElement(selector: string) {
return browser.isElementPresent(by.css(selector) as ProtractorBy);
}

/**
* Determines the current scroll position of the page.
*/
export async function getScrollPosition(): Promise<Point> {
const snippet = `
var documentRect = document.documentElement.getBoundingClientRect();
var x = -documentRect.left || document.body.scrollLeft || window.scrollX ||
document.documentElement.scrollLeft || 0;
var y = -documentRect.top || document.body.scrollTop || window.scrollY ||
document.documentElement.scrollTop || 0;
return {x: x, y: y};
`;

return await browser.executeScript<Point>(snippet);
}

export type FinderResult = ElementFinder | string;
29 changes: 29 additions & 0 deletions src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.spacer {
background: #3f51b5;
margin-bottom: 10px;
}

.spacer.vertical {
width: 100px;
height: 3000px;
}

.spacer.horizontal {
width: 3000px;
height: 100px;
}

.scroller {
width: 100px;
height: 100px;
overflow: auto;
position: absolute;
top: 100px;
left: 200px;
}

.scroller-spacer {
width: 200px;
height: 200px;
background: #ff4081;
}
10 changes: 10 additions & 0 deletions src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<p>
<button id="enable" (click)="scrollStrategy.enable()">Enable scroll blocking</button>
<button id="disable" (click)="scrollStrategy.disable()">Disable scroll blocking</button>
</p>
<div class="spacer vertical"></div>
<!-- this one needs a tabindex so protractor can trigger key presses inside it -->
<div class="scroller" id="scroller" tabindex="-1">
<div class="scroller-spacer"></div>
</div>
<div class="spacer horizontal"></div>
13 changes: 13 additions & 0 deletions src/e2e-app/block-scroll-strategy/block-scroll-strategy-e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Component} from '@angular/core';
import {BlockScrollStrategy, ViewportRuler} from '@angular/material';

@Component({
moduleId: module.id,
selector: 'block-scroll-strategy-e2e',
templateUrl: 'block-scroll-strategy-e2e.html',
styleUrls: ['block-scroll-strategy-e2e.css'],
})
export class BlockScrollStrategyE2E {
constructor(private _viewportRuler: ViewportRuler) { }
scrollStrategy = new BlockScrollStrategy(this._viewportRuler);
}
4 changes: 3 additions & 1 deletion src/e2e-app/e2e-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {MaterialModule, OverlayContainer, FullscreenOverlayContainer} from '@ang
import {E2E_APP_ROUTES} from './e2e-app/routes';
import {SlideToggleE2E} from './slide-toggle/slide-toggle-e2e';
import {InputE2E} from './input/input-e2e';
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';

@NgModule({
imports: [
Expand All @@ -45,7 +46,8 @@ import {InputE2E} from './input/input-e2e';
SimpleRadioButtons,
SlideToggleE2E,
TestDialog,
TestDialogFullScreen
TestDialogFullScreen,
BlockScrollStrategyE2E
],
bootstrap: [E2EApp],
providers: [
Expand Down
1 change: 1 addition & 0 deletions src/e2e-app/e2e-app/e2e-app.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<button (click)="showLinks = !showLinks">Toggle Navigation Links</button>

<md-nav-list *ngIf="showLinks">
<a md-list-item [routerLink]="['block-scroll-strategy']">Block scroll strategy</a>
<a md-list-item [routerLink]="['button']">Button</a>
<a md-list-item [routerLink]="['checkbox']">Checkbox</a>
<a md-list-item [routerLink]="['dialog']">Dialog</a>
Expand Down
2 changes: 2 additions & 0 deletions src/e2e-app/e2e-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {ProgressSpinnerE2E} from '../progress-spinner/progress-spinner-e2e';
import {SlideToggleE2E} from '../slide-toggle/slide-toggle-e2e';
import {FullscreenE2E} from '../fullscreen/fullscreen-e2e';
import {InputE2E} from '../input/input-e2e';
import {BlockScrollStrategyE2E} from '../block-scroll-strategy/block-scroll-strategy-e2e';

export const E2E_APP_ROUTES: Routes = [
{path: '', component: Home},
{path: 'block-scroll-strategy', component: BlockScrollStrategyE2E},
{path: 'button', component: ButtonE2E},
{path: 'checkbox', component: SimpleCheckboxes},
{path: 'dialog', component: DialogE2E},
Expand Down
13 changes: 13 additions & 0 deletions src/lib/core/overlay/_overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,17 @@
.cdk-overlay-transparent-backdrop {
background: none;
}

// Used when disabling global scrolling.
.cdk-global-scrollblock {
position: fixed;

// Necessary for iOS not to expand past the viewport.
max-width: 100vw;

// Note: this will always add a scrollbar to whatever element it is on, which can
// potentially result in double scrollbars. It shouldn't be an issue, because we won't
// block scrolling on a page that doesn't have a scrollbar in the first place.
overflow-y: scroll;
}
}
2 changes: 2 additions & 0 deletions src/lib/core/overlay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {OverlayRef} from './overlay-ref';
export {OverlayState} from './overlay-state';
export {ConnectedOverlayDirective, OverlayOrigin, OverlayModule} from './overlay-directives';
export {ScrollDispatcher} from './scroll/scroll-dispatcher';
export {ViewportRuler} from './position/viewport-ruler';

export * from './position/connected-position';

Expand All @@ -18,3 +19,4 @@ export {ScrollStrategy} from './scroll/scroll-strategy';
export {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
export {CloseScrollStrategy} from './scroll/close-scroll-strategy';
export {NoopScrollStrategy} from './scroll/noop-scroll-strategy';
export {BlockScrollStrategy} from './scroll/block-scroll-strategy';
7 changes: 5 additions & 2 deletions src/lib/core/overlay/position/viewport-ruler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ export class ViewportRuler {
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
// `document.documentElement` works consistently, where the `top` and `left` values will
// equal negative the scroll position.
const top = -documentRect.top || document.body.scrollTop || window.scrollY || 0;
const left = -documentRect.left || document.body.scrollLeft || window.scrollX || 0;
const top = -documentRect.top || document.body.scrollTop || window.scrollY ||
document.documentElement.scrollTop || 0;

const left = -documentRect.left || document.body.scrollLeft || window.scrollX ||
document.documentElement.scrollLeft || 0;

return {top, left};
}
Expand Down
Loading

0 comments on commit 6842046

Please sign in to comment.