diff --git a/packages/core/demos/action-sheet/basic.html b/packages/core/demos/action-sheet/basic.html
new file mode 100644
index 0000000000..132fe35343
--- /dev/null
+++ b/packages/core/demos/action-sheet/basic.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Ionic Slides Basic
+
+
+
+
+
+
+
+
+ Show ActionSheet
+
+
+
+
+
+
diff --git a/packages/core/demos/popover/basic.html b/packages/core/demos/popover/basic.html
new file mode 100644
index 0000000000..6fc0328ea8
--- /dev/null
+++ b/packages/core/demos/popover/basic.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Ionic Slides Basic
+
+
+
+
+
+
+
+
+ Show Popover
+
+
+
+
+
+
diff --git a/packages/core/src/components/action-sheet-controller/action-sheet-controller.scss b/packages/core/src/components/action-sheet-controller/action-sheet-controller.scss
new file mode 100644
index 0000000000..a54010e525
--- /dev/null
+++ b/packages/core/src/components/action-sheet-controller/action-sheet-controller.scss
@@ -0,0 +1,32 @@
+@import "../../themes/ionic.globals";
+
+
+// Loading Controller
+// --------------------------------------------------
+
+ion-loading-controller {
+ display: none;
+}
+
+
+// Loading Controller Backdrop
+// --------------------------------------------------
+
+/// @prop - Color of the backdrop
+$loading-backdrop-color: #000 !default;
+
+
+.loading-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: $z-index-backdrop;
+ display: block;
+
+ width: 100%;
+ height: 100%;
+
+ background-color: $loading-backdrop-color;
+ opacity: .01;
+ transform: translateZ(0);
+}
diff --git a/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx b/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx
new file mode 100644
index 0000000000..1397c482f2
--- /dev/null
+++ b/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx
@@ -0,0 +1,79 @@
+import { Component, Listen } from '@stencil/core';
+import { ActionSheetEvent, ActionSheetOptions, ActionSheet, IonicControllerApi } from '../../index';
+
+
+@Component({
+ tag: 'ion-action-sheet-controller',
+ styleUrl: 'action-sheet-controller.scss'
+})
+export class ActionSheetController implements IonicControllerApi {
+ private ids = 0;
+ private actionsheetResolves: {[actionsheetId: string]: Function} = {};
+ private actionsheets: ActionSheet[] = [];
+ private appRoot: Element;
+
+
+ ionViewDidLoad() {
+ this.appRoot = document.querySelector('ion-app') || document.body;
+ Ionic.loadController('action-sheet', this);
+ }
+
+
+ load(opts?: ActionSheetOptions) {
+ // create ionic's wrapping ion-actionsheet component
+ const actionsheet = document.createElement('ion-action-sheet');
+ const id = this.ids++;
+
+ // give this actionsheet a unique id
+ actionsheet.id = `action-sheet-${id}`;
+ actionsheet.style.zIndex = (20000 + id).toString();
+
+ // convert the passed in actionsheet options into props
+ // that get passed down into the new actionsheet
+ Object.assign(actionsheet, opts);
+
+ // append the actionsheet element to the document body
+ this.appRoot.appendChild(actionsheet as any);
+
+ // store the resolve function to be called later up when the actionsheet loads
+ return new Promise(resolve => {
+ this.actionsheetResolves[actionsheet.id] = resolve;
+ });
+ }
+
+
+ @Listen('body:ionActionSheetDidLoad')
+ viewDidLoad(ev) {
+ const actionsheet = ev.detail.actionsheet;
+ const actionsheetResolve = this.actionsheetResolves[actionsheet.id];
+ if (actionsheetResolve) {
+ actionsheetResolve(actionsheet);
+ delete this.actionsheetResolves[actionsheet.id];
+ }
+ }
+
+
+ @Listen('body:ionActionSheetWillPresent')
+ willPresent(ev: ActionSheetEvent) {
+ this.actionsheets.push(ev.actionsheet);
+ }
+
+
+ @Listen('body:ionActionSheetWillDismiss, body:ionActionSheetDidUnload')
+ willDismiss(ev: ActionSheetEvent) {
+ const index = this.actionsheets.indexOf(ev.actionsheet);
+ if (index > -1) {
+ this.actionsheets.splice(index, 1);
+ }
+ }
+
+
+ @Listen('body:keyup.escape')
+ escapeKeyUp() {
+ const lastActionSheet = this.actionsheets[this.actionsheets.length - 1];
+ if (lastActionSheet) {
+ lastActionSheet.dismiss();
+ }
+ }
+
+}
diff --git a/packages/core/src/components/action-sheet/action-sheet.tsx b/packages/core/src/components/action-sheet/action-sheet.tsx
new file mode 100644
index 0000000000..d2593d6493
--- /dev/null
+++ b/packages/core/src/components/action-sheet/action-sheet.tsx
@@ -0,0 +1,192 @@
+import {
+ Component,
+ Element,
+ Event,
+ EventEmitter,
+ Listen,
+ Prop,
+ State
+} from '@stencil/core';
+import { AnimationBuilder, Animation } from '../../index';
+
+import iOSEnterAnimation from './animations/ios.enter';
+import iOSLeaveAnimation from './animations/ios.leave';
+
+@Component({
+ tag: 'ion-action-sheet',
+ // styleUrls: {
+ // ios: 'action-sheet.ios.scss',
+ // md: 'action-sheet.md.scss',
+ // wp: 'action-sheet.wp.scss'
+ // },
+ host: {
+ theme: 'action-sheet'
+ }
+})
+export class ActionSheet {
+ private animation: Animation;
+ private durationTimeout: any;
+
+ @Element() private el: HTMLElement;
+
+ @Event() ionActionSheetDidLoad: EventEmitter;
+ @Event() ionActionSheetWillPresent: EventEmitter;
+ @Event() ionActionSheetDidPresent: EventEmitter;
+ @Event() ionActionSheetWillDismiss: EventEmitter;
+ @Event() ionActionSheetDidDismiss: EventEmitter;
+ @Event() ionActionSheetDidUnload: EventEmitter;
+
+ @Prop() cssClass: string;
+ @Prop() title: string;
+ @Prop() subTitle: string;
+ @Prop() buttons: ActionSheetButtons[];
+ @Prop() enableBackdropDismiss: boolean = true;
+ @Prop() showBackdrop: boolean = true;
+
+ @Prop() enterAnimation: AnimationBuilder;
+ @Prop() exitAnimation: AnimationBuilder;
+ @Prop() id: string;
+
+ @Listen('ionDismiss')
+ onDismiss(ev: UIEvent) {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ this.dismiss();
+ }
+
+ ionViewDidLoad() {
+ this.ionActionSheetDidLoad.emit({ actionsheet: this });
+ }
+
+ present() {
+ return new Promise(resolve => {
+ this._present(resolve);
+ });
+ }
+
+ private _present(resolve: Function) {
+ // if (this.animation) {
+ // this.animation.destroy();
+ // this.animation = null;
+ // }
+ this.ionActionSheetWillPresent.emit(
+ { actionsheet: this } as ActionSheetEvent
+ );
+
+ // let animationBuilder = this.enterAnimation
+ // ? this.enterAnimation
+ // : iOSEnterAnimation;
+
+ // build the animation and kick it off
+ // this.animation = animationBuilder(this.el);
+
+ // this.animation.onFinish((a: any) => {
+ // a.destroy();
+ // this.ionViewDidLoad();
+ resolve();
+ // }).play();
+ }
+
+ dismiss() {
+ //
+ // if (this.animation) {
+ // this.animation.destroy();
+ // this.animation = null;
+ // }
+
+ return new Promise(resolve => {
+ this.ionActionSheetWillDismiss.emit(
+ { actionsheet: this } as ActionSheetEvent
+ );
+
+ // get the user's animation fn if one was provided
+ let animationBuilder = this.exitAnimation;
+
+ // let animationBuilder = this.exitAnimation
+ // ? this.exitAnimation
+ // : iOSLeaveAnimation;
+
+ // build the animation and kick it off
+ // this.animation = animationBuilder(this.el);
+ // this.animation.onFinish((a: any) => {
+ // a.destroy();
+ this.ionActionSheetDidDismiss.emit(
+ { actionsheet: this } as ActionSheetEvent
+ );
+
+ Core.dom.write(() => {
+ this.el.parentNode.removeChild(this.el);
+ });
+
+ resolve();
+ // }).play();
+ });
+ }
+
+ ionViewDidUnload() {
+ this.ionActionSheetDidUnload.emit(
+ { actionsheet: this } as ActionSheetEvent
+ );
+ }
+
+ backdropClick() {
+ if (this.enableBackdropDismiss) {
+ // const opts: NavOptions = {
+ // minClickBlockDuration: 400
+ // };
+ this.dismiss();
+ }
+ }
+
+ render() {
+ let userCssClass = 'action-sheet-content';
+ if (this.cssClass) {
+ userCssClass += ' ' + this.cssClass;
+ }
+ return [
+ ,
+
+
+
+ {this.title
+ ?
{this.title}
+ : null}
+ {this.subTitle
+ ?
{this.subTitle}
+ : null}
+ {this.buttons.map(b =>
+
b.handler()}>{b.text}
+ )}
+
+
+
+ ];
+ }
+}
+
+export interface ActionSheetOptions {
+ title?: string;
+ subTitle?: string;
+ cssClass?: string;
+ buttons?: (ActionSheetButton | string)[];
+ enableBackdropDismiss?: boolean;
+}
+
+export interface ActionSheetButtons {
+ text?: string;
+ role?: string;
+ icon?: string;
+ cssClass?: string;
+ handler?: () => boolean | void;
+}
+
+export interface ActionSheetEvent {
+ actionsheet: ActionSheet;
+}
diff --git a/packages/core/src/components/action-sheet/actionsheet.ios.scss b/packages/core/src/components/action-sheet/actionsheet.ios.scss
new file mode 100644
index 0000000000..ce21fb9f27
--- /dev/null
+++ b/packages/core/src/components/action-sheet/actionsheet.ios.scss
@@ -0,0 +1,109 @@
+@import "../../themes/ionic.globals.ios";
+@import "./loading";
+
+
+// iOS Loading Indicator
+// --------------------------------------------------
+
+// deprecated
+$loading-ios-padding: null !default;
+
+/// @prop - Padding top of the loading wrapper
+$loading-ios-padding-top: 24px !default;
+
+/// @prop - Padding end of the loading wrapper
+$loading-ios-padding-end: 34px !default;
+
+/// @prop - Padding bottom of the loading wrapper
+$loading-ios-padding-bottom: $loading-ios-padding-top !default;
+
+/// @prop - Padding start of the loading wrapper
+$loading-ios-padding-start: $loading-ios-padding-end !default;
+
+/// @prop - Max width of the loading wrapper
+$loading-ios-max-width: 270px !default;
+
+/// @prop - Maximum height of the loading wrapper
+$loading-ios-max-height: 90% !default;
+
+/// @prop - Border radius of the loading wrapper
+$loading-ios-border-radius: 8px !default;
+
+/// @prop - Text color of the loading wrapper
+$loading-ios-text-color: #000 !default;
+
+/// @prop - Background of the loading wrapper
+$loading-ios-background: #f8f8f8 !default;
+
+/// @prop - Font weight of the loading content
+$loading-ios-content-font-weight: bold !default;
+
+/// @prop - Color of the loading spinner
+$loading-ios-spinner-color: #69717d !default;
+
+/// @prop - Color of the ios loading spinner
+$loading-ios-spinner-ios-color: $loading-ios-spinner-color !default;
+
+/// @prop - Color of the bubbles loading spinner
+$loading-ios-spinner-bubbles-color: $loading-ios-spinner-color !default;
+
+/// @prop - Color of the circles loading spinner
+$loading-ios-spinner-circles-color: $loading-ios-spinner-color !default;
+
+/// @prop - Color of the crescent loading spinner
+$loading-ios-spinner-crescent-color: $loading-ios-spinner-color !default;
+
+/// @prop - Color of the dots loading spinner
+$loading-ios-spinner-dots-color: $loading-ios-spinner-color !default;
+
+
+.loading-ios .loading-wrapper {
+ @include border-radius($loading-ios-border-radius);
+
+ max-width: $loading-ios-max-width;
+ max-height: $loading-ios-max-height;
+
+ color: $loading-ios-text-color;
+ background: $loading-ios-background;
+
+ @include deprecated-variable(padding, $loading-ios-padding) {
+ @include padding($loading-ios-padding-top, $loading-ios-padding-end, $loading-ios-padding-bottom, $loading-ios-padding-start);
+ }
+}
+
+
+// iOS Loading Content
+// -----------------------------------------
+
+.loading-ios .loading-content {
+ font-weight: $loading-ios-content-font-weight;
+}
+
+.loading-ios .loading-spinner + .loading-content {
+ @include margin-horizontal(16px, null);
+}
+
+
+// iOS Loading Spinner fill colors
+// -----------------------------------------
+
+.loading-ios .spinner-ios line,
+.loading-ios .spinner-ios-small line {
+ stroke: $loading-ios-spinner-ios-color;
+}
+
+.loading-ios .spinner-bubbles circle {
+ fill: $loading-ios-spinner-bubbles-color;
+}
+
+.loading-ios .spinner-circles circle {
+ fill: $loading-ios-spinner-circles-color;
+}
+
+.loading-ios .spinner-crescent circle {
+ stroke: $loading-ios-spinner-crescent-color;
+}
+
+.loading-ios .spinner-dots circle {
+ fill: $loading-ios-spinner-dots-color;
+}
diff --git a/packages/core/src/components/action-sheet/actionsheet.md.scss b/packages/core/src/components/action-sheet/actionsheet.md.scss
new file mode 100644
index 0000000000..85f57c693f
--- /dev/null
+++ b/packages/core/src/components/action-sheet/actionsheet.md.scss
@@ -0,0 +1,110 @@
+@import "../../themes/ionic.globals.md";
+@import "./loading";
+
+
+// Material Design Loading Indicator
+// --------------------------------------------------
+
+// deprecated
+$loading-md-padding: null !default;
+
+/// @prop - Padding top of the loading wrapper
+$loading-md-padding-top: 24px !default;
+
+/// @prop - Padding end of the loading wrapper
+$loading-md-padding-end: $loading-md-padding-top !default;
+
+/// @prop - Padding bottom of the loading wrapper
+$loading-md-padding-bottom: $loading-md-padding-top !default;
+
+/// @prop - Padding start of the loading wrapper
+$loading-md-padding-start: $loading-md-padding-end !default;
+
+/// @prop - Max width of the loading wrapper
+$loading-md-max-width: 280px !default;
+
+/// @prop - Maximum height of the loading wrapper
+$loading-md-max-height: 90% !default;
+
+/// @prop - Border radius of the loading wrapper
+$loading-md-border-radius: 2px !default;
+
+/// @prop - Text color of the loading wrapper
+$loading-md-text-color: rgba(0, 0, 0, .5) !default;
+
+/// @prop - Background of the loading wrapper
+$loading-md-background: #fafafa !default;
+
+/// @prop - Box shadow color of the loading wrapper
+$loading-md-box-shadow-color: rgba(0, 0, 0, .4) !default;
+
+/// @prop - Box shadow of the loading wrapper
+$loading-md-box-shadow: 0 16px 20px $loading-md-box-shadow-color !default;
+
+/// @prop - Color of the loading spinner
+$loading-md-spinner-color: color($colors-md, primary) !default;
+
+/// @prop - Color of the ios loading spinner
+$loading-md-spinner-ios-color: $loading-md-spinner-color !default;
+
+/// @prop - Color of the bubbles loading spinner
+$loading-md-spinner-bubbles-color: $loading-md-spinner-color !default;
+
+/// @prop - Color of the circles loading spinner
+$loading-md-spinner-circles-color: $loading-md-spinner-color !default;
+
+/// @prop - Color of the crescent loading spinner
+$loading-md-spinner-crescent-color: $loading-md-spinner-color !default;
+
+/// @prop - Color of the dots loading spinner
+$loading-md-spinner-dots-color: $loading-md-spinner-color !default;
+
+
+.loading-md .loading-wrapper {
+ @include border-radius($loading-md-border-radius);
+
+ max-width: $loading-md-max-width;
+ max-height: $loading-md-max-height;
+
+ color: $loading-md-text-color;
+ background: $loading-md-background;
+
+ box-shadow: $loading-md-box-shadow;
+
+ @include deprecated-variable(padding, $loading-md-padding) {
+ @include padding($loading-md-padding-top, $loading-md-padding-end, $loading-md-padding-bottom, $loading-md-padding-start);
+ }
+}
+
+
+// Material Design Loading Content
+// -----------------------------------------
+
+.loading-md .loading-spinner + .loading-content {
+ @include margin-horizontal(16px, null);
+}
+
+
+// Material Design Loading Spinner fill colors
+// -----------------------------------------
+
+.loading-md .spinner-ios line,
+.loading-md .spinner-ios-small line {
+ stroke: $loading-md-spinner-ios-color;
+}
+
+.loading-md .spinner-bubbles circle {
+ fill: $loading-md-spinner-bubbles-color;
+}
+
+.loading-md .spinner-circles circle {
+ fill: $loading-md-spinner-circles-color;
+}
+
+.loading-md .spinner-crescent circle {
+ stroke: $loading-md-spinner-crescent-color;
+}
+
+.loading-md .spinner-dots circle {
+ fill: $loading-md-spinner-dots-color;
+}
diff --git a/packages/core/src/components/action-sheet/actionsheet.scss b/packages/core/src/components/action-sheet/actionsheet.scss
new file mode 100644
index 0000000000..77b4f8a8a8
--- /dev/null
+++ b/packages/core/src/components/action-sheet/actionsheet.scss
@@ -0,0 +1,37 @@
+@import "../../themes/ionic.globals";
+
+
+// Loading Indicator
+// --------------------------------------------------
+
+ion-loading {
+ @include position(0, 0, 0, 0);
+
+ position: absolute;
+ z-index: $z-index-overlay;
+
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ contain: strict;
+}
+
+ion-loading ion-gesture {
+ display: block;
+
+ width: 100%;
+ height: 100%;
+
+ visibility: inherit;
+}
+
+.loading-wrapper {
+ z-index: $z-index-overlay-wrapper;
+ display: flex;
+
+ align-items: center;
+
+ opacity: 0;
+}
diff --git a/packages/core/src/components/action-sheet/actionsheet.wp.scss b/packages/core/src/components/action-sheet/actionsheet.wp.scss
new file mode 100644
index 0000000000..e373bd6cb3
--- /dev/null
+++ b/packages/core/src/components/action-sheet/actionsheet.wp.scss
@@ -0,0 +1,102 @@
+@import "../../themes/ionic.globals.wp";
+@import "./loading";
+
+
+// Windows Loading Indicator
+// --------------------------------------------------
+
+// deprecated
+$loading-wp-padding: null !default;
+
+/// @prop - Padding top of the loading wrapper
+$loading-wp-padding-top: 20px !default;
+
+/// @prop - Padding end of the loading wrapper
+$loading-wp-padding-end: $loading-wp-padding-top !default;
+
+/// @prop - Padding bottom of the loading wrapper
+$loading-wp-padding-bottom: $loading-wp-padding-top !default;
+
+/// @prop - Padding start of the loading wrapper
+$loading-wp-padding-start: $loading-wp-padding-end !default;
+
+/// @prop - Max width of the loading wrapper
+$loading-wp-max-width: 280px !default;
+
+/// @prop - Maximum height of the loading wrapper
+$loading-wp-max-height: 90% !default;
+
+/// @prop - Border radius of the loading wrapper
+$loading-wp-border-radius: 2px !default;
+
+/// @prop - Text color of the loading wrapper
+$loading-wp-text-color: #fff !default;
+
+/// @prop - Background of the loading wrapper
+$loading-wp-background: #000 !default;
+
+/// @prop - Color of the loading spinner
+$loading-wp-spinner-color: $loading-wp-text-color !default;
+
+/// @prop - Color of the ios loading spinner
+$loading-wp-spinner-ios-color: $loading-wp-spinner-color !default;
+
+/// @prop - Color of the bubbles loading spinner
+$loading-wp-spinner-bubbles-color: $loading-wp-spinner-color !default;
+
+/// @prop - Color of the circles loading spinner
+$loading-wp-spinner-circles-color: $loading-wp-spinner-color !default;
+
+/// @prop - Color of the crescent loading spinner
+$loading-wp-spinner-crescent-color: $loading-wp-spinner-color !default;
+
+/// @prop - Color of the dots loading spinner
+$loading-wp-spinner-dots-color: $loading-wp-spinner-color !default;
+
+
+.loading-wp .loading-wrapper {
+ @include border-radius($loading-wp-border-radius);
+
+ max-width: $loading-wp-max-width;
+ max-height: $loading-wp-max-height;
+
+ color: $loading-wp-text-color;
+ background: $loading-wp-background;
+
+ @include deprecated-variable(padding, $loading-wp-padding) {
+ @include padding($loading-wp-padding-top, $loading-wp-padding-end, $loading-wp-padding-bottom, $loading-wp-padding-start);
+ }
+}
+
+
+// Windows Loading Content
+// -----------------------------------------
+
+.loading-wp .loading-spinner + .loading-content {
+ @include margin-horizontal(16px, null);
+}
+
+
+// Windows Loading Spinner fill colors
+// -----------------------------------------
+
+.loading-wp .spinner-ios line,
+.loading-wp .spinner-ios-small line {
+ stroke: $loading-wp-spinner-ios-color;
+}
+
+.loading-wp .spinner-bubbles circle {
+ fill: $loading-wp-spinner-bubbles-color;
+}
+
+.loading-wp .spinner-circles circle {
+ fill: $loading-wp-spinner-circles-color;
+}
+
+.loading-wp .spinner-crescent circle {
+ stroke: $loading-wp-spinner-crescent-color;
+}
+
+.loading-wp .spinner-dots circle {
+ fill: $loading-wp-spinner-dots-color;
+}
diff --git a/packages/core/src/components/action-sheet/animations/ios.enter.ts b/packages/core/src/components/action-sheet/animations/ios.enter.ts
new file mode 100644
index 0000000000..5bfbacd55c
--- /dev/null
+++ b/packages/core/src/components/action-sheet/animations/ios.enter.ts
@@ -0,0 +1,26 @@
+
+
+/**
+ * iOS Loading Enter Animation
+ */
+export default function(baseElm: HTMLElement) {
+ const baseAnimation = new Ionic.Animation();
+
+ const backdropAnimation = new Ionic.Animation();
+ backdropAnimation.addElement(baseElm.querySelector('.loading-backdrop'));
+
+ const wrapperAnimation = new Ionic.Animation();
+ wrapperAnimation.addElement(baseElm.querySelector('.loading-wrapper'));
+
+ backdropAnimation.fromTo('opacity', 0.01, 0.3);
+
+ wrapperAnimation.fromTo('opacity', 0.01, 1)
+ .fromTo('scale', 1.1, 1);
+
+ return baseAnimation
+ .addElement(baseElm)
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdropAnimation)
+ .add(wrapperAnimation);
+}
diff --git a/packages/core/src/components/action-sheet/animations/ios.leave.ts b/packages/core/src/components/action-sheet/animations/ios.leave.ts
new file mode 100644
index 0000000000..f7b4c1b3e2
--- /dev/null
+++ b/packages/core/src/components/action-sheet/animations/ios.leave.ts
@@ -0,0 +1,27 @@
+
+
+/**
+ * iOS Loading Leave Animation
+ */
+export default function(baseElm: HTMLElement) {
+ const baseAnimation = new Ionic.Animation();
+
+ const backdropAnimation = new Ionic.Animation();
+ backdropAnimation.addElement(baseElm.querySelector('.loading-backdrop'));
+
+ const wrapperAnimation = new Ionic.Animation();
+ wrapperAnimation.addElement(baseElm.querySelector('.loading-wrapper'));
+
+ backdropAnimation.fromTo('opacity', 0.3, 0);
+
+ wrapperAnimation.fromTo('opacity', 0.99, 0)
+ .fromTo('scale', 1, 0.9);
+
+
+ return baseAnimation
+ .addElement(baseElm)
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdropAnimation)
+ .add(wrapperAnimation);
+}
diff --git a/packages/core/src/components/popover-controller/popover-controller.scss b/packages/core/src/components/popover-controller/popover-controller.scss
new file mode 100644
index 0000000000..2367a64ab2
--- /dev/null
+++ b/packages/core/src/components/popover-controller/popover-controller.scss
@@ -0,0 +1,36 @@
+@import "../../themes/ionic.globals";
+
+
+// Popover Controller
+// --------------------------------------------------
+
+ion-popover-controller {
+ display: none;
+}
+
+
+// Popover Controller Backdrop
+// --------------------------------------------------
+
+/// @prop - Color of the backdrop
+$popover-backdrop-color: #000 !default;
+
+
+.popover-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: $z-index-backdrop;
+ display: block;
+
+ width: 100%;
+ height: 100%;
+
+ background-color: $popover-backdrop-color;
+ opacity: .01;
+ transform: translateZ(0);
+}
+
+.popover-backdrop.backdrop-no-tappable {
+ cursor: auto;
+}
diff --git a/packages/core/src/components/popover-controller/popover-controller.tsx b/packages/core/src/components/popover-controller/popover-controller.tsx
new file mode 100644
index 0000000000..0cf6fd677d
--- /dev/null
+++ b/packages/core/src/components/popover-controller/popover-controller.tsx
@@ -0,0 +1,79 @@
+import { Component, Listen } from '@stencil/core';
+import { PopoverEvent, PopoverOptions, Popover, IonicControllerApi } from '../../index';
+
+
+@Component({
+ tag: 'ion-popover-controller',
+ // styleUrl: 'popover-controller.scss'
+})
+export class PopoverController implements IonicControllerApi {
+ private ids = 0;
+ private popoverResolves: {[popoverId: string]: Function} = {};
+ private popovers: Popover[] = [];
+ private appRoot: Element;
+
+
+ ionViewDidLoad() {
+ this.appRoot = document.querySelector('ion-app') || document.body;
+ Ionic.loadController('popover', this);
+ }
+
+
+ load(opts?: PopoverOptions) {
+ // create ionic's wrapping ion-popover component
+ const popover = document.createElement('ion-popover');
+ const id = this.ids++;
+
+ // give this popover a unique id
+ popover.id = `popover-${id}`;
+ popover.style.zIndex = (10000 + id).toString();
+
+ // convert the passed in popover options into props
+ // that get passed down into the new popover
+ Object.assign(popover, opts);
+
+ // append the popover element to the document body
+ this.appRoot.appendChild(popover as any);
+
+ // store the resolve function to be called later up when the popover loads
+ return new Promise(resolve => {
+ this.popoverResolves[popover.id] = resolve;
+ });
+ }
+
+
+ @Listen('body:ionPopoverDidLoad')
+ viewDidLoad(ev) {
+ const popover = ev.detail.popover;
+ const popoverResolve = this.popoverResolves[popover.id];
+ if (popoverResolve) {
+ popoverResolve(popover);
+ delete this.popoverResolves[popover.id];
+ }
+ }
+
+
+ @Listen('body:ionPopoverWillPresent')
+ willPresent(ev: PopoverEvent) {
+ this.popovers.push(ev.popover);
+ }
+
+
+ @Listen('body:ionPopoverWillDismiss, body:ionPopoverDidUnload')
+ willDismiss(ev: PopoverEvent) {
+ const index = this.popovers.indexOf(ev.popover);
+ if (index > -1) {
+ this.popovers.splice(index, 1);
+ }
+ }
+
+
+ @Listen('body:keyup.escape')
+ escapeKeyUp() {
+ const lastPopover = this.popovers[this.popovers.length - 1];
+ if (lastPopover) {
+ lastPopover.dismiss();
+ }
+ }
+
+}
diff --git a/packages/core/src/components/popover/animations/ios.enter.ts b/packages/core/src/components/popover/animations/ios.enter.ts
new file mode 100644
index 0000000000..af964d5800
--- /dev/null
+++ b/packages/core/src/components/popover/animations/ios.enter.ts
@@ -0,0 +1,22 @@
+
+export default function(baseElm: HTMLElement) {
+ const baseAnimation = new Ionic.Animation();
+
+ const backdropAnimation = new Ionic.Animation();
+ backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop'));
+
+ const wrapperAnimation = new Ionic.Animation();
+ wrapperAnimation.addElement(baseElm.querySelector('.popover-wrapper'));
+
+ backdropAnimation.fromTo('opacity', 0.01, 0.3);
+
+ wrapperAnimation.fromTo('opacity', 0.01, 1)
+ .fromTo('scale', 1.1, 1);
+
+ return baseAnimation
+ .addElement(baseElm)
+ .easing('ease-in-out')
+ .duration(200)
+ .add(backdropAnimation)
+ .add(wrapperAnimation);
+}
diff --git a/packages/core/src/components/popover/animations/ios.leave.ts b/packages/core/src/components/popover/animations/ios.leave.ts
new file mode 100644
index 0000000000..6c07e4749d
--- /dev/null
+++ b/packages/core/src/components/popover/animations/ios.leave.ts
@@ -0,0 +1,28 @@
+
+
+/**
+ * iOS Modal Leave Animation
+ */
+export default function(baseElm: HTMLElement) {
+ const baseAnimation = new Ionic.Animation();
+
+ const backdropAnimation = new Ionic.Animation();
+ backdropAnimation.addElement(baseElm.querySelector('.modal-backdrop'));
+
+ const wrapperAnimation = new Ionic.Animation();
+ const wrapperElm = baseElm.querySelector('.modal-wrapper');
+ wrapperAnimation.addElement(wrapperElm);
+ const wrapperElmRect = wrapperElm.getBoundingClientRect();
+
+ wrapperAnimation.beforeStyles({ 'opacity': 1 })
+ .fromTo('translateY', '0%', `${window.innerHeight - wrapperElmRect.top}px`);
+
+ backdropAnimation.fromTo('opacity', 0.4, 0.0);
+
+ return baseAnimation
+ .addElement(baseElm)
+ .easing('ease-out')
+ .duration(250)
+ .add(backdropAnimation)
+ .add(wrapperAnimation);
+}
diff --git a/packages/core/src/components/popover/popover.ios.scss b/packages/core/src/components/popover/popover.ios.scss
new file mode 100644
index 0000000000..30a48e7da9
--- /dev/null
+++ b/packages/core/src/components/popover/popover.ios.scss
@@ -0,0 +1,24 @@
+@import "../../themes/ionic.globals.ios";
+@import "./modal";
+
+
+// iOS Modals
+// --------------------------------------------------
+
+/// @prop - Background color for the modal
+$modal-ios-background-color: $background-ios-color !default;
+
+/// @prop - Border radius for the modal
+$modal-ios-border-radius: 10px !default;
+
+
+.modal-wrapper-ios {
+ // hidden by default to prevent flickers, the animation will show it
+ @include transform(translate3d(0, 100%, 0));
+
+ @media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-small) {
+ @include border-radius($modal-ios-border-radius);
+
+ overflow: hidden;
+ }
+}
diff --git a/packages/core/src/components/popover/popover.md.scss b/packages/core/src/components/popover/popover.md.scss
new file mode 100644
index 0000000000..aaeb75a9de
--- /dev/null
+++ b/packages/core/src/components/popover/popover.md.scss
@@ -0,0 +1,54 @@
+@import "../../themes/ionic.globals";
+@import "./popover";
+// Material Design Popover
+// --------------------------------------------------
+
+/// @prop - Width of the popover content
+$popover-md-width: 250px !default;
+
+/// @prop - Min width of the popover content
+$popover-md-min-width: 0 !default;
+
+/// @prop - Minimum height of the popover content
+$popover-md-min-height: 0 !default;
+
+/// @prop - Maximum height of the popover content
+$popover-md-max-height: 90% !default;
+
+/// @prop - Border radius of the popover content
+$popover-md-border-radius: 2px !default;
+
+/// @prop - Text color of the popover content
+// $popover-md-text-color: $text-md-color !default;
+$popover-md-text-color: black !default;
+
+/// @prop - Background of the popover content
+// $popover-md-background: $background-md-color !default;
+$popover-md-background: black !default;
+
+/// @prop - Box shadow color of the popover content
+$popover-md-box-shadow-color: rgba(0, 0, 0, .3) !default;
+
+/// @prop - Box shadow of the popover content
+$popover-md-box-shadow: 0 3px 12px 2px $popover-md-box-shadow-color !default;
+
+
+.popover-md .popover-content {
+ @include border-radius($popover-md-border-radius);
+ @include transform-origin(start, top);
+
+ width: $popover-md-width;
+ min-width: $popover-md-min-width;
+ min-height: $popover-md-min-height;
+ max-height: $popover-md-max-height;
+
+ color: $popover-md-text-color;
+ // background: $popover-md-background;
+ background: #ffffff;
+ box-shadow: $popover-md-box-shadow;
+}
+
+.popover-md .popover-viewport {
+ opacity: 0;
+ transition-delay: 100ms;
+}
diff --git a/packages/core/src/components/popover/popover.scss b/packages/core/src/components/popover/popover.scss
new file mode 100644
index 0000000000..721af8564d
--- /dev/null
+++ b/packages/core/src/components/popover/popover.scss
@@ -0,0 +1,55 @@
+@import "../../themes/ionic.globals";
+
+// Popover
+// --------------------------------------------------
+
+ion-popover {
+ @include position(0, 0, 0, 0);
+
+ position: absolute;
+
+ z-index: $z-index-overlay;
+
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ .popover-backdrop {
+ left: 0;
+ top: 0;
+ position: absolute;
+ bottom: 0px;
+ z-index: 2;
+ display: block;
+ width: 100%;
+ background-color: #000;
+ opacity: 0.3;
+ }
+}
+
+.popover-wrapper {
+ z-index: $z-index-overlay-wrapper;
+
+ // opacity: 0;
+ opacity: 1;
+}
+
+.popover-content {
+ position: absolute;
+ z-index: $z-index-overlay-wrapper;
+
+ display: flex;
+
+ overflow: auto;
+
+ flex-direction: column;
+}
+
+.popover-content ion-content, .popover-content .scroll-content {
+ contain: none;
+}
+
+.popover-content .scroll-content {
+ position: relative;
+}
diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx
new file mode 100644
index 0000000000..8d91c674c0
--- /dev/null
+++ b/packages/core/src/components/popover/popover.tsx
@@ -0,0 +1,180 @@
+import { Component, Element, Event, EventEmitter, Listen, Prop } from '@stencil/core';
+import { AnimationBuilder, Animation } from '../../index';
+import { createThemedClasses } from '../../utils/theme';
+
+import iOSEnterAnimation from './animations/ios.enter';
+import iOSLeaveAnimation from './animations/ios.leave';
+
+
+@Component({
+ tag: 'ion-popover',
+ styleUrls: {
+ // ios: 'popover.ios.scss',
+ md: 'popover.md.scss',
+ // wp: 'popover.wp.scss'
+ },
+ host: {
+ theme: 'popover'
+ }
+})
+export class Popover {
+ @Element() private el: HTMLElement;
+
+ @Event() ionPopoverDidLoad: EventEmitter;
+ @Event() ionPopoverWillPresent: EventEmitter;
+ @Event() ionPopoverDidPresent: EventEmitter;
+ @Event() ionPopoverWillDismiss: EventEmitter;
+ @Event() ionPopoverDidDismiss: EventEmitter;
+ @Event() ionPopoverDidUnload: EventEmitter;
+
+ @Prop() mode: string;
+ @Prop() color: string;
+ @Prop() component: string;
+ @Prop() componentProps: any = {};
+ @Prop() cssClass: string;
+ @Prop() enableBackdropDismiss: boolean = true;
+ @Prop() enterAnimation: AnimationBuilder;
+ @Prop() exitAnimation: AnimationBuilder;
+ @Prop() ev: Event;
+ @Prop() id: string;
+ @Prop() showBackdrop: boolean = true;
+
+
+
+ private animation: Animation;
+
+ @Listen('ionDismiss')
+ onDismiss(ev: UIEvent) {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ this.dismiss();
+ }
+
+ ionViewDidLoad() {
+ this.ionPopoverDidLoad.emit({ popover: this });
+ }
+
+ present() {
+ return new Promise(resolve => {
+ this._present(resolve);
+ });
+ }
+
+ private _present(resolve: Function) {
+ if (this.animation) {
+ this.animation.destroy();
+ this.animation = null;
+ }
+ this.ionPopoverWillPresent.emit({ popover: this });
+
+ // get the user's animation fn if one was provided
+ // let animationBuilder = this.enterAnimation
+ // ? this.enterAnimation
+ // : iOSEnterAnimation;
+ //
+ // build the animation and kick it off
+ // this.animation = animationBuilder(this.el);
+
+ // this.animation.onFinish((a: any) => {
+ // a.destroy();
+ // this.ionPopoverDidPresent.emit({ popover: this });
+ resolve();
+ // }).play();
+ }
+
+ dismiss() {
+ // if (this.animation) {
+ // this.animation.destroy();
+ // this.animation = null;
+ // }
+
+ return new Promise(resolve => {
+ this.ionPopoverWillDismiss.emit({ popover: this });
+
+ // get the user's animation fn if one was provided
+ // let animationBuilder = this.exitAnimation;
+ //
+ // if (!animationBuilder) {
+ // // user did not provide a custom animation fn
+ // // decide from the config which animation to use
+ // // TODO!!
+ // animationBuilder = iOSLeaveAnimation;
+ // }
+
+ // build the animation and kick it off
+ // this.animation = animationBuilder(this.el);
+ // this.animation.onFinish((a: any) => {
+ // a.destroy();
+ this.ionPopoverDidDismiss.emit({ popover: this });
+
+ Core.dom.write(() => {
+ this.el.parentNode.removeChild(this.el);
+ });
+ resolve();
+ // }).play();
+ });
+ }
+
+ ionViewDidUnload() {
+ this.ionPopoverDidUnload.emit({ popover: this });
+ }
+
+ backdropClick() {
+ if (this.enableBackdropDismiss) {
+ // const opts: NavOptions = {
+ // minClickBlockDuration: 400
+ // };
+ this.dismiss();
+ }
+ }
+
+ render() {
+ const ThisComponent = this.component;
+
+ let userCssClasses = 'popover-content';
+ if (this.cssClass) {
+ userCssClasses += ` ${this.cssClass}`;
+ }
+
+ const dialogClasses = createThemedClasses(this.mode, this.color, 'popover-wrapper');
+ const thisComponentClasses = createThemedClasses(this.mode, this.color, userCssClasses);
+
+ return [
+ ,
+
+
+
+
+ ];
+ }
+}
+
+
+export interface PopoverOptions {
+ component: string;
+ componentProps?: any;
+ showBackdrop?: boolean;
+ enableBackdropDismiss?: boolean;
+ enterAnimation?: AnimationBuilder;
+ exitAnimation?: AnimationBuilder;
+ cssClass?: string;
+ ev: Event;
+}
+
+
+export interface PopoverEvent {
+ popover: Popover;
+}
diff --git a/packages/core/src/components/popover/popover.wp.scss b/packages/core/src/components/popover/popover.wp.scss
new file mode 100644
index 0000000000..6b36281214
--- /dev/null
+++ b/packages/core/src/components/popover/popover.wp.scss
@@ -0,0 +1,16 @@
+@import "../../themes/ionic.globals.wp";
+@import "./modal";
+
+
+// Windows Modals
+// --------------------------------------------------
+
+/// @prop - Background color for the modal
+$modal-wp-background-color: $background-wp-color !default;
+
+
+.modal-wrapper-wp {
+ @include transform(translate3d(0, 40px, 0));
+
+ opacity: .01;
+}
diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts
index 862d1e2e4f..a2ce1df3f0 100644
--- a/packages/core/src/index.d.ts
+++ b/packages/core/src/index.d.ts
@@ -1,5 +1,7 @@
import { AnimationController } from './components/animation/animation';
import { Animation, AnimationBuilder } from './components/animation/animation-interface';
+import { ActionSheet, ActionSheetButtons, ActionSheetEvent, ActionSheetOptions } from './components/action-sheet/action-sheet';
+import { ActionSheetController } from './components/action-sheet-controller/action-sheet-controller';
import { Loading, LoadingEvent, LoadingOptions } from './components/loading/loading';
import { LoadingController } from './components/loading-controller/loading-controller';
import { GestureDetail, GestureCallback } from './components/gesture/gesture';
@@ -8,6 +10,10 @@ import { MenuType } from './components/menu/menu-types';
import { MenuController } from './components/menu/menu-controller';
import { Modal, ModalOptions, ModalEvent } from './components/modal/modal';
import { ModalController } from './components/modal-controller/modal-controller';
+
+import { Popover, PopoverEvent, PopoverOptions } from './components/popover/popover'
+import { PopoverController } from './components/popover-controller/popover-controller'
+
import { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
import { Segment } from './components/segment/segment';
import { SegmentButton, SegmentButtonEvent } from './components/segment-button/segment-button';
@@ -41,6 +47,11 @@ export interface BooleanInputComponent extends BaseInputComponent {
export {
+ ActionSheet,
+ ActionSheetButtons,
+ ActionSheetEvent,
+ ActionSheetOptions,
+ ActionSheetController,
Animation,
AnimationBuilder,
AnimationController,
@@ -57,6 +68,10 @@ export {
ModalController,
ModalOptions,
ModalEvent,
+ Popover,
+ PopoverController,
+ PopoverEvent,
+ PopoverOptions,
Scroll,
ScrollCallback,
ScrollDetail,
diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js
index d664815dca..8edf712309 100644
--- a/packages/core/stencil.config.js
+++ b/packages/core/stencil.config.js
@@ -6,6 +6,7 @@ exports.config = {
bundles: [
{ components: ['ion-animation'] },
{ components: ['ion-app', 'ion-content', 'ion-fixed', 'ion-footer', 'ion-header', 'ion-navbar', 'ion-page', 'ion-title', 'ion-toolbar'] },
+ { components: ['ion-action-sheet', 'ion-action-sheet-controller'] },
{ components: ['ion-avatar', 'ion-badge', 'ion-thumbnail'] },
{ components: ['ion-button', 'ion-buttons', 'ion-icon'] },
{ components: ['ion-card', 'ion-card-content', 'ion-card-header', 'ion-card-title'] },
@@ -18,6 +19,7 @@ exports.config = {
{ components: ['ion-loading', 'ion-loading-ctrl'] },
{ components: ['ion-menu'], priority: 'low' },
{ components: ['ion-modal', 'ion-modal-ctrl'] },
+ { components: ['ion-popover', 'ion-popover-controller'] },
{ components: ['ion-searchbar'] },
{ components: ['ion-segment', 'ion-segment-button'] },
{ components: ['ion-slides', 'ion-slide'] },