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(overlay): add support for flexible connected positioning #9153

Merged
merged 1 commit into from
Mar 14, 2018
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
/src/demo-app/card/** @jelbourn
/src/demo-app/checkbox/** @tinayuangao @devversion
/src/demo-app/chips/** @tinayuangao
/src/demo-app/connected-overlay/** @jelbourn @crisbeto
/src/demo-app/dataset/** @andrewseguin
/src/demo-app/datepicker/** @mmalerba
/src/demo-app/demo-app/** @jelbourn
Expand Down
25 changes: 25 additions & 0 deletions src/cdk/overlay/_overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
// A single overlay pane.
.cdk-overlay-pane {
position: absolute;

pointer-events: auto;
box-sizing: border-box;
z-index: $cdk-z-index-overlay;

// For connected-position overlays, we set `display: flex` in
// order to force `max-width` and `max-height` to take effect.
display: flex;
max-width: 100%;
max-height: 100%;
}

.cdk-overlay-backdrop {
Expand Down Expand Up @@ -96,6 +103,24 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
}
}

// Overlay parent element used with the connected position strategy. Used to constrain the
// overlay element's size to fit within the viewport.
.cdk-overlay-connected-position-bounding-box {
position: absolute;
z-index: $cdk-z-index-overlay;

// We use `display: flex` on this element exclusively for centering connected overlays.
// When *not* centering, a top/left/bottom/right will be set which overrides the normal
// flex layout.
display: flex;
justify-content: center;
align-items: center;

// Add some dimensions so the element has an `innerText` which some people depend on in tests.
min-width: 1px;
min-height: 1px;
}

// Used when disabling global scrolling.
.cdk-global-scrollblock {
position: fixed;
Expand Down
41 changes: 14 additions & 27 deletions src/cdk/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {dispatchKeyboardEvent} from '@angular/cdk/testing';
import {ESCAPE} from '@angular/cdk/keycodes';
import {CdkConnectedOverlay, OverlayModule, CdkOverlayOrigin} from './index';
import {OverlayContainer} from './overlay-container';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {
ConnectedOverlayPositionChange,
ConnectionPositionPair,
} from './position/connected-position';
import {FlexibleConnectedPositionStrategy} from './position/flexible-connected-position-strategy';


describe('Overlay directives', () => {
Expand Down Expand Up @@ -79,13 +79,11 @@ describe('Overlay directives', () => {
let testComponent: ConnectedOverlayDirectiveTest =
fixture.debugElement.componentInstance;
let overlayDirective = testComponent.connectedOverlayDirective;

let strategy =
<ConnectedPositionStrategy> overlayDirective.overlayRef.getConfig().positionStrategy;
expect(strategy instanceof ConnectedPositionStrategy).toBe(true);
overlayDirective.overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy;

let positions = strategy.positions;
expect(positions.length).toBeGreaterThan(0);
expect(strategy instanceof FlexibleConnectedPositionStrategy).toBe(true);
expect(strategy.positions.length).toBeGreaterThan(0);
});

it('should set and update the `dir` attribute', () => {
Expand Down Expand Up @@ -138,7 +136,7 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.width).toEqual('250px');

fixture.componentInstance.isOpen = false;
Expand All @@ -156,7 +154,7 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.height).toEqual('100vh');

fixture.componentInstance.isOpen = false;
Expand All @@ -174,7 +172,7 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.minWidth).toEqual('250px');

fixture.componentInstance.isOpen = false;
Expand All @@ -192,7 +190,7 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
expect(pane.style.minHeight).toEqual('500px');

fixture.componentInstance.isOpen = false;
Expand Down Expand Up @@ -233,18 +231,13 @@ describe('Overlay directives', () => {
});

it('should set the offsetX', () => {
const trigger = fixture.debugElement.query(By.css('button')).nativeElement;
const startX = trigger.getBoundingClientRect().left;

fixture.componentInstance.offsetX = 5;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

expect(pane.style.left)
.toBe(startX + 5 + 'px',
`Expected overlay translateX to equal the original X + the offsetX.`);
expect(pane.style.transform).toContain('translateX(5px)');

fixture.componentInstance.isOpen = false;
fixture.detectChanges();
Expand All @@ -253,9 +246,7 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

expect(pane.style.left)
.toBe(startX + 15 + 'px',
`Expected overlay directive to reflect new offsetX if it changes.`);
expect(pane.style.transform).toContain('translateX(15px)');
});

it('should set the offsetY', () => {
Expand All @@ -268,21 +259,17 @@ describe('Overlay directives', () => {
fixture.componentInstance.isOpen = true;
fixture.detectChanges();

// expected y value is the starting y + trigger height + offset y
// 30 + 20 + 45 = 95px
const pane = overlayContainerElement.children[0] as HTMLElement;
const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;

expect(pane.style.top)
.toBe('95px', `Expected overlay translateY to equal the start Y + height + offsetY.`);
expect(pane.style.transform).toContain('translateY(45px)');

fixture.componentInstance.isOpen = false;
fixture.detectChanges();

fixture.componentInstance.offsetY = 55;
fixture.componentInstance.isOpen = true;
fixture.detectChanges();
expect(pane.style.top)
.toBe('105px', `Expected overlay directive to reflect new offsetY if it changes.`);
expect(pane.style.transform).toContain('translateY(55px)');
});

it('should be able to update the origin after init', () => {
Expand Down
101 changes: 64 additions & 37 deletions src/cdk/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,39 @@ import {Overlay} from './overlay';
import {OverlayConfig} from './overlay-config';
import {OverlayRef} from './overlay-ref';
import {
ConnectedOverlayPositionChange,
ConnectionPositionPair,
} from './position/connected-position';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
FlexibleConnectedPositionStrategy,
ConnectedPosition,
} from './position/flexible-connected-position-strategy';
import {ConnectedOverlayPositionChange} from './position/connected-position';
import {RepositionScrollStrategy, ScrollStrategy} from './scroll/index';


/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
const defaultPositionList = [
new ConnectionPositionPair(
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'}),
new ConnectionPositionPair(
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'bottom'}),
new ConnectionPositionPair(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'}),
new ConnectionPositionPair(
{originX: 'end', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'}),
const defaultPositionList: ConnectedPosition[] = [
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom'
},
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
}
];

/** Injection token that determines the scroll handling while the connected overlay is open. */
Expand Down Expand Up @@ -101,21 +113,22 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
private _backdropSubscription = Subscription.EMPTY;
private _offsetX: number = 0;
private _offsetY: number = 0;
private _position: ConnectedPositionStrategy;
private _position: FlexibleConnectedPositionStrategy;

/** Origin for the connected overlay. */
@Input('cdkConnectedOverlayOrigin') origin: CdkOverlayOrigin;

/** Registered connected position pairs. */
@Input('cdkConnectedOverlayPositions') positions: ConnectionPositionPair[];
@Input('cdkConnectedOverlayPositions') positions: ConnectedPosition[];

/** The offset in pixels for the overlay connection point on the x-axis */
@Input('cdkConnectedOverlayOffsetX')
get offsetX(): number { return this._offsetX; }
set offsetX(offsetX: number) {
this._offsetX = offsetX;

if (this._position) {
this._position.withOffsetX(offsetX);
this._setPositions(this._position);
}
}

Expand All @@ -124,8 +137,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
get offsetY() { return this._offsetY; }
set offsetY(offsetY: number) {
this._offsetY = offsetY;

if (this._position) {
this._position.withOffsetY(offsetY);
this._setPositions(this._position);
}
}

Expand Down Expand Up @@ -264,28 +278,42 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
}

/** Returns the position strategy of the overlay to be set on the overlay config */
private _createPositionStrategy(): ConnectedPositionStrategy {
const primaryPosition = this.positions[0];
const originPoint = {originX: primaryPosition.originX, originY: primaryPosition.originY};
const overlayPoint = {overlayX: primaryPosition.overlayX, overlayY: primaryPosition.overlayY};
private _createPositionStrategy(): FlexibleConnectedPositionStrategy {
const strategy = this._overlay.position()
.connectedTo(this.origin.elementRef, originPoint, overlayPoint)
.withOffsetX(this.offsetX)
.withOffsetY(this.offsetY)
.flexibleConnectedTo(this.origin.elementRef)
// Turn off all of the flexible positioning features for now to have it behave
// the same way as the old ConnectedPositionStrategy and to avoid breaking changes.
// TODO(crisbeto): make these on by default and add inputs for them
// next time we do breaking changes.
.withFlexibleHeight(false)
.withFlexibleWidth(false)
.withPush(false)
.withGrowAfterOpen(false)
.withLockedPosition(this.lockPosition);

for (let i = 1; i < this.positions.length; i++) {
strategy.withFallbackPosition(
{originX: this.positions[i].originX, originY: this.positions[i].originY},
{overlayX: this.positions[i].overlayX, overlayY: this.positions[i].overlayY}
);
}

strategy.onPositionChange.subscribe(pos => this.positionChange.emit(pos));
this._setPositions(strategy);
strategy.positionChanges.subscribe(p => this.positionChange.emit(p));

return strategy;
}

/**
* Sets the primary and fallback positions of a positions strategy,
* based on the current directive inputs.
*/
private _setPositions(positionStrategy: FlexibleConnectedPositionStrategy) {
const positions: ConnectedPosition[] = this.positions.map(pos => ({
originX: pos.originX,
originY: pos.originY,
overlayX: pos.overlayX,
overlayY: pos.overlayY,
offsetX: this.offsetX,
offsetY: this.offsetY
}));

positionStrategy.withPositions(positions);
}

/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
private _attachOverlay() {
if (!this._overlayRef) {
Expand All @@ -306,7 +334,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
});
}

this._position.withDirection(this.dir);
this._overlayRef.setDirection(this.dir);

if (!this._overlayRef.hasAttached()) {
Expand Down
Loading