From 02670801d57a97b878399c9f5f9f3ffdb4deedc6 Mon Sep 17 00:00:00 2001 From: mhartington Date: Wed, 2 Aug 2017 13:02:42 -0400 Subject: [PATCH] feat(popover, actionsheet): add inital render --- packages/core/demos/action-sheet/basic.html | 50 +++++ packages/core/demos/popover/basic.html | 63 ++++++ .../action-sheet-controller.scss | 32 +++ .../action-sheet-controller.tsx | 79 +++++++ .../components/action-sheet/action-sheet.tsx | 192 ++++++++++++++++++ .../action-sheet/actionsheet.ios.scss | 109 ++++++++++ .../action-sheet/actionsheet.md.scss | 110 ++++++++++ .../components/action-sheet/actionsheet.scss | 37 ++++ .../action-sheet/actionsheet.wp.scss | 102 ++++++++++ .../action-sheet/animations/ios.enter.ts | 26 +++ .../action-sheet/animations/ios.leave.ts | 27 +++ .../popover-controller.scss | 36 ++++ .../popover-controller/popover-controller.tsx | 79 +++++++ .../popover/animations/ios.enter.ts | 22 ++ .../popover/animations/ios.leave.ts | 28 +++ .../src/components/popover/popover.ios.scss | 24 +++ .../src/components/popover/popover.md.scss | 54 +++++ .../core/src/components/popover/popover.scss | 55 +++++ .../core/src/components/popover/popover.tsx | 180 ++++++++++++++++ .../src/components/popover/popover.wp.scss | 16 ++ packages/core/src/index.d.ts | 15 ++ packages/core/stencil.config.js | 2 + 22 files changed, 1338 insertions(+) create mode 100644 packages/core/demos/action-sheet/basic.html create mode 100644 packages/core/demos/popover/basic.html create mode 100644 packages/core/src/components/action-sheet-controller/action-sheet-controller.scss create mode 100644 packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx create mode 100644 packages/core/src/components/action-sheet/action-sheet.tsx create mode 100644 packages/core/src/components/action-sheet/actionsheet.ios.scss create mode 100644 packages/core/src/components/action-sheet/actionsheet.md.scss create mode 100644 packages/core/src/components/action-sheet/actionsheet.scss create mode 100644 packages/core/src/components/action-sheet/actionsheet.wp.scss create mode 100644 packages/core/src/components/action-sheet/animations/ios.enter.ts create mode 100644 packages/core/src/components/action-sheet/animations/ios.leave.ts create mode 100644 packages/core/src/components/popover-controller/popover-controller.scss create mode 100644 packages/core/src/components/popover-controller/popover-controller.tsx create mode 100644 packages/core/src/components/popover/animations/ios.enter.ts create mode 100644 packages/core/src/components/popover/animations/ios.leave.ts create mode 100644 packages/core/src/components/popover/popover.ios.scss create mode 100644 packages/core/src/components/popover/popover.md.scss create mode 100644 packages/core/src/components/popover/popover.scss create mode 100644 packages/core/src/components/popover/popover.tsx create mode 100644 packages/core/src/components/popover/popover.wp.scss 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 [ +
, + + ]; + } +} + +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'] },