Skip to content

Commit

Permalink
feat(overlay): add support for flexible connected positioning
Browse files Browse the repository at this point in the history
* Adds the `FlexibleConnectedPositionStrategy` that builds on top of the `ConnectedPositionStrategy` adds the following:
  * The ability to have overlays with flexible sizing.
  * Being able to push overlays into the viewport if they don't fit.
  * Having a margin between the overlay and the viewport edge.
  * Being able to assign weights to the overlay positions.
* Refactors the `ConnectedPositionStrategy` to use the `FlexibleConnectedPositionStrategy` in order to avoid breaking API changes.
* Switches all of the components to the new position strategy.
* Adds an API to the `OverlayRef` that allows for the consumer to wrap the pane in a div. This is a common requirement between the `GlobalPositionStrategy` and the `FlexibleConnectedPositionStrategy`, and it's easier to keep track of the elements when the attaching and detaching is done in the `OverlayRef`.

Fixes angular#6534.
Fixes angular#2725.
Fixes angular#5267.
  • Loading branch information
crisbeto committed Jan 20, 2018
1 parent 4523556 commit 3726d81
Show file tree
Hide file tree
Showing 31 changed files with 3,150 additions and 742 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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
21 changes: 21 additions & 0 deletions src/cdk/overlay/_overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,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 @@ -76,6 +83,20 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
background: none;
}

// 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;
}

// 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,7 +6,7 @@ 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 {FlexibleConnectedPositionStrategy} from './position/flexible-connected-position-strategy';
import {ConnectedOverlayPositionChange} from './position/connected-position';


Expand Down Expand Up @@ -80,13 +80,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 @@ -139,7 +137,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');
});

Expand All @@ -148,7 +146,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');
});

Expand All @@ -157,7 +155,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');
});

Expand All @@ -166,7 +164,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');
});

Expand Down Expand Up @@ -198,18 +196,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 @@ -218,9 +211,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 @@ -233,21 +224,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)');
});

});
Expand Down
110 changes: 66 additions & 44 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 _positionSubscription = 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 @@ -163,8 +177,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {

/** @deprecated */
@Input('positions')
get _deprecatedPositions(): ConnectionPositionPair[] { return this.positions; }
set _deprecatedPositions(_positions: ConnectionPositionPair[]) { this.positions = _positions; }
get _deprecatedPositions(): ConnectedPosition[] { return this.positions; }
set _deprecatedPositions(_positions: ConnectedPosition[]) { this.positions = _positions; }

/** @deprecated */
@Input('offsetX')
Expand Down Expand Up @@ -303,31 +317,40 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
}

/** Returns the position strategy of the overlay to be set on the overlay config */
private _createPositionStrategy(): ConnectedPositionStrategy {
const pos = this.positions[0];
const originPoint = {originX: pos.originX, originY: pos.originY};
const overlayPoint = {overlayX: pos.overlayX, overlayY: pos.overlayY};

private _createPositionStrategy(): FlexibleConnectedPositionStrategy {
const strategy = this._overlay.position()
.connectedTo(this.origin.elementRef, originPoint, overlayPoint)
.withOffsetX(this.offsetX)
.withOffsetY(this.offsetY);

this._handlePositionChanges(strategy);
.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);

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

return strategy;
}

private _handlePositionChanges(strategy: ConnectedPositionStrategy): void {
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}
);
}

this._positionSubscription =
strategy.onPositionChange.subscribe(pos => this.positionChange.emit(pos));
/**
* 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 */
Expand All @@ -342,7 +365,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
});
}

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

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

0 comments on commit 3726d81

Please sign in to comment.