Skip to content

Commit

Permalink
feat(overlay): add support for automatically setting the transform-or…
Browse files Browse the repository at this point in the history
…igin based on the current position (#10868)

Currently we have a handful of components where we set the `transform-origin` depending on the position of their overlay. This ends up being a fair bit of similar logic that is scattered across the different components. These changes consolidate that logic into an option on the `FlexibleConnectedPositionStrategy`.
  • Loading branch information
crisbeto authored and jelbourn committed May 11, 2018
1 parent 521b111 commit d26735c
Show file tree
Hide file tree
Showing 15 changed files with 177 additions and 314 deletions.
5 changes: 4 additions & 1 deletion src/cdk/overlay/overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ overlay relative to another element on the page. These features include the abil
overlay become scrollable once its content reaches the viewport edge, being able to configure a
margin between the overlay and the viewport edge, having an overlay be pushed into the viewport if
it doesn't fit into any of its preferred positions, as well as configuring whether the overlay's
size can grow while the overlay is open.
size can grow while the overlay is open. The flexible position strategy also allows for the
`transform-origin` of an element, inside the overlay, to be set based on the current position using
the `withTransformOriginOn`. This is useful when animating an overlay in and having the animation
originate from the point at which it connects with the origin.

A custom position strategy can be created by implementing the `PositionStrategy` interface.
Each `PositionStrategy` defines an `apply` method that is called whenever the overlay's position
Expand Down
114 changes: 113 additions & 1 deletion src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,114 @@ describe('FlexibleConnectedPositionStrategy', () => {

});

describe('with transform origin', () => {
it('should set the proper transform-origin when aligning to start/bottom', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
}]);

attachOverlay({positionStrategy});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('left top');
});

it('should set the proper transform-origin when aligning to end/bottom', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
}]);

attachOverlay({positionStrategy});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('right top');
});

it('should set the proper transform-origin when centering vertically', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'start',
originY: 'center',
overlayX: 'start',
overlayY: 'center'
}]);

attachOverlay({positionStrategy});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('left center');
});

it('should set the proper transform-origin when centering horizontally', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'top'
}]);

attachOverlay({positionStrategy});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('center top');
});

it('should set the proper transform-origin when aligning to start/top', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
}]);

attachOverlay({positionStrategy});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('left bottom');
});

it('should set the proper transform-origin when aligning to start/bottom in rtl', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
}]);

attachOverlay({positionStrategy, direction: 'rtl'});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('right top');
});

it('should set the proper transform-origin when aligning to end/bottom in rtl', () => {
positionStrategy.withTransformOriginOn('.transform-origin').withPositions([{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
}]);

attachOverlay({positionStrategy, direction: 'rtl'});

const target = overlayRef.overlayElement.querySelector('.transform-origin')! as HTMLElement;

expect(target.style.transformOrigin).toContain('left top');
});

});

it('should account for the `offsetX` pushing the overlay out of the screen', () => {
// Position the element so it would have enough space to fit.
originElement.style.top = '200px';
Expand Down Expand Up @@ -1676,7 +1784,11 @@ function createOverflowContainerElement() {


@Component({
template: `<div style="width: ${DEFAULT_WIDTH}px; height: ${DEFAULT_HEIGHT}px;"></div>`
template: `
<div
class="transform-origin"
style="width: ${DEFAULT_WIDTH}px; height: ${DEFAULT_HEIGHT}px;"></div>
`
})
class TestOverlay { }

Expand Down
44 changes: 41 additions & 3 deletions src/cdk/overlay/position/flexible-connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import {Platform} from '@angular/cdk/platform';


// TODO: refactor clipping detection into a separate thing (part of scrolling module)
// TODO: attribute selector to specify the transform-origin inside the overlay content
// TODO: flexible position + centering doesn't work on IE11 (works on Edge).
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis.

/**
Expand Down Expand Up @@ -108,6 +106,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
/** Default offset for the overlay along the y axis. */
private _offsetY = 0;

/** Selector to be used when finding the elements on which to set the transform origin. */
private _transformOriginSelector: string;

/** Observable sequence of position changes. */
positionChanges: Observable<ConnectedOverlayPositionChange> =
this._positionChanges.asObservable();
Expand Down Expand Up @@ -392,6 +393,19 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
return this;
}

/**
* Configures that the position strategy should set a `transform-origin` on some elements
* inside the overlay, depending on the current position that is being applied. This is
* useful for the cases where the origin of an animation can change depending on the
* alignment of the overlay.
* @param selector CSS selector that will be used to find the target
* elements onto which to set the transform origin.
*/
withTransformOriginOn(selector: string): this {
this._transformOriginSelector = selector;
return this;
}

/**
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
*/
Expand Down Expand Up @@ -556,11 +570,11 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {

/**
* Applies a computed position to the overlay and emits a position change.
*
* @param position The position preference
* @param originPoint The point on the origin element where the overlay is connected.
*/
private _applyPosition(position: ConnectedPosition, originPoint: Point) {
this._setTransformOrigin(position);
this._setOverlayElementStyles(originPoint, position);
this._setBoundingBoxStyles(originPoint, position);

Expand All @@ -574,6 +588,30 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
this._isInitialRender = false;
}

/** Sets the transform origin based on the configured selector and the passed-in position. */
private _setTransformOrigin(position: ConnectedPosition) {
if (!this._transformOriginSelector) {
return;
}

const elements: NodeListOf<HTMLElement> =
this._boundingBox!.querySelectorAll(this._transformOriginSelector);
let xOrigin: 'left' | 'right' | 'center';
let yOrigin: 'top' | 'bottom' | 'center' = position.overlayY;

if (position.overlayX === 'center') {
xOrigin = 'center';
} else if (this._isRtl()) {
xOrigin = position.overlayX === 'start' ? 'right' : 'left';
} else {
xOrigin = position.overlayX === 'start' ? 'left' : 'right';
}

for (let i = 0; i < elements.length; i++) {
elements[i].style.transformOrigin = `${xOrigin} ${yOrigin}`;
}
}

/**
* Gets the position and size of the overlay's sizing container.
*
Expand Down
40 changes: 0 additions & 40 deletions src/lib/core/style/_menu-common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,43 +57,3 @@ $mat-menu-icon-margin: 16px !default;
}
}
}

/**
* This mixin adds the correct panel transform styles based
* on the direction that the menu panel opens.
*/
@mixin mat-menu-positions() {
&.mat-menu-after.mat-menu-below {
transform-origin: left top;
}

&.mat-menu-after.mat-menu-above {
transform-origin: left bottom;
}

&.mat-menu-before.mat-menu-below {
transform-origin: right top;
}

&.mat-menu-before.mat-menu-above {
transform-origin: right bottom;
}

[dir='rtl'] & {
&.mat-menu-after.mat-menu-below {
transform-origin: right top;
}

&.mat-menu-after.mat-menu-above {
transform-origin: right bottom;
}

&.mat-menu-before.mat-menu-below {
transform-origin: left top;
}

&.mat-menu-before.mat-menu-above {
transform-origin: left bottom;
}
}
}
5 changes: 0 additions & 5 deletions src/lib/datepicker/datepicker-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@ $mat-datepicker-touch-max-height: 788px;

display: block;
border-radius: 2px;
transform-origin: top center;

.mat-calendar {
width: $mat-datepicker-non-touch-calendar-width;
height: $mat-datepicker-non-touch-calendar-height;
}
}

.mat-datepicker-content-above {
transform-origin: bottom center;
}

.mat-datepicker-content-touch {
@include mat-elevation(0);

Expand Down
46 changes: 0 additions & 46 deletions src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1476,52 +1476,6 @@ describe('MatDatepicker', () => {
}));
});

describe('popup animations', () => {
let fixture: ComponentFixture<StandardDatepicker>;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatDatepickerModule, MatNativeDateModule, NoopAnimationsModule],
declarations: [StandardDatepicker],
}).compileComponents();

fixture = TestBed.createComponent(StandardDatepicker);
fixture.detectChanges();
}));

it('should not set the `mat-datepicker-content-above` class when opening downwards',
fakeAsync(() => {
fixture.componentInstance.datepicker.open();
fixture.detectChanges();
flush();
fixture.detectChanges();

const content =
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;

expect(content.classList).not.toContain('mat-datepicker-content-above');
}));

it('should set the `mat-datepicker-content-above` class when opening upwards', fakeAsync(() => {
const input = fixture.debugElement.nativeElement.querySelector('input');

// Push the input to the bottom of the page to force the calendar to open upwards
input.style.position = 'fixed';
input.style.bottom = '0';

fixture.componentInstance.datepicker.open();
fixture.detectChanges();
flush();
fixture.detectChanges();

const content =
document.querySelector('.cdk-overlay-pane mat-datepicker-content')! as HTMLElement;

expect(content.classList).toContain('mat-datepicker-content-above');
}));

});

describe('datepicker with custom header', () => {
let fixture: ComponentFixture<DatepickerWithCustomHeader>;
let testComponent: DatepickerWithCustomHeader;
Expand Down
Loading

0 comments on commit d26735c

Please sign in to comment.