diff --git a/core/src/components/refresher/refresher.ios.scss b/core/src/components/refresher/refresher.ios.scss index cc6a925e3f..a29d743d53 100644 --- a/core/src/components/refresher/refresher.ios.scss +++ b/core/src/components/refresher/refresher.ios.scss @@ -30,22 +30,27 @@ ion-refresher.refresher-native { display: block; z-index: 1; - + ion-spinner { @include margin(0, auto, 0, auto); } } -.refresher-native { - .refresher-refreshing ion-spinner { - --refreshing-rotation-duration: 2s; - display: none; - animation: var(--refreshing-rotation-duration) ease-out refresher-rotate forwards; - } - .refresher-refreshing { - display: none; - animation: 250ms linear refresher-pop forwards; - } +.refresher-native .refresher-refreshing ion-spinner { + --refreshing-rotation-duration: 2s; + display: none; + animation: var(--refreshing-rotation-duration) ease-out refresher-rotate forwards; +} +.refresher-native .refresher-refreshing { + display: none; + animation: 250ms linear refresher-pop forwards; +} + +.refresher-native ion-spinner { + width: #{$refresher-ios-native-spinner-width}; + height: #{$refresher-ios-native-spinner-height}; + + color: #{$refresher-ios-native-spinner-color}; } .refresher-native.refresher-refreshing, @@ -67,6 +72,12 @@ ion-refresher.refresher-native { } } +.refresher-native.refresher-completing ion-refresher-content .refresher-refreshing-icon { + transform: scale(0) rotate(180deg); + + transition: 300ms; +} + @keyframes refresher-pop { 0% { transform: scale(1); @@ -88,4 +99,4 @@ ion-refresher.refresher-native { to { transform: rotate(180deg); } -} \ No newline at end of file +} diff --git a/core/src/components/refresher/refresher.ios.vars.scss b/core/src/components/refresher/refresher.ios.vars.scss index 5f98c15b18..fbc1f7c432 100644 --- a/core/src/components/refresher/refresher.ios.vars.scss +++ b/core/src/components/refresher/refresher.ios.vars.scss @@ -1,7 +1,16 @@ @import "../../themes/ionic.globals.ios"; /// @prop - Color of the refresher icon -$refresher-ios-icon-color: $text-color !default; +$refresher-ios-icon-color: $text-color !default; /// @prop - Text color of the refresher content -$refresher-ios-text-color: $text-color !default; +$refresher-ios-text-color: $text-color !default; + +/// @prop - Color of the native refresher spinner +$refresher-ios-native-spinner-color: var(--ion-color-step-450, #747577) !default; + +/// @prop - Width of the native refresher spinner +$refresher-ios-native-spinner-width: 32px !default; + +/// @prop - Height of the native refresher spinner +$refresher-ios-native-spinner-height: 32px !default; diff --git a/core/src/components/refresher/refresher.scss b/core/src/components/refresher/refresher.scss index b0b4c5febc..29949ebc1c 100644 --- a/core/src/components/refresher/refresher.scss +++ b/core/src/components/refresher/refresher.scss @@ -112,4 +112,4 @@ ion-refresher-content .arrow-container { .refresher-pulling-text, .refresher-refreshing-text { display: none; } -} \ No newline at end of file +} diff --git a/core/src/components/refresher/refresher.tsx b/core/src/components/refresher/refresher.tsx index c15f800ee3..08af0a2703 100644 --- a/core/src/components/refresher/refresher.tsx +++ b/core/src/components/refresher/refresher.tsx @@ -6,7 +6,17 @@ import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { clamp, componentOnReady, getElementRoot, raf } from '../../utils/helpers'; import { hapticImpact } from '../../utils/native/haptic'; -import { createPullingAnimation, createSnapBackAnimation, getRefresherAnimationType, handleScrollWhilePulling, handleScrollWhileRefreshing, setSpinnerOpacity, shouldUseNativeRefresher, transitionEndAsync, translateElement } from './refresher.utils'; +import { + createPullingAnimation, + createSnapBackAnimation, + getRefresherAnimationType, + handleScrollWhilePulling, + handleScrollWhileRefreshing, + setSpinnerOpacity, + shouldUseNativeRefresher, + transitionEndAsync, + translateElement +} from './refresher.utils'; @Component({ tag: 'ion-refresher', @@ -147,7 +157,7 @@ export class Refresher implements ComponentInterface { this.state = state; if (getIonMode(this) === 'ios') { - await translateElement(el, undefined); + await translateElement(el, undefined, 300); } else { await transitionEndAsync(this.el.querySelector('.refresher-refreshing-icon'), 200); } @@ -190,7 +200,6 @@ export class Refresher implements ComponentInterface { return; } - writeTask(() => setSpinnerOpacity(pullingSpinner, 0)); return; } @@ -206,11 +215,16 @@ export class Refresher implements ComponentInterface { } } - // delay showing the next tick marks until user has pulled 30px - const opacity = clamp(0, Math.abs(scrollTop) / refresherHeight, 0.99); - const pullAmount = this.progress = clamp(0, (Math.abs(scrollTop) - 30) / MAX_PULL, 1); - const currentTickToShow = clamp(0, Math.floor(pullAmount * NUM_TICKS), NUM_TICKS - 1); - const shouldShowRefreshingSpinner = this.state === RefresherState.Refreshing || currentTickToShow === NUM_TICKS - 1; + /** + * We want to delay the start of this gesture by ~30px + * when initially pulling down so the refresher does not + * overlap with the content. But when letting go of the + * gesture before the refresher completes, we want the + * refresher tick marks to quickly fade out. + */ + const offset = (this.didStart) ? 30 : 0; + const pullAmount = this.progress = clamp(0, (Math.abs(scrollTop) - offset) / MAX_PULL, 1); + const shouldShowRefreshingSpinner = this.state === RefresherState.Refreshing || pullAmount === 1; if (shouldShowRefreshingSpinner) { if (this.pointerDown) { @@ -232,7 +246,7 @@ export class Refresher implements ComponentInterface { } } else { this.state = RefresherState.Pulling; - handleScrollWhilePulling(pullingSpinner, ticks, opacity, currentTickToShow); + handleScrollWhilePulling(ticks, NUM_TICKS, pullAmount); } }); }; diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index d7b073a165..bcf7c90551 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -1,7 +1,7 @@ import { writeTask } from '@stencil/core'; import { createAnimation } from '../../utils/animation/animation'; -import { componentOnReady } from '../../utils/helpers'; +import { clamp, componentOnReady } from '../../utils/helpers'; import { isPlatform } from '../../utils/platform'; // MD Native Refresher @@ -124,14 +124,26 @@ export const setSpinnerOpacity = (spinner: HTMLElement, opacity: number) => { }; export const handleScrollWhilePulling = ( - spinner: HTMLElement, ticks: NodeListOf, - opacity: number, - currentTickToShow: number + numTicks: number, + pullAmount: number ) => { + const max = 1; writeTask(() => { - setSpinnerOpacity(spinner, opacity); - ticks.forEach((el, i) => el.style.setProperty('opacity', (i <= currentTickToShow) ? '0.99' : '0')); + ticks.forEach((el, i) => { + /** + * Compute the opacity of each tick + * mark as a percentage of the pullAmount + * offset by max / numTicks so + * the tick marks are shown staggered. + */ + const min = i * (max / numTicks); + const range = max - min; + const start = pullAmount - min; + const progression = clamp(0, start / range, 1); + + el.style.setProperty('opacity', progression.toString()); + }); }); }; @@ -146,13 +158,13 @@ export const handleScrollWhileRefreshing = ( }); }; -export const translateElement = (el?: HTMLElement, value?: string) => { +export const translateElement = (el?: HTMLElement, value?: string, duration = 200) => { if (!el) { return Promise.resolve(); } - const trans = transitionEndAsync(el, 200); + const trans = transitionEndAsync(el, duration); writeTask(() => { - el.style.setProperty('transition', '0.2s all ease-out'); + el.style.setProperty('transition', `${duration}ms all ease-out`); if (value === undefined) { el.style.removeProperty('transform');