Skip to content

Commit ce32dcb

Browse files
committed
feat(reposition-scroll-strategy): add option for closing once the user scrolls away
Adds an option to the `RepositionScrollStrategy` that tells it to close the overlay once the user has scrolled away. This is a steps towards moving the scroll clipping logic away from the `ConnectedPositionStrategy`.
1 parent 24f0471 commit ce32dcb

File tree

3 files changed

+69
-7
lines changed

3 files changed

+69
-7
lines changed

src/cdk/overlay/scroll/reposition-scroll-strategy.spec.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414

1515
describe('RepositionScrollStrategy', () => {
1616
let overlayRef: OverlayRef;
17+
let overlay: Overlay;
1718
let componentPortal: ComponentPortal<PastaMsg>;
1819
let scrolledSubject = new Subject();
1920

@@ -30,9 +31,8 @@ describe('RepositionScrollStrategy', () => {
3031
TestBed.compileComponents();
3132
}));
3233

33-
beforeEach(inject([Overlay], (overlay: Overlay) => {
34-
let overlayConfig = new OverlayConfig({scrollStrategy: overlay.scrollStrategies.reposition()});
35-
overlayRef = overlay.create(overlayConfig);
34+
beforeEach(inject([Overlay], (o: Overlay) => {
35+
overlay = o;
3636
componentPortal = new ComponentPortal(PastaMsg);
3737
}));
3838

@@ -42,6 +42,11 @@ describe('RepositionScrollStrategy', () => {
4242
}));
4343

4444
it('should update the overlay position when the page is scrolled', () => {
45+
const overlayConfig = new OverlayConfig({
46+
scrollStrategy: overlay.scrollStrategies.reposition()
47+
});
48+
49+
overlayRef = overlay.create(overlayConfig);
4550
overlayRef.attach(componentPortal);
4651
spyOn(overlayRef, 'updatePosition');
4752

@@ -53,6 +58,11 @@ describe('RepositionScrollStrategy', () => {
5358
});
5459

5560
it('should not be updating the position after the overlay is detached', () => {
61+
const overlayConfig = new OverlayConfig({
62+
scrollStrategy: overlay.scrollStrategies.reposition()
63+
});
64+
65+
overlayRef = overlay.create(overlayConfig);
5666
overlayRef.attach(componentPortal);
5767
spyOn(overlayRef, 'updatePosition');
5868

@@ -63,6 +73,11 @@ describe('RepositionScrollStrategy', () => {
6373
});
6474

6575
it('should not be updating the position after the overlay is destroyed', () => {
76+
const overlayConfig = new OverlayConfig({
77+
scrollStrategy: overlay.scrollStrategies.reposition()
78+
});
79+
80+
overlayRef = overlay.create(overlayConfig);
6681
overlayRef.attach(componentPortal);
6782
spyOn(overlayRef, 'updatePosition');
6883

@@ -72,6 +87,30 @@ describe('RepositionScrollStrategy', () => {
7287
expect(overlayRef.updatePosition).not.toHaveBeenCalled();
7388
});
7489

90+
it('should be able to close the overlay once it is out of view', () => {
91+
const overlayConfig = new OverlayConfig({
92+
scrollStrategy: overlay.scrollStrategies.reposition({
93+
autoClose: true
94+
})
95+
});
96+
97+
overlayRef = overlay.create(overlayConfig);
98+
overlayRef.attach(componentPortal);
99+
spyOn(overlayRef, 'updatePosition');
100+
spyOn(overlayRef, 'detach');
101+
spyOn(overlayRef.overlayElement, 'getBoundingClientRect').and.returnValue({
102+
top: -1000,
103+
bottom: -900,
104+
left: 0,
105+
right: 100,
106+
width: 100,
107+
height: 100
108+
});
109+
110+
scrolledSubject.next();
111+
expect(overlayRef.detach).toHaveBeenCalledTimes(1);
112+
});
113+
75114
});
76115

77116

src/cdk/overlay/scroll/reposition-scroll-strategy.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {NgZone} from '@angular/core';
910
import {Subscription} from 'rxjs/Subscription';
1011
import {ScrollStrategy, getMatScrollStrategyAlreadyAttachedError} from './scroll-strategy';
1112
import {OverlayRef} from '../overlay-ref';
12-
import {ScrollDispatcher} from '@angular/cdk/scrolling';
13+
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
14+
import {isElementScrolledOutsideView, isElementClippedByScrolling} from '../position/scroll-clip';
1315

1416
/**
1517
* Config options for the RepositionScrollStrategy.
1618
*/
1719
export interface RepositionScrollStrategyConfig {
20+
/** Time in milliseconds to throttle the scroll events. */
1821
scrollThrottle?: number;
22+
23+
/** Whether to close the overlay once the user has scrolled away completely. */
24+
autoClose?: boolean;
1925
}
2026

2127
/**
@@ -27,6 +33,8 @@ export class RepositionScrollStrategy implements ScrollStrategy {
2733

2834
constructor(
2935
private _scrollDispatcher: ScrollDispatcher,
36+
private _viewportRuler: ViewportRuler,
37+
private _ngZone: NgZone,
3038
private _config?: RepositionScrollStrategyConfig) { }
3139

3240
/** Attaches this scroll strategy to an overlay. */
@@ -41,10 +49,25 @@ export class RepositionScrollStrategy implements ScrollStrategy {
4149
/** Enables repositioning of the attached overlay on scroll. */
4250
enable() {
4351
if (!this._scrollSubscription) {
44-
let throttle = this._config ? this._config.scrollThrottle : 0;
52+
const throttle = this._config ? this._config.scrollThrottle : 0;
4553

4654
this._scrollSubscription = this._scrollDispatcher.scrolled(throttle).subscribe(() => {
4755
this._overlayRef.updatePosition();
56+
57+
// TODO(crisbeto): make `close` on by default once all components can handle it.
58+
if (this._config && this._config.autoClose) {
59+
const overlayRect = this._overlayRef.overlayElement.getBoundingClientRect();
60+
const {width, height} = this._viewportRuler.getViewportSize();
61+
62+
// TODO(crisbeto): include all ancestor scroll containers here once
63+
// we have a way of exposing the trigger element to the scroll strategy.
64+
const parentRects = [{width, height, bottom: height, right: width, top: 0, left: 0}];
65+
66+
if (isElementScrolledOutsideView(overlayRect, parentRects)) {
67+
this.disable();
68+
this._ngZone.run(() => this._overlayRef.detach());
69+
}
70+
}
4871
});
4972
}
5073
}

src/cdk/overlay/scroll/scroll-strategy-options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@ export class ScrollStrategyOptions {
4545
* @param config Configuration to be used inside the scroll strategy.
4646
* Allows debouncing the reposition calls.
4747
*/
48-
reposition = (config?: RepositionScrollStrategyConfig) =>
49-
new RepositionScrollStrategy(this._scrollDispatcher, config)
48+
reposition = (config?: RepositionScrollStrategyConfig) => new RepositionScrollStrategy(
49+
this._scrollDispatcher, this._viewportRuler, this._ngZone, config)
5050
}

0 commit comments

Comments
 (0)