diff --git a/src/components.core.scss b/src/components.core.scss
index 5c03c0d153..719e70881e 100644
--- a/src/components.core.scss
+++ b/src/components.core.scss
@@ -21,6 +21,7 @@
"components/loading/loading",
"components/menu/menu",
"components/modal/modal",
+ "components/popover/popover",
"components/refresher/refresher",
"components/scroll/scroll",
"components/show-hide-when/show-hide-when",
diff --git a/src/components.ios.scss b/src/components.ios.scss
index d92468af94..44fd2e067b 100644
--- a/src/components.ios.scss
+++ b/src/components.ios.scss
@@ -23,6 +23,7 @@
"components/menu/menu.ios",
"components/modal/modal.ios",
"components/picker/picker.ios",
+ "components/popover/popover.ios",
"components/radio/radio.ios",
"components/searchbar/searchbar.ios",
"components/segment/segment.ios",
diff --git a/src/components.md.scss b/src/components.md.scss
index 3b38526227..f5f4a4b93a 100644
--- a/src/components.md.scss
+++ b/src/components.md.scss
@@ -22,6 +22,7 @@
"components/menu/menu.md",
"components/modal/modal.md",
"components/picker/picker.md",
+ "components/popover/popover.md",
"components/radio/radio.md",
"components/searchbar/searchbar.md",
"components/segment/segment.md",
diff --git a/src/components.ts b/src/components.ts
index 75898f1580..8e7738428b 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -31,6 +31,7 @@ export * from './components/nav/nav-router';
export * from './components/navbar/navbar';
export * from './components/option/option';
export * from './components/picker/picker';
+export * from './components/popover/popover';
export * from './components/radio/radio-button';
export * from './components/radio/radio-group';
export * from './components/refresher/refresher';
diff --git a/src/components.wp.scss b/src/components.wp.scss
index 8061150743..a9ad7f635e 100644
--- a/src/components.wp.scss
+++ b/src/components.wp.scss
@@ -22,6 +22,7 @@
"components/menu/menu.wp",
"components/modal/modal.wp",
"components/picker/picker.wp",
+ "components/popover/popover.wp",
"components/radio/radio.wp",
"components/searchbar/searchbar.wp",
"components/segment/segment.wp",
diff --git a/src/components/popover/popover.ios.scss b/src/components/popover/popover.ios.scss
new file mode 100644
index 0000000000..10c00f74ab
--- /dev/null
+++ b/src/components/popover/popover.ios.scss
@@ -0,0 +1,63 @@
+@import "../../globals.core";
+@import "./popover";
+
+// iOS Popover
+// --------------------------------------------------
+
+$popover-ios-padding: 24px 34px !default;
+$popover-ios-min-width: 150px !default;
+$popover-ios-max-width: 270px !default;
+$popover-ios-max-height: 90% !default;
+$popover-ios-border-radius: 10px !default;
+$popover-ios-text-color: #000 !default;
+$popover-ios-background: #f3f3f3 !default;
+
+
+.popover-wrapper {
+ padding: $popover-ios-padding;
+
+ min-width: $popover-ios-min-width;
+ max-width: $popover-ios-max-width;
+
+ max-height: $popover-ios-max-height;
+
+ border-radius: $popover-ios-border-radius;
+ color: $popover-ios-text-color;
+ background: $popover-ios-background;
+}
+
+
+// Popover Arrow
+// -----------------------------------------
+
+.popover-arrow {
+ position: absolute;
+ display: block;
+ top: -20px;
+ width: 30px;
+ height: 19px;
+ overflow: hidden;
+
+ &:after {
+ position: absolute;
+
+ z-index: $z-index-overlay-wrapper;
+
+ top: 12px;
+ left: 5px;
+ width: 20px;
+ height: 20px;
+ background-color: $popover-ios-background;
+ border-radius: 3px;
+ content: '';
+ transform: rotate(-45deg);
+ }
+}
+
+.popover-bottom .popover-arrow {
+ top: auto;
+ bottom: -10px;
+ &:after {
+ top: -6px;
+ }
+}
diff --git a/src/components/popover/popover.md.scss b/src/components/popover/popover.md.scss
new file mode 100644
index 0000000000..cf55cb4145
--- /dev/null
+++ b/src/components/popover/popover.md.scss
@@ -0,0 +1,35 @@
+@import "../../globals.core";
+@import "./popover";
+
+// Material Design Popover
+// --------------------------------------------------
+
+$popover-md-padding: 24px 34px !default;
+$popover-md-min-width: 150px !default;
+$popover-md-max-width: 270px !default;
+$popover-md-max-height: 90% !default;
+$popover-md-border-radius: 2px !default;
+$popover-md-text-color: #000 !default;
+$popover-md-background: #fafafa !default;
+
+
+.popover-wrapper {
+ padding: $popover-md-padding;
+
+ min-width: $popover-md-min-width;
+ max-width: $popover-md-max-width;
+
+ max-height: $popover-md-max-height;
+
+ border-radius: $popover-md-border-radius;
+ color: $popover-md-text-color;
+ background: $popover-md-background;
+}
+
+
+// Material Design Popover Template
+// -----------------------------------------
+
+.popover-template {
+
+}
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
new file mode 100644
index 0000000000..b91090877d
--- /dev/null
+++ b/src/components/popover/popover.scss
@@ -0,0 +1,44 @@
+@import "../../globals.core";
+
+// Popover
+// --------------------------------------------------
+
+$popover-min-width: 150px !default;
+$popover-max-height: 90% !default;
+
+
+ion-popover {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: $z-index-overlay;
+
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+}
+
+.popover-wrapper {
+ position: absolute;
+
+ z-index: $z-index-overlay-wrapper;
+ display: flex;
+
+ flex-direction: column;
+
+ min-width: $popover-min-width;
+ max-height: $popover-max-height;
+
+ opacity: 0;
+}
+
+
+// Popover Backdrop
+// -----------------------------------------
+
+.hide-backdrop {
+ display: none;
+}
diff --git a/src/components/popover/popover.ts b/src/components/popover/popover.ts
new file mode 100644
index 0000000000..8240323fa6
--- /dev/null
+++ b/src/components/popover/popover.ts
@@ -0,0 +1,334 @@
+import {Component, Renderer, ElementRef, HostListener, ViewEncapsulation} from '@angular/core';
+
+import {Animation} from '../../animations/animation';
+import {Transition, TransitionOptions} from '../../transitions/transition';
+import {Config} from '../../config/config';
+import {NavParams} from '../nav/nav-params';
+import {isPresent, isUndefined, isDefined} from '../../util/util';
+import {ViewController} from '../nav/view-controller';
+
+const POPOVER_BODY_PADDING = 6;
+
+/**
+ * @name Popover
+ * @description
+ *
+ */
+export class Popover extends ViewController {
+
+ constructor(opts: PopoverOptions = {}) {
+ opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true;
+ opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true;
+
+ super(PopoverCmp, opts);
+ this.viewType = 'popover';
+ this.isOverlay = true;
+ this.usePortal = false;
+
+ // by default, popovers should not fire lifecycle events of other views
+ // for example, when a popover enters, the current active view should
+ // not fire its lifecycle events because it's not conceptually leaving
+ this.fireOtherLifecycles = false;
+ }
+
+ /**
+ * @private
+ */
+ getTransitionName(direction: string) {
+ let key = (direction === 'back' ? 'popoverLeave' : 'popoverEnter');
+ return this._nav && this._nav.config.get(key);
+ }
+
+ /**
+ * Create a popover with the following options
+ *
+ * | Option | Type | Description |
+ * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------|
+ * | template |`string` | The html content for the popover. |
+ * | cssClass |`string` | An additional class for custom styles. |
+ * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. |
+ * | enableBackdropDismiss |`boolean` | Wheather the popover should be dismissed by tapping the backdrop. Default true. |
+ *
+ *
+ * @param {object} opts Popover options
+ */
+ static create(opts: PopoverOptions = {}) {
+ return new Popover(opts);
+ }
+
+ }
+
+/**
+* @private
+*/
+@Component({
+ selector: 'ion-popover',
+ template:
+ '
' +
+ '' +
+ '',
+ host: {
+ 'role': 'dialog'
+ },
+ encapsulation: ViewEncapsulation.None,
+})
+class PopoverCmp {
+ private d: any;
+ private id: number;
+ private created: number;
+ private showSpinner: boolean;
+
+ constructor(
+ private _viewCtrl: ViewController,
+ private _config: Config,
+ private _elementRef: ElementRef,
+ private _renderer: Renderer,
+ params: NavParams
+ ) {
+ this.d = params.data;
+ this.created = Date.now();
+
+ if (this.d.cssClass) {
+ _renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true);
+ }
+
+ this.id = (++popoverIds);
+ }
+
+ ngOnInit() {
+ if (this.d.element && this.d.event) {
+ this.positionView(this.d.element, this.d.event);
+ }
+ }
+
+ onPageDidEnter() {
+ let activeElement: any = document.activeElement;
+ if (document.activeElement) {
+ activeElement.blur();
+ }
+ }
+
+ positionView(targetEle, ev) {
+ let popoverEle = this._elementRef.nativeElement;
+ let popoverWrapperEle = popoverEle.querySelector('.popover-wrapper');
+
+ // Popover width and height
+ let popoverWidth = popoverWrapperEle.offsetWidth;
+ let popoverHeight = popoverWrapperEle.offsetHeight;
+
+ // Window body width and height
+ let bodyWidth = window.innerWidth;
+ let bodyHeight = window.innerHeight;
+
+ // Clicked element width and height
+ targetEle = targetEle._elementRef.nativeElement;
+ let targetWidth = targetEle.offsetWidth;
+ let targetHeight = targetEle.offsetHeight;
+
+ // console.log("Popover Wrapper Element", popoverWrapperEle);
+ // console.log("Popover Wrapper Width & Height", popoverWidth, popoverHeight);
+ // console.log("Body Width & Height", bodyWidth, bodyHeight);
+ // console.log("Target", targetEle);
+ // console.log("Target Width & Height", targetWidth, targetHeight);
+
+ let popoverCSS = {
+ top: ev.clientY + targetHeight - (popoverHeight / 2),
+ left: ev.clientX - popoverWidth / 2
+ };
+
+ // The arrow that shows above the popover on iOS
+ var arrowEle = popoverEle.querySelector('.popover-arrow');
+ var arrowWidth = arrowEle.offsetWidth;
+ var arrowHeight = arrowEle.offsetHeight;
+
+ let arrowLeft = targetWidth + targetWidth / 2 -
+ arrowEle.offsetWidth / 2 - popoverCSS.left;
+
+ let arrowCSS = {
+ top: ev.clientY + targetHeight - (popoverHeight / 2) - arrowHeight,
+ left: ev.clientX - (arrowWidth / 2)
+ }
+
+ // If the popover left is less than the padding it is off screen
+ // to the left so adjust it, else if the width of the popover
+ // exceeds the body width it is off screen to the right so adjust
+ if (popoverCSS.left < POPOVER_BODY_PADDING) {
+ popoverCSS.left = POPOVER_BODY_PADDING;
+ arrowCSS.left = (POPOVER_BODY_PADDING * 2);
+ } else if (popoverWidth + POPOVER_BODY_PADDING + popoverCSS.left > bodyWidth) {
+ popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
+ arrowCSS.left = bodyWidth - (POPOVER_BODY_PADDING * 2) - arrowWidth;
+ }
+
+ // If the popover when popped down stretches past bottom of screen,
+ // make it pop up if there's room above
+ if (popoverCSS.top + POPOVER_BODY_PADDING + popoverHeight > bodyHeight &&
+ popoverCSS.top - popoverHeight > 0) {
+ popoverCSS.top = popoverCSS.top - targetHeight - popoverHeight;
+ this._renderer.setElementClass(this._elementRef.nativeElement, 'popover-bottom', true);
+ }
+
+ this._renderer.setElementStyle(arrowEle, 'top', arrowCSS.top + 'px');
+ this._renderer.setElementStyle(arrowEle, 'left', arrowCSS.left + 'px');
+
+ this._renderer.setElementStyle(popoverWrapperEle, 'top', popoverCSS.top + 'px');
+ this._renderer.setElementStyle(popoverWrapperEle, 'left', popoverCSS.left + 'px');
+ }
+
+ dismiss(role): Promise {
+ return this._viewCtrl.dismiss(null, role);
+ }
+
+ bdClick() {
+ if (this.isEnabled() && this.d.enableBackdropDismiss) {
+ this.dismiss('backdrop');
+ }
+ }
+
+ isEnabled() {
+ let tm = this._config.getNumber('overlayCreatedDiff', 750);
+ return (this.created + tm < Date.now());
+ }
+}
+
+export interface PopoverOptions {
+ template?: string;
+ element?: any;
+ event?: any;
+ cssClass?: string;
+ showBackdrop?: boolean;
+ enableBackdropDismiss?: boolean;
+}
+
+/**
+ * Animations for popover
+ */
+class PopoverPopIn extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = enteringView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1');
+ backdrop.fromTo('opacity', '0.01', '0.3');
+
+ this
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-pop-in', PopoverPopIn);
+
+
+class PopoverPopOut extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = leavingView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9');
+ backdrop.fromTo('opacity', '0.3', '0');
+
+ this
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-pop-out', PopoverPopOut);
+
+
+class PopoverMdPopIn extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = enteringView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1');
+ backdrop.fromTo('opacity', '0.01', '0.5');
+
+ this
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-md-pop-in', PopoverMdPopIn);
+
+
+class PopoverMdPopOut extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = leavingView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9');
+ backdrop.fromTo('opacity', '0.5', '0');
+
+ this
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-md-pop-out', PopoverMdPopOut);
+
+
+
+class PopoverWpPopIn extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = enteringView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.3', '1');
+ backdrop.fromTo('opacity', '0.01', '0.5');
+
+ this
+ .easing('cubic-bezier(0,0 0.05,1)')
+ .duration(200)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-wp-pop-in', PopoverWpPopIn);
+
+
+class PopoverWpPopOut extends Transition {
+ constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
+ super(opts);
+
+ let ele = leavingView.pageRef().nativeElement;
+ let backdrop = new Animation(ele.querySelector('.backdrop'));
+ let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
+
+ wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '1.3');
+ backdrop.fromTo('opacity', '0.5', '0');
+
+ this
+ .easing('ease-out')
+ .duration(150)
+ .add(backdrop)
+ .add(wrapper);
+ }
+}
+Transition.register('popover-wp-pop-out', PopoverWpPopOut);
+
+let popoverIds = -1;
diff --git a/src/components/popover/popover.wp.scss b/src/components/popover/popover.wp.scss
new file mode 100644
index 0000000000..bd63fe78a4
--- /dev/null
+++ b/src/components/popover/popover.wp.scss
@@ -0,0 +1,35 @@
+@import "../../globals.core";
+@import "./popover";
+
+// Windows Popover
+// --------------------------------------------------
+
+$popover-wp-padding: 24px 34px !default;
+$popover-wp-min-width: 150px !default;
+$popover-wp-max-width: 270px !default;
+$popover-wp-max-height: 90% !default;
+$popover-wp-border-radius: 2px !default;
+$popover-wp-text-color: #fff !default;
+$popover-wp-background: #000 !default;
+
+
+.popover-wrapper {
+ padding: $popover-wp-padding;
+
+ min-width: $popover-wp-min-width;
+ max-width: $popover-wp-max-width;
+
+ max-height: $popover-wp-max-height;
+
+ border-radius: $popover-wp-border-radius;
+ color: $popover-wp-text-color;
+ background: $popover-wp-background;
+}
+
+
+// Windows Popover Template
+// -----------------------------------------
+
+.popover-template {
+
+}
diff --git a/src/components/popover/test/basic/index.ts b/src/components/popover/test/basic/index.ts
new file mode 100644
index 0000000000..6607382ea3
--- /dev/null
+++ b/src/components/popover/test/basic/index.ts
@@ -0,0 +1,35 @@
+import {App, Page, Popover, NavController} from '../../../../../src';
+
+
+@Page({
+ templateUrl: 'main.html'
+})
+class E2EPage {
+ constructor(private nav: NavController) {}
+
+ presentPopover(ele, ev) {
+ console.log(ev);
+
+ let popover = Popover.create({
+ template: `
+ My Popover
+ `,
+ element: ele,
+ event: ev
+ });
+
+ this.nav.present(popover);
+ }
+
+}
+
+
+@App({
+ template: ''
+})
+class E2EApp {
+ root;
+ constructor() {
+ this.root = E2EPage;
+ }
+}
diff --git a/src/components/popover/test/basic/main.html b/src/components/popover/test/basic/main.html
new file mode 100644
index 0000000000..35165d2a02
--- /dev/null
+++ b/src/components/popover/test/basic/main.html
@@ -0,0 +1,25 @@
+
+ Popover
+
+
+
+
+
+
+
+
+
+
+
+
+ Popover
+
+
+
+
diff --git a/src/config/modes.ts b/src/config/modes.ts
index 71fc23ed9e..81dfbeaccf 100644
--- a/src/config/modes.ts
+++ b/src/config/modes.ts
@@ -32,6 +32,9 @@ Config.setModeConfig('ios', {
pickerLeave: 'picker-slide-out',
pickerRotateFactor: -0.46,
+ popoverEnter: 'popover-pop-in',
+ popoverLeave: 'popover-pop-out',
+
spinner: 'ios',
tabbarPlacement: 'bottom',
@@ -70,6 +73,9 @@ Config.setModeConfig('md', {
pickerEnter: 'picker-slide-in',
pickerLeave: 'picker-slide-out',
+ popoverEnter: 'popover-md-pop-in',
+ popoverLeave: 'popover-md-pop-out',
+
spinner: 'crescent',
tabbarHighlight: true,
@@ -111,6 +117,9 @@ Config.setModeConfig('wp', {
pickerEnter: 'picker-slide-in',
pickerLeave: 'picker-slide-out',
+ popoverEnter: 'popover-wp-pop-in',
+ popoverLeave: 'popover-wp-pop-out',
+
spinner: 'circles',
tabbarPlacement: 'top',