mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add connected position strategy (#335)
This commit is contained in:
@@ -9,6 +9,13 @@
|
||||
"deploy",
|
||||
"typings"
|
||||
],
|
||||
"headers": [{
|
||||
"source": "*",
|
||||
"headers": [{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}]
|
||||
}],
|
||||
"rewrites": [{
|
||||
"source": "/**/!(*.@(js|ts|html|css|json|svg|png|jpg|jpeg))",
|
||||
"destination": "/index.html"
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import {PortalHost, Portal} from '../portal/portal';
|
||||
import {OverlayState} from './overlay-state';
|
||||
|
||||
/**
|
||||
* Reference to an overlay that has been created with the Overlay service.
|
||||
* Used to manipulate or dispose of said overlay.
|
||||
*/
|
||||
export class OverlayRef implements PortalHost {
|
||||
constructor(private _portalHost: PortalHost) { }
|
||||
constructor(
|
||||
private _portalHost: PortalHost,
|
||||
private _pane: HTMLElement,
|
||||
private _state: OverlayState) { }
|
||||
|
||||
attach(portal: Portal<any>): Promise<any> {
|
||||
return this._portalHost.attach(portal);
|
||||
return this._portalHost.attach(portal).then(() => {
|
||||
this._updatePosition();
|
||||
});
|
||||
}
|
||||
|
||||
detach(): Promise<any> {
|
||||
@@ -23,5 +29,12 @@ export class OverlayRef implements PortalHost {
|
||||
return this._portalHost.hasAttached();
|
||||
}
|
||||
|
||||
/** Updates the position of the overlay based on the position strategy. */
|
||||
private _updatePosition() {
|
||||
if (this._state.positionStrategy) {
|
||||
this._state.positionStrategy.apply(this._pane);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jelbourn): add additional methods for manipulating the overlay.
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@
|
||||
.md-overlay-pane {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import {Overlay, OVERLAY_CONTAINER_TOKEN} from './overlay';
|
||||
import {OverlayRef} from './overlay-ref';
|
||||
import {OverlayState} from './overlay-state';
|
||||
import {PositionStrategy} from './position/position-strategy';
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {ViewportRuler} from './position/viewport-ruler';
|
||||
|
||||
|
||||
export function main() {
|
||||
@@ -32,6 +34,8 @@ export function main() {
|
||||
|
||||
beforeEachProviders(() => [
|
||||
Overlay,
|
||||
OverlayPositionBuilder,
|
||||
ViewportRuler,
|
||||
provide(OVERLAY_CONTAINER_TOKEN, {useFactory: () => {
|
||||
overlayContainerElement = document.createElement('div');
|
||||
return overlayContainerElement;
|
||||
|
||||
+19
-39
@@ -3,13 +3,13 @@ import {
|
||||
OpaqueToken,
|
||||
Inject,
|
||||
Injectable,
|
||||
ElementRef
|
||||
} from '@angular/core';
|
||||
import {OverlayState} from './overlay-state';
|
||||
import {DomPortalHost} from '../portal/dom-portal-host';
|
||||
import {OverlayRef} from './overlay-ref';
|
||||
import {GlobalPositionStrategy} from './position/global-position-strategy';
|
||||
import {RelativePositionStrategy} from './position/relative-position-strategy';
|
||||
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {ViewportRuler} from './position/viewport-ruler';
|
||||
|
||||
|
||||
// Re-export overlay-related modules so they can be imported directly from here.
|
||||
@@ -39,7 +39,8 @@ let defaultState = new OverlayState();
|
||||
export class Overlay {
|
||||
constructor(
|
||||
@Inject(OVERLAY_CONTAINER_TOKEN) private _overlayContainerElement: HTMLElement,
|
||||
private _dynamicComponentLoader: DynamicComponentLoader) {
|
||||
private _dynamicComponentLoader: DynamicComponentLoader,
|
||||
private _positionBuilder: OverlayPositionBuilder) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,7 @@ export class Overlay {
|
||||
* @returns A reference to the created overlay.
|
||||
*/
|
||||
create(state: OverlayState = defaultState): Promise<OverlayRef> {
|
||||
return this._createPaneElement(state).then(pane => this._createOverlayRef(pane));
|
||||
return this._createPaneElement().then(pane => this._createOverlayRef(pane, state));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,36 +57,23 @@ export class Overlay {
|
||||
* to construct and configure a position strategy.
|
||||
*/
|
||||
position() {
|
||||
return POSITION_BUILDER;
|
||||
return this._positionBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the DOM element for an overlay.
|
||||
* @param state State to apply to the created element.
|
||||
* Creates the DOM element for an overlay and appends it to the overlay container.
|
||||
* @returns Promise resolving to the created element.
|
||||
*/
|
||||
private _createPaneElement(state: OverlayState): Promise<HTMLElement> {
|
||||
private _createPaneElement(): Promise<HTMLElement> {
|
||||
var pane = document.createElement('div');
|
||||
pane.id = `md-overlay-${nextUniqueId++}`;
|
||||
pane.id = `md-overlay-${nextUniqueId++}`;
|
||||
pane.classList.add('md-overlay-pane');
|
||||
|
||||
this.applyState(pane, state);
|
||||
this._overlayContainerElement.appendChild(pane);
|
||||
|
||||
return Promise.resolve(pane);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a given state to the given pane element.
|
||||
* @param pane The pane to modify.
|
||||
* @param state The state to apply.
|
||||
*/
|
||||
applyState(pane: HTMLElement, state: OverlayState) {
|
||||
if (state.positionStrategy != null) {
|
||||
state.positionStrategy.apply(pane);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DomPortalHost into which the overlay content can be loaded.
|
||||
* @param pane The DOM element to turn into a portal host.
|
||||
@@ -100,26 +88,18 @@ export class Overlay {
|
||||
/**
|
||||
* Creates an OverlayRef for an overlay in the given DOM element.
|
||||
* @param pane DOM element for the overlay
|
||||
* @param state
|
||||
* @returns {OverlayRef}
|
||||
*/
|
||||
private _createOverlayRef(pane: HTMLElement): OverlayRef {
|
||||
return new OverlayRef(this._createPortalHost(pane));
|
||||
private _createOverlayRef(pane: HTMLElement, state: OverlayState): OverlayRef {
|
||||
return new OverlayRef(this._createPortalHost(pane), pane, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Builder for overlay position strategy. */
|
||||
export class OverlayPositionBuilder {
|
||||
/** Creates a global position strategy. */
|
||||
global() {
|
||||
return new GlobalPositionStrategy();
|
||||
}
|
||||
|
||||
/** Creates a relative position strategy. */
|
||||
relativeTo(elementRef: ElementRef) {
|
||||
return new RelativePositionStrategy(elementRef);
|
||||
}
|
||||
}
|
||||
|
||||
// We only ever need one position builder.
|
||||
let POSITION_BUILDER: OverlayPositionBuilder = new OverlayPositionBuilder();
|
||||
/** Providers for Overlay and its related injectables. */
|
||||
export const OVERLAY_PROVIDERS = [
|
||||
ViewportRuler,
|
||||
OverlayPositionBuilder,
|
||||
Overlay,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import {it, describe, expect, beforeEach} from '@angular/core/testing';
|
||||
import {ElementRef} from '@angular/core';
|
||||
import {ConnectedPositionStrategy, RelativePosition} from './connected-position-strategy';
|
||||
import {ViewportRuler} from './viewport-ruler';
|
||||
|
||||
|
||||
// Default width and height of the overlay and origin panels throughout these tests.
|
||||
const DEFAULT_HEIGHT = 30;
|
||||
const DEFAULT_WIDTH = 60;
|
||||
|
||||
export function main() {
|
||||
|
||||
// For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight).
|
||||
// The karma config has been set to this for local tests, and it is the default size
|
||||
// for tests on CI (both SauceLabs and Browserstack).
|
||||
|
||||
describe('ConnectedPositionStrategy', () => {
|
||||
const ORIGIN_HEIGHT = DEFAULT_HEIGHT;
|
||||
const ORIGIN_WIDTH = DEFAULT_WIDTH;
|
||||
const OVERLAY_HEIGHT = DEFAULT_HEIGHT;
|
||||
const OVERLAY_WIDTH = DEFAULT_WIDTH;
|
||||
|
||||
let originElement: HTMLElement;
|
||||
let overlayElement: HTMLElement;
|
||||
let strategy: ConnectedPositionStrategy;
|
||||
let fakeElementRef: ElementRef;
|
||||
let fakeViewportRuler: FakeViewportRuler;
|
||||
|
||||
let originRect: ClientRect;
|
||||
let originCenterX: number;
|
||||
let originCenterY: number;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeViewportRuler = new FakeViewportRuler();
|
||||
|
||||
// The origin and overlay elements need to be in the document body in order to have geometry.
|
||||
originElement = createPositionedBlockElement();
|
||||
overlayElement = createPositionedBlockElement();
|
||||
document.body.appendChild(originElement);
|
||||
document.body.appendChild(overlayElement);
|
||||
|
||||
fakeElementRef = new FakeElementRef(originElement);
|
||||
|
||||
strategy = new ConnectedPositionStrategy(fakeElementRef, new ViewportRuler());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(originElement);
|
||||
document.body.removeChild(overlayElement);
|
||||
|
||||
// Reset the origin geometry after each test so we don't accidently keep state between tests.
|
||||
originRect = null;
|
||||
originCenterX = null;
|
||||
originCenterY = null;
|
||||
});
|
||||
|
||||
describe('when not near viewport edge, not scrolled', () => {
|
||||
// Place the original element close to the center of the window.
|
||||
// (1024 / 2, 768 / 2). It's not exact, since outerWidth/Height includes browser
|
||||
// chrome, but it doesn't really matter for these tests.
|
||||
const ORIGIN_LEFT = 500;
|
||||
const ORIGIN_TOP = 350;
|
||||
|
||||
beforeEach(() => {
|
||||
originElement.style.left = `${ORIGIN_LEFT}px`;
|
||||
originElement.style.top = `${ORIGIN_TOP}px`;
|
||||
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
|
||||
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
|
||||
});
|
||||
|
||||
// Preconditions are set, now just run the full set of simple position tests.
|
||||
runSimplePositionTests();
|
||||
});
|
||||
|
||||
describe('when scrolled', () => {
|
||||
// Place the original element decently far outside the unscrolled document (1024x768).
|
||||
const ORIGIN_LEFT = 2500;
|
||||
const ORIGIN_TOP = 2500;
|
||||
|
||||
// Create a very large element that will make the page scrollable.
|
||||
let veryLargeElement: HTMLElement = document.createElement('div');
|
||||
veryLargeElement.style.width = '4000px';
|
||||
veryLargeElement.style.height = '4000px';
|
||||
|
||||
beforeEach(() => {
|
||||
// Scroll the page such that the origin element is roughly in the
|
||||
// center of the visible viewport (2500 - 1024/2, 2500 - 768/2).
|
||||
document.body.appendChild(veryLargeElement);
|
||||
document.body.scrollTop = 2100;
|
||||
document.body.scrollLeft = 2100;
|
||||
|
||||
originElement.style.top = `${ORIGIN_TOP}px`;
|
||||
originElement.style.left = `${ORIGIN_LEFT}px`;
|
||||
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
originCenterX = originRect.left + (ORIGIN_WIDTH / 2);
|
||||
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(veryLargeElement);
|
||||
document.body.scrollTop = 0;
|
||||
document.body.scrollLeft = 0;
|
||||
});
|
||||
|
||||
// Preconditions are set, now just run the full set of simple position tests.
|
||||
runSimplePositionTests();
|
||||
});
|
||||
|
||||
describe('when near viewport edge', () => {
|
||||
it('should reposition the overlay if it would go off the top of the screen', () => {
|
||||
// We can use the real ViewportRuler in this test since we know that zero is
|
||||
// always the top of the viewport.
|
||||
|
||||
originElement.style.top = '5px';
|
||||
originElement.style.left = '200px';
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
|
||||
// Above, right aligned.
|
||||
let offscreenPos = new RelativePosition();
|
||||
offscreenPos.originX = 'end';
|
||||
offscreenPos.originY = 'start';
|
||||
offscreenPos.overlayX = 'end';
|
||||
offscreenPos.overlayY = 'end';
|
||||
|
||||
// Below, left aligned.
|
||||
let fallbackPosition = new RelativePosition();
|
||||
fallbackPosition.originX = 'start';
|
||||
fallbackPosition.originY = 'end';
|
||||
fallbackPosition.overlayX = 'start';
|
||||
fallbackPosition.overlayY = 'start';
|
||||
|
||||
strategy.addPreferredPosition(offscreenPos);
|
||||
strategy.addPreferredPosition(fallbackPosition);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.bottom);
|
||||
expect(overlayRect.left).toBe(originRect.left);
|
||||
});
|
||||
|
||||
it('should reposition the overlay if it would go off the left of the screen', () => {
|
||||
// We can use the real ViewportRuler in this test since we know that zero is
|
||||
// always the left edge of the viewport.
|
||||
|
||||
originElement.style.top = '200px';
|
||||
originElement.style.left = '5px';
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
originCenterY = originRect.top + (ORIGIN_HEIGHT / 2);
|
||||
|
||||
// To the left, below.
|
||||
let offscreenPos = new RelativePosition();
|
||||
offscreenPos.originX = 'start';
|
||||
offscreenPos.originY = 'end';
|
||||
offscreenPos.overlayX = 'end';
|
||||
offscreenPos.overlayY = 'start';
|
||||
|
||||
// To the right, centered.
|
||||
let fallbackPos = new RelativePosition();
|
||||
fallbackPos.originX = 'end';
|
||||
fallbackPos.originY = 'center';
|
||||
fallbackPos.overlayX = 'start';
|
||||
fallbackPos.overlayY = 'center';
|
||||
|
||||
strategy.addPreferredPosition(offscreenPos);
|
||||
strategy.addPreferredPosition(fallbackPos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
|
||||
expect(overlayRect.left).toBe(originRect.right);
|
||||
});
|
||||
|
||||
it('should reposition the overlay if it would go off the bottom of the screen', () => {
|
||||
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
|
||||
fakeViewportRuler.fakeRect = {
|
||||
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
|
||||
};
|
||||
strategy = new ConnectedPositionStrategy(fakeElementRef, fakeViewportRuler);
|
||||
|
||||
originElement.style.top = '475px';
|
||||
originElement.style.left = '200px';
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
|
||||
// Below, left aligned.
|
||||
let offscreenPos = new RelativePosition();
|
||||
offscreenPos.originX = 'start';
|
||||
offscreenPos.originY = 'end';
|
||||
offscreenPos.overlayX = 'start';
|
||||
offscreenPos.overlayY = 'start';
|
||||
|
||||
// Above, right aligned.
|
||||
let fallbackPos = new RelativePosition();
|
||||
fallbackPos.originX = 'end';
|
||||
fallbackPos.originY = 'start';
|
||||
fallbackPos.overlayX = 'end';
|
||||
fallbackPos.overlayY = 'end';
|
||||
|
||||
strategy.addPreferredPosition(offscreenPos);
|
||||
strategy.addPreferredPosition(fallbackPos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.bottom).toBe(originRect.top);
|
||||
expect(overlayRect.right).toBe(originRect.right);
|
||||
});
|
||||
|
||||
it('should reposition the overlay if it would go off the right of the screen', () => {
|
||||
// Use the fake viewport ruler because we don't know *exactly* how big the viewport is.
|
||||
fakeViewportRuler.fakeRect = {
|
||||
top: 0, left: 0, width: 500, height: 500, right: 500, bottom: 500
|
||||
};
|
||||
strategy = new ConnectedPositionStrategy(fakeElementRef, fakeViewportRuler);
|
||||
|
||||
originElement.style.top = '200px';
|
||||
originElement.style.left = '475px';
|
||||
originRect = originElement.getBoundingClientRect();
|
||||
|
||||
// To the right, centered.
|
||||
let offscreenPos = new RelativePosition();
|
||||
offscreenPos.originX = 'end';
|
||||
offscreenPos.originY = 'center';
|
||||
offscreenPos.overlayX = 'start';
|
||||
offscreenPos.overlayY = 'center';
|
||||
|
||||
// To the left, below.
|
||||
let fallbackPos = new RelativePosition();
|
||||
fallbackPos.originX = 'start';
|
||||
fallbackPos.originY = 'end';
|
||||
fallbackPos.overlayX = 'end';
|
||||
fallbackPos.overlayY = 'start';
|
||||
|
||||
strategy.addPreferredPosition(offscreenPos);
|
||||
strategy.addPreferredPosition(fallbackPos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.bottom);
|
||||
expect(overlayRect.right).toBe(originRect.left);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Run all tests for connecting the overlay to the origin such that first preferred
|
||||
* position does not go off-screen. We do this because there are several cases where we
|
||||
* want to run the exact same tests with different preconditions (e.g., not scroll, scrolled,
|
||||
* different element sized, etc.).
|
||||
*/
|
||||
function runSimplePositionTests() {
|
||||
it('should position a panel below, left-aligned', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'start';
|
||||
pos.originY = 'end';
|
||||
pos.overlayX = 'start';
|
||||
pos.overlayY = 'start';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.bottom);
|
||||
expect(overlayRect.left).toBe(originRect.left);
|
||||
});
|
||||
|
||||
it('should position to the right, center aligned vertically', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'end';
|
||||
pos.originY = 'center';
|
||||
pos.overlayX = 'start';
|
||||
pos.overlayY = 'center';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originCenterY - (OVERLAY_HEIGHT / 2));
|
||||
expect(overlayRect.left).toBe(originRect.right);
|
||||
});
|
||||
|
||||
it('should position to the left, below', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'start';
|
||||
pos.originY = 'end';
|
||||
pos.overlayX = 'end';
|
||||
pos.overlayY = 'start';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.bottom);
|
||||
expect(overlayRect.right).toBe(originRect.left);
|
||||
});
|
||||
|
||||
it('should position above, right aligned', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'end';
|
||||
pos.originY = 'start';
|
||||
pos.overlayX = 'end';
|
||||
pos.overlayY = 'end';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.bottom).toBe(originRect.top);
|
||||
expect(overlayRect.right).toBe(originRect.right);
|
||||
});
|
||||
|
||||
it('should position below, centered', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'center';
|
||||
pos.originY = 'end';
|
||||
pos.overlayX = 'center';
|
||||
pos.overlayY = 'start';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.bottom);
|
||||
expect(overlayRect.left).toBe(originCenterX - (OVERLAY_WIDTH / 2));
|
||||
});
|
||||
|
||||
it('should center the overlay on the origin', () => {
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'center';
|
||||
pos.originY = 'center';
|
||||
pos.overlayX = 'center';
|
||||
pos.overlayY = 'center';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
strategy.apply(overlayElement);
|
||||
|
||||
let overlayRect = overlayElement.getBoundingClientRect();
|
||||
expect(overlayRect.top).toBe(originRect.top);
|
||||
expect(overlayRect.left).toBe(originRect.left);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** Creates an absolutely positioned, display: block element with a default size. */
|
||||
function createPositionedBlockElement() {
|
||||
let element = document.createElement('div');
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = '0';
|
||||
element.style.left = '0';
|
||||
element.style.width = `${DEFAULT_WIDTH}px`;
|
||||
element.style.height = `${DEFAULT_HEIGHT}px`;
|
||||
element.style.backgroundColor = 'rebeccapurple';
|
||||
element.style.zIndex = '100';
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
/** Fake implementation of ViewportRuler that just returns the previously given ClientRect. */
|
||||
class FakeViewportRuler implements ViewportRuler {
|
||||
fakeRect: ClientRect = {left: 0, top: 0, width: 1014, height: 686, bottom: 686, right: 1014};
|
||||
fakeScrollPos: {top: number, left: number} = {top: 0, left: 0};
|
||||
|
||||
getViewportRect() {
|
||||
return this.fakeRect;
|
||||
}
|
||||
|
||||
getViewportScrollPosition(documentRect?: ClientRect): {top: number; left: number} {
|
||||
return this.fakeScrollPos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Fake implementation of ElementRef that is just a simple container for nativeElement. */
|
||||
class FakeElementRef implements ElementRef {
|
||||
constructor(public nativeElement: HTMLElement) { }
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import {PositionStrategy} from './position-strategy';
|
||||
import {ElementRef} from '@angular/core';
|
||||
import {ViewportRuler} from './viewport-ruler';
|
||||
import {applyCssTransform} from '../../style/apply-transform';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A strategy for positioning overlays. Using this strategy, an overlay is given an
|
||||
* implict position relative some origin element. The relative position is defined in terms of
|
||||
* a point on the origin element that is connected to a point on the overlay element. For example,
|
||||
* a basic dropdown is connecting the bottom-left corner of the origin to the top-left corner
|
||||
* of the overlay.
|
||||
*/
|
||||
export class ConnectedPositionStrategy implements PositionStrategy {
|
||||
// TODO(jelbourn): set RTL to the actual value from the app.
|
||||
/** Whether the we're dealing with an RTL context */
|
||||
_isRtl: boolean = false;
|
||||
|
||||
/** Ordered list of preferred positions, from most to least desirable. */
|
||||
_preferredPositions: RelativePosition[] = [];
|
||||
|
||||
/** The origin element against which the overlay will be positioned. */
|
||||
private _origin: HTMLElement;
|
||||
|
||||
|
||||
constructor(private _connectedTo: ElementRef, private _viewportRuler: ViewportRuler) {
|
||||
this._origin = this._connectedTo.nativeElement;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the position of the overlay element, using whichever preferred position relative
|
||||
* to the origin fits on-screen.
|
||||
*/
|
||||
apply(element: HTMLElement): Promise<void> {
|
||||
// We need the bounding rects for the origin and the overlay to determine how to position
|
||||
// the overlay relative to the origin.
|
||||
const originRect = this._origin.getBoundingClientRect();
|
||||
const overlayRect = element.getBoundingClientRect();
|
||||
|
||||
// We use the viewport rect to determine whether a position would go off-screen.
|
||||
const viewportRect = this._viewportRuler.getViewportRect();
|
||||
let firstOverlayPoint: Point = null;
|
||||
|
||||
// We want to place the overlay in the first of the preferred positions such that the
|
||||
// overlay fits on-screen.
|
||||
for (let pos of this._preferredPositions) {
|
||||
// Get the (x, y) point of connection on the origin, and then use that to get the
|
||||
// (top, left) coordinate for the overlay at `pos`.
|
||||
let originPoint = this._getOriginConnectionPoint(originRect, pos);
|
||||
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos);
|
||||
firstOverlayPoint = firstOverlayPoint || overlayPoint;
|
||||
|
||||
// If the overlay in the calculated position fits on-screen, put it there and we're done.
|
||||
if (this._willOverlayFitWithinViewport(overlayPoint, overlayRect, viewportRect)) {
|
||||
this._setElementPosition(element, overlayPoint);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jelbourn): fallback behavior for when none of the preferred positions fit on-screen.
|
||||
// For now, just stick it in the first position and let it go off-screen.
|
||||
this._setElementPosition(element, firstOverlayPoint);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
/** Adds a preferred position to the end of the ordered preferred position list. */
|
||||
addPreferredPosition(pos: RelativePosition): void {
|
||||
this._preferredPositions.push(pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the horizontal (x) "start" dimension based on whether the overlay is in an RTL context.
|
||||
* @param rect
|
||||
*/
|
||||
_getStartX(rect: ClientRect): number {
|
||||
return this._isRtl ? rect.right : rect.left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the horizontal (x) "end" dimension based on whether the overlay is in an RTL context.
|
||||
* @param rect
|
||||
*/
|
||||
_getEndX(rect: ClientRect): number {
|
||||
return this._isRtl ? rect.left : rect.right;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
|
||||
* @param originRect
|
||||
* @param pos
|
||||
*/
|
||||
private _getOriginConnectionPoint(originRect: ClientRect, pos: RelativePosition): Point {
|
||||
const originStartX = this._getStartX(originRect);
|
||||
const originEndX = this._getEndX(originRect);
|
||||
|
||||
let x: number;
|
||||
if (pos.originX == 'center') {
|
||||
x = originStartX + (originRect.width / 2);
|
||||
} else {
|
||||
x = pos.originX == 'start' ? originStartX : originEndX;
|
||||
}
|
||||
|
||||
let y: number;
|
||||
if (pos.originY == 'center') {
|
||||
y = originRect.top + (originRect.height / 2);
|
||||
} else {
|
||||
y = pos.originY == 'start' ? originRect.top : originRect.bottom;
|
||||
}
|
||||
|
||||
return {x, y};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the (x, y) coordinate of the top-left corner of the overlay given a given position and
|
||||
* origin point to which the overlay should be connected.
|
||||
* @param originPoint
|
||||
* @param overlayRect
|
||||
* @param pos
|
||||
*/
|
||||
private _getOverlayPoint(
|
||||
originPoint: Point,
|
||||
overlayRect: ClientRect,
|
||||
pos: RelativePosition): Point {
|
||||
// Calculate the (overlayStartX, overlayStartY), the start of the potential overlay position
|
||||
// relative to the origin point.
|
||||
let overlayStartX: number;
|
||||
if (pos.overlayX == 'center') {
|
||||
overlayStartX = -overlayRect.width / 2;
|
||||
} else {
|
||||
overlayStartX = pos.overlayX == 'start' ? 0 : -overlayRect.width;
|
||||
}
|
||||
|
||||
let overlayStartY: number;
|
||||
if (pos.overlayY == 'center') {
|
||||
overlayStartY = -overlayRect.height / 2;
|
||||
} else {
|
||||
overlayStartY = pos.overlayY == 'start' ? 0 : -overlayRect.height;
|
||||
}
|
||||
|
||||
return {
|
||||
x: originPoint.x + overlayStartX,
|
||||
y: originPoint.y + overlayStartY
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets whether the overlay positioned at the given point will fit on-screen.
|
||||
* @param overlayPoint The top-left coordinate of the overlay.
|
||||
* @param overlayRect Bounding rect of the overlay, used to get its size.
|
||||
* @param viewportRect The bounding viewport.
|
||||
*/
|
||||
private _willOverlayFitWithinViewport(
|
||||
overlayPoint: Point,
|
||||
overlayRect: ClientRect,
|
||||
viewportRect: ClientRect): boolean {
|
||||
|
||||
// TODO(jelbourn): probably also want some space between overlay edge and viewport edge.
|
||||
return overlayPoint.x >= viewportRect.left &&
|
||||
overlayPoint.x + overlayRect.width <= viewportRect.right &&
|
||||
overlayPoint.y >= viewportRect.top &&
|
||||
overlayPoint.y + overlayRect.height <= viewportRect.bottom;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Physically positions the overlay element to the given coordinate.
|
||||
* @param element
|
||||
* @param overlayPoint
|
||||
*/
|
||||
private _setElementPosition(element: HTMLElement, overlayPoint: Point) {
|
||||
let scrollPos = this._viewportRuler.getViewportScrollPosition();
|
||||
|
||||
let x = overlayPoint.x + scrollPos.left;
|
||||
let y = overlayPoint.y + scrollPos.top;
|
||||
|
||||
// TODO(jelbourn): we don't want to always overwrite the transform property here,
|
||||
// because it will need to be used for animations.
|
||||
applyCssTransform(element, `translateX(${x}px) translateY(${y}px)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** A simple (x, y) coordinate. */
|
||||
type Point = {x: number, y: number};
|
||||
|
||||
|
||||
/** One dimension of a connection point on the perimeter of the origin or overlay element. */
|
||||
export type ConnectionPoint = 'start' | 'center' | 'end';
|
||||
|
||||
|
||||
/** The points of the origin element and the overlay element to connect. */
|
||||
export class RelativePosition {
|
||||
originX: ConnectionPoint;
|
||||
originY: ConnectionPoint;
|
||||
overlayX: ConnectionPoint;
|
||||
overlayY: ConnectionPoint;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {PositionStrategy} from './position-strategy';
|
||||
import {applyCssTransform} from '../../style/apply-transform';
|
||||
|
||||
|
||||
/**
|
||||
@@ -97,9 +98,7 @@ export class GlobalPositionStrategy implements PositionStrategy {
|
||||
let tranlateX = this._reduceTranslateValues('translateX', this._translateX);
|
||||
let translateY = this._reduceTranslateValues('translateY', this._translateY);
|
||||
|
||||
// It's important to trim the result, because the browser will ignore the set operation
|
||||
// if the string contains only whitespace.
|
||||
element.style.transform = `${tranlateX} ${translateY}`.trim();
|
||||
applyCssTransform(element, `${tranlateX} ${translateY}`);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {ViewportRuler} from './viewport-ruler';
|
||||
import {ConnectedPositionStrategy} from './connected-position-strategy';
|
||||
import {ElementRef, Injectable} from '@angular/core';
|
||||
import {GlobalPositionStrategy} from './global-position-strategy';
|
||||
|
||||
|
||||
|
||||
/** Builder for overlay position strategy. */
|
||||
@Injectable()
|
||||
export class OverlayPositionBuilder {
|
||||
constructor(private _viewportRuler: ViewportRuler) { }
|
||||
|
||||
/** Creates a global position strategy. */
|
||||
global() {
|
||||
return new GlobalPositionStrategy();
|
||||
}
|
||||
|
||||
/** Creates a relative position strategy. */
|
||||
connectedTo(elementRef: ElementRef) {
|
||||
return new ConnectedPositionStrategy(elementRef, this._viewportRuler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import {it, describe, expect, beforeEach} from '@angular/core/testing';
|
||||
import {ViewportRuler} from './viewport-ruler';
|
||||
|
||||
|
||||
export function main() {
|
||||
|
||||
// For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight).
|
||||
// The karma config has been set to this for local tests, and it is the default size
|
||||
// for tests on CI (both SauceLabs and Browserstack).
|
||||
|
||||
// While we know the *outer* window width/height, the innerWidth and innerHeight depend on the
|
||||
// the size of the individual browser's chrome, so we have to use window.innerWidth and
|
||||
// window.innerHeight in the unit test instead of hard-coded values.
|
||||
|
||||
describe('ViewportRuler', () => {
|
||||
let ruler: ViewportRuler;
|
||||
|
||||
let startingWindowWidth = window.innerWidth;
|
||||
let startingWindowHeight = window.innerHeight;
|
||||
|
||||
// Create a very large element that will make the page scrollable.
|
||||
let veryLargeElement: HTMLElement = document.createElement('div');
|
||||
veryLargeElement.style.width = '6000px';
|
||||
veryLargeElement.style.height = '6000px';
|
||||
|
||||
beforeEach(() => {
|
||||
ruler = new ViewportRuler();
|
||||
scrollTo(0, 0);
|
||||
});
|
||||
|
||||
it('should get the viewport bounds when the page is not scrolled', () => {
|
||||
let bounds = ruler.getViewportRect();
|
||||
expect(bounds.top).toBe(0);
|
||||
expect(bounds.left).toBe(0);
|
||||
expect(bounds.bottom).toBe(window.innerHeight);
|
||||
expect(bounds.right).toBe(window.innerWidth);
|
||||
});
|
||||
|
||||
it('should get the viewport bounds when the page is scrolled', () => {
|
||||
document.body.appendChild(veryLargeElement);
|
||||
scrollTo(1500, 2000);
|
||||
|
||||
let bounds = ruler.getViewportRect();
|
||||
|
||||
// In the iOS simulator (BrowserStack & SauceLabs), adding the content to the
|
||||
// body causes karma's iframe for the test to stretch to fit that content once we attempt to
|
||||
// scroll the page. Setting width / height / maxWidth / maxHeight on the iframe does not
|
||||
// successfully constrain its size. As such, skip assertions in environments where the
|
||||
// window size has changed since the start of the test.
|
||||
if (window.innerWidth > startingWindowWidth || window.innerHeight > startingWindowHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(bounds.top).toBe(2000);
|
||||
expect(bounds.left).toBe(1500);
|
||||
expect(bounds.bottom).toBe(2000 + window.innerHeight);
|
||||
expect(bounds.right).toBe(1500 + window.innerWidth);
|
||||
|
||||
document.body.removeChild(veryLargeElement);
|
||||
});
|
||||
|
||||
it('should get the bounds based on client coordinates when the page is pinch-zoomed', () => {
|
||||
// There is no API to make the browser pinch-zoom, so there's no real way to automate
|
||||
// tests for this behavior. Leaving this test here as documentation for the behavior.
|
||||
});
|
||||
|
||||
it('should get the scroll position when the page is not scrolled', () => {
|
||||
var scrollPos = ruler.getViewportScrollPosition();
|
||||
expect(scrollPos.top).toBe(0);
|
||||
expect(scrollPos.left).toBe(0);
|
||||
});
|
||||
|
||||
it('should get the scroll position when the page is scrolled', () => {
|
||||
document.body.appendChild(veryLargeElement);
|
||||
scrollTo(1500, 2000);
|
||||
|
||||
// In the iOS simulator (BrowserStack & SauceLabs), adding the content to the
|
||||
// body causes karma's iframe for the test to stretch to fit that content once we attempt to
|
||||
// scroll the page. Setting width / height / maxWidth / maxHeight on the iframe does not
|
||||
// successfully constrain its size. As such, skip assertions in environments where the
|
||||
// window size has changed since the start of the test.
|
||||
if (window.innerWidth > startingWindowWidth || window.innerHeight > startingWindowHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollPos = ruler.getViewportScrollPosition();
|
||||
expect(scrollPos.top).toBe(2000);
|
||||
expect(scrollPos.left).toBe(1500);
|
||||
|
||||
document.body.removeChild(veryLargeElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Simple utility for getting the bounds of the browser viewport.
|
||||
* @internal
|
||||
*/
|
||||
@Injectable()
|
||||
export class ViewportRuler {
|
||||
// TODO(jelbourn): cache the document's bounding rect and only update it when the window
|
||||
// is resized (debounced).
|
||||
|
||||
|
||||
/** Gets a ClientRect for the viewport's bounds. */
|
||||
getViewportRect(): ClientRect {
|
||||
// Use the document element's bounding rect rather than the window scroll properties
|
||||
// (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll
|
||||
// properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different
|
||||
// conceptual viewports. Under most circumstances these viewports are equivalent, but they
|
||||
// can disagree when the page is pinch-zoomed (on devices that support touch).
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=489206#c4
|
||||
// We use the documentElement instead of the body because, by default (without a css reset)
|
||||
// browsers typically give the document body an 8px margin, which is not included in
|
||||
// getBoundingClientRect().
|
||||
const documentRect = document.documentElement.getBoundingClientRect();
|
||||
const scrollPosition = this.getViewportScrollPosition(documentRect);
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
|
||||
return {
|
||||
top: scrollPosition.top,
|
||||
left: scrollPosition.left,
|
||||
bottom: scrollPosition.top + height,
|
||||
right: scrollPosition.left + width,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the (top, left) scroll position of the viewport.
|
||||
* @param documentRect
|
||||
*/
|
||||
getViewportScrollPosition(documentRect = document.documentElement.getBoundingClientRect()) {
|
||||
// The top-left-corner of the viewport is determined by the scroll position of the document
|
||||
// body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about
|
||||
// whether `document.body` or `document.documentElement` is the scrolled element, so reading
|
||||
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
|
||||
// `document.documentElement` works consistently, where the `top` and `left` values will
|
||||
// equal negative the scroll position.
|
||||
const top = documentRect.top < 0 && document.body.scrollTop == 0 ?
|
||||
-documentRect.top :
|
||||
document.body.scrollTop;
|
||||
const left = documentRect.left < 0 && document.body.scrollLeft == 0 ?
|
||||
-documentRect.left :
|
||||
document.body.scrollLeft;
|
||||
|
||||
return {top, left};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Applies a CSS transform to an element, including browser-prefixed properties.
|
||||
* @param element
|
||||
* @param transformValue
|
||||
*/
|
||||
export function applyCssTransform(element: HTMLElement, transformValue: string) {
|
||||
// It's important to trim the result, because the browser will ignore the set operation
|
||||
// if the string contains only whitespace.
|
||||
let value = transformValue.trim();
|
||||
|
||||
element.style.transform = value;
|
||||
element.style.webkitTransform = value;
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
Pasta 2
|
||||
</button>
|
||||
|
||||
<button overlay-origin (click)="openSpaghettiPanel()">
|
||||
Pasta 3
|
||||
</button>
|
||||
|
||||
<!-- Template to load into an overlay. -->
|
||||
<template portal>
|
||||
<p class="demo-fusilli"> Fusilli </p>
|
||||
|
||||
@@ -9,3 +9,12 @@
|
||||
border: 1px solid black;
|
||||
background-color: lightgoldenrodyellow;
|
||||
}
|
||||
|
||||
.demo-spagetti {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
color: white;
|
||||
background-color: rebeccapurple;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import {
|
||||
Component, ViewChildren, QueryList, ViewEncapsulation,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
Directive,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Overlay,
|
||||
OverlayState} from '../../core/overlay/overlay';
|
||||
import {Overlay, OverlayState, OVERLAY_PROVIDERS} from '../../core/overlay/overlay';
|
||||
import {ComponentPortal, Portal} from '../../core/portal/portal';
|
||||
import {TemplatePortalDirective} from '../../core/portal/portal-directives';
|
||||
import {RelativePosition} from '../../core/overlay/position/connected-position-strategy';
|
||||
|
||||
|
||||
|
||||
@Directive({selector: '[overlay-origin]'})
|
||||
class OverlayOrigin {
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'overlay-demo',
|
||||
templateUrl: 'demo-app/overlay/overlay-demo.html',
|
||||
styleUrls: ['demo-app/overlay/overlay-demo.css'],
|
||||
directives: [TemplatePortalDirective],
|
||||
providers: [Overlay],
|
||||
directives: [TemplatePortalDirective, OverlayOrigin],
|
||||
providers: [OVERLAY_PROVIDERS],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class OverlayDemo {
|
||||
nextPosition: number = 0;
|
||||
|
||||
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;
|
||||
@ViewChild(OverlayOrigin) private _overlayOrigin: OverlayOrigin;
|
||||
|
||||
constructor(public overlay: Overlay, public viewContainerRef: ViewContainerRef) { }
|
||||
|
||||
@@ -35,7 +48,7 @@ export class OverlayDemo {
|
||||
this.nextPosition += 30;
|
||||
|
||||
this.overlay.create(config).then(ref => {
|
||||
ref.attach(new ComponentPortal(PastaPanel, this.viewContainerRef));
|
||||
ref.attach(new ComponentPortal(RotiniPanel, this.viewContainerRef));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,13 +66,41 @@ export class OverlayDemo {
|
||||
ref.attach(this.templatePortals.first);
|
||||
});
|
||||
}
|
||||
|
||||
openSpaghettiPanel() {
|
||||
// TODO(jelbourn): make a nice API for this.
|
||||
// TODO(jelbourn): separate overlay demo for connected positioning.
|
||||
let strategy = this.overlay.position().connectedTo(this._overlayOrigin.elementRef);
|
||||
var pos = new RelativePosition();
|
||||
pos.originX = 'start';
|
||||
pos.originY = 'end';
|
||||
pos.overlayX = 'start';
|
||||
pos.overlayY = 'start';
|
||||
strategy.addPreferredPosition(pos);
|
||||
|
||||
let config = new OverlayState();
|
||||
config.positionStrategy = strategy;
|
||||
|
||||
this.overlay.create(config).then(ref => {
|
||||
ref.attach(new ComponentPortal(SpagettiPanel, this.viewContainerRef));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Simple component to load into an overlay */
|
||||
@Component({
|
||||
selector: 'pasta-panel',
|
||||
selector: 'rotini-panel',
|
||||
template: '<p class="demo-rotini">Rotini {{value}}</p>'
|
||||
})
|
||||
class PastaPanel {
|
||||
class RotiniPanel {
|
||||
value: number = 9000;
|
||||
}
|
||||
|
||||
/** Simple component to load into an overlay */
|
||||
@Component({
|
||||
selector: 'spagetti-panel',
|
||||
template: '<div class="demo-spagetti">Spagetti {{value}}</div>'
|
||||
})
|
||||
class SpagettiPanel {
|
||||
value: string = 'Omega';
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"inlineSources": true
|
||||
"inlineSources": true,
|
||||
"stripInternal": true
|
||||
},
|
||||
"files": [
|
||||
"main.ts",
|
||||
|
||||
@@ -43,18 +43,25 @@ const configuration: { [name: string]: ConfigurationInfo } = {
|
||||
'Safari7': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
'Safari8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'Safari9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS7': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
|
||||
'iOS8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS7': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'iOS8': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
|
||||
// TODO(mlaval): iOS9 deactivated as not reliable, reactivate after
|
||||
// https://github.com/angular/angular/issues/5408
|
||||
'iOS9': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
'iOS9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
|
||||
'WindowsPhone': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}
|
||||
};
|
||||
|
||||
export const customLaunchers: { [name: string]: BrowserLauncherInfo } = {
|
||||
'ChromeNoSandbox': {
|
||||
base: 'Chrome',
|
||||
flags: ['--no-sandbox'] },
|
||||
flags: ['--no-sandbox']
|
||||
},
|
||||
// Chrome set to 1024x768 resolution for *local testing only*.
|
||||
// On CI, both SauceLabs and Browserstack already default all browser window sizes to 1024x768.
|
||||
'Chrome_1024x768': {
|
||||
base : 'Chrome',
|
||||
flags: ['--window-size=1024,768']
|
||||
},
|
||||
'SL_CHROME': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'chrome',
|
||||
@@ -210,19 +217,22 @@ export const customLaunchers: { [name: string]: BrowserLauncherInfo } = {
|
||||
base: 'BrowserStack',
|
||||
device: 'iPhone 5S',
|
||||
os: 'ios',
|
||||
os_version: '7.0'
|
||||
os_version: '7.0',
|
||||
resolution: '1024x768'
|
||||
},
|
||||
'BS_IOS8': {
|
||||
base: 'BrowserStack',
|
||||
device: 'iPhone 6',
|
||||
os: 'ios',
|
||||
os_version: '8.3'
|
||||
os_version: '8.3',
|
||||
resolution: '1024x768'
|
||||
},
|
||||
'BS_IOS9': {
|
||||
base: 'BrowserStack',
|
||||
device: 'iPhone 6S',
|
||||
os: 'ios',
|
||||
os_version: '9.0'
|
||||
os_version: '9.0',
|
||||
resolution: '1024x768'
|
||||
},
|
||||
'BS_IE9': {
|
||||
base: 'BrowserStack',
|
||||
|
||||
@@ -27,7 +27,7 @@ export function config(config) {
|
||||
{pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false},
|
||||
{pattern: 'dist/vendor/zone.js/dist/fake-async-test.js', included: true, watched: false},
|
||||
|
||||
{pattern: 'test/karma-test-shim.js', included: true, watched: true},
|
||||
{pattern: 'test/karma-test-shim.js', included: true, watched: false},
|
||||
|
||||
// paths loaded via module imports
|
||||
{pattern: 'dist/**/*.js', included: false, watched: true},
|
||||
@@ -81,7 +81,7 @@ export function config(config) {
|
||||
browserDisconnectTimeout: 20000,
|
||||
browserNoActivityTimeout: 240000,
|
||||
captureTimeout: 120000,
|
||||
browsers: ['Chrome'],
|
||||
browsers: ['Chrome_1024x768'],
|
||||
|
||||
singleRun: false
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user