Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,8 @@ ion-refresher,method,cancel,cancel() => Promise<void>
ion-refresher,method,complete,complete() => Promise<void>
ion-refresher,method,getProgress,getProgress() => Promise<number>
ion-refresher,event,ionPull,void,true
ion-refresher,event,ionPullEnd,RefresherPullEndEventDetail,true
ion-refresher,event,ionPullStart,void,true
ion-refresher,event,ionRefresh,RefresherEventDetail,true
ion-refresher,event,ionStart,void,true

Expand Down
21 changes: 16 additions & 5 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { PickerButton, PickerColumn } from "./components/picker-legacy/picker-in
import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from "./components/popover/popover-interface";
import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
import { RefresherEventDetail } from "./components/refresher/refresher-interface";
import { RefresherEventDetail, RefresherPullEndEventDetail } from "./components/refresher/refresher-interface";
import { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
Expand Down Expand Up @@ -67,7 +67,7 @@ export { PickerButton, PickerColumn } from "./components/picker-legacy/picker-in
export { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from "./components/popover/popover-interface";
export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
export { RefresherEventDetail } from "./components/refresher/refresher-interface";
export { RefresherEventDetail, RefresherPullEndEventDetail } from "./components/refresher/refresher-interface";
export { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
Expand Down Expand Up @@ -2745,7 +2745,7 @@ export namespace Components {
*/
"mode"?: "ios" | "md";
/**
* How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example: If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher.
* How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example, If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher.
* @default 1
*/
"pullFactor": number;
Expand Down Expand Up @@ -4749,6 +4749,8 @@ declare global {
"ionRefresh": RefresherEventDetail;
"ionPull": void;
"ionStart": void;
"ionPullStart": void;
"ionPullEnd": RefresherPullEndEventDetail;
}
interface HTMLIonRefresherElement extends Components.IonRefresher, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonRefresherElementEventMap>(type: K, listener: (this: HTMLIonRefresherElement, ev: IonRefresherCustomEvent<HTMLIonRefresherElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
Expand Down Expand Up @@ -8009,16 +8011,25 @@ declare namespace LocalJSX {
* Emitted while the user is pulling down the content and exposing the refresher.
*/
"onIonPull"?: (event: IonRefresherCustomEvent<void>) => void;
/**
* Emitted when the refresher has returned to the inactive state after a pull gesture. This fires whether the refresh completed successfully or was canceled.
*/
"onIonPullEnd"?: (event: IonRefresherCustomEvent<RefresherPullEndEventDetail>) => void;
/**
* Emitted when the user begins to start pulling down.
*/
"onIonPullStart"?: (event: IonRefresherCustomEvent<void>) => void;
/**
* Emitted when the user lets go of the content and has pulled down further than the `pullMin` or pulls the content down and exceeds the pullMax. Updates the refresher state to `refreshing`. The `complete()` method should be called when the async operation has completed.
*/
"onIonRefresh"?: (event: IonRefresherCustomEvent<RefresherEventDetail>) => void;
/**
* Emitted when the user begins to start pulling down.
* Emitted when the user begins to start pulling down. TODO(FW-7044): Remove this in a major release
* @deprecated Use `ionPullStart` instead.
*/
"onIonStart"?: (event: IonRefresherCustomEvent<void>) => void;
/**
* How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example: If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher.
* How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example, If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher.
* @default 1
*/
"pullFactor"?: number;
Expand Down
9 changes: 9 additions & 0 deletions core/src/components/refresher/refresher-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ export interface RefresherEventDetail {
complete(): void;
}

export interface RefresherPullEndEventDetail {
reason: 'complete' | 'cancel';
}

export interface RefresherCustomEvent extends CustomEvent {
detail: RefresherEventDetail;
target: HTMLIonRefresherElement;
}

export interface RefresherPullEndCustomEvent extends CustomEvent {
detail: RefresherPullEndEventDetail;
target: HTMLIonRefresherElement;
}
50 changes: 47 additions & 3 deletions core/src/components/refresher/refresher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ImpactStyle, hapticImpact } from '@utils/native/haptic';
import { getIonMode } from '../../global/ionic-global';
import type { Animation, Gesture, GestureDetail } from '../../interface';

import type { RefresherEventDetail } from './refresher-interface';
import type { RefresherEventDetail, RefresherPullEndEventDetail } from './refresher-interface';
import {
createPullingAnimation,
createSnapBackAnimation,
Expand Down Expand Up @@ -107,8 +107,8 @@ export class Refresher implements ComponentInterface {
* than `1`. The default value is `1` which is equal to the speed of the cursor.
* If a negative value is passed in, the factor will be `1` instead.
*
* For example: If the value passed is `1.2` and the content is dragged by
* `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
* For example, If the value passed is `1.2` and the content is dragged by
* `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
* (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
* will be `8` pixels, less than the amount the cursor has moved.
*
Expand Down Expand Up @@ -143,9 +143,24 @@ export class Refresher implements ComponentInterface {

/**
* Emitted when the user begins to start pulling down.
* TODO(FW-7044): Remove this in a major release
*
* @deprecated Use `ionPullStart` instead.
*/
@Event() ionStart!: EventEmitter<void>;

/**
* Emitted when the user begins to start pulling down.
*/
@Event() ionPullStart!: EventEmitter<void>;

/**
* Emitted when the refresher has returned to the inactive state
* after a pull gesture. This fires whether the refresh completed
* successfully or was canceled.
*/
@Event() ionPullEnd!: EventEmitter<RefresherPullEndEventDetail>;

private async checkNativeRefresher() {
const useNativeRefresher = await shouldUseNativeRefresher(this.el, getIonMode(this));
if (useNativeRefresher && !this.nativeRefresher) {
Expand Down Expand Up @@ -182,6 +197,10 @@ export class Refresher implements ComponentInterface {
this.progress = 0;

this.state = RefresherState.Inactive;

this.ionPullEnd.emit({
reason: state === RefresherState.Completing ? 'complete' : 'cancel',
Comment thread
thetaPC marked this conversation as resolved.
});
}

private async setupiOSNativeRefresher(
Expand Down Expand Up @@ -224,6 +243,7 @@ export class Refresher implements ComponentInterface {
if (!this.didStart) {
this.didStart = true;
this.ionStart.emit();
this.ionPullStart.emit();
}

// emit "pulling" on every move
Expand Down Expand Up @@ -308,6 +328,7 @@ export class Refresher implements ComponentInterface {
this.lastVelocityY = ev.velocityY;
},
onEnd: () => {
const hadStarted = this.didStart;
Comment thread
thetaPC marked this conversation as resolved.
this.pointerDown = false;
this.didStart = false;

Expand All @@ -316,6 +337,12 @@ export class Refresher implements ComponentInterface {
this.needsCompletion = false;
} else if (this.didRefresh) {
readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
} else if (hadStarted) {
/**
* User started pulling but released before reaching the refresh threshold.
* Emit ionPullEnd to complete the event pair.
*/
this.ionPullEnd.emit({ reason: 'cancel' });
}
},
});
Expand Down Expand Up @@ -378,6 +405,7 @@ export class Refresher implements ComponentInterface {
ev.data.animation = animation;
animation.progressStart(false, 0);
this.ionStart.emit();
this.ionPullStart.emit();
this.animations.push(animation);

return;
Expand Down Expand Up @@ -405,6 +433,7 @@ export class Refresher implements ComponentInterface {
this.animations = [];
this.gesture!.enable(true);
this.state = RefresherState.Inactive;
this.ionPullEnd.emit({ reason: 'cancel' });
});
return;
}
Expand Down Expand Up @@ -684,6 +713,7 @@ export class Refresher implements ComponentInterface {
if (!this.didStart) {
this.didStart = true;
this.ionStart.emit();
this.ionPullStart.emit();
}

// emit "pulling" on every move
Expand Down Expand Up @@ -731,6 +761,16 @@ export class Refresher implements ComponentInterface {
* available right away.
*/
this.restoreOverflowStyle();

/**
* If ionPullStart was emitted, we need to emit ionPullEnd
* even though the gesture was aborted before reaching the
* pulling threshold.
*/
if (this.didStart) {
this.didStart = false;
this.ionPullEnd.emit({ reason: 'cancel' });
}
}
}

Expand Down Expand Up @@ -783,6 +823,10 @@ export class Refresher implements ComponentInterface {
if (this.contentFullscreen && this.backgroundContentEl) {
this.backgroundContentEl?.style.removeProperty('--offset-top');
}

this.ionPullEnd.emit({
reason: state === RefresherState.Completing ? 'complete' : 'cancel',
});
}, 600);

// reset the styles on the scroll element
Expand Down
11 changes: 11 additions & 0 deletions core/src/components/refresher/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@
window.dispatchEvent(new CustomEvent('ionRefreshComplete'));
});

// Event listeners for new ionPullStart and ionPullEnd events
refresher.addEventListener('ionPullStart', function () {
console.log('ionPullStart fired');
window.dispatchEvent(new CustomEvent('ionPullStartFired'));
});

refresher.addEventListener('ionPullEnd', function (event) {
console.log('ionPullEnd fired', event.detail);
window.dispatchEvent(new CustomEvent('ionPullEndFired', { detail: event.detail }));
});

function render() {
let html = '';
for (let item of items) {
Expand Down
61 changes: 60 additions & 1 deletion core/src/components/refresher/test/basic/refresher.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import { configs, dragElementByYAxis, test } from '@utils/test/playwright';

import { pullToRefresh } from '../test.utils';

Expand All @@ -22,6 +22,41 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {

expect(await items.count()).toBe(60);
});

test('should emit ionPullStart and ionPullEnd with reason complete', async ({ page }) => {
await page.locator('ion-refresher.hydrated').waitFor({ state: 'attached' });
Comment thread
thetaPC marked this conversation as resolved.
Outdated

const ionPullStartEvent = await page.spyOnEvent('ionPullStartFired');
const ionPullEndEvent = await page.spyOnEvent('ionPullEndFired');

await pullToRefresh(page);

// Wait for the close animation timeout (600ms) to complete
Comment thread
thetaPC marked this conversation as resolved.
Outdated
await page.waitForTimeout(700);

expect(ionPullStartEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventDetail({ reason: 'complete' });
});

test('should emit ionPullEnd with reason cancel when pull is released early', async ({ page }) => {
const target = page.locator('body');

await page.locator('ion-refresher.hydrated').waitFor({ state: 'attached' });
Comment thread
thetaPC marked this conversation as resolved.
Outdated

const ionPullStartEvent = await page.spyOnEvent('ionPullStartFired');
const ionPullEndEvent = await page.spyOnEvent('ionPullEndFired');

// Pull down only 40px (less than pullMin of 60px) to trigger cancel
await dragElementByYAxis(target, page, 40);

// Wait for the cancel animation to complete
await page.waitForTimeout(700);

expect(ionPullStartEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventDetail({ reason: 'cancel' });
});
});

test.describe('native refresher', () => {
Expand All @@ -41,6 +76,30 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {

expect(await items.count()).toBe(60);
});

test('should emit ionPullStart and ionPullEnd with reason complete', async ({ page }) => {
const refresherContent = page.locator('ion-refresher-content');
refresherContent.evaluateHandle((el: any) => {
// Resets the pullingIcon to enable the native refresher
el.pullingIcon = undefined;
});

await page.waitForChanges();

await page.locator('ion-refresher.hydrated').waitFor({ state: 'attached' });
Comment thread
thetaPC marked this conversation as resolved.
Outdated

const ionPullStartEvent = await page.spyOnEvent('ionPullStartFired');
const ionPullEndEvent = await page.spyOnEvent('ionPullEndFired');

await pullToRefresh(page);

// Wait for the reset animation to complete (native refresher takes longer due to CSS transitions)
await page.waitForTimeout(1500);

expect(ionPullStartEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventTimes(1);
expect(ionPullEndEvent).toHaveReceivedEventDetail({ reason: 'complete' });
});
});
});
});
2 changes: 1 addition & 1 deletion core/src/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export { PopoverOptions } from './components/popover/popover-interface';
export { RadioGroupCustomEvent } from './components/radio-group/radio-group-interface';
export { RangeCustomEvent, PinFormatter } from './components/range/range-interface';
export { RouterCustomEvent } from './components/router/utils/interface';
export { RefresherCustomEvent } from './components/refresher/refresher-interface';
export { RefresherCustomEvent, RefresherPullEndCustomEvent } from './components/refresher/refresher-interface';
export {
ItemReorderCustomEvent,
ReorderEndCustomEvent,
Expand Down
14 changes: 13 additions & 1 deletion packages/angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1810,12 +1810,13 @@ export class IonRefresher {
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionRefresh', 'ionPull', 'ionStart']);
proxyOutputs(this, this.el, ['ionRefresh', 'ionPull', 'ionStart', 'ionPullStart', 'ionPullEnd']);
}
}


import type { RefresherEventDetail as IIonRefresherRefresherEventDetail } from '@ionic/core';
import type { RefresherPullEndEventDetail as IIonRefresherRefresherPullEndEventDetail } from '@ionic/core';

export declare interface IonRefresher extends Components.IonRefresher {
/**
Expand All @@ -1831,8 +1832,19 @@ called when the async operation has completed.
ionPull: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the user begins to start pulling down.
TODO(FW-7044): Remove this in a major release @deprecated Use `ionPullStart` instead.
*/
ionStart: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the user begins to start pulling down.
*/
ionPullStart: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the refresher has returned to the inactive state
after a pull gesture. This fires whether the refresh completed
successfully or was canceled.
*/
ionPullEnd: EventEmitter<CustomEvent<IIonRefresherRefresherPullEndEventDetail>>;
}


Expand Down
Loading
Loading