diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index b78594d3da..55e6ee1715 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -993,6 +993,7 @@ declare global { onPress?: GestureCallback; onStart?: GestureCallback; onWillStart?: (_: GestureDetail) => Promise; + passive?: boolean; threshold?: number; type?: string; } @@ -2143,6 +2144,73 @@ declare global { } +import { + RefresherContent as IonRefresherContent +} from './components/refresher-content/refresher-content'; + +declare global { + interface HTMLIonRefresherContentElement extends IonRefresherContent, HTMLElement { + } + var HTMLIonRefresherContentElement: { + prototype: HTMLIonRefresherContentElement; + new (): HTMLIonRefresherContentElement; + }; + interface HTMLElementTagNameMap { + "ion-refresher-content": HTMLIonRefresherContentElement; + } + interface ElementTagNameMap { + "ion-refresher-content": HTMLIonRefresherContentElement; + } + namespace JSX { + interface IntrinsicElements { + "ion-refresher-content": JSXElements.IonRefresherContentAttributes; + } + } + namespace JSXElements { + export interface IonRefresherContentAttributes extends HTMLAttributes { + pullingIcon?: string; + pullingText?: string; + refreshingSpinner?: string; + refreshingText?: string; + } + } +} + + +import { + Refresher as IonRefresher +} from './components/refresher/refresher'; + +declare global { + interface HTMLIonRefresherElement extends IonRefresher, HTMLElement { + } + var HTMLIonRefresherElement: { + prototype: HTMLIonRefresherElement; + new (): HTMLIonRefresherElement; + }; + interface HTMLElementTagNameMap { + "ion-refresher": HTMLIonRefresherElement; + } + interface ElementTagNameMap { + "ion-refresher": HTMLIonRefresherElement; + } + namespace JSX { + interface IntrinsicElements { + "ion-refresher": JSXElements.IonRefresherAttributes; + } + } + namespace JSXElements { + export interface IonRefresherAttributes extends HTMLAttributes { + closeDuration?: string; + enabled?: boolean; + pullDelta?: number; + pullMin?: number; + snapbackDuration?: string; + } + } +} + + import { ReorderGroup as IonReorderGroup } from './components/reorder-group/reorder-group'; diff --git a/packages/core/src/components/gesture/gesture.tsx b/packages/core/src/components/gesture/gesture.tsx index 9b1b743b3d..c47539de94 100644 --- a/packages/core/src/components/gesture/gesture.tsx +++ b/packages/core/src/components/gesture/gesture.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core'; import { ElementRef, applyStyles, assert, getElementReference, updateDetail } from '../../utils/helpers'; import { BLOCK_ALL, BlockerDelegate, GestureController, GestureDelegate } from '../gesture-controller/gesture-controller'; import { DomController } from '../../index'; @@ -28,7 +28,7 @@ export class Gesture { @Element() private el: HTMLElement; @Prop({ context: 'dom' }) dom: DomController; - @Prop({ context: 'enableListener' }) enableListener: any; + @Prop({ context: 'enableListener' }) enableListener: EventListenerEnable; @Prop() enabled = true; @Prop() attachTo: ElementRef = 'child'; @@ -38,6 +38,7 @@ export class Gesture { @Prop() direction = 'x'; @Prop() gestureName = ''; @Prop() gesturePriority = 0; + @Prop() passive = true; @Prop() maxAngle = 40; @Prop() threshold = 10; @Prop() type = 'pan'; @@ -105,8 +106,8 @@ export class Gesture { @Watch('enabled') protected enabledChanged(isEnabled: boolean) { if (this.pan || this.hasPress) { - this.enableListener(this, 'touchstart', isEnabled, this.attachTo); - this.enableListener(this, 'mousedown', isEnabled, this.attachTo); + this.enableListener(this, 'touchstart', isEnabled, this.attachTo, this.passive); + this.enableListener(this, 'mousedown', isEnabled, this.attachTo, this.passive); if (!isEnabled) { this.abortGesture(); } @@ -433,18 +434,18 @@ export class Gesture { private enableMouse(shouldEnable: boolean) { if (this.pan) { - this.enableListener(this, 'document:mousemove', shouldEnable); + this.enableListener(this, 'document:mousemove', shouldEnable, undefined, this.passive); } - this.enableListener(this, 'document:mouseup', shouldEnable); + this.enableListener(this, 'document:mouseup', shouldEnable, undefined, this.passive); } private enableTouch(shouldEnable: boolean) { if (this.pan) { - this.enableListener(this, 'touchmove', shouldEnable, this.attachTo); + this.enableListener(this, 'touchmove', shouldEnable, this.attachTo, this.passive); } - this.enableListener(this, 'touchcancel', shouldEnable, this.attachTo); - this.enableListener(this, 'touchend', shouldEnable, this.attachTo); + this.enableListener(this, 'touchcancel', shouldEnable, this.attachTo, this.passive); + this.enableListener(this, 'touchend', shouldEnable, this.attachTo, this.passive); } diff --git a/packages/core/src/components/gesture/readme.md b/packages/core/src/components/gesture/readme.md index ed4569ea2d..c588b8af96 100644 --- a/packages/core/src/components/gesture/readme.md +++ b/packages/core/src/components/gesture/readme.md @@ -87,6 +87,11 @@ any any +#### passive + +boolean + + #### threshold number @@ -179,6 +184,11 @@ any any +#### passive + +boolean + + #### threshold number diff --git a/packages/core/src/components/refresher-content/readme.md b/packages/core/src/components/refresher-content/readme.md new file mode 100644 index 0000000000..073ad20d85 --- /dev/null +++ b/packages/core/src/components/refresher-content/readme.md @@ -0,0 +1,55 @@ +# ion-refresher-content + + + + + + +## Properties + +#### pullingIcon + +string + + +#### pullingText + +string + + +#### refreshingSpinner + +string + + +#### refreshingText + +string + + +## Attributes + +#### pullingIcon + +string + + +#### pullingText + +string + + +#### refreshingSpinner + +string + + +#### refreshingText + +string + + + +---------------------------------------------- + +*Built by [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/refresher-content/refresher-content.tsx b/packages/core/src/components/refresher-content/refresher-content.tsx new file mode 100644 index 0000000000..df281549e8 --- /dev/null +++ b/packages/core/src/components/refresher-content/refresher-content.tsx @@ -0,0 +1,68 @@ +import { Component, Prop } from '@stencil/core'; +import { Config } from '../../index'; + +/** + * @hidden + */ +@Component({ + tag: 'ion-refresher-content' +}) +export class RefresherContent { + + @Prop({ context: 'config' }) config: Config; + + /** + * @input {string} a static icon to display when you begin to pull down + */ + @Prop({mutable: true}) pullingIcon: string; + + /** + * @input {string} the text you want to display when you begin to pull down + */ + @Prop() pullingText: string; + + /** + * @input {string} An animated SVG spinner that shows when refreshing begins + */ + @Prop({mutable: true}) refreshingSpinner: string; + + /** + * @input {string} the text you want to display when performing a refresh + */ + @Prop() refreshingText: string; + + + protected componentDidLoad() { + if (!this.pullingIcon) { + this.pullingIcon = this.config.get('ionPullIcon', 'arrow-down'); + } + if (!this.refreshingSpinner) { + this.refreshingSpinner = this.config.get('ionRefreshingSpinner', this.config.get('spinner', 'lines')); + } + } + + protected render() { + return [ +
+ {this.pullingIcon && +
+ +
+ } + {this.pullingText && +
+ } +
, +
+ {this.refreshingSpinner && +
+ +
+ } + {this.refreshingText && +
+ } +
+ ]; + } +} diff --git a/packages/core/src/components/refresher/readme.md b/packages/core/src/components/refresher/readme.md new file mode 100644 index 0000000000..55d8fb50b8 --- /dev/null +++ b/packages/core/src/components/refresher/readme.md @@ -0,0 +1,105 @@ +# ion-refresher-content + + + + + + +## Properties + +#### closeDuration + +string + + +#### enabled + +boolean + + +#### pullDelta + +number + + +#### pullMin + +number + + +#### snapbackDuration + +string + + +## Attributes + +#### closeDuration + +string + + +#### enabled + +boolean + + +#### pullDelta + +number + + +#### pullMin + +number + + +#### snapbackDuration + +string + + +## Events + +#### ionPull + + +#### ionRefresh + + +#### ionStart + + +## Methods + +#### cancel() + +Changes the refresher's state from `refreshing` to `cancelling`. + + +#### complete() + +Call `complete()` when your async operation has completed. +For example, the `refreshing` state is while the app is performing +an asynchronous operation, such as receiving more data from an +AJAX request. Once the data has been received, you then call this +method to signify that the refreshing has completed and to close +the refresher. This method also changes the refresher's state from +`refreshing` to `completing`. + + +#### getProgress() + +A number representing how far down the user has pulled. +The number `0` represents the user hasn't pulled down at all. The +number `1`, and anything greater than `1`, represents that the user +has pulled far enough down that when they let go then the refresh will +happen. If they let go and the number is less than `1`, then the +refresh will not happen, and the content will return to it's original +position. + + + +---------------------------------------------- + +*Built by [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/refresher/refresher.scss b/packages/core/src/components/refresher/refresher.scss new file mode 100644 index 0000000000..d56cf92ae7 --- /dev/null +++ b/packages/core/src/components/refresher/refresher.scss @@ -0,0 +1,146 @@ +@import "../../themes/ionic.globals"; + +// Refresher +// -------------------------------------------------- + +/// @prop - Height of the refresher +$refresher-height: 60px !default; + +/// @prop - Color of the refresher icon +$refresher-icon-color: #000 !default; + +/// @prop - Font size of the refresher icon +$refresher-icon-font-size: 30px !default; + +/// @prop - Text color of the refresher content +$refresher-text-color: #000 !default; + +/// @prop - Font size of the refresher content +$refresher-text-font-size: 16px !default; + +/// @prop - Border color of the refresher +$refresher-border-color: #ddd !default; + + +ion-refresher { + @include position(0, null, null, 0); + + position: absolute; + top: 0; + z-index: $z-index-refresher; + + display: none; + + width: 100%; + height: $refresher-height; + + &.refresher-active { + display: block; + } +} + +.has-refresher > .scroll-content { + // when the refresher is let go or has completed + // this transition is what is used to put the + // scroll content back into it's original location + @include margin(-1px, null, null, null); + + border-top: 1px solid $refresher-border-color; + transition: transform 320ms cubic-bezier(.36, .66, .04, 1); +} + + +// Refresher Content +// -------------------------------------------------- + +ion-refresher-content { + display: flex; + + flex-direction: column; + justify-content: center; + + height: 100%; +} + +.refresher-pulling, +.refresher-refreshing { + display: none; + + width: 100%; +} + +.refresher-pulling-icon, +.refresher-refreshing-icon { + @include text-align(center); + @include transform-origin(center); + + font-size: $refresher-icon-font-size; + color: $refresher-icon-color; + transition: 200ms; +} + +.refresher-pulling-text, +.refresher-refreshing-text { + @include text-align(center); + + font-size: $refresher-text-font-size; + color: $refresher-text-color; +} + +.refresher-refreshing .spinner-ios line, +.refresher-refreshing .spinner-ios-small line, +.refresher-refreshing .spinner-crescent circle { + stroke: $refresher-icon-color; +} + +.refresher-refreshing .spinner-bubbles circle, +.refresher-refreshing .spinner-circles circle, +.refresher-refreshing .spinner-dots circle { + fill: $refresher-icon-color; +} + + +// Refresher Content States +// -------------------------------------------------- + +.refresher-pulling ion-refresher-content { + .refresher-pulling { + display: block; + } +} + +.refresher-ready ion-refresher-content { + .refresher-pulling { + display: block; + } + + .refresher-pulling-icon { + transform: rotate(180deg); + } +} + +.refresher-refreshing ion-refresher-content { + .refresher-refreshing { + display: block; + } +} + +.refresher-cancelling ion-refresher-content { + .refresher-pulling { + display: block; + } + + .refresher-pulling-icon { + transform: scale(0); + } +} + +.refresher-completing ion-refresher-content { + .refresher-refreshing { + display: block; + } + + .refresher-refreshing-icon { + transform: scale(0); + } +} diff --git a/packages/core/src/components/refresher/refresher.tsx b/packages/core/src/components/refresher/refresher.tsx new file mode 100644 index 0000000000..77be55d90b --- /dev/null +++ b/packages/core/src/components/refresher/refresher.tsx @@ -0,0 +1,448 @@ +import { Component, Element, Event, EventEmitter, Method, Prop, State } from '@stencil/core'; +import { DomController, GestureDetail } from '../../index'; + +const enum RefresherState { + Inactive = 1 << 0, + Pulling = 1 << 1, + Ready = 1 << 2, + Refreshing = 1 << 3, + Cancelling = 1 << 4, + Completing = 1 << 5, + + _BUSY_ = Refreshing | Cancelling | Completing, +} + +/** + * @name Refresher + * @description + * The Refresher provides pull-to-refresh functionality on a content component. + * Place the `ion-refresher` as the first child of your `ion-content` element. + * + * Pages can then listen to the refresher's various output events. The + * `refresh` output event is fired when the user has pulled down far + * enough to kick off the refreshing process. Once the async operation + * has completed and the refreshing should end, call `complete()`. + * + * Note: Do not wrap the `ion-refresher` in a `*ngIf`. It will not render + * properly this way. Please use the `enabled` property instead to + * display or hide the refresher. + * + * @usage + * ```html + * + * + * + * + * + * + * + * ``` + * + * ```ts + * @Component({...}) + * export class NewsFeedPage { + * + * doRefresh(refresher) { + * console.log('Begin async operation', refresher); + * + * setTimeout(() => { + * console.log('Async operation has ended'); + * refresher.complete(); + * }, 2000); + * } + * + * } + * ``` + * + * + * ## Refresher Content + * + * By default, Ionic provides the pulling icon and refreshing spinner that + * looks best for the platform the user is on. However, you can change the + * default icon and spinner, along with adding text for each state by + * adding properties to the child `ion-refresher-content` component. + * + * ```html + * + * + * + * + * + * + * + * + * ``` + * + * + * ## Further Customizing Refresher Content + * + * The `ion-refresher` component holds the refresh logic. + * It requires a child component in order to display the content. + * Ionic uses `ion-refresher-content` by default. This component + * displays the refresher and changes the look depending + * on the refresher's state. Separating these components + * allows developers to create their own refresher content + * components. You could replace our default content with + * custom SVG or CSS animations. + * + * @demo /docs/demos/src/refresher/ + * + */ +@Component({ + tag: 'ion-refresher', + styleUrl: 'refresher.scss' +}) +export class Refresher { + + private appliedStyles = false; + private didStart = false; + private gestureConfig: any; + private progress = 0; + + scrollEl: HTMLElement; + + @Prop({ context: 'dom' }) dom: DomController; + + /** + * The current state which the refresher is in. The refresher's states include: + * + * - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden. + * - `pulling` - The user is actively pulling down the refresher, but has not reached the point yet that if the user lets go, it'll refresh. + * - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed. + * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state. + * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state. + * - `completing` - The `refreshing` state has finished and the refresher is in the process of closing itself. Once closed, the refresher will go back to the `inactive` state. + */ + @State() state: RefresherState = RefresherState.Inactive; + + + @Element() el: HTMLElement; + + /** + * @input {number} The min distance the user must pull down until the + * refresher can go into the `refreshing` state. Default is `60`. + */ + @Prop() pullMin = 60; + + /** + * @input {number} The maximum distance of the pull until the refresher + * will automatically go into the `refreshing` state. By default, the pull + * maximum will be the result of `pullMin + 60`. + */ + @Prop() pullDelta = 60; + + // TODO: NEVER USED + /** + * @input {number} Time it takes to close the refresher. Default is `280ms`. + */ + @Prop() closeDuration = '280ms'; + + /** + * @input {string} Time it takes the refresher to to snap back to the `refreshing` state. Default is `280ms`. + */ + @Prop() snapbackDuration = '280ms'; + + /** + * @input {boolean} If the refresher is enabled or not. This should be used in place of an `ngIf`. Default is `true`. + */ + @Prop() enabled = false; + + /** + * @output {event} Emitted when the user lets go and has pulled down + * far enough, which would be farther than the `pullMin`, then your refresh hander if + * fired and the state is updated to `refreshing`. From within your refresh handler, + * you must call the `complete()` method when your async operation has completed. + */ + @Event() ionRefresh: EventEmitter; + + /** + * @output {event} Emitted while the user is pulling down the content and exposing the refresher. + */ + @Event() ionPull: EventEmitter; + + /** + * @output {event} Emitted when the user begins to start pulling down. + */ + @Event() ionStart: EventEmitter; + + constructor() { + this.gestureConfig = { + 'canStart': this.canStart.bind(this), + 'onStart': this.onStart.bind(this), + 'onMove': this.onMove.bind(this), + 'onEnd': this.onEnd.bind(this), + 'gestureName': 'refresher', + 'gesturePriority': 10, + 'type': 'pan', + // 'passive': false, + 'direction': 'y', + 'threshold': 0, + 'attachTo': 'body' + }; + } + + componentDidLoad() { + if (this.el.getAttribute('slot') !== 'fixed') { + console.error('Make sure you use: '); + return; + } + this.scrollEl = this.el.parentElement.querySelector('ion-scroll') as HTMLElement; + if (!this.scrollEl) { + console.error('ion-refresher didn\'t attached, make sure if parent is a ion-content'); + } + } + + componentDidUnload() { + this.scrollEl = null; + } + + /** + * Call `complete()` when your async operation has completed. + * For example, the `refreshing` state is while the app is performing + * an asynchronous operation, such as receiving more data from an + * AJAX request. Once the data has been received, you then call this + * method to signify that the refreshing has completed and to close + * the refresher. This method also changes the refresher's state from + * `refreshing` to `completing`. + */ + @Method() + complete() { + this.close(RefresherState.Completing, '120ms'); + } + + /** + * Changes the refresher's state from `refreshing` to `cancelling`. + */ + @Method() + cancel() { + this.close(RefresherState.Cancelling, ''); + } + + /** + * A number representing how far down the user has pulled. + * The number `0` represents the user hasn't pulled down at all. The + * number `1`, and anything greater than `1`, represents that the user + * has pulled far enough down that when they let go then the refresh will + * happen. If they let go and the number is less than `1`, then the + * refresh will not happen, and the content will return to it's original + * position. + */ + @Method() + getProgress() { + return this.progress; + } + + private canStart(): boolean { + if (!this.scrollEl) { + return false; + } + if (this.state !== RefresherState.Inactive) { + return false; + } + // if the scrollTop is greater than zero then it's + // not possible to pull the content down yet + if (this.scrollEl.scrollTop > 0) { + return false; + } + return true; + } + + private onStart() { + this.progress = 0; + this.state = RefresherState.Inactive; + } + + private onMove(detail: GestureDetail) { + // this method can get called like a bazillion times per second, + // so it's built to be as efficient as possible, and does its + // best to do any DOM read/writes only when absolutely necessary + // if multitouch then get out immediately + const ev = detail.event as TouchEvent; + if (ev.touches && ev.touches.length > 1) { + return 1; + } + + // do nothing if it's actively refreshing + // or it's in the process of closing + // or this was never a startY + if (this.state & RefresherState._BUSY_) { + return 2; + } + + const deltaY = detail.deltaY; + // don't bother if they're scrolling up + // and have not already started dragging + if (deltaY <= 0) { + // the current Y is higher than the starting Y + // so they scrolled up enough to be ignored + this.progress = 0; + this.state = RefresherState.Inactive; + + if (this.appliedStyles) { + // reset the styles only if they were applied + this.setCss(0, '', false, ''); + return 5; + } + + return 6; + } + + if (this.state === RefresherState.Inactive) { + // this refresh is not already actively pulling down + // get the content's scrollTop + const scrollHostScrollTop = this.scrollEl.scrollTop; + + // if the scrollTop is greater than zero then it's + // not possible to pull the content down yet + if (scrollHostScrollTop > 0) { + this.progress = 0; + return 7; + } + + // content scrolled all the way to the top, and dragging down + this.state = RefresherState.Pulling; + } + + // prevent native scroll events + ev.preventDefault(); + + // the refresher is actively pulling at this point + // move the scroll element within the content element + this.setCss(deltaY, '0ms', true, ''); + + if (!deltaY) { + // don't continue if there's no delta yet + this.progress = 0; + return 8; + } + + const pullMin = this.pullMin; + // set pull progress + this.progress = deltaY / pullMin; + + // emit "start" if it hasn't started yet + if (!this.didStart) { + this.didStart = true; + this.ionStart.emit(this); + } + + // emit "pulling" on every move + this.ionPull.emit(this); + + // do nothing if the delta is less than the pull threshold + if (deltaY < pullMin) { + // ensure it stays in the pulling state, cuz its not ready yet + this.state = RefresherState.Pulling; + return 2; + } + + if (deltaY > pullMin + this.pullDelta) { + // they pulled farther than the max, so kick off the refresh + this.beginRefresh(); + return 3; + } + + // pulled farther than the pull min!! + // it is now in the `ready` state!! + // if they let go then it'll refresh, kerpow!! + this.state = RefresherState.Ready; + + return 4; + } + + private onEnd() { + // only run in a zone when absolutely necessary + if (this.state === RefresherState.Ready) { + // they pulled down far enough, so it's ready to refresh + this.beginRefresh(); + + } else if (this.state === RefresherState.Pulling) { + // they were pulling down, but didn't pull down far enough + // set the content back to it's original location + // and close the refresher + // set that the refresh is actively cancelling + this.cancel(); + } + } + + private beginRefresh() { + // assumes we're already back in a zone + // they pulled down far enough, so it's ready to refresh + this.state = RefresherState.Refreshing; + + // place the content in a hangout position while it thinks + this.setCss(this.pullMin, this.snapbackDuration, true, ''); + + // emit "refresh" because it was pulled down far enough + // and they let go to begin refreshing + this.ionRefresh.emit(this); + } + + private close(state: RefresherState, delay: string) { + let timer: number; + + function close(ev: TransitionEvent) { + // closing is done, return to inactive state + if (ev) { + clearTimeout(timer); + } + + this.state = RefresherState.Inactive; + this.progress = 0; + this.didStart = false; + this.setCss(0, '0ms', false, ''); + } + + // create fallback timer incase something goes wrong with transitionEnd event + timer = setTimeout(close.bind(this), 600); + + // create transition end event on the content's scroll element + // TODO: what is this? + // this.scrollEl.onScrollElementTransitionEnd(close.bind(this)); + + // reset set the styles on the scroll element + // set that the refresh is actively cancelling/completing + this.state = state; + this.setCss(0, '', true, delay); + + // TODO: stop gesture + // if (this._pointerEvents) { + // this._pointerEvents.stop(); + // } + } + + private setCss(y: number, duration: string, overflowVisible: boolean, delay: string) { + this.appliedStyles = (y > 0); + this.dom.write(() => { + const style = this.scrollEl.style; + style.transform = ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)'); + style.transitionDuration = duration; + style.transitionDelay = delay; + style.overflow = (overflowVisible ? 'hidden' : ''); + }); + } + + hostData() { + return { + class: { + 'refresher-active': this.state !== RefresherState.Inactive, + 'refresher-pulling': this.state === RefresherState.Pulling, + 'refresher-ready': this.state === RefresherState.Ready, + 'refresher-refreshing': this.state === RefresherState.Refreshing, + 'refresher-cancelling': this.state === RefresherState.Cancelling, + 'refresher-completing': this.state === RefresherState.Completing + } + }; + } + + render() { + return + + ; + } +} diff --git a/packages/core/src/components/refresher/test/basic.html b/packages/core/src/components/refresher/test/basic.html new file mode 100644 index 0000000000..0d5dd678d6 --- /dev/null +++ b/packages/core/src/components/refresher/test/basic.html @@ -0,0 +1,72 @@ + + + + + + Ionic Item Sliding + + + + + + + + + + + Ionic CDN demo + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index e8ce75bb91..073876fcba 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -37,6 +37,7 @@ exports.config = { { components: ['ion-spinner'] }, { components: ['ion-split-pane'] }, { components: ['ion-range', 'ion-range-knob']}, + { components: ['ion-refresher', 'ion-refresher-content']}, { components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button'] }, { components: ['ion-toggle'] }, { components: ['ion-toast', 'ion-toast-controller'] },