Skip to content
Open
Show file tree
Hide file tree
Changes from all 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',
});
}

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;
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' });

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

await pullToRefresh(page);

// Wait for the close animation timeout (600ms) to complete
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' });

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' });

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