Skip to content

Commit

Permalink
feat(overlay): support custom transform origins
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Nov 11, 2016
1 parent 77701cc commit db7f108
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 8 deletions.
58 changes: 58 additions & 0 deletions src/lib/core/overlay/position/connected-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,64 @@ describe('ConnectedPositionStrategy', () => {

});

describe('transform origin', () => {
let animatedDiv: HTMLElement;

beforeEach(() => {
// append a child to the overlay to act as the animation root
animatedDiv = document.createElement('div');
animatedDiv.classList.add('md-animated-div');
overlayElement.appendChild(animatedDiv);
});

it('should apply the default transform origin to the animation root', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'})
.withTransformOrigin('.md-animated-div', 'start top');

strategy.apply(overlayElement);
expect(animatedDiv.style.transformOrigin).toEqual('left top 0px');
});

it('should apply the correct transform origin in RTL', () => {
strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'})
.withTransformOrigin('.md-animated-div', 'start top')
.withDirection('rtl');

strategy.apply(overlayElement);
expect(animatedDiv.style.transformOrigin).toEqual('right top 0px');
});

it('should apply the correct transform origin given a fallback position', () => {
fakeViewportRuler.fakeRect = {
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
};
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);

originElement.style.top = '200px';
originElement.style.left = '475px';

strategy = positionBuilder.connectedTo(
fakeElementRef,
{originX: 'end', originY: 'center'},
{overlayX: 'start', overlayY: 'center'})
.withTransformOrigin('.md-animated-div', 'start top')
.withFallbackPosition(
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'},
'end top');

strategy.apply(overlayElement);
expect(animatedDiv.style.transformOrigin).toEqual('right top 0px');
});

});


/**
* Run all tests for connecting the overlay to the origin such that first preferred
Expand Down
40 changes: 32 additions & 8 deletions src/lib/core/overlay/position/connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {applyCssTransform} from '../../style/apply-transform';
import {
ConnectionPositionPair,
OriginConnectionPosition,
OverlayConnectionPosition
OverlayConnectionPosition,
PreferredPosition,
TransformOrigin
} from './connected-position';


Expand All @@ -25,13 +27,16 @@ export class ConnectedPositionStrategy implements PositionStrategy {
/** The offset in pixels for the overlay connection point on the y-axis */
private _offsetY: number = 0;

/** The selector for the overlay descendant that controls the animation. */
private _animationRootSelector: string;

/** Whether the we're dealing with an RTL context */
get _isRtl() {
return this._dir === 'rtl';
}

/** Ordered list of preferred positions, from most to least desirable. */
_preferredPositions: ConnectionPositionPair[] = [];
_preferredPositions: PreferredPosition[] = [];

/** The origin element against which the overlay will be positioned. */
private _origin: HTMLElement;
Expand Down Expand Up @@ -70,13 +75,16 @@ export class ConnectedPositionStrategy implements PositionStrategy {
for (let pos of this._preferredPositions) {
// Get the (x, y) point of connection on the origin, and then use that to get the
// (top, left) coordinate for the overlay at `pos`.
let originPoint = this._getOriginConnectionPoint(originRect, pos);
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos);
let originPoint = this._getOriginConnectionPoint(originRect, pos.position);
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos.position);
firstOverlayPoint = firstOverlayPoint || overlayPoint;

// If the overlay in the calculated position fits on-screen, put it there and we're done.
if (this._willOverlayFitWithinViewport(overlayPoint, overlayRect, viewportRect)) {
this._setElementPosition(element, overlayPoint);
if (pos.transformOrigin) {
this._setElementTransformOrigin(element, pos.transformOrigin);
}
return Promise.resolve(null);
}
}
Expand All @@ -89,8 +97,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {

withFallbackPosition(
originPos: OriginConnectionPosition,
overlayPos: OverlayConnectionPosition): this {
this._preferredPositions.push(new ConnectionPositionPair(originPos, overlayPos));
overlayPos: OverlayConnectionPosition, transformOrigin?: TransformOrigin): this {
this._preferredPositions.push({
position: new ConnectionPositionPair(originPos, overlayPos),
transformOrigin: transformOrigin
});
return this;
}

Expand All @@ -112,6 +123,13 @@ export class ConnectedPositionStrategy implements PositionStrategy {
return this;
}

/** Sets a custom transform origin on the element that matches the given selector. */
withTransformOrigin(animationRootSelector: string, transformOrigin: TransformOrigin): this {
this._animationRootSelector = animationRootSelector;
this._preferredPositions[0].transformOrigin = transformOrigin;
return this;
}

/**
* Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context.
* @param rect
Expand Down Expand Up @@ -224,10 +242,16 @@ export class ConnectedPositionStrategy implements PositionStrategy {
// because it will need to be used for animations.
applyCssTransform(element, `translateX(${x}px) translateY(${y}px)`);
}

private _setElementTransformOrigin(element: HTMLElement,
transformOrigin: TransformOrigin): void {
const animationRoot = element.querySelector(this._animationRootSelector) as HTMLElement;
const [start, end] = this._isRtl ? ['right', 'left'] : ['left', 'right'];
animationRoot.style.transformOrigin = (transformOrigin as string).replace('start', start)
.replace('end', end);
}
}


/** A simple (x, y) coordinate. */
type Point = {x: number, y: number};


13 changes: 13 additions & 0 deletions src/lib/core/overlay/position/connected-position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,16 @@ export class ConnectionPositionPair {
this.overlayY = overlay.overlayY;
}
}

/** The connection position and transform origin. */
export type PreferredPosition = {
position: ConnectionPositionPair,
transformOrigin: TransformOrigin
};

/**
* Supported transform origin property values. Values with 'start' or 'end' will be
* converted to 'left' or 'right' depending on the text direction.
*/
export type TransformOrigin = 'start' | 'end' | 'top' | 'bottom' |
'start top' | 'end top' | 'start bottom' | 'end bottom';

0 comments on commit db7f108

Please sign in to comment.