From 53fd3c39734389bcf0e58d93043371880793f77d Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 23 May 2016 10:54:14 -0400 Subject: [PATCH] feat(popover): add popover component references #5420 --- src/components.core.scss | 1 + src/components.ios.scss | 1 + src/components.md.scss | 1 + src/components.ts | 1 + src/components.wp.scss | 1 + src/components/popover/popover.ios.scss | 63 ++++ src/components/popover/popover.md.scss | 35 ++ src/components/popover/popover.scss | 44 +++ src/components/popover/popover.ts | 334 ++++++++++++++++++++ src/components/popover/popover.wp.scss | 35 ++ src/components/popover/test/basic/index.ts | 35 ++ src/components/popover/test/basic/main.html | 25 ++ src/config/modes.ts | 9 + 13 files changed, 585 insertions(+) create mode 100644 src/components/popover/popover.ios.scss create mode 100644 src/components/popover/popover.md.scss create mode 100644 src/components/popover/popover.scss create mode 100644 src/components/popover/popover.ts create mode 100644 src/components/popover/popover.wp.scss create mode 100644 src/components/popover/test/basic/index.ts create mode 100644 src/components/popover/test/basic/main.html 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',