Skip to content

feat(overlay): add scroll blocking strategy #4500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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