From 765990e7761211332eb49c0e3f14b54e2e3915ad Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 22 Aug 2018 16:29:02 +0200 Subject: [PATCH] feat(overlay): add the ability to set a panelClass based on the current connected position (#12631) Adds a property on the `ConnectedPosition` to allow the consumer to set a class on the overlay pane, depending on which position is currently active. --- src/cdk/overlay/overlay-directives.spec.ts | 4 +- .../overlay/position/connected-position.ts | 6 +- ...exible-connected-position-strategy.spec.ts | 143 ++++++++++++++++++ .../flexible-connected-position-strategy.ts | 32 +++- 4 files changed, 182 insertions(+), 3 deletions(-) diff --git a/src/cdk/overlay/overlay-directives.spec.ts b/src/cdk/overlay/overlay-directives.spec.ts index 685052565e40..245cc0aecd17 100644 --- a/src/cdk/overlay/overlay-directives.spec.ts +++ b/src/cdk/overlay/overlay-directives.spec.ts @@ -323,6 +323,7 @@ describe('Overlay directives', () => { // TODO(jelbourn) figure out why, when compiling with bazel, these offsets are required. offsetX: 0, offsetY: 0, + panelClass: 'custom-class' }]; fixture.componentInstance.isOpen = true; @@ -348,7 +349,8 @@ describe('Overlay directives', () => { overlayX: 'start', overlayY: 'top', offsetX: 20, - offsetY: 10 + offsetY: 10, + panelClass: 'custom-class' }]; fixture.componentInstance.isOpen = true; diff --git a/src/cdk/overlay/position/connected-position.ts b/src/cdk/overlay/position/connected-position.ts index 9dd364e7c236..9afe5ca4c4cf 100644 --- a/src/cdk/overlay/position/connected-position.ts +++ b/src/cdk/overlay/position/connected-position.ts @@ -40,8 +40,12 @@ export class ConnectionPositionPair { constructor( origin: OriginConnectionPosition, overlay: OverlayConnectionPosition, + /** Offset along the X axis. */ public offsetX?: number, - public offsetY?: number) { + /** Offset along the Y axis. */ + public offsetY?: number, + /** Class(es) to be applied to the panel while this position is active. */ + public panelClass?: string | string[]) { this.originX = origin.originX; this.originY = origin.originY; diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts index 26efa78890a5..dea11557ad98 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts @@ -1799,6 +1799,149 @@ describe('FlexibleConnectedPositionStrategy', () => { }); }); + describe('panel classes', () => { + let originElement: HTMLElement; + let positionStrategy: FlexibleConnectedPositionStrategy; + + beforeEach(() => { + originElement = createPositionedBlockElement(); + document.body.appendChild(originElement); + positionStrategy = overlay.position() + .flexibleConnectedTo(originElement) + .withFlexibleDimensions(false) + .withPush(false); + }); + + afterEach(() => { + document.body.removeChild(originElement); + }); + + it('should be able to apply a class based on the position', () => { + positionStrategy.withPositions([{ + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + panelClass: 'is-below' + }]); + + attachOverlay({positionStrategy}); + + expect(overlayRef.overlayElement.classList).toContain('is-below'); + }); + + it('should be able to apply multiple classes based on the position', () => { + positionStrategy.withPositions([{ + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + panelClass: ['is-below', 'is-under'] + }]); + + attachOverlay({positionStrategy}); + + expect(overlayRef.overlayElement.classList).toContain('is-below'); + expect(overlayRef.overlayElement.classList).toContain('is-under'); + }); + + it('should remove the panel class when detaching', () => { + positionStrategy.withPositions([{ + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + panelClass: 'is-below' + }]); + + attachOverlay({positionStrategy}); + expect(overlayRef.overlayElement.classList).toContain('is-below'); + + overlayRef.detach(); + expect(overlayRef.overlayElement.classList).not.toContain('is-below'); + }); + + it('should clear the previous classes when the position changes', () => { + originElement.style.top = '200px'; + originElement.style.right = '25px'; + + positionStrategy.withPositions([ + { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + panelClass: ['is-center', 'is-in-the-middle'] + }, + { + originX: 'start', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + panelClass: 'is-below' + } + ]); + + attachOverlay({positionStrategy}); + + const overlayClassList = overlayRef.overlayElement.classList; + + expect(overlayClassList).not.toContain('is-center'); + expect(overlayClassList).not.toContain('is-in-the-middle'); + expect(overlayClassList).toContain('is-below'); + + // Move the element so another position is applied. + originElement.style.top = '200px'; + originElement.style.left = '200px'; + + overlayRef.updatePosition(); + + expect(overlayClassList).toContain('is-center'); + expect(overlayClassList).toContain('is-in-the-middle'); + expect(overlayClassList).not.toContain('is-below'); + }); + + it('should not clear the existing `panelClass` from the `OverlayRef`', () => { + originElement.style.top = '200px'; + originElement.style.right = '25px'; + + positionStrategy.withPositions([ + { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + panelClass: ['is-center', 'is-in-the-middle'] + }, + { + originX: 'start', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + panelClass: 'is-below' + } + ]); + + attachOverlay({ + panelClass: 'custom-panel-class', + positionStrategy + }); + + const overlayClassList = overlayRef.overlayElement.classList; + + expect(overlayClassList).toContain('custom-panel-class'); + + // Move the element so another position is applied. + originElement.style.top = '200px'; + originElement.style.left = '200px'; + + overlayRef.updatePosition(); + + expect(overlayClassList).toContain('custom-panel-class'); + }); + + }); + }); /** Creates an absolutely positioned, display: block element with a default size. */ diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 75791a3c7c16..5785d267b1b5 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -19,7 +19,7 @@ import { import {Observable, Subscription, Subject} from 'rxjs'; import {OverlayReference} from '../overlay-reference'; import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip'; -import {coerceCssPixelValue} from '@angular/cdk/coercion'; +import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion'; import {Platform} from '@angular/cdk/platform'; import {OverlayContainer} from '../overlay-container'; @@ -112,6 +112,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { /** Amount of subscribers to the `positionChanges` stream. */ private _positionChangeSubscriptions = 0; + /** Keeps track of the CSS classes that the position strategy has applied on the overlay panel. */ + private _appliedPanelClasses: string[] = []; + /** Observable sequence of position changes. */ positionChanges: Observable = Observable.create(observer => { const subscription = this._positionChanges.subscribe(observer); @@ -184,6 +187,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { return; } + this._clearPanelClasses(); this._resetOverlayElementStyles(); this._resetBoundingBoxStyles(); @@ -282,6 +286,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { } detach() { + this._clearPanelClasses(); this._resizeSubscription.unsubscribe(); } @@ -590,6 +595,10 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { this._setOverlayElementStyles(originPoint, position); this._setBoundingBoxStyles(originPoint, position); + if (position.panelClass) { + this._addPanelClasses(position.panelClass); + } + // Save the last connected position in case the position needs to be re-calculated. this._lastPosition = position; @@ -991,6 +1000,26 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { validateVerticalPosition('overlayY', pair.overlayY); }); } + + /** Adds a single CSS class or an array of classes on the overlay panel. */ + private _addPanelClasses(cssClasses: string | string[]) { + if (this._pane) { + coerceArray(cssClasses).forEach(cssClass => { + if (this._appliedPanelClasses.indexOf(cssClass) === -1) { + this._appliedPanelClasses.push(cssClass); + this._pane.classList.add(cssClass); + } + }); + } + } + + /** Clears the classes that the position strategy has applied from the overlay panel. */ + private _clearPanelClasses() { + if (this._pane) { + this._appliedPanelClasses.forEach(cssClass => this._pane.classList.remove(cssClass)); + this._appliedPanelClasses = []; + } + } } /** A simple (x, y) coordinate. */ @@ -1052,6 +1081,7 @@ export interface ConnectedPosition { weight?: number; offsetX?: number; offsetY?: number; + panelClass?: string | string[]; } /** Shallow-extends a stylesheet object with another stylesheet object. */