Skip to content

Commit 765990e

Browse files
crisbetojelbourn
authored andcommitted
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.
1 parent 3d4fc82 commit 765990e

File tree

4 files changed

+182
-3
lines changed

4 files changed

+182
-3
lines changed

src/cdk/overlay/overlay-directives.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ describe('Overlay directives', () => {
323323
// TODO(jelbourn) figure out why, when compiling with bazel, these offsets are required.
324324
offsetX: 0,
325325
offsetY: 0,
326+
panelClass: 'custom-class'
326327
}];
327328

328329
fixture.componentInstance.isOpen = true;
@@ -348,7 +349,8 @@ describe('Overlay directives', () => {
348349
overlayX: 'start',
349350
overlayY: 'top',
350351
offsetX: 20,
351-
offsetY: 10
352+
offsetY: 10,
353+
panelClass: 'custom-class'
352354
}];
353355

354356
fixture.componentInstance.isOpen = true;

src/cdk/overlay/position/connected-position.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ export class ConnectionPositionPair {
4040
constructor(
4141
origin: OriginConnectionPosition,
4242
overlay: OverlayConnectionPosition,
43+
/** Offset along the X axis. */
4344
public offsetX?: number,
44-
public offsetY?: number) {
45+
/** Offset along the Y axis. */
46+
public offsetY?: number,
47+
/** Class(es) to be applied to the panel while this position is active. */
48+
public panelClass?: string | string[]) {
4549

4650
this.originX = origin.originX;
4751
this.originY = origin.originY;

src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,149 @@ describe('FlexibleConnectedPositionStrategy', () => {
17991799
});
18001800
});
18011801

1802+
describe('panel classes', () => {
1803+
let originElement: HTMLElement;
1804+
let positionStrategy: FlexibleConnectedPositionStrategy;
1805+
1806+
beforeEach(() => {
1807+
originElement = createPositionedBlockElement();
1808+
document.body.appendChild(originElement);
1809+
positionStrategy = overlay.position()
1810+
.flexibleConnectedTo(originElement)
1811+
.withFlexibleDimensions(false)
1812+
.withPush(false);
1813+
});
1814+
1815+
afterEach(() => {
1816+
document.body.removeChild(originElement);
1817+
});
1818+
1819+
it('should be able to apply a class based on the position', () => {
1820+
positionStrategy.withPositions([{
1821+
originX: 'start',
1822+
originY: 'bottom',
1823+
overlayX: 'start',
1824+
overlayY: 'top',
1825+
panelClass: 'is-below'
1826+
}]);
1827+
1828+
attachOverlay({positionStrategy});
1829+
1830+
expect(overlayRef.overlayElement.classList).toContain('is-below');
1831+
});
1832+
1833+
it('should be able to apply multiple classes based on the position', () => {
1834+
positionStrategy.withPositions([{
1835+
originX: 'start',
1836+
originY: 'bottom',
1837+
overlayX: 'start',
1838+
overlayY: 'top',
1839+
panelClass: ['is-below', 'is-under']
1840+
}]);
1841+
1842+
attachOverlay({positionStrategy});
1843+
1844+
expect(overlayRef.overlayElement.classList).toContain('is-below');
1845+
expect(overlayRef.overlayElement.classList).toContain('is-under');
1846+
});
1847+
1848+
it('should remove the panel class when detaching', () => {
1849+
positionStrategy.withPositions([{
1850+
originX: 'start',
1851+
originY: 'bottom',
1852+
overlayX: 'start',
1853+
overlayY: 'top',
1854+
panelClass: 'is-below'
1855+
}]);
1856+
1857+
attachOverlay({positionStrategy});
1858+
expect(overlayRef.overlayElement.classList).toContain('is-below');
1859+
1860+
overlayRef.detach();
1861+
expect(overlayRef.overlayElement.classList).not.toContain('is-below');
1862+
});
1863+
1864+
it('should clear the previous classes when the position changes', () => {
1865+
originElement.style.top = '200px';
1866+
originElement.style.right = '25px';
1867+
1868+
positionStrategy.withPositions([
1869+
{
1870+
originX: 'end',
1871+
originY: 'center',
1872+
overlayX: 'start',
1873+
overlayY: 'center',
1874+
panelClass: ['is-center', 'is-in-the-middle']
1875+
},
1876+
{
1877+
originX: 'start',
1878+
originY: 'bottom',
1879+
overlayX: 'end',
1880+
overlayY: 'top',
1881+
panelClass: 'is-below'
1882+
}
1883+
]);
1884+
1885+
attachOverlay({positionStrategy});
1886+
1887+
const overlayClassList = overlayRef.overlayElement.classList;
1888+
1889+
expect(overlayClassList).not.toContain('is-center');
1890+
expect(overlayClassList).not.toContain('is-in-the-middle');
1891+
expect(overlayClassList).toContain('is-below');
1892+
1893+
// Move the element so another position is applied.
1894+
originElement.style.top = '200px';
1895+
originElement.style.left = '200px';
1896+
1897+
overlayRef.updatePosition();
1898+
1899+
expect(overlayClassList).toContain('is-center');
1900+
expect(overlayClassList).toContain('is-in-the-middle');
1901+
expect(overlayClassList).not.toContain('is-below');
1902+
});
1903+
1904+
it('should not clear the existing `panelClass` from the `OverlayRef`', () => {
1905+
originElement.style.top = '200px';
1906+
originElement.style.right = '25px';
1907+
1908+
positionStrategy.withPositions([
1909+
{
1910+
originX: 'end',
1911+
originY: 'center',
1912+
overlayX: 'start',
1913+
overlayY: 'center',
1914+
panelClass: ['is-center', 'is-in-the-middle']
1915+
},
1916+
{
1917+
originX: 'start',
1918+
originY: 'bottom',
1919+
overlayX: 'end',
1920+
overlayY: 'top',
1921+
panelClass: 'is-below'
1922+
}
1923+
]);
1924+
1925+
attachOverlay({
1926+
panelClass: 'custom-panel-class',
1927+
positionStrategy
1928+
});
1929+
1930+
const overlayClassList = overlayRef.overlayElement.classList;
1931+
1932+
expect(overlayClassList).toContain('custom-panel-class');
1933+
1934+
// Move the element so another position is applied.
1935+
originElement.style.top = '200px';
1936+
originElement.style.left = '200px';
1937+
1938+
overlayRef.updatePosition();
1939+
1940+
expect(overlayClassList).toContain('custom-panel-class');
1941+
});
1942+
1943+
});
1944+
18021945
});
18031946

18041947
/** Creates an absolutely positioned, display: block element with a default size. */

src/cdk/overlay/position/flexible-connected-position-strategy.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import {Observable, Subscription, Subject} from 'rxjs';
2020
import {OverlayReference} from '../overlay-reference';
2121
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
22-
import {coerceCssPixelValue} from '@angular/cdk/coercion';
22+
import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion';
2323
import {Platform} from '@angular/cdk/platform';
2424
import {OverlayContainer} from '../overlay-container';
2525

@@ -112,6 +112,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
112112
/** Amount of subscribers to the `positionChanges` stream. */
113113
private _positionChangeSubscriptions = 0;
114114

115+
/** Keeps track of the CSS classes that the position strategy has applied on the overlay panel. */
116+
private _appliedPanelClasses: string[] = [];
117+
115118
/** Observable sequence of position changes. */
116119
positionChanges: Observable<ConnectedOverlayPositionChange> = Observable.create(observer => {
117120
const subscription = this._positionChanges.subscribe(observer);
@@ -184,6 +187,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
184187
return;
185188
}
186189

190+
this._clearPanelClasses();
187191
this._resetOverlayElementStyles();
188192
this._resetBoundingBoxStyles();
189193

@@ -282,6 +286,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
282286
}
283287

284288
detach() {
289+
this._clearPanelClasses();
285290
this._resizeSubscription.unsubscribe();
286291
}
287292

@@ -590,6 +595,10 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
590595
this._setOverlayElementStyles(originPoint, position);
591596
this._setBoundingBoxStyles(originPoint, position);
592597

598+
if (position.panelClass) {
599+
this._addPanelClasses(position.panelClass);
600+
}
601+
593602
// Save the last connected position in case the position needs to be re-calculated.
594603
this._lastPosition = position;
595604

@@ -991,6 +1000,26 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
9911000
validateVerticalPosition('overlayY', pair.overlayY);
9921001
});
9931002
}
1003+
1004+
/** Adds a single CSS class or an array of classes on the overlay panel. */
1005+
private _addPanelClasses(cssClasses: string | string[]) {
1006+
if (this._pane) {
1007+
coerceArray(cssClasses).forEach(cssClass => {
1008+
if (this._appliedPanelClasses.indexOf(cssClass) === -1) {
1009+
this._appliedPanelClasses.push(cssClass);
1010+
this._pane.classList.add(cssClass);
1011+
}
1012+
});
1013+
}
1014+
}
1015+
1016+
/** Clears the classes that the position strategy has applied from the overlay panel. */
1017+
private _clearPanelClasses() {
1018+
if (this._pane) {
1019+
this._appliedPanelClasses.forEach(cssClass => this._pane.classList.remove(cssClass));
1020+
this._appliedPanelClasses = [];
1021+
}
1022+
}
9941023
}
9951024

9961025
/** A simple (x, y) coordinate. */
@@ -1052,6 +1081,7 @@ export interface ConnectedPosition {
10521081
weight?: number;
10531082
offsetX?: number;
10541083
offsetY?: number;
1084+
panelClass?: string | string[];
10551085
}
10561086

10571087
/** Shallow-extends a stylesheet object with another stylesheet object. */

0 commit comments

Comments
 (0)