Skip to content

Commit

Permalink
feat(overlay): more flexible scroll strategy API and ability to defin…
Browse files Browse the repository at this point in the history
…e/override custom strategies

* Refactors the overlay setup to allow for scroll strategies to be passed in by name, instead of by instance.
* Handles the scroll strategy dependency injection automatically.
* Adds an API for registering custom scroll strategies and overriding the existing ones.
* Adds a second parameter to the `attach` method, allowing for a config object to be passed in.
* Throws an error if there's an attempt to attach a scroll strategy multiple times. This is mostly a sanity check to ensure that we don't cache the scroll strategy instances.

Relates to angular#4093.
  • Loading branch information
crisbeto committed Jun 2, 2017
1 parent 970847e commit 59129f8
Show file tree
Hide file tree
Showing 20 changed files with 211 additions and 89 deletions.
4 changes: 1 addition & 3 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {MdInputContainer} from '../input/input-container';
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/fromEvent';
Expand Down Expand Up @@ -104,7 +103,6 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
constructor(private _element: ElementRef, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
private _changeDetectorRef: ChangeDetectorRef,
private _scrollDispatcher: ScrollDispatcher,
@Optional() private _dir: Dir, private _zone: NgZone,
@Optional() @Host() private _inputContainer: MdInputContainer,
@Optional() @Inject(DOCUMENT) private _document: any) {}
Expand Down Expand Up @@ -368,7 +366,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
overlayState.positionStrategy = this._getOverlayPosition();
overlayState.width = this._getHostWidth();
overlayState.direction = this._dir ? this._dir.value : 'ltr';
overlayState.scrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
overlayState.scrollStrategy = 'reposition';
return overlayState;
}

Expand Down
6 changes: 2 additions & 4 deletions src/lib/core/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import {Overlay, OVERLAY_PROVIDERS} from './overlay';
import {OverlayRef} from './overlay-ref';
import {TemplatePortal} from '../portal/portal';
import {OverlayState} from './overlay-state';
import {OverlayState, OverlayStateScrollStrategy} from './overlay-state';
import {
ConnectionPositionPair,
ConnectedOverlayPositionChange
Expand All @@ -29,7 +29,6 @@ import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
import {ScrollStrategy} from './scroll/scroll-strategy';
import {coerceBooleanProperty} from '../coercion/boolean-property';
import {ESCAPE} from '../keyboard/keycodes';
import {ScrollDispatcher} from './scroll/scroll-dispatcher';
import {Subscription} from 'rxjs/Subscription';
import {ScrollDispatchModule} from './scroll/index';

Expand Down Expand Up @@ -125,7 +124,7 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
@Input() backdropClass: string;

/** Strategy to be used when handling scroll events while the overlay is open. */
@Input() scrollStrategy: ScrollStrategy = new RepositionScrollStrategy(this._scrollDispatcher);
@Input() scrollStrategy: OverlayStateScrollStrategy = 'reposition';

/** Whether the overlay is open. */
@Input() open: boolean = false;
Expand Down Expand Up @@ -157,7 +156,6 @@ export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
constructor(
private _overlay: Overlay,
private _renderer: Renderer2,
private _scrollDispatcher: ScrollDispatcher,
templateRef: TemplateRef<any>,
viewContainerRef: ViewContainerRef,
@Optional() private _dir: Dir) {
Expand Down
14 changes: 10 additions & 4 deletions src/lib/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export class OverlayRef implements PortalHost {
private _portalHost: PortalHost,
private _pane: HTMLElement,
private _state: OverlayState,
private _scrollStrategy: ScrollStrategy,
private _ngZone: NgZone) {

this._state.scrollStrategy.attach(this);
_scrollStrategy.attach(this,
typeof _state.scrollStrategy === 'string' ? null : _state.scrollStrategy.config);
}

/** The overlay's HTML element */
Expand All @@ -44,7 +46,7 @@ export class OverlayRef implements PortalHost {
this.updateDirection();
this.updatePosition();
this._attachments.next();
this._state.scrollStrategy.enable();
this._scrollStrategy.enable();

// Enable pointer events for the overlay pane element.
this._togglePointerEvents(true);
Expand All @@ -71,7 +73,7 @@ export class OverlayRef implements PortalHost {
// This is necessary because otherwise the pane element will cover the page and disable
// pointer events therefore. Depends on the position strategy and the applied pane boundaries.
this._togglePointerEvents(false);
this._state.scrollStrategy.disable();
this._scrollStrategy.disable();
this._detachments.next();

return this._portalHost.detach();
Expand All @@ -85,9 +87,13 @@ export class OverlayRef implements PortalHost {
this._state.positionStrategy.dispose();
}

if (this._scrollStrategy) {
this._scrollStrategy.disable();
this._scrollStrategy = null;
}

this.detachBackdrop();
this._portalHost.dispose();
this._state.scrollStrategy.disable();
this._detachments.next();
this._detachments.complete();
this._attachments.complete();
Expand Down
3 changes: 2 additions & 1 deletion src/lib/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {LayoutDirection} from '../rtl/dir';
import {ScrollStrategy} from './scroll/scroll-strategy';
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';

export type OverlayStateScrollStrategy = string | {name: string; config: any};

/**
* OverlayState is a bag of values for either the initial configuration or current state of an
Expand All @@ -13,7 +14,7 @@ export class OverlayState {
positionStrategy: PositionStrategy;

/** Strategy to be used when handling scroll events while the overlay is open. */
scrollStrategy: ScrollStrategy = new NoopScrollStrategy();
scrollStrategy: OverlayStateScrollStrategy = 'noop';

/** Custom class to add to the overlay pane. */
panelClass: string = '';
Expand Down
46 changes: 24 additions & 22 deletions src/lib/core/overlay/overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ describe('Overlay', () => {
return {getContainerElement: () => overlayContainerElement};
}}
]
});

TestBed.compileComponents();
}).compileComponents();
}));

beforeEach(inject([Overlay], (o: Overlay) => {
Expand Down Expand Up @@ -355,10 +353,31 @@ describe('Overlay', () => {
let fakeScrollStrategy: FakeScrollStrategy;
let config: OverlayState;

class FakeScrollStrategy implements ScrollStrategy {
isEnabled = false;
overlayRef: OverlayRef;

constructor() {
fakeScrollStrategy = this;
}

attach(overlayRef: OverlayRef) {
this.overlayRef = overlayRef;
}

enable() {
this.isEnabled = true;
}

disable() {
this.isEnabled = false;
}
}

beforeEach(() => {
config = new OverlayState();
fakeScrollStrategy = new FakeScrollStrategy();
config.scrollStrategy = fakeScrollStrategy;
overlay.registerScrollStrategy('fake', FakeScrollStrategy);
config.scrollStrategy = 'fake';
});

it('should attach the overlay ref to the scroll strategy', () => {
Expand Down Expand Up @@ -465,20 +484,3 @@ class FakePositionStrategy implements PositionStrategy {

dispose() {}
}

class FakeScrollStrategy implements ScrollStrategy {
isEnabled = false;
overlayRef: OverlayRef;

attach(overlayRef: OverlayRef) {
this.overlayRef = overlayRef;
}

enable() {
this.isEnabled = true;
}

disable() {
this.isEnabled = false;
}
}
54 changes: 51 additions & 3 deletions src/lib/core/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import {
Injector,
NgZone,
Provider,
ReflectiveInjector,
} from '@angular/core';
import {OverlayState} from './overlay-state';
import {DomPortalHost} from '../portal/dom-portal-host';
import {OverlayRef} from './overlay-ref';
import {OverlayPositionBuilder} from './position/overlay-position-builder';
import {VIEWPORT_RULER_PROVIDER} from './position/viewport-ruler';
import {OverlayContainer, OVERLAY_CONTAINER_PROVIDER} from './overlay-container';
import {ScrollStrategy} from './scroll/scroll-strategy';
import {RepositionScrollStrategy} from './scroll/reposition-scroll-strategy';
import {BlockScrollStrategy} from './scroll/block-scroll-strategy';
import {CloseScrollStrategy} from './scroll/close-scroll-strategy';
import {NoopScrollStrategy} from './scroll/noop-scroll-strategy';


/** Next overlay unique ID. */
Expand All @@ -31,12 +37,22 @@ let defaultState = new OverlayState();
*/
@Injectable()
export class Overlay {
// Create a child ReflectiveInjector, allowing us to instantiate scroll
// strategies without going throught the injector cache.
private _reflectiveInjector = ReflectiveInjector.resolveAndCreate([], this._injector);
private _scrollStrategies = {
reposition: RepositionScrollStrategy,
block: BlockScrollStrategy,
close: CloseScrollStrategy,
noop: NoopScrollStrategy
};

constructor(private _overlayContainer: OverlayContainer,
private _componentFactoryResolver: ComponentFactoryResolver,
private _positionBuilder: OverlayPositionBuilder,
private _appRef: ApplicationRef,
private _injector: Injector,
private _ngZone: NgZone) {}
private _ngZone: NgZone) { }

/**
* Creates an overlay.
Expand All @@ -55,15 +71,26 @@ export class Overlay {
return this._positionBuilder;
}

/**
* Registers a scroll strategy to be available for use when creating an overlay.
* @param name Name of the scroll strategy.
* @param constructor Class to be used to instantiate the scroll strategy.
*/
registerScrollStrategy(name: string, constructor: Function): void {
if (name && constructor) {
this._scrollStrategies[name] = constructor;
}
}

/**
* Creates the DOM element for an overlay and appends it to the overlay container.
* @returns Newly-created pane element
*/
private _createPaneElement(): HTMLElement {
let pane = document.createElement('div');

pane.id = `cdk-overlay-${nextUniqueId++}`;
pane.classList.add('cdk-overlay-pane');

this._overlayContainer.getContainerElement().appendChild(pane);

return pane;
Expand All @@ -84,7 +111,28 @@ export class Overlay {
* @param state
*/
private _createOverlayRef(pane: HTMLElement, state: OverlayState): OverlayRef {
return new OverlayRef(this._createPortalHost(pane), pane, state, this._ngZone);
let portalHost = this._createPortalHost(pane);
let scrollStrategy = this._createScrollStrategy(state);
return new OverlayRef(portalHost, pane, state, scrollStrategy, this._ngZone);
}

/**
* Resolves the scroll strategy of an overlay state.
* @param state State for which to resolve the scroll strategy.
*/
private _createScrollStrategy(state: OverlayState): ScrollStrategy {
let strategyName = typeof state.scrollStrategy === 'string' ?
state.scrollStrategy :
state.scrollStrategy.name;

if (!this._scrollStrategies.hasOwnProperty(strategyName)) {
throw new Error(`Unsupported scroll strategy "${strategyName}". The available scroll ` +
`strategies are ${Object.keys(this._scrollStrategies).join(', ')}.`);
}

// Note that we use `resolveAndInstantiate` which will instantiate
// the scroll strategy without putting it in the injector cache.
return this._reflectiveInjector.resolveAndInstantiate(this._scrollStrategies[strategyName]);
}
}

Expand Down

0 comments on commit 59129f8

Please sign in to comment.