Skip to content

Commit

Permalink
feat(close-scroll-strategy): add scroll threshold option (#8656)
Browse files Browse the repository at this point in the history
Adds the `threshold` option to the `CloseScrollStrategy` that allows for the consumer to specify the amount of pixels that the user has to scroll before the overlay is closed.
  • Loading branch information
crisbeto authored and andrewseguin committed Dec 13, 2017
1 parent 693c8e8 commit c0ff761
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 20 deletions.
66 changes: 63 additions & 3 deletions src/cdk/overlay/scroll/close-scroll-strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {inject, TestBed, async} from '@angular/core/testing';
import {inject, TestBed, fakeAsync} from '@angular/core/testing';
import {NgModule, Component, NgZone} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import {ComponentPortal, PortalModule} from '@angular/cdk/portal';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
import {
Overlay,
OverlayConfig,
Expand All @@ -16,13 +16,19 @@ describe('CloseScrollStrategy', () => {
let overlayRef: OverlayRef;
let componentPortal: ComponentPortal<MozarellaMsg>;
let scrolledSubject = new Subject();
let scrollPosition: number;

beforeEach(fakeAsync(() => {
scrollPosition = 0;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [OverlayModule, PortalModule, OverlayTestModule],
providers: [
{provide: ScrollDispatcher, useFactory: () => ({
scrolled: () => scrolledSubject.asObservable()
})},
{provide: ViewportRuler, useFactory: () => ({
getViewportScrollPosition: () => ({top: scrollPosition})
})}
]
});
Expand Down Expand Up @@ -70,6 +76,60 @@ describe('CloseScrollStrategy', () => {
subscription.unsubscribe();
});

it('should be able to reposition the overlay up to a certain threshold before closing',
inject([Overlay], (overlay: Overlay) => {
overlayRef.dispose();

overlayRef = overlay.create({
scrollStrategy: overlay.scrollStrategies.close({threshold: 50})
});

overlayRef.attach(componentPortal);
spyOn(overlayRef, 'updatePosition');
spyOn(overlayRef, 'detach');

for (let i = 0; i < 50; i++) {
scrollPosition++;
scrolledSubject.next();
}

expect(overlayRef.updatePosition).toHaveBeenCalledTimes(50);
expect(overlayRef.detach).not.toHaveBeenCalled();

scrollPosition++;
scrolledSubject.next();

expect(overlayRef.detach).toHaveBeenCalledTimes(1);
}));

it('should not close if the user starts scrolling away and comes back',
inject([Overlay], (overlay: Overlay) => {
overlayRef.dispose();
scrollPosition = 100;

overlayRef = overlay.create({
scrollStrategy: overlay.scrollStrategies.close({threshold: 50})
});

overlayRef.attach(componentPortal);
spyOn(overlayRef, 'updatePosition');
spyOn(overlayRef, 'detach');

// Scroll down 30px.
for (let i = 0; i < 30; i++) {
scrollPosition++;
scrolledSubject.next();
}

// Scroll back up 30px.
for (let i = 0; i < 30; i++) {
scrollPosition--;
scrolledSubject.next();
}

expect(overlayRef.updatePosition).toHaveBeenCalledTimes(60);
expect(overlayRef.detach).not.toHaveBeenCalled();
}));
});


Expand Down
56 changes: 43 additions & 13 deletions src/cdk/overlay/scroll/close-scroll-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NgZone} from '@angular/core';
import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy';
import {OverlayRef} from '../overlay-ref';
import {Subscription} from 'rxjs/Subscription';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';

/**
* Config options for the CloseScrollStrategy.
*/
export interface CloseScrollStrategyConfig {
/** Amount of pixels the user has to scroll before the overlay is closed. */
threshold?: number;
}

/**
* Strategy that will close the overlay as soon as the user starts scrolling.
*/
export class CloseScrollStrategy implements ScrollStrategy {
private _scrollSubscription: Subscription|null = null;
private _overlayRef: OverlayRef;
private _initialScrollPosition: number;

constructor(private _scrollDispatcher: ScrollDispatcher, private _ngZone: NgZone) { }
constructor(
private _scrollDispatcher: ScrollDispatcher,
private _ngZone: NgZone,
private _viewportRuler: ViewportRuler,
private _config?: CloseScrollStrategyConfig) {}

/** Attaches this scroll strategy to an overlay. */
attach(overlayRef: OverlayRef) {
Expand All @@ -31,18 +42,28 @@ export class CloseScrollStrategy implements ScrollStrategy {
this._overlayRef = overlayRef;
}

/** Enables the closing of the attached on scroll. */
/** Enables the closing of the attached overlay on scroll. */
enable() {
if (!this._scrollSubscription) {
this._scrollSubscription = this._scrollDispatcher.scrolled(0).subscribe(() => {
this._ngZone.run(() => {
this.disable();

if (this._overlayRef.hasAttached()) {
this._overlayRef.detach();
}
});
if (this._scrollSubscription) {
return;
}

const stream = this._scrollDispatcher.scrolled(0);

if (this._config && this._config.threshold && this._config.threshold > 1) {
this._initialScrollPosition = this._viewportRuler.getViewportScrollPosition().top;

this._scrollSubscription = stream.subscribe(() => {
const scrollPosition = this._viewportRuler.getViewportScrollPosition().top;

if (Math.abs(scrollPosition - this._initialScrollPosition) > this._config!.threshold!) {
this._detach();
} else {
this._overlayRef.updatePosition();
}
});
} else {
this._scrollSubscription = stream.subscribe(this._detach);
}
}

Expand All @@ -53,4 +74,13 @@ export class CloseScrollStrategy implements ScrollStrategy {
this._scrollSubscription = null;
}
}

/** Detaches the overlay ref and disables the scroll strategy. */
private _detach = () => {
this.disable();

if (this._overlayRef.hasAttached()) {
this._ngZone.run(() => this._overlayRef.detach());
}
}
}
11 changes: 7 additions & 4 deletions src/cdk/overlay/scroll/scroll-strategy-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable, NgZone} from '@angular/core';
import {CloseScrollStrategy} from './close-scroll-strategy';
import {CloseScrollStrategy, CloseScrollStrategyConfig} from './close-scroll-strategy';
import {NoopScrollStrategy} from './noop-scroll-strategy';
import {BlockScrollStrategy} from './block-scroll-strategy';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
Expand All @@ -34,8 +33,12 @@ export class ScrollStrategyOptions {
/** Do nothing on scroll. */
noop = () => new NoopScrollStrategy();

/** Close the overlay as soon as the user scrolls. */
close = () => new CloseScrollStrategy(this._scrollDispatcher, this._ngZone);
/**
* Close the overlay as soon as the user scrolls.
* @param config Configuration to be used inside the scroll strategy.
*/
close = (config?: CloseScrollStrategyConfig) => new CloseScrollStrategy(this._scrollDispatcher,
this._ngZone, this._viewportRuler, config)

/** Block scrolling. */
block = () => new BlockScrollStrategy(this._viewportRuler);
Expand Down

0 comments on commit c0ff761

Please sign in to comment.