Skip to content

Commit

Permalink
feat(overlay): add the ability to set a panelClass based on the curre…
Browse files Browse the repository at this point in the history
…nt 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.
  • Loading branch information
crisbeto authored and jelbourn committed Aug 22, 2018
1 parent 3d4fc82 commit 765990e
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/cdk/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -348,7 +349,8 @@ describe('Overlay directives', () => {
overlayX: 'start',
overlayY: 'top',
offsetX: 20,
offsetY: 10
offsetY: 10,
panelClass: 'custom-class'
}];

fixture.componentInstance.isOpen = true;
Expand Down
6 changes: 5 additions & 1 deletion src/cdk/overlay/position/connected-position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
143 changes: 143 additions & 0 deletions src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
32 changes: 31 additions & 1 deletion src/cdk/overlay/position/flexible-connected-position-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<ConnectedOverlayPositionChange> = Observable.create(observer => {
const subscription = this._positionChanges.subscribe(observer);
Expand Down Expand Up @@ -184,6 +187,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
return;
}

this._clearPanelClasses();
this._resetOverlayElementStyles();
this._resetBoundingBoxStyles();

Expand Down Expand Up @@ -282,6 +286,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
}

detach() {
this._clearPanelClasses();
this._resizeSubscription.unsubscribe();
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand Down

0 comments on commit 765990e

Please sign in to comment.