From d5068f8430eb9f01045c3665abd7aa4bea85135a Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 21 Apr 2016 23:46:00 -0500 Subject: [PATCH] feat(picker): init picker --- ionic/components.ios.scss | 1 + ionic/components.md.scss | 1 + ionic/components.ts | 102 ++-- ionic/components.wp.scss | 1 + ionic/components/picker/picker.ios.scss | 79 ++++ ionic/components/picker/picker.md.scss | 5 + ionic/components/picker/picker.scss | 72 +++ ionic/components/picker/picker.ts | 464 +++++++++++++++++++ ionic/components/picker/picker.wp.scss | 5 + ionic/components/picker/test/basic/index.ts | 77 +++ ionic/components/picker/test/basic/main.html | 9 + ionic/config/modes.ts | 9 + 12 files changed, 774 insertions(+), 51 deletions(-) create mode 100644 ionic/components/picker/picker.ios.scss create mode 100644 ionic/components/picker/picker.md.scss create mode 100644 ionic/components/picker/picker.scss create mode 100644 ionic/components/picker/picker.ts create mode 100644 ionic/components/picker/picker.wp.scss create mode 100644 ionic/components/picker/test/basic/index.ts create mode 100644 ionic/components/picker/test/basic/main.html diff --git a/ionic/components.ios.scss b/ionic/components.ios.scss index aa02af24f1..38f765f97d 100644 --- a/ionic/components.ios.scss +++ b/ionic/components.ios.scss @@ -21,6 +21,7 @@ "components/loading/loading.ios", "components/menu/menu.ios", "components/modal/modal.ios", + "components/picker/picker.ios", "components/radio/radio.ios", "components/searchbar/searchbar.ios", "components/segment/segment.ios", diff --git a/ionic/components.md.scss b/ionic/components.md.scss index 91911f4c62..af46292e28 100644 --- a/ionic/components.md.scss +++ b/ionic/components.md.scss @@ -21,6 +21,7 @@ "components/loading/loading.md", "components/menu/menu.md", "components/modal/modal.md", + "components/picker/picker.md", "components/radio/radio.md", "components/searchbar/searchbar.md", "components/segment/segment.md", diff --git a/ionic/components.ts b/ionic/components.ts index 86455e54bb..03a35dd926 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -1,51 +1,51 @@ -export * from './components/app/app' -export * from './components/app/id' - -export * from './components/action-sheet/action-sheet' -export * from './components/alert/alert' -export * from './components/badge/badge' -export * from './components/blur/blur' -export * from './components/button/button' -export * from './components/checkbox/checkbox' -export * from './components/content/content' -export * from './components/icon/icon' -export * from './components/img/img' -export * from './components/infinite-scroll/infinite-scroll' -export * from './components/infinite-scroll/infinite-scroll-content' -export * from './components/input/input' -export * from './components/item/item' -export * from './components/item/item-sliding' -export * from './components/label/label' -export * from './components/list/list' -export * from './components/loading/loading' -export * from './components/menu/menu-controller' -export * from './components/menu/menu' -export * from './components/menu/menu-types' -export * from './components/menu/menu-toggle' -export * from './components/menu/menu-close' -export * from './components/modal/modal' -export * from './components/nav/nav' -export * from './components/nav/nav-controller' -export * from './components/nav/view-controller' -export * from './components/nav/nav-params' -export * from './components/nav/nav-push' -export * from './components/nav/nav-router' -export * from './components/navbar/navbar' -export * from './components/option/option' -export * from './components/radio/radio-button' -export * from './components/radio/radio-group' -export * from './components/refresher/refresher' -export * from './components/refresher/refresher-content' -export * from './components/scroll/scroll' -export * from './components/searchbar/searchbar' -export * from './components/segment/segment' -export * from './components/select/select' -export * from './components/show-hide-when/show-hide-when' -export * from './components/slides/slides' -export * from './components/spinner/spinner' -export * from './components/tabs/tabs' -export * from './components/tabs/tab' -export * from './components/tap-click/tap-click' -export * from './components/toggle/toggle' -export * from './components/toolbar/toolbar' -export * from './components/virtual-scroll/virtual-scroll' +export * from './components/app/app'; +export * from './components/app/id'; +export * from './components/action-sheet/action-sheet'; +export * from './components/alert/alert'; +export * from './components/badge/badge'; +export * from './components/blur/blur'; +export * from './components/button/button'; +export * from './components/checkbox/checkbox'; +export * from './components/content/content'; +export * from './components/icon/icon'; +export * from './components/img/img'; +export * from './components/infinite-scroll/infinite-scroll'; +export * from './components/infinite-scroll/infinite-scroll-content'; +export * from './components/input/input'; +export * from './components/item/item'; +export * from './components/item/item-sliding'; +export * from './components/label/label'; +export * from './components/list/list'; +export * from './components/loading/loading'; +export * from './components/menu/menu-controller'; +export * from './components/menu/menu'; +export * from './components/menu/menu-types'; +export * from './components/menu/menu-toggle'; +export * from './components/menu/menu-close'; +export * from './components/modal/modal'; +export * from './components/nav/nav'; +export * from './components/nav/nav-controller'; +export * from './components/nav/view-controller'; +export * from './components/nav/nav-params'; +export * from './components/nav/nav-push'; +export * from './components/nav/nav-router'; +export * from './components/navbar/navbar'; +export * from './components/option/option'; +export * from './components/picker/picker'; +export * from './components/radio/radio-button'; +export * from './components/radio/radio-group'; +export * from './components/refresher/refresher'; +export * from './components/refresher/refresher-content'; +export * from './components/scroll/scroll'; +export * from './components/searchbar/searchbar'; +export * from './components/segment/segment'; +export * from './components/select/select'; +export * from './components/show-hide-when/show-hide-when'; +export * from './components/slides/slides'; +export * from './components/spinner/spinner'; +export * from './components/tabs/tabs'; +export * from './components/tabs/tab'; +export * from './components/tap-click/tap-click'; +export * from './components/toggle/toggle'; +export * from './components/toolbar/toolbar'; +export * from './components/virtual-scroll/virtual-scroll'; diff --git a/ionic/components.wp.scss b/ionic/components.wp.scss index 5cdf5f81d1..a799c828ec 100644 --- a/ionic/components.wp.scss +++ b/ionic/components.wp.scss @@ -21,6 +21,7 @@ "components/loading/loading.wp", "components/menu/menu.wp", "components/modal/modal.wp", + "components/picker/picker.wp", "components/radio/radio.wp", "components/searchbar/searchbar.wp", "components/segment/segment.wp", diff --git a/ionic/components/picker/picker.ios.scss b/ionic/components/picker/picker.ios.scss new file mode 100644 index 0000000000..b8b68cef65 --- /dev/null +++ b/ionic/components/picker/picker.ios.scss @@ -0,0 +1,79 @@ +@import "../../globals.ios"; +@import "./picker"; + +// iOS Picker +// -------------------------------------------------- + +$picker-ios-height: 260px !default; +$picker-ios-background-color: #cfd5da !default; + +$picker-ios-toolbar-height: 44px !default; +$picker-ios-toolbar-background-color: #f7f7f8 !default; + +$picker-ios-button-height: $picker-ios-toolbar-height !default; +$picker-ios-button-text-color: color($colors-ios, primary) !default; +$picker-ios-button-background-color: transparent !default; + +$picker-ios-option-offset-y: 90px !default; +$picker-ios-option-font-size: 18px !default; +$picker-ios-option-line-height: 24px !default; + + +.picker-wrapper { + height: $picker-ios-height; + + border-top: 1px solid #929499; + + background: $picker-ios-background-color; +} + +.hairlines .picker-wrapper { + border-width: $hairlines-width; +} + +.picker-toolbar { + display: flex; + + height: $picker-ios-toolbar-height; + background: $picker-ios-toolbar-background-color; +} + +.picker-toolbar-button { + flex: 1; + text-align: right; +} + +.picker-toolbar-cancel { + text-align: left; +} + +.picker-button, +.picker-button.activated { + margin: 0; + + height: $picker-ios-button-height; + + color: $picker-ios-button-text-color; + background: $picker-ios-button-background-color; +} + +.picker-offset { + transform: translateY($picker-ios-option-offset-y); +} + +.picker-column { + padding: 0 10px; +} + +.picker-prefix, +.picker-suffix, +.picker-options { + padding: 0 8px; + font-size: $picker-ios-option-font-size; + line-height: $picker-ios-option-line-height; +} + +.picker-prefix, +.picker-suffix { + padding: 0 +} diff --git a/ionic/components/picker/picker.md.scss b/ionic/components/picker/picker.md.scss new file mode 100644 index 0000000000..6e999c22ce --- /dev/null +++ b/ionic/components/picker/picker.md.scss @@ -0,0 +1,5 @@ +@import "../../globals.md"; +@import "./picker"; + +// Material Design Picker +// -------------------------------------------------- diff --git a/ionic/components/picker/picker.scss b/ionic/components/picker/picker.scss new file mode 100644 index 0000000000..910b3af9b1 --- /dev/null +++ b/ionic/components/picker/picker.scss @@ -0,0 +1,72 @@ +@import "../../globals.core"; + +// Picker +// -------------------------------------------------- + +$picker-width: 100% !default; +$picker-max-width: 500px !default; + + +ion-picker-cmp { + position: absolute; + top: 0; + left: 0; + z-index: $z-index-overlay; + display: block; + + width: $picker-width; + height: $picker-width; +} + +.picker-toolbar { + z-index: 1; +} + +.picker-wrapper { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: $z-index-overlay-wrapper; + display: flex; + flex-direction: column; + overflow: hidden; + + margin: auto; + + width: $picker-width; + max-width: $picker-max-width; + + transform: translate3d(0, 100%, 0); +} + +.picker-columns { + display: flex; + flex: 1; + overflow: hidden; +} + +.picker-offset { + display: flex; +} + +.picker-column { + flex: 1; +} + +.picker-prefix { + flex: 1; + text-align: right; +} + +.picker-suffix { + flex: 1; + text-align: left; +} + +.picker-option { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/ionic/components/picker/picker.ts b/ionic/components/picker/picker.ts new file mode 100644 index 0000000000..92f1350e38 --- /dev/null +++ b/ionic/components/picker/picker.ts @@ -0,0 +1,464 @@ +import {Component, ElementRef, Input, ViewChild, Renderer, HostListener, ChangeDetectionStrategy, ViewEncapsulation} from 'angular2/core'; +import {NgClass, NgIf, NgFor} from 'angular2/common'; + +import {Animation} from '../../animations/animation'; +import {Transition, TransitionOptions} from '../../transitions/transition'; +import {Config} from '../../config/config'; +import {isPresent} from '../../util/util'; +import {NavParams} from '../nav/nav-params'; +import {ViewController} from '../nav/view-controller'; +import {raf, CSS, pointerCoord} from '../../util/dom'; + + + +/** + * @name Picker + * @description + * + * @usage + * ```ts + * constructor(private nav: NavController) {} + * + * presentSelector() { + * let picker = Picker.create({ + * + * }); + * this.nav.present(picker); + * } + * + * ``` + * + */ +export class Picker extends ViewController { + + constructor(opts: PickerOptions = {}) { + opts.columns = opts.columns || []; + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + + super(PickerDisplayCmp, opts); + this.viewType = 'picker'; + this.isOverlay = true; + + // by default, pickers should not fire lifecycle events of other views + // for example, when an picker enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + this.usePortal = true; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = (direction === 'back' ? 'pickerLeave' : 'pickerEnter'); + return this._nav && this._nav.config.get(key); + } + + /** + * @param {string} cssClass CSS class name to add to the picker's outer wrapper. + */ + setCssClass(cssClass: string) { + this.data.cssClass = cssClass; + } + + static create(opts: PickerOptions = {}) { + return new Picker(opts); + } + +} + + + +/** + * @private + */ +@Component({ + selector: '.picker-column', + template: + '
' + + '
{{col.prefix}}
' + + '
' + + '
' + + '{{o.text}}' + + '
' + + '
' + + '
{{col.suffix}}
' + + '
', + host: { + '[style.flex]': 'col.flex', + '(touchstart)': 'pointerStart($event)', + '(touchmove)': 'pointerMove($event)', + '(touchend)': 'pointerEnd($event)', + '(mousedown)': 'pointerStart($event)', + '(mousemove)': 'pointerMove($event)', + '(mouseup)': 'pointerEnd($event)', + } +}) +class PickerColumnCmp { + @ViewChild('colEle') colEle: ElementRef; + @Input() col: PickerColumn; + y: number; + colHeight: number; + optHeight: number; + velocity: number; + pos: number[] = []; + scrollingDown: boolean; + msPrv: number = 0; + startY: number = null; + + ngAfterViewInit() { + let colEle: HTMLElement = this.colEle.nativeElement; + + this.colHeight = colEle.clientHeight; + this.optHeight = (colEle.firstElementChild ? colEle.firstElementChild.clientHeight : 0); + + this.setY(0, true); + } + + pointerStart(ev) { + if (this.isPrevented(ev)) { + return; + } + + this.startY = pointerCoord(ev).y; + + this.velocity = 0; + this.pos.length = 0; + this.pos.push(this.startY, Date.now()); + + console.debug('picker, pointerStart', ev.type, this.startY); + } + + pointerMove(ev) { + if (this.startY !== null) { + if (this.isPrevented(ev)) { + return; + } + + let currentY = pointerCoord(ev).y; + console.debug('picker, pointerMove', ev.type, currentY); + + this.pos.push(currentY, Date.now()); + this.setY(this.startY + currentY, false); + } + } + + pointerEnd(ev) { + if (this.startY !== null) { + + if (this.isPrevented(ev)) { + return; + } + + var endY = pointerCoord(ev).y; + + console.debug('picker, pointerEnd', ev.type, endY); + + this.pos.push(endY, Date.now()); + this.velocity = 0; + this.scrollingDown = (endY < this.startY); + + var endPos = (this.pos.length - 1); + var startPos = endPos; + var timeRange = (Date.now() - 100); + + // move pointer to position measured 100ms ago + for (var i = endPos; i > 0 && this.pos[i] > timeRange; i -= 2) { + startPos = i; + } + + if (startPos !== endPos) { + // compute relative movement between these two points + var timeOffset = (this.pos[endPos] - this.pos[startPos]); + var movedTop = (this.pos[startPos - 1] - this.pos[endPos - 1]); + + // based on XXms compute the movement to apply for each render step + this.velocity = ((movedTop / timeOffset) * FRAME_MS); + } + + this.setY(this.startY + endY, true); + + this.decelerate(); + + this.startY = null; + } + } + + decelerate() { + var self = this; + + if (self.velocity) { + self.velocity *= DECELERATION_FRICTION; + console.log(`decelerate velocity ${self.velocity}`); + + var y = self.y + self.velocity; + self.setY(y, true); + + raf(self.decelerate.bind(self)); + + } else if (self.y % this.optHeight !== 0) { + + self.y = self.y + (this.scrollingDown ? -1 : 1); + + console.log(`lock in ${self.y}`); + + self.setY(self.y, true); + + raf(self.decelerate.bind(self)); + } + } + + setY(yOffset: number, saveY: boolean) { + let y = yOffset + this.y; + + console.log(`y: ${y}, yOffset: ${yOffset}, colHeight: ${this.colHeight}, optHeight: ${this.optHeight}`); + + let colEleStyle = this.colEle.nativeElement.style; + colEleStyle[CSS.transform] = `translate3d(0px,${y}px,0px)`; + + if (saveY) { + this.y = y; + } + } + + isPrevented(ev) { + if (ev.type.indexOf('touch') > -1) { + this.msPrv = Date.now() + 2000; + + } else if (this.msPrv > Date.now() && ev.type.indexOf('mouse') > -1) { + ev.preventDefault(); + ev.stopPropagation(); + return true; + } + } + +} + + +/** + * @private + */ +@Component({ + selector: 'ion-picker-cmp', + template: + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
', + host: { + 'role': 'dialog' + }, + directives: [NgClass, NgIf, NgFor, PickerColumnCmp], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +class PickerDisplayCmp { + private d: PickerOptions; + private created: number; + private lastClick: number; + private id: number; + + constructor( + private _viewCtrl: ViewController, + private _elementRef: ElementRef, + private _config: Config, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + + if (this.d.cssClass) { + this.d.cssClass.split(' ').forEach(cssClass => { + renderer.setElementClass(_elementRef.nativeElement, cssClass, true); + }); + } + + this.id = (++pickerIds); + this.created = Date.now(); + this.lastClick = 0; + } + + onPageLoaded() { + // normalize the data + let data = this.d; + + data.buttons = data.buttons.map(button => { + if (typeof button === 'string') { + return { text: button }; + } + if (button.role) { + button.cssRole = `picker-toolbar-${button.role}`; + } + return button; + }); + + data.columns = data.columns.map(column => { + if (!column.flex) { + column.flex = 1; + } + return column; + }); + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.isEnabled() && this._viewCtrl.isLast()) { + if (ev.keyCode === 13) { + if (this.lastClick + 1000 < Date.now()) { + // do not fire this click if there recently was already a click + // this can happen when the button has focus and used the enter + // key to click the button. However, both the click handler and + // this keyup event will fire, so only allow one of them to go. + console.debug('picker, enter button'); + let button = this.d.buttons[this.d.buttons.length - 1]; + this.btnClick(button); + } + + } else if (ev.keyCode === 27) { + console.debug('picker, escape button'); + this.bdClick(); + } + } + } + + onPageDidEnter() { + let activeElement: any = document.activeElement; + if (activeElement) { + activeElement.blur(); + } + + let focusableEle = this._elementRef.nativeElement.querySelector('button'); + if (focusableEle) { + focusableEle.focus(); + } + } + + btnClick(button, dismissDelay?) { + if (!this.isEnabled()) { + return; + } + + // keep the time of the most recent button click + this.lastClick = Date.now(); + + let shouldDismiss = true; + + if (button.handler) { + // a handler has been provided, execute it + // pass the handler the values from the inputs + if (button.handler(this.getValues()) === false) { + // if the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + setTimeout(() => { + this.dismiss(button.role); + }, dismissDelay || this._config.get('pageTransitionDelay')); + } + } + + bdClick() { + if (this.isEnabled() && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + dismiss(role): Promise { + return this._viewCtrl.dismiss(this.getValues(), role); + } + + getValues() { + // this is an alert with text inputs + // return an object of all the values with the input name as the key + let values = {}; + this.d.columns.forEach(col => { + values[col.name] = col.value; + }); + return values; + } + + isEnabled() { + let tm = this._config.getNumber('overlayCreatedDiff', 750); + return (this.created + tm < Date.now()); + } +} + +export interface PickerOptions { + buttons?: any[]; + columns?: PickerColumn[]; + cssClass?: string; + enableBackdropDismiss?: boolean; +} + +export interface PickerColumn { + name?: string; + value?: string; + prefix?: string; + suffix?: string; + options: PickerColumnOption[]; + flex?: number; + cssClass?: string; +} + +export interface PickerColumnOption { + value?: string; + text?: string; + checked?: boolean; + id?: string; +} + + +/** + * Animations for pickers + */ +class PickerSlideIn 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('.picker-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.26); + wrapper.fromTo('translateY', '100%', '0%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); + } +} +Transition.register('picker-slide-in', PickerSlideIn); + + +class PickerSlideOut 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('.picker-wrapper')); + + backdrop.fromTo('opacity', 0.26, 0); + wrapper.fromTo('translateY', '0%', '100%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); + } +} +Transition.register('picker-slide-out', PickerSlideOut); + + +let pickerIds = -1; +const MIN_VELOCITY_START_DECELERATION = 4; +const MIN_VELOCITY_CONTINUE_DECELERATION = 0.12; +const DECELERATION_FRICTION = 0.97; +const FRAME_MS = (1000 / 60); diff --git a/ionic/components/picker/picker.wp.scss b/ionic/components/picker/picker.wp.scss new file mode 100644 index 0000000000..466cd79ef2 --- /dev/null +++ b/ionic/components/picker/picker.wp.scss @@ -0,0 +1,5 @@ +@import "../../globals.wp"; +@import "./picker"; + +// Windows Picker +// -------------------------------------------------- diff --git a/ionic/components/picker/test/basic/index.ts b/ionic/components/picker/test/basic/index.ts new file mode 100644 index 0000000000..ac9e51ac1f --- /dev/null +++ b/ionic/components/picker/test/basic/index.ts @@ -0,0 +1,77 @@ +import {App, Page, Picker, NavController} from 'ionic-angular'; + + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + + constructor(private nav: NavController) { + setTimeout(() => { + this.presentPicker() + }, 250); + } + + presentPicker() { + let picker = Picker.create({ + buttons: [ + { + text: 'Cancel', + role: 'cancel' + }, + 'Done' + ], + columns: [ + { + prefix: 'prefix', + suffix: 'suffix', + options: [ + { text: 'Jan' }, + { text: 'Feb' }, + { text: 'Mar' }, + { text: 'Apr' }, + { text: 'May' }, + { text: 'Jun' }, + { text: 'Jul' }, + { text: 'Aug' }, + { text: 'Sep' }, + { text: 'Oct' }, + { text: 'Nov' }, + { text: 'Dec' }, + ] + }, + // { + // prefix: 'prefix', + // suffix: 'suffix', + // options: [ + // { text: 'Jan' }, + // { text: 'Feb' }, + // { text: 'Mar' }, + // { text: 'Apr' }, + // { text: 'May' }, + // { text: 'Jun' }, + // { text: 'Jul' }, + // { text: 'Aug' }, + // { text: 'Sep' }, + // { text: 'Oct' }, + // { text: 'Nov' }, + // { text: 'Dec' }, + // ] + // }, + ] + }); + + this.nav.present(picker); + } +} + + +@App({ + template: '' +}) +class E2EApp { + root; + constructor() { + this.root = E2EPage; + } +} diff --git a/ionic/components/picker/test/basic/main.html b/ionic/components/picker/test/basic/main.html new file mode 100644 index 0000000000..90bd279550 --- /dev/null +++ b/ionic/components/picker/test/basic/main.html @@ -0,0 +1,9 @@ + + Picker + + + + + + + diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 590952479a..dae7588cd1 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -28,6 +28,9 @@ Config.setModeConfig('ios', { pageTransition: 'ios-transition', pageTransitionDelay: 16, + pickerEnter: 'picker-slide-in', + pickerLeave: 'picker-slide-out', + spinner: 'ios', tabbarPlacement: 'bottom', @@ -60,6 +63,9 @@ Config.setModeConfig('md', { pageTransition: 'md-transition', pageTransitionDelay: 96, + pickerEnter: 'picker-slide-in', + pickerLeave: 'picker-slide-out', + spinner: 'crescent', tabbarHighlight: true, @@ -95,6 +101,9 @@ Config.setModeConfig('wp', { pageTransition: 'wp-transition', pageTransitionDelay: 96, + pickerEnter: 'picker-slide-in', + pickerLeave: 'picker-slide-out', + spinner: 'circles', tabbarPlacement: 'top',