mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(overlay): validate that ConnectedPositionStrategy positions are passed in correctly at runtime (#9466)
Since we have some cases where we pass in positions verbatim to the connected position strategy (the connected position directive), we could end up in a situation where the consumer passed in a set of invalid positions that don't break necessarily, but don't work correctly either. These changes add some sanity checks at runtime to make debugging easier.
This commit is contained in:
committed by
mmalerba
parent
b68d38c51f
commit
6a8ce02775
@@ -793,6 +793,60 @@ describe('ConnectedPositionStrategy', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('validations', () => {
|
||||
let overlayElement: HTMLElement;
|
||||
let originElement: HTMLElement;
|
||||
let strategy: ConnectedPositionStrategy;
|
||||
|
||||
beforeEach(() => {
|
||||
overlayElement = createPositionedBlockElement();
|
||||
overlayContainerElement.appendChild(overlayElement);
|
||||
originElement = createBlockElement();
|
||||
|
||||
strategy = positionBuilder.connectedTo(
|
||||
new ElementRef(originElement),
|
||||
{originX: 'start', originY: 'bottom'},
|
||||
{overlayX: 'start', overlayY: 'top'});
|
||||
strategy.attach(fakeOverlayRef(overlayElement));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
strategy.dispose();
|
||||
});
|
||||
|
||||
it('should throw when attaching without any positions', () => {
|
||||
strategy.withPositions([]);
|
||||
expect(() => strategy.apply()).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when passing in something that is missing a connection point', () => {
|
||||
strategy.withPositions([{originY: 'top', overlayX: 'start', overlayY: 'top'} as any]);
|
||||
expect(() => strategy.apply()).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when passing in something that has an invalid X position', () => {
|
||||
strategy.withPositions([{
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
overlayX: 'left',
|
||||
overlayY: 'top'
|
||||
} as any]);
|
||||
|
||||
expect(() => strategy.apply()).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when passing in something that has an invalid Y position', () => {
|
||||
strategy.withPositions([{
|
||||
originX: 'start',
|
||||
originY: 'middle',
|
||||
overlayX: 'start',
|
||||
overlayY: 'middle'
|
||||
} as any]);
|
||||
|
||||
expect(() => strategy.apply()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/** Creates an absolutely positioned, display: block element with a default size. */
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
OverlayConnectionPosition,
|
||||
ConnectedOverlayPositionChange,
|
||||
ScrollingVisibility,
|
||||
validateHorizontalPosition,
|
||||
validateVerticalPosition,
|
||||
} from './connected-position';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
@@ -131,6 +133,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
this._validatePositions();
|
||||
this._applied = true;
|
||||
|
||||
// We need the bounding rects for the origin and the overlay to determine how to position
|
||||
@@ -184,6 +187,8 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
this._validatePositions();
|
||||
|
||||
const originRect = this._origin.getBoundingClientRect();
|
||||
const overlayRect = this._pane.getBoundingClientRect();
|
||||
const viewportSize = this._viewportRuler.getViewportSize();
|
||||
@@ -439,14 +444,28 @@ export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
this._onPositionChange.next(positionChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the amount that an element is overflowing on an axis from it's length.
|
||||
*/
|
||||
/** Subtracts the amount that an element is overflowing on an axis from it's length. */
|
||||
private _subtractOverflows(length: number, ...overflows: number[]): number {
|
||||
return overflows.reduce((currentValue: number, currentOverflow: number) => {
|
||||
return currentValue - Math.max(currentOverflow, 0);
|
||||
}, length);
|
||||
}
|
||||
|
||||
/** Validates that the current position match the expected values. */
|
||||
private _validatePositions(): void {
|
||||
if (!this._preferredPositions.length) {
|
||||
throw Error('ConnectedPositionStrategy: At least one position is required.');
|
||||
}
|
||||
|
||||
// TODO(crisbeto): remove these once Angular's template type
|
||||
// checking is advanced enough to catch these cases.
|
||||
this._preferredPositions.forEach(pair => {
|
||||
validateHorizontalPosition('originX', pair.originX);
|
||||
validateVerticalPosition('originY', pair.originY);
|
||||
validateHorizontalPosition('overlayX', pair.overlayX);
|
||||
validateVerticalPosition('overlayY', pair.overlayY);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** A simple (x, y) coordinate. */
|
||||
|
||||
@@ -90,3 +90,29 @@ export class ConnectedOverlayPositionChange {
|
||||
/** @docs-private */
|
||||
@Optional() public scrollableViewProperties: ScrollingVisibility) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a vertical position property matches the expected values.
|
||||
* @param property Name of the property being validated.
|
||||
* @param value Value of the property being validated.
|
||||
* @docs-private
|
||||
*/
|
||||
export function validateVerticalPosition(property: string, value: VerticalConnectionPos) {
|
||||
if (value !== 'top' && value !== 'bottom' && value !== 'center') {
|
||||
throw Error(`ConnectedPosition: Invalid ${property} "${value}". ` +
|
||||
`Expected "top", "bottom" or "center".`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a horizontal position property matches the expected values.
|
||||
* @param property Name of the property being validated.
|
||||
* @param value Value of the property being validated.
|
||||
* @docs-private
|
||||
*/
|
||||
export function validateHorizontalPosition(property: string, value: HorizontalConnectionPos) {
|
||||
if (value !== 'start' && value !== 'end' && value !== 'center') {
|
||||
throw Error(`ConnectedPosition: Invalid ${property} "${value}". ` +
|
||||
`Expected "start", "end" or "center".`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user