Skip to content

Commit e85f664

Browse files
committed
feat(overlay): support custom transform origins
1 parent 77701cc commit e85f664

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

src/lib/core/overlay/position/connected-position-strategy.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,64 @@ describe('ConnectedPositionStrategy', () => {
259259

260260
});
261261

262+
describe('transform origin', () => {
263+
let animatedDiv: HTMLElement;
264+
265+
beforeEach(() => {
266+
// append a child to the overlay to act as the animation root
267+
animatedDiv = document.createElement('div');
268+
animatedDiv.classList.add('md-animated-div');
269+
overlayElement.appendChild(animatedDiv);
270+
});
271+
272+
it('should apply the default transform origin to the animation root', () => {
273+
strategy = positionBuilder.connectedTo(
274+
fakeElementRef,
275+
{originX: 'start', originY: 'top'},
276+
{overlayX: 'start', overlayY: 'top'})
277+
.withTransformOrigin('.md-animated-div', 'start top');
278+
279+
strategy.apply(overlayElement);
280+
expect(animatedDiv.style.transformOrigin).toEqual('left top 0px');
281+
});
282+
283+
it('should apply the correct transform origin in RTL', () => {
284+
strategy = positionBuilder.connectedTo(
285+
fakeElementRef,
286+
{originX: 'start', originY: 'top'},
287+
{overlayX: 'start', overlayY: 'top'})
288+
.withTransformOrigin('.md-animated-div', 'start top')
289+
.withDirection('rtl');
290+
291+
strategy.apply(overlayElement);
292+
expect(animatedDiv.style.transformOrigin).toEqual('right top 0px');
293+
});
294+
295+
it('should apply the correct transform origin given a fallback position', () => {
296+
fakeViewportRuler.fakeRect = {
297+
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
298+
};
299+
positionBuilder = new OverlayPositionBuilder(fakeViewportRuler);
300+
301+
originElement.style.top = '200px';
302+
originElement.style.left = '475px';
303+
304+
strategy = positionBuilder.connectedTo(
305+
fakeElementRef,
306+
{originX: 'end', originY: 'center'},
307+
{overlayX: 'start', overlayY: 'center'})
308+
.withTransformOrigin('.md-animated-div', 'start top')
309+
.withFallbackPosition(
310+
{originX: 'start', originY: 'bottom'},
311+
{overlayX: 'end', overlayY: 'top'},
312+
'end top');
313+
314+
strategy.apply(overlayElement);
315+
expect(animatedDiv.style.transformOrigin).toEqual('right top 0px');
316+
});
317+
318+
});
319+
262320

263321
/**
264322
* Run all tests for connecting the overlay to the origin such that first preferred

src/lib/core/overlay/position/connected-position-strategy.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {applyCssTransform} from '../../style/apply-transform';
55
import {
66
ConnectionPositionPair,
77
OriginConnectionPosition,
8-
OverlayConnectionPosition
8+
OverlayConnectionPosition,
9+
PreferredPosition,
10+
TransformOrigin
911
} from './connected-position';
1012

1113

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

30+
/** The selector for the overlay descendant that controls the animation. */
31+
private _animationRootSelector: string;
32+
2833
/** Whether the we're dealing with an RTL context */
2934
get _isRtl() {
3035
return this._dir === 'rtl';
3136
}
3237

3338
/** Ordered list of preferred positions, from most to least desirable. */
34-
_preferredPositions: ConnectionPositionPair[] = [];
39+
_preferredPositions: PreferredPosition[] = [];
3540

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

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

9098
withFallbackPosition(
9199
originPos: OriginConnectionPosition,
92-
overlayPos: OverlayConnectionPosition): this {
93-
this._preferredPositions.push(new ConnectionPositionPair(originPos, overlayPos));
100+
overlayPos: OverlayConnectionPosition, transformOrigin?: TransformOrigin): this {
101+
this._preferredPositions.push({
102+
position: new ConnectionPositionPair(originPos, overlayPos),
103+
transformOrigin: transformOrigin
104+
});
94105
return this;
95106
}
96107

@@ -112,6 +123,13 @@ export class ConnectedPositionStrategy implements PositionStrategy {
112123
return this;
113124
}
114125

126+
/** Sets a custom transform origin on the element that matches the given selector. */
127+
withTransformOrigin(animationRootSelector: string, transformOrigin: TransformOrigin): this {
128+
this._animationRootSelector = animationRootSelector;
129+
this._preferredPositions[0].transformOrigin = transformOrigin;
130+
return this;
131+
}
132+
115133
/**
116134
* Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context.
117135
* @param rect
@@ -224,10 +242,21 @@ export class ConnectedPositionStrategy implements PositionStrategy {
224242
// because it will need to be used for animations.
225243
applyCssTransform(element, `translateX(${x}px) translateY(${y}px)`);
226244
}
245+
246+
/**
247+
* Sets the transform-origin property on the animation root of the overlay.
248+
* If the transform-origin provided includes 'start' or 'end', it will be converted to
249+
* 'left' or 'right' depending on the current text direction.
250+
*/
251+
private _setElementTransformOrigin(element: HTMLElement,
252+
transformOrigin: TransformOrigin): void {
253+
const animationRoot = element.querySelector(this._animationRootSelector) as HTMLElement;
254+
const [start, end] = this._isRtl ? ['right', 'left'] : ['left', 'right'];
255+
animationRoot.style.transformOrigin = (transformOrigin as string).replace('start', start)
256+
.replace('end', end);
257+
}
227258
}
228259

229260

230261
/** A simple (x, y) coordinate. */
231262
type Point = {x: number, y: number};
232-
233-

src/lib/core/overlay/position/connected-position.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ export class ConnectionPositionPair {
3131
this.overlayY = overlay.overlayY;
3232
}
3333
}
34+
35+
/** The connection position and corresponding transform origin. */
36+
export type PreferredPosition = {
37+
position: ConnectionPositionPair,
38+
transformOrigin: TransformOrigin
39+
};
40+
41+
/**
42+
* Supported transform origin property values. Values with 'start' or 'end' will be
43+
* converted to 'left' or 'right' depending on the text direction.
44+
*/
45+
export type TransformOrigin = 'start' | 'end' | 'top' | 'bottom' |
46+
'start top' | 'end top' | 'start bottom' | 'end bottom';

0 commit comments

Comments
 (0)