From 08e736d38c4ac7ee5613ee7e2eec5067654feb38 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 1 Jun 2018 11:59:06 +0300 Subject: [PATCH] fix(menu): reintroduce panel position classes Reintroduces adding CSS classes to the `mat-menu` based on its position. This is useful to some consumers which may want to style the panel based on how it's being displayed. Fixes #11597. --- src/lib/menu/menu-directive.ts | 24 ++++++++++- src/lib/menu/menu.spec.ts | 73 +++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index b70d203a8d9c..4d15036d544a 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -28,6 +28,7 @@ import { QueryList, ViewChild, ViewEncapsulation, + OnInit, } from '@angular/core'; import {merge, Observable, Subject, Subscription} from 'rxjs'; import {startWith, switchMap, take} from 'rxjs/operators'; @@ -97,7 +98,7 @@ const MAT_MENU_BASE_ELEVATION = 2; {provide: MAT_MENU_PANEL, useExisting: MatMenu} ] }) -export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy { +export class MatMenu implements AfterContentInit, MatMenuPanel, OnInit, OnDestroy { private _keyManager: FocusKeyManager; private _xPosition: MenuPositionX = this._defaultOptions.xPosition; private _yPosition: MenuPositionY = this._defaultOptions.yPosition; @@ -141,6 +142,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnD throwMatMenuInvalidPositionX(); } this._xPosition = value; + this.setPositionClasses(); } /** Position of the menu in the Y axis. */ @@ -151,6 +153,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnD throwMatMenuInvalidPositionY(); } this._yPosition = value; + this.setPositionClasses(); } /** @docs-private */ @@ -230,6 +233,10 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnD private _ngZone: NgZone, @Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { } + ngOnInit() { + this.setPositionClasses(); + } + ngAfterContentInit() { this._keyManager = new FocusKeyManager(this._items).withWrap().withTypeAhead(); this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('tab')); @@ -347,6 +354,21 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnD } } + /** + * Adds classes to the menu panel based on its position. Can be used by + * consumers to add specific styling based on the position. + * @param posX Position of the menu along the x axis. + * @param posY Position of the menu along the y axis. + * @docs-private + */ + setPositionClasses(posX: MenuPositionX = this.xPosition, posY: MenuPositionY = this.yPosition) { + const classes = this._classList; + classes['mat-menu-before'] = posX === 'before'; + classes['mat-menu-after'] = posX === 'after'; + classes['mat-menu-above'] = posY === 'above'; + classes['mat-menu-below'] = posY === 'below'; + } + /** Starts the enter animation. */ _startAnimation() { // @deletion-target 7.0.0 Combine with _resetAnimation. diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 74a80ccaaf46..4b33bc4c14ec 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -50,7 +50,8 @@ describe('MatMenu', () => { let overlayContainerElement: HTMLElement; let focusMonitor: FocusMonitor; - function createComponent(component: Type, providers: Provider[] = [], + function createComponent(component: Type, + providers: Provider[] = [], declarations: any[] = []): ComponentFixture { TestBed.configureTestingModule({ imports: [MatMenuModule, NoopAnimationsModule], @@ -491,6 +492,68 @@ describe('MatMenu', () => { })); }); + describe('positions', () => { + let fixture: ComponentFixture; + 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', () => { @@ -696,6 +759,14 @@ 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'); + }); }); });