Skip to content

Commit

Permalink
feat(overlay): add start and end positions to GlobalPositionStrategy
Browse files Browse the repository at this point in the history
* Makes some things easier to follow in the `GlobalPositionStrategy`.
* Adds the ability to position a global overlay to the start and end of the viewport, based on its layout direction.
* Fixes the offset in the `center` position always being from the left.
* Adds better docs for the various methods.
  • Loading branch information
crisbeto committed Jul 2, 2018
1 parent 444fb38 commit b3eb690
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 82 deletions.
91 changes: 90 additions & 1 deletion src/cdk/overlay/position/global-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,18 @@ describe('GlobalPositonStrategy', () => {
expect(parentStyle.alignItems).toBe('flex-end');
});

it('should center the element', () => {
it('should center the element by default', () => {
attachOverlay({
positionStrategy: overlay.position().global()
});

const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(parentStyle.justifyContent).toBe('center');
expect(parentStyle.alignItems).toBe('center');
});

it('should center the element explicitly', () => {
attachOverlay({
positionStrategy: overlay.position()
.global()
Expand All @@ -136,13 +147,34 @@ describe('GlobalPositonStrategy', () => {
const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginRight).toBe('');
expect(elementStyle.marginLeft).toBe('10px');
expect(elementStyle.marginTop).toBe('15px');

expect(parentStyle.justifyContent).toBe('center');
expect(parentStyle.alignItems).toBe('center');
});

it('should center the element with an offset in rtl', () => {
attachOverlay({
direction: 'rtl',
positionStrategy: overlay.position()
.global()
.centerHorizontally('10px')
.centerVertically('15px')
});

const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginLeft).toBe('');
expect(elementStyle.marginRight).toBe('10px');
expect(elementStyle.marginTop).toBe('15px');

expect(parentStyle.justifyContent).toBe('center');
expect(parentStyle.alignItems).toBe('center');
});

it('should make the element position: static', () => {
attachOverlay({
positionStrategy: overlay.position().global()
Expand Down Expand Up @@ -303,6 +335,63 @@ describe('GlobalPositonStrategy', () => {
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;
expect(parentStyle.justifyContent).toBe('flex-start');
});

it('should position the overlay to the start in ltr', () => {
attachOverlay({
direction: 'ltr',
positionStrategy: overlay.position().global().start('40px')
});

const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginLeft).toBe('40px');
expect(elementStyle.marginRight).toBe('');
expect(parentStyle.justifyContent).toBe('flex-start');
});

it('should position the overlay to the start in rtl', () => {
attachOverlay({
direction: 'rtl',
positionStrategy: overlay.position().global().start('50px')
});

const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginLeft).toBe('');
expect(elementStyle.marginRight).toBe('50px');
expect(parentStyle.justifyContent).toBe('flex-start');
});

it('should position the overlay to the end in ltr', () => {
attachOverlay({
direction: 'ltr',
positionStrategy: overlay.position().global().end('60px')
});

const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginRight).toBe('60px');
expect(elementStyle.marginLeft).toBe('');
expect(parentStyle.justifyContent).toBe('flex-end');
});

it('should position the overlay to the end in rtl', () => {
attachOverlay({
direction: 'rtl',
positionStrategy: overlay.position().global().end('70px')
});

const elementStyle = overlayRef.overlayElement.style;
const parentStyle = (overlayRef.overlayElement.parentNode as HTMLElement).style;

expect(elementStyle.marginLeft).toBe('70px');
expect(elementStyle.marginRight).toBe('');
expect(parentStyle.justifyContent).toBe('flex-end');
});

});


Expand Down
195 changes: 130 additions & 65 deletions src/cdk/overlay/position/global-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ import {OverlayReference} from '../overlay-reference';
export class GlobalPositionStrategy implements PositionStrategy {
/** The overlay to which this strategy is attached. */
private _overlayRef: OverlayReference;
private _cssPosition: string = 'static';
private _topOffset: string = '';
private _bottomOffset: string = '';
private _leftOffset: string = '';
private _rightOffset: string = '';
private _alignItems: string = '';
private _justifyContent: string = '';
private _width: string = '';
private _height: string = '';
private _yPosition: 'top' | 'bottom' | 'center' = 'center';
private _yOffset = '';
private _xPosition: 'left' | 'right' | 'start' | 'end' | 'center' = 'center';
private _xOffset = '';

attach(overlayRef: OverlayReference): void {
const config = overlayRef.getConfig();
Expand All @@ -46,46 +43,64 @@ export class GlobalPositionStrategy implements PositionStrategy {
}

/**
* Sets the top position of the overlay. Clears any previously set vertical position.
* @param value New top offset.
* Positions the overlay to the top of the viewport.
* @param offset Offset from the top of the viewport.
*/
top(value: string = ''): this {
this._bottomOffset = '';
this._topOffset = value;
this._alignItems = 'flex-start';
top(offset: string = ''): this {
this._yOffset = offset;
this._yPosition = 'top';
return this;
}

/**
* Sets the left position of the overlay. Clears any previously set horizontal position.
* @param value New left offset.
* Positions the overlay to the left of the viewport, no matter what layout direction it has.
* @param offset Offset from the left of the viewport.
*/
left(value: string = ''): this {
this._rightOffset = '';
this._leftOffset = value;
this._justifyContent = 'flex-start';
left(offset: string = ''): this {
this._xOffset = offset;
this._xPosition = 'left';
return this;
}

/**
* Sets the bottom position of the overlay. Clears any previously set vertical position.
* @param value New bottom offset.
* Positions the overlay to the bottom of the viewport.
* @param offset Offset from the bottom of the viewport.
*/
bottom(value: string = ''): this {
this._topOffset = '';
this._bottomOffset = value;
this._alignItems = 'flex-end';
bottom(offset: string = ''): this {
this._yOffset = offset;
this._yPosition = 'bottom';
return this;
}

/**
* Sets the right position of the overlay. Clears any previously set horizontal position.
* @param value New right offset.
* Positions the overlay to the right of the viewport, no matter what layout direction it has.
* @param offset Offset from the right of the viewport.
*/
right(value: string = ''): this {
this._leftOffset = '';
this._rightOffset = value;
this._justifyContent = 'flex-end';
right(offset: string = ''): this {
this._xOffset = offset;
this._xPosition = 'right';
return this;
}

/**
* Sets the overlay to the start of the viewport, depending on the overlay direction.
* This will be to the left in LTR layouts and to the right in RTL.
* @param offset Offset from the edge of the screen.
*/
start(offset: string = ''): this {
this._xOffset = offset;
this._xPosition = 'start';
return this;
}

/**
* Sets the overlay to the end of the viewport, depending on the overlay direction.
* This will be to the right in LTR layouts and to the left in RTL.
* @param offset Offset from the edge of the screen.
*/
end(offset: string = ''): this {
this._xOffset = offset;
this._xPosition = 'end';
return this;
}

Expand Down Expand Up @@ -122,26 +137,22 @@ export class GlobalPositionStrategy implements PositionStrategy {
}

/**
* Centers the overlay horizontally with an optional offset.
* Clears any previously set horizontal position.
*
* @param offset Overlay offset from the horizontal center.
* Centers the overlay horizontally in the viewport.
* @param offset Offset from the center of the viewport.
*/
centerHorizontally(offset: string = ''): this {
this.left(offset);
this._justifyContent = 'center';
this._xOffset = offset;
this._xPosition = 'center';
return this;
}

/**
* Centers the overlay vertically with an optional offset.
* Clears any previously set vertical position.
*
* @param offset Overlay offset from the vertical center.
* Centers the overlay vertically in the viewport.
* @param offset Offset from the center of the viewport.
*/
centerVertically(offset: string = ''): this {
this.top(offset);
this._alignItems = 'center';
this._yPosition = 'center';
this._yOffset = offset;
return this;
}

Expand All @@ -150,42 +161,96 @@ export class GlobalPositionStrategy implements PositionStrategy {
* @docs-private
*/
apply(): void {
// Since the overlay ref applies the strategy asynchronously, it could
// have been disposed before it ends up being applied. If that is the
// case, we shouldn't do anything.
// Since the overlay ref applies the strategy asynchronously, it could have been
// disposed before it gets applied. If that is the case, we shouldn't do anything.
if (!this._overlayRef.hasAttached()) {
return;
}

this._overlayRef.overlayElement.style.position = 'static';
this._applyYPosition();
this._applyXPosition();
}

private _applyYPosition() {
const styles = this._overlayRef.overlayElement.style;
const parentStyles = this._overlayRef.hostElement.style;
const config = this._overlayRef.getConfig();

styles.position = this._cssPosition;
styles.marginLeft = config.width === '100%' ? '0' : this._leftOffset;
styles.marginTop = config.height === '100%' ? '0' : this._topOffset;
styles.marginBottom = this._bottomOffset;
styles.marginRight = this._rightOffset;
if (config.height === '100%') {
parentStyles.alignItems = 'flex-start';
styles.marginTop = styles.marginBottom = '0';
return;
}

switch (this._yPosition) {
case 'top':
case 'center':
parentStyles.alignItems = this._yPosition === 'center' ? 'center' : 'flex-start';
styles.marginTop = this._yOffset;
styles.marginBottom = '';
break;

case 'bottom':
parentStyles.alignItems = 'flex-end';
styles.marginTop = '';
styles.marginBottom = this._yOffset;
break;

default:
throw Error(`Unsupported Y axis position ${this._xPosition}.`);
}
}

private _applyXPosition() {
const styles = this._overlayRef.overlayElement.style;
const parentStyles = this._overlayRef.hostElement.style;
const config = this._overlayRef.getConfig();
const isRtl = this._overlayRef.getConfig().direction === 'rtl';

if (config.width === '100%') {
parentStyles.justifyContent = 'flex-start';
} else if (this._justifyContent === 'center') {
styles.marginLeft = styles.marginRight = '0';
return;
}

switch (this._xPosition) {
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we don't
// want that if the positioning is explicitly `left` and `right`, hence why we do another
// inversion to ensure that the overlay stays in the same position.
case 'left':
parentStyles.justifyContent = isRtl ? 'flex-end' : 'flex-start';
styles.marginLeft = this._xOffset;
styles.marginRight = '';
break;

case 'right':
parentStyles.justifyContent = isRtl ? 'flex-start' : 'flex-end';
styles.marginRight = this._xOffset;
styles.marginLeft = '';
break;

case 'center':
parentStyles.justifyContent = 'center';
} else if (this._overlayRef.getConfig().direction === 'rtl') {
// In RTL the browser will invert `flex-start` and `flex-end` automatically, but we
// don't want that because our positioning is explicitly `left` and `right`, hence
// why we do another inversion to ensure that the overlay stays in the same position.
// TODO: reconsider this if we add `start` and `end` methods.
if (this._justifyContent === 'flex-start') {
parentStyles.justifyContent = 'flex-end';
} else if (this._justifyContent === 'flex-end') {
styles.marginLeft = isRtl ? '' : this._xOffset;
styles.marginRight = isRtl ? this._xOffset : '';
break;

case 'start':
parentStyles.justifyContent = 'flex-start';
}
} else {
parentStyles.justifyContent = this._justifyContent;
}
styles.marginLeft = isRtl ? '' : this._xOffset;
styles.marginRight = isRtl ? this._xOffset : '';
break;

case 'end':
parentStyles.justifyContent = 'flex-end';
styles.marginLeft = isRtl ? this._xOffset : '';
styles.marginRight = isRtl ? '' : this._xOffset;
break;

parentStyles.alignItems = config.height === '100%' ? 'flex-start' : this._alignItems;
default:
throw Error(`Unsupported X axis position ${this._xPosition}.`);
}
}

/**
Expand Down
Loading

0 comments on commit b3eb690

Please sign in to comment.