Skip to content
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

feat: allow changing the scroll strategy #52

Merged
merged 8 commits into from
Oct 20, 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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ If used, the default backdrop will be transparent. You can add any custom backdr
keep in mind that a backdrop will block pointer events once it is open, immediately triggering
a `mouseleave` event.

### Scrolling

By default, when a popover is open and the user scrolls the container, the popover will reposition
itself to stay attached to its anchor. You can adjust this behavior with `scrollStrategy`.

```html
<sat-popover #importantPopover scrollStrategy="block">
<!-- so important that the user must interact with it -->
</sat-popover>
```

| Strategy | Description
|----------------|------------------------------------------------
| `'noop'` | Don't update position.
| `'block'` | Block page scrolling while open.
| `'reposition'` | Reposition the popover on scroll.

### Animations

By default, the opening and closing animations of a popover are quick with a simple easing curve.
Expand Down
1 change: 1 addition & 0 deletions src/demo/app/demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Component } from '@angular/core';
<div class="page-content">
<demo-positioning></demo-positioning>
<demo-action-api></demo-action-api>
<demo-scroll-strategies></demo-scroll-strategies>
<demo-select-trigger></demo-select-trigger>
<demo-focus></demo-focus>
<demo-transitions></demo-transitions>
Expand Down
2 changes: 2 additions & 0 deletions src/demo/app/demo.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { DemoComponent } from './demo.component';
import { PositioningDemo } from './positioning/positioning.component';
import { ActionAPIDemo } from './action-api/action-api.component';
import { ScrollStrategiesDemo } from './scroll-strategies/scroll-strategies.component';
import { SelectTriggerDemo } from './select-trigger/select-trigger.component';
import { FocusDemo } from './focus/focus.component';
import { TransitionsDemo } from './transitions/transitions.component';
Expand All @@ -43,6 +44,7 @@ export class DemoMaterialModule { }
DemoComponent,
PositioningDemo,
ActionAPIDemo,
ScrollStrategiesDemo,
SelectTriggerDemo,
FocusDemo,
TransitionsDemo,
Expand Down
14 changes: 14 additions & 0 deletions src/demo/app/scroll-strategies/scroll-strategies.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:host {
display: block;
}

.anchor {
margin: 48px;
}

.popover {
padding: 48px;
color: white;
background: black;
}

47 changes: 47 additions & 0 deletions src/demo/app/scroll-strategies/scroll-strategies.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Component } from '@angular/core';

@Component({
selector: 'demo-scroll-strategies',
styleUrls: ['./scroll-strategies.component.scss'],
template: `
<mat-card>
<mat-card-title>Scroll Strategies</mat-card-title>
<mat-card-content>
<mat-form-field>
<mat-select [(ngModel)]="strategy">
<mat-option *ngFor="let option of scrollOptions" [value]="option.value">
{{ option.name }} (<code>{{ option.value }}</code>)
</mat-option>
</mat-select>
</mat-form-field>

<button mat-raised-button
class="anchor"
color="primary"
[satPopoverAnchorFor]="p"
(click)="p.toggle()">
TOGGLE
</button>

<sat-popover #p xPosition="after" hasBackdrop
[overlapAnchor]="false"
[scrollStrategy]="strategy">
<div class="popover mat-body-1">Scroll the page to observe behavior.</div>
</sat-popover>

</mat-card-content>
</mat-card>
`
})
export class ScrollStrategiesDemo {

strategy = 'reposition';

scrollOptions = [
// TODO: support close on resolution of https://github.com/angular/material2/issues/7922
{ value: 'noop', name: 'Do nothing' },
{ value: 'block', name: 'Block scrolling' },
{ value: 'reposition', name: 'Reposition on scroll' },
{ value: 'rugrats', name: 'Invalid option' },
];
}
2 changes: 2 additions & 0 deletions src/lib/popover/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export enum NotificationAction {
TOGGLE,
/** Popover has new target positions. */
REPOSITION,
/** Popover needs new configuration. */
UPDATE_CONFIG,
}

/** Event object for dispatching to anchor. */
Expand Down
32 changes: 25 additions & 7 deletions src/lib/popover/popover-anchor.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
Overlay,
OverlayRef,
OverlayConfig,
VerticalConnectionPos
ScrollStrategy,
VerticalConnectionPos,
} from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { Subject } from 'rxjs/Subject';
Expand All @@ -25,7 +26,8 @@ import 'rxjs/add/operator/switchMap';
import {
SatPopover,
SatPopoverPositionX,
SatPopoverPositionY
SatPopoverPositionY,
SatPopoverScrollStrategy,
} from './popover.component';
import { NotificationAction, PopoverNotificationService } from './notification.service';
import { getInvalidPopoverError } from './popover.errors';
Expand Down Expand Up @@ -148,6 +150,7 @@ export class SatPopoverAnchor implements OnInit, OnDestroy {
this.togglePopover();
break;
case NotificationAction.REPOSITION:
case NotificationAction.UPDATE_CONFIG:
// TODO: When the overlay's position can be dynamically changed, do not destroy
this.destroyPopover();
break;
Expand Down Expand Up @@ -192,11 +195,12 @@ export class SatPopoverAnchor implements OnInit, OnDestroy {

/** Create and return a config for creating the overlay. */
private _getOverlayConfig(): OverlayConfig {
const config = new OverlayConfig();
config.positionStrategy = this._getPosition();
config.hasBackdrop = this.attachedPopover.hasBackdrop;
config.backdropClass = this.attachedPopover.backdropClass || 'cdk-overlay-transparent-backdrop';
config.scrollStrategy = this._overlay.scrollStrategies.reposition();
const config = new OverlayConfig({
positionStrategy: this._getPosition(),
hasBackdrop: this.attachedPopover.hasBackdrop,
backdropClass: this.attachedPopover.backdropClass || 'cdk-overlay-transparent-backdrop',
scrollStrategy: this._getScrollStrategyInstance(this.attachedPopover.scrollStrategy),
});

return config;
}
Expand All @@ -215,6 +219,20 @@ export class SatPopoverAnchor implements OnInit, OnDestroy {
});
}

/** Map a scroll strategy string type to an instance of a scroll strategy. */
private _getScrollStrategyInstance(strategy: SatPopoverScrollStrategy): ScrollStrategy {
// TODO support 'close' on resolution of https://github.com/angular/material2/issues/7922
switch (strategy) {
case 'block':
return this._overlay.scrollStrategies.block();
case 'reposition':
return this._overlay.scrollStrategies.reposition();
case 'noop':
default:
return this._overlay.scrollStrategies.noop();
}
}

/** Create and return a position strategy based on config provided to the component instance. */
private _getPosition(): ConnectedPositionStrategy {
// Get config values from the popover
Expand Down
25 changes: 24 additions & 1 deletion src/lib/popover/popover.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ import {
import {
getUnanchoredPopoverError,
getInvalidXPositionError,
getInvalidYPositionError
getInvalidYPositionError,
getInvalidScrollStrategyError,
} from './popover.errors';

export type SatPopoverPositionX = 'before' | 'center' | 'after';
export type SatPopoverPositionY = 'above' | 'center' | 'below';
// TODO: support close on resolution of https://github.com/angular/material2/issues/7922
export type SatPopoverScrollStrategy = 'noop' | 'block' | 'reposition';

export const VALID_POSX: SatPopoverPositionX[] = ['before', 'center', 'after'];
export const VALID_POSY: SatPopoverPositionY[] = ['above', 'center', 'below'];
export const VALID_SCROLL: SatPopoverScrollStrategy[] = ['noop', 'block', 'reposition'];

// See http://cubic-bezier.com/#.25,.8,.25,1 for reference.
const OPEN_TRANSITION = '200ms cubic-bezier(0.25, 0.8, 0.25, 1)';
Expand Down Expand Up @@ -87,6 +91,18 @@ export class SatPopover implements AfterViewInit {
}
private _overlapAnchor = true;

/** How the popover should handle scrolling. */
@Input()
get scrollStrategy() { return this._scrollStrategy; }
set scrollStrategy(val: SatPopoverScrollStrategy) {
this._validateScrollStrategy(val);
if (this._scrollStrategy !== val) {
this._scrollStrategy = val;
this._dispatchNotification(new PopoverNotification(NotificationAction.UPDATE_CONFIG));
}
}
private _scrollStrategy: SatPopoverScrollStrategy = 'reposition';

/** Whether the popover should have a backdrop (includes closing on click). */
@Input()
get hasBackdrop() { return this._hasBackdrop; }
Expand Down Expand Up @@ -262,4 +278,11 @@ export class SatPopover implements AfterViewInit {
throw getInvalidYPositionError(pos);
}
}

/** Throws an error if the scroll strategy is not a valid strategy. */
private _validateScrollStrategy(strategy: SatPopoverScrollStrategy): void {
if (VALID_SCROLL.indexOf(strategy) === -1) {
throw getInvalidScrollStrategyError(strategy);
}
}
}
19 changes: 12 additions & 7 deletions src/lib/popover/popover.errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VALID_POSX, VALID_POSY } from './popover.component';
import { VALID_POSX, VALID_POSY, VALID_SCROLL } from './popover.component';

export function getInvalidPopoverError(): Error {
return Error('SatPopoverAnchor must be provided an SatPopover component instance.');
Expand All @@ -9,13 +9,18 @@ export function getUnanchoredPopoverError(): Error {
}

export function getInvalidXPositionError(pos): Error {
const errorString = `Invalid xPosition: '${pos}'. Valid options are ` +
`${VALID_POSX.map(x => `'${x}'`).join(', ')}.`;
return Error(errorString);
return Error(generateGenericError('xPosition', pos, VALID_POSX));
}

export function getInvalidYPositionError(pos): Error {
const errorString = `Invalid yPosition: '${pos}'. Valid options are ` +
`${VALID_POSY.map(x => `'${x}'`).join(', ')}.`;
return Error(errorString);
return Error(generateGenericError('yPosition', pos, VALID_POSY));
}

export function getInvalidScrollStrategyError(strategy): Error {
return Error(generateGenericError('scrollStrategy', strategy, VALID_SCROLL));
}

function generateGenericError(apiName: string, invalid: any, valid: string[]): string {
return `Invalid ${apiName}: '${invalid}'. Valid options are ` +
`${valid.map(v => `'${v}'`).join(', ')}.`;
}
85 changes: 83 additions & 2 deletions src/lib/popover/popover.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { OverlayContainer } from '@angular/cdk/overlay';
import {
OverlayContainer,
RepositionScrollStrategy,
BlockScrollStrategy,
} from '@angular/cdk/overlay';
import { ESCAPE } from '@angular/cdk/keycodes';
import { Subject } from 'rxjs/Subject';

import { SatPopoverModule } from './popover.module';
import { SatPopover } from './popover.component';
Expand All @@ -11,7 +16,8 @@ import {
getInvalidPopoverError,
getUnanchoredPopoverError,
getInvalidXPositionError,
getInvalidYPositionError
getInvalidYPositionError,
getInvalidScrollStrategyError,
} from './popover.errors';


Expand Down Expand Up @@ -449,6 +455,66 @@ describe('SatPopover', () => {

});

describe('scrolling', () => {
let fixture: ComponentFixture<ScrollingTestComponent>;
let comp: ScrollingTestComponent;
let overlayContainerElement: HTMLElement;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SatPopoverModule,
NoopAnimationsModule,
],
declarations: [ScrollingTestComponent],
providers: [
{provide: OverlayContainer, useFactory: overlayContainerFactory}
]
});

fixture = TestBed.createComponent(ScrollingTestComponent);
comp = fixture.componentInstance;

overlayContainerElement = fixture.debugElement.injector.get(OverlayContainer)
.getContainerElement();
});

afterEach(() => {
document.body.removeChild(overlayContainerElement);
});

it('should allow changing the strategy dynamically', fakeAsync(() => {
let strategy;
fixture.detectChanges();
comp.popover.open();

strategy = comp.anchor._overlayRef.getConfig().scrollStrategy;
expect(strategy instanceof RepositionScrollStrategy).toBe(true, 'reposition strategy');

comp.popover.close();
fixture.detectChanges();
tick();

comp.strategy = 'block';
fixture.detectChanges();
comp.popover.open();

strategy = comp.anchor._overlayRef.getConfig().scrollStrategy;
expect(strategy instanceof BlockScrollStrategy).toBe(true, 'block strategy');
}));

it('should throw an error when an invalid scrollStrategy is provided', () => {
fixture.detectChanges();

// set invalid scrollStrategy
comp.strategy = 'rambutan';

expect(() => {
fixture.detectChanges();
}).toThrow(getInvalidScrollStrategyError('rambutan'));
});
});

});

/**
Expand Down Expand Up @@ -546,6 +612,21 @@ export class PositioningTestComponent {
overlap = true;
}

/** This component is for testing scroll behavior. */
@Component({
template: `
<div [satPopoverAnchorFor]="p">Anchor</div>
<sat-popover #p [scrollStrategy]="strategy">
Popover
</sat-popover>
`
})
export class ScrollingTestComponent {
@ViewChild(SatPopoverAnchor) anchor: SatPopoverAnchor;
@ViewChild(SatPopover) popover: SatPopover;
strategy = 'reposition';
}

/** This factory function provides an overlay container under test control. */
const overlayContainerFactory = () => {
const element = document.createElement('div');
Expand Down
3 changes: 2 additions & 1 deletion src/lib/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { SatPopoverAnchor } from './popover/popover-anchor.directive';
export {
SatPopover,
SatPopoverPositionX,
SatPopoverPositionY
SatPopoverPositionY,
SatPopoverScrollStrategy,
} from './popover/popover.component';