mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add support for automatically setting the transform-origin 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`.
This commit is contained in:
committed by
Jeremy Elbourn
parent
521b111f4a
commit
d26735ccad
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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 { }
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ $mat-datepicker-touch-max-height: 788px;
|
||||
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
transform-origin: top center;
|
||||
|
||||
.mat-calendar {
|
||||
width: $mat-datepicker-non-touch-calendar-width;
|
||||
@@ -37,10 +36,6 @@ $mat-datepicker-touch-max-height: 788px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-datepicker-content-above {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.mat-datepicker-content-touch {
|
||||
@include mat-elevation(0);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {Directionality} from '@angular/cdk/bidi';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {ESCAPE, UP_ARROW} from '@angular/cdk/keycodes';
|
||||
import {
|
||||
FlexibleConnectedPositionStrategy,
|
||||
Overlay,
|
||||
OverlayConfig,
|
||||
OverlayRef,
|
||||
@@ -23,7 +22,6 @@ import {take, filter} from 'rxjs/operators';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
@@ -33,13 +31,12 @@ import {
|
||||
InjectionToken,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Optional,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import {CanColor, DateAdapter, mixinColor, ThemePalette} from '@angular/material/core';
|
||||
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
@@ -88,7 +85,6 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
|
||||
'class': 'mat-datepicker-content',
|
||||
'[@transformPanel]': '"enter"',
|
||||
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
|
||||
'[class.mat-datepicker-content-above]': '_isAbove',
|
||||
},
|
||||
animations: [
|
||||
matDatepickerAnimations.transformPanel,
|
||||
@@ -100,10 +96,7 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
|
||||
inputs: ['color'],
|
||||
})
|
||||
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
|
||||
implements AfterViewInit, CanColor, OnInit, OnDestroy {
|
||||
|
||||
/** Subscription to changes in the overlay's position. */
|
||||
private _positionChange: Subscription|null;
|
||||
implements AfterViewInit, CanColor {
|
||||
|
||||
/** Reference to the internal calendar component. */
|
||||
@ViewChild(MatCalendar) _calendar: MatCalendar<D>;
|
||||
@@ -114,43 +107,13 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
|
||||
/** Whether the datepicker is above or below the input. */
|
||||
_isAbove: boolean;
|
||||
|
||||
constructor(
|
||||
elementRef: ElementRef,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _ngZone: NgZone) {
|
||||
constructor(elementRef: ElementRef) {
|
||||
super(elementRef);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.datepicker._popupRef || this._positionChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionStrategy =
|
||||
this.datepicker._popupRef.getConfig().positionStrategy! as FlexibleConnectedPositionStrategy;
|
||||
|
||||
this._positionChange = positionStrategy.positionChanges.subscribe(change => {
|
||||
const isAbove = change.connectionPair.overlayY === 'bottom';
|
||||
|
||||
if (isAbove !== this._isAbove) {
|
||||
this._ngZone.run(() => {
|
||||
this._isAbove = isAbove;
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this._calendar.focusActiveCell();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this._positionChange) {
|
||||
this._positionChange.unsubscribe();
|
||||
this._positionChange = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -478,6 +441,7 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
|
||||
private _createPopupPositionStrategy(): PositionStrategy {
|
||||
return this._overlay.position()
|
||||
.flexibleConnectedTo(this._datepickerInput.getPopupConnectionElementRef())
|
||||
.withTransformOriginOn('.mat-datepicker-content')
|
||||
.withFlexibleDimensions(false)
|
||||
.withViewportMargin(8)
|
||||
.withPush(false)
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
TemplateRef,
|
||||
QueryList,
|
||||
@@ -97,7 +96,7 @@ const MAT_MENU_BASE_ELEVATION = 2;
|
||||
{provide: MAT_MENU_PANEL, useExisting: MatMenu}
|
||||
]
|
||||
})
|
||||
export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuItem>, OnDestroy {
|
||||
export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnDestroy {
|
||||
private _keyManager: FocusKeyManager<MatMenuItem>;
|
||||
private _xPosition: MenuPositionX = this._defaultOptions.xPosition;
|
||||
private _yPosition: MenuPositionY = this._defaultOptions.yPosition;
|
||||
@@ -141,7 +140,6 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuIt
|
||||
throwMatMenuInvalidPositionX();
|
||||
}
|
||||
this._xPosition = value;
|
||||
this.setPositionClasses();
|
||||
}
|
||||
|
||||
/** Position of the menu in the Y axis. */
|
||||
@@ -152,7 +150,6 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuIt
|
||||
throwMatMenuInvalidPositionY();
|
||||
}
|
||||
this._yPosition = value;
|
||||
this.setPositionClasses();
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
@@ -202,7 +199,6 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuIt
|
||||
}, {});
|
||||
|
||||
this._elementRef.nativeElement.className = '';
|
||||
this.setPositionClasses();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +229,6 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuIt
|
||||
private _ngZone: NgZone,
|
||||
@Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.setPositionClasses();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._keyManager = new FocusKeyManager<MatMenuItem>(this._items).withWrap().withTypeAhead();
|
||||
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('tab'));
|
||||
@@ -306,17 +298,6 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel<MatMenuIt
|
||||
this._keyManager.setActiveItem(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* It's necessary to set position-based classes to ensure the menu panel animation
|
||||
* folds out from the correct direction.
|
||||
*/
|
||||
setPositionClasses(posX: MenuPositionX = this.xPosition, posY: MenuPositionY = this.yPosition) {
|
||||
this._classList['mat-menu-before'] = posX === 'before';
|
||||
this._classList['mat-menu-after'] = posX === 'after';
|
||||
this._classList['mat-menu-above'] = posY === 'above';
|
||||
this._classList['mat-menu-below'] = posY === 'below';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the menu panel elevation.
|
||||
* @param depth Number of parent menus that come before the menu.
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface MatMenuPanel<T = any> {
|
||||
direction?: Direction;
|
||||
focusFirstItem: (origin?: FocusOrigin) => void;
|
||||
resetActiveItem: () => void;
|
||||
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
|
||||
setPositionClasses?: (x: MenuPositionX, y: MenuPositionY) => void;
|
||||
setElevation?(depth: number): void;
|
||||
lazyContent?: MatMenuContent;
|
||||
backdropClass?: string;
|
||||
|
||||
@@ -353,12 +353,14 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
* correct, even if a fallback position is used for the overlay.
|
||||
*/
|
||||
private _subscribeToPositions(position: FlexibleConnectedPositionStrategy): void {
|
||||
position.positionChanges.subscribe(change => {
|
||||
const posX: MenuPositionX = change.connectionPair.overlayX === 'start' ? 'after' : 'before';
|
||||
const posY: MenuPositionY = change.connectionPair.overlayY === 'top' ? 'below' : 'above';
|
||||
if (this.menu.setPositionClasses) {
|
||||
position.positionChanges.subscribe(change => {
|
||||
const posX: MenuPositionX = change.connectionPair.overlayX === 'start' ? 'after' : 'before';
|
||||
const posY: MenuPositionY = change.connectionPair.overlayY === 'top' ? 'below' : 'above';
|
||||
|
||||
this.menu.setPositionClasses(posX, posY);
|
||||
});
|
||||
this.menu.setPositionClasses!(posX, posY);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,6 +392,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
|
||||
return this._overlay.position()
|
||||
.flexibleConnectedTo(this._element)
|
||||
.withTransformOriginOn('.mat-menu-panel')
|
||||
.withPositions([
|
||||
{originX, originY, overlayX, overlayY, offsetY},
|
||||
{originX: originFallbackX, originY, overlayX: overlayFallbackX, overlayY, offsetY},
|
||||
|
||||
@@ -11,7 +11,6 @@ $mat-menu-submenu-indicator-size: 10px !default;
|
||||
|
||||
.mat-menu-panel {
|
||||
@include mat-menu-base(2);
|
||||
@include mat-menu-positions();
|
||||
max-height: calc(100vh - #{$mat-menu-item-height});
|
||||
border-radius: $mat-menu-border-radius;
|
||||
outline: 0;
|
||||
|
||||
@@ -463,68 +463,6 @@ describe('MatMenu', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('positions', () => {
|
||||
let fixture: ComponentFixture<PositionedMenu>;
|
||||
let panel: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = createComponent(PositionedMenu);
|
||||
fixture.detectChanges();
|
||||
|
||||
const trigger = fixture.componentInstance.triggerEl.nativeElement;
|
||||
|
||||
// Push trigger to the bottom edge of viewport,so it has space to open "above"
|
||||
trigger.style.position = 'fixed';
|
||||
trigger.style.top = '600px';
|
||||
|
||||
// Push trigger to the right, so it has space to open "before"
|
||||
trigger.style.left = '100px';
|
||||
|
||||
fixture.componentInstance.trigger.openMenu();
|
||||
fixture.detectChanges();
|
||||
panel = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement;
|
||||
});
|
||||
|
||||
it('should append mat-menu-before if the x position is changed', () => {
|
||||
expect(panel.classList).toContain('mat-menu-before');
|
||||
expect(panel.classList).not.toContain('mat-menu-after');
|
||||
|
||||
fixture.componentInstance.xPosition = 'after';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(panel.classList).toContain('mat-menu-after');
|
||||
expect(panel.classList).not.toContain('mat-menu-before');
|
||||
});
|
||||
|
||||
it('should append mat-menu-above if the y position is changed', () => {
|
||||
expect(panel.classList).toContain('mat-menu-above');
|
||||
expect(panel.classList).not.toContain('mat-menu-below');
|
||||
|
||||
fixture.componentInstance.yPosition = 'below';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(panel.classList).toContain('mat-menu-below');
|
||||
expect(panel.classList).not.toContain('mat-menu-above');
|
||||
});
|
||||
|
||||
it('should default to the "below" and "after" positions', () => {
|
||||
overlayContainer.ngOnDestroy();
|
||||
fixture.destroy();
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const newFixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
|
||||
newFixture.detectChanges();
|
||||
newFixture.componentInstance.trigger.openMenu();
|
||||
newFixture.detectChanges();
|
||||
panel = overlayContainerElement.querySelector('.mat-menu-panel') as HTMLElement;
|
||||
|
||||
expect(panel.classList).toContain('mat-menu-below');
|
||||
expect(panel.classList).toContain('mat-menu-after');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fallback positions', () => {
|
||||
|
||||
it('should fall back to "before" mode if "after" mode would not fit on screen', () => {
|
||||
@@ -730,14 +668,6 @@ describe('MatMenu', () => {
|
||||
.toBe(Math.floor(subject.triggerRect.top),
|
||||
`Expected menu to open in "above" position if "below" position wouldn't fit.`);
|
||||
});
|
||||
|
||||
it('repositions the origin to be below, so the menu opens from the trigger', () => {
|
||||
subject.openMenu();
|
||||
subject.fixture.detectChanges();
|
||||
|
||||
expect(subject.menuPanel!.classList).toContain('mat-menu-below');
|
||||
expect(subject.menuPanel!.classList).not.toContain('mat-menu-above');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="mat-tooltip"
|
||||
[ngClass]="tooltipClass"
|
||||
[class.mat-tooltip-handset]="(_isHandset | async)!.matches"
|
||||
[style.transform-origin]="_transformOrigin"
|
||||
[@state]="_visibility"
|
||||
(@state.start)="_animationStart()"
|
||||
(@state.done)="_animationDone($event)">{{message}}</div>
|
||||
|
||||
@@ -482,54 +482,6 @@ describe('MatTooltip', () => {
|
||||
expect(tooltipDirective._getOverlayPosition().main).toEqual(rightOverlayPosition);
|
||||
});
|
||||
|
||||
it('should have consistent left transform origin in ltr', () => {
|
||||
tooltipDirective.position = 'right';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('left');
|
||||
|
||||
tooltipDirective.position = 'after';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('left');
|
||||
});
|
||||
|
||||
it('should have consistent left transform origin in rtl', () => {
|
||||
dir.value = 'rtl';
|
||||
tooltipDirective.position = 'before';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('left');
|
||||
});
|
||||
|
||||
it('should have consistent right transform origin in ltr', () => {
|
||||
// Move the button away from the edge of the screen so
|
||||
// we don't get into the fallback positions.
|
||||
fixture.componentInstance.button.nativeElement.style.margin = '300px';
|
||||
|
||||
tooltipDirective.position = 'left';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('right');
|
||||
|
||||
tooltipDirective.position = 'before';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('right');
|
||||
});
|
||||
|
||||
it('should have consistent right transform origin in rtl', () => {
|
||||
// Move the button away from the edge of the screen so
|
||||
// we don't get into the fallback positions.
|
||||
fixture.componentInstance.button.nativeElement.style.margin = '300px';
|
||||
|
||||
dir.value = 'rtl';
|
||||
tooltipDirective.position = 'after';
|
||||
tooltipDirective.show();
|
||||
fixture.detectChanges();
|
||||
expect(tooltipDirective._tooltipInstance!._transformOrigin).toBe('right');
|
||||
});
|
||||
|
||||
it('should throw when trying to assign an invalid position', () => {
|
||||
expect(() => {
|
||||
fixture.componentInstance.position = 'everywhere';
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
*/
|
||||
import {AnimationEvent} from '@angular/animations';
|
||||
import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y';
|
||||
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {ESCAPE} from '@angular/cdk/keycodes';
|
||||
import {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout';
|
||||
import {
|
||||
ConnectionPositionPair,
|
||||
FlexibleConnectedPositionStrategy,
|
||||
HorizontalConnectionPos,
|
||||
OriginConnectionPosition,
|
||||
@@ -129,7 +128,7 @@ export class MatTooltip implements OnDestroy {
|
||||
this._updatePosition();
|
||||
|
||||
if (this._tooltipInstance) {
|
||||
this._tooltipInstance!.show(value, 0);
|
||||
this._tooltipInstance!.show(0);
|
||||
}
|
||||
|
||||
this._overlayRef.updatePosition();
|
||||
@@ -270,7 +269,7 @@ export class MatTooltip implements OnDestroy {
|
||||
.subscribe(() => this._detach());
|
||||
this._setTooltipClass(this._tooltipClass);
|
||||
this._updateTooltipMessage();
|
||||
this._tooltipInstance!.show(this._position, delay);
|
||||
this._tooltipInstance!.show(delay);
|
||||
}
|
||||
|
||||
/** Hides the tooltip after the delay in ms, defaults to tooltip-delay-hide or 0ms if no input */
|
||||
@@ -316,6 +315,7 @@ export class MatTooltip implements OnDestroy {
|
||||
// Create connected position strategy that listens for scroll events to reposition.
|
||||
const strategy = this._overlay.position()
|
||||
.flexibleConnectedTo(this._elementRef)
|
||||
.withTransformOriginOn('.mat-tooltip')
|
||||
.withFlexibleDimensions(false)
|
||||
.withViewportMargin(8)
|
||||
.withPositions([
|
||||
@@ -334,9 +334,6 @@ export class MatTooltip implements OnDestroy {
|
||||
// After position changes occur and the overlay is clipped by
|
||||
// a parent scrollable then close the tooltip.
|
||||
this._ngZone.run(() => this.hide(0));
|
||||
} else {
|
||||
// Otherwise recalculate the origin based on the new position.
|
||||
this._tooltipInstance._setTransformOrigin(change.connectionPair, direction);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -531,12 +528,6 @@ export class TooltipComponent {
|
||||
/** Whether interactions on the page should close the tooltip */
|
||||
private _closeOnInteraction: boolean = false;
|
||||
|
||||
/** The transform origin used in the animation for showing and hiding the tooltip */
|
||||
_transformOrigin: 'top' | 'bottom' | 'left' | 'right' = 'bottom';
|
||||
|
||||
/** Current position of the tooltip. */
|
||||
private _position: TooltipPosition;
|
||||
|
||||
/** Subject for notifying that the tooltip has been hidden from the view */
|
||||
private readonly _onHide: Subject<any> = new Subject();
|
||||
|
||||
@@ -549,10 +540,9 @@ export class TooltipComponent {
|
||||
|
||||
/**
|
||||
* Shows the tooltip with an animation originating from the provided origin
|
||||
* @param position Position of the tooltip.
|
||||
* @param delay Amount of milliseconds to the delay showing the tooltip.
|
||||
*/
|
||||
show(position: TooltipPosition, delay: number): void {
|
||||
show(delay: number): void {
|
||||
// Cancel the delayed hide if it is scheduled
|
||||
if (this._hideTimeoutId) {
|
||||
clearTimeout(this._hideTimeoutId);
|
||||
@@ -560,7 +550,6 @@ export class TooltipComponent {
|
||||
|
||||
// Body interactions should cancel the tooltip if there is a delay in showing.
|
||||
this._closeOnInteraction = true;
|
||||
this._position = position;
|
||||
this._showTimeoutId = setTimeout(() => {
|
||||
this._visibility = 'visible';
|
||||
|
||||
@@ -599,22 +588,6 @@ export class TooltipComponent {
|
||||
return this._visibility === 'visible';
|
||||
}
|
||||
|
||||
/** Sets the tooltip transform origin according to the position of the tooltip overlay. */
|
||||
_setTransformOrigin(overlayPosition: ConnectionPositionPair, direction: Direction) {
|
||||
const axis = (this._position === 'above' || this._position === 'below') ? 'Y' : 'X';
|
||||
const position = axis == 'X' ? overlayPosition.overlayX : overlayPosition.overlayY;
|
||||
|
||||
if (position === 'top' || position === 'bottom') {
|
||||
this._transformOrigin = position;
|
||||
} else if (position === 'start') {
|
||||
this._transformOrigin = direction === 'ltr' ? 'left' : 'right';
|
||||
} else if (position === 'end') {
|
||||
this._transformOrigin = direction === 'ltr' ? 'right' : 'left';
|
||||
} else {
|
||||
throw getMatTooltipInvalidPositionError(this._position);
|
||||
}
|
||||
}
|
||||
|
||||
_animationStart() {
|
||||
this._closeOnInteraction = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user