feat(reorder-group): add ionReorderStart, ionReorderMove, ionReorderEnd events (#30471)

Issue number: resolves #23148 resolves #27614

---------

The `ion-reorder-group` only emits an `ionItemReorder` event when the reorder gesture ends AND the item position has changed. There is no way to listen for when the gesture starts, is actively moving, or ends without the item changing position.

- Adds an `ionReorderStart` event that is fired without any details on the start of the gesture.
- Adds an `ionReorderMove` event that is fired continuously during gesture move and includes the `from` and `to` detail.
- Adds an `ionReorderEnd` event that is fired at the end of the gesture and always includes the `from` and `to` detail, even if they are the same.
- Deprecates the `ionItemReorder` event, recommending to use the `ionReorderEnd` instead.

- [ ] Yes
- [x] No

While this does not introduce a breaking change, it does deprecate the `ionItemReorder` event in favor of the `ionReorderEnd` event. This event behaves a bit differently since it is always emitted on end. If the `from` and `to` are the same, it will still emit them, so it's possible to check if they are the same to determine if `ionReorderEnd` fired without moving item positions.

----

Co-authored-by: sfinktah <sfinktah@github.spamtrak.org>
Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-06-23 09:51:43 -04:00
committed by Brandy Smith
parent d25b8a34f2
commit b154f4ed09
22 changed files with 528 additions and 54 deletions

View File

@ -1508,6 +1508,9 @@ ion-reorder-group,none
ion-reorder-group,prop,disabled,boolean,true,false,false ion-reorder-group,prop,disabled,boolean,true,false,false
ion-reorder-group,method,complete,complete(listOrReorder?: boolean | any[]) => Promise<any> ion-reorder-group,method,complete,complete(listOrReorder?: boolean | any[]) => Promise<any>
ion-reorder-group,event,ionItemReorder,ItemReorderEventDetail,true ion-reorder-group,event,ionItemReorder,ItemReorderEventDetail,true
ion-reorder-group,event,ionReorderEnd,ReorderEndEventDetail,true
ion-reorder-group,event,ionReorderMove,ReorderMoveEventDetail,true
ion-reorder-group,event,ionReorderStart,void,true
ion-ripple-effect,shadow ion-ripple-effect,shadow
ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false

View File

@ -30,7 +30,7 @@ import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface"; import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface"; import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
import { RefresherEventDetail } from "./components/refresher/refresher-interface"; import { RefresherEventDetail } from "./components/refresher/refresher-interface";
import { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface"; import { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
import { NavigationHookCallback } from "./components/route/route-interface"; import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface"; import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface"; import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
@ -68,7 +68,7 @@ export { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface"; export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface"; export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
export { RefresherEventDetail } from "./components/refresher/refresher-interface"; export { RefresherEventDetail } from "./components/refresher/refresher-interface";
export { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface"; export { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
export { NavigationHookCallback } from "./components/route/route-interface"; export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface"; export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface"; export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
@ -2783,7 +2783,7 @@ export namespace Components {
} }
interface IonReorderGroup { interface IonReorderGroup {
/** /**
* Completes the reorder operation. Must be called by the `ionItemReorder` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position. * Completes the reorder operation. Must be called by the `ionReorderEnd` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position.
* @param listOrReorder A list of items to be sorted and returned in the new order or a boolean of whether or not the reorder should reposition the item. * @param listOrReorder A list of items to be sorted and returned in the new order or a boolean of whether or not the reorder should reposition the item.
*/ */
"complete": (listOrReorder?: boolean | any[]) => Promise<any>; "complete": (listOrReorder?: boolean | any[]) => Promise<any>;
@ -4769,6 +4769,9 @@ declare global {
}; };
interface HTMLIonReorderGroupElementEventMap { interface HTMLIonReorderGroupElementEventMap {
"ionItemReorder": ItemReorderEventDetail; "ionItemReorder": ItemReorderEventDetail;
"ionReorderStart": void;
"ionReorderMove": ReorderMoveEventDetail;
"ionReorderEnd": ReorderEndEventDetail;
} }
interface HTMLIonReorderGroupElement extends Components.IonReorderGroup, HTMLStencilElement { interface HTMLIonReorderGroupElement extends Components.IonReorderGroup, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonReorderGroupElementEventMap>(type: K, listener: (this: HTMLIonReorderGroupElement, ev: IonReorderGroupCustomEvent<HTMLIonReorderGroupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void; addEventListener<K extends keyof HTMLIonReorderGroupElementEventMap>(type: K, listener: (this: HTMLIonReorderGroupElement, ev: IonReorderGroupCustomEvent<HTMLIonReorderGroupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
@ -8053,9 +8056,22 @@ declare namespace LocalJSX {
*/ */
"disabled"?: boolean; "disabled"?: boolean;
/** /**
* Event that needs to be listened to in order to complete the reorder action. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action. * Event that needs to be listened to in order to complete the reorder action.
* @deprecated Use `ionReorderEnd` instead. If you are accessing `event.detail.from` or `event.detail.to` and relying on them being different you should now add checks as they are always emitted in `ionReorderEnd`, even when they are the same.
*/ */
"onIonItemReorder"?: (event: IonReorderGroupCustomEvent<ItemReorderEventDetail>) => void; "onIonItemReorder"?: (event: IonReorderGroupCustomEvent<ItemReorderEventDetail>) => void;
/**
* Event that is emitted when the reorder gesture ends. The from and to properties are always available, regardless of if the reorder gesture moved the item. If the item did not change from its start position, the from and to properties will be the same. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action.
*/
"onIonReorderEnd"?: (event: IonReorderGroupCustomEvent<ReorderEndEventDetail>) => void;
/**
* Event that is emitted as the reorder gesture moves.
*/
"onIonReorderMove"?: (event: IonReorderGroupCustomEvent<ReorderMoveEventDetail>) => void;
/**
* Event that is emitted when the reorder gesture starts.
*/
"onIonReorderStart"?: (event: IonReorderGroupCustomEvent<void>) => void;
} }
interface IonRippleEffect { interface IonRippleEffect {
/** /**

View File

@ -84,7 +84,7 @@
} }
function initGroup(group) { function initGroup(group) {
var groupEl = document.getElementById(group.id); var groupEl = document.getElementById(group.id);
groupEl.addEventListener('ionItemReorder', function (ev) { groupEl.addEventListener('ionReorderEnd', function (ev) {
ev.detail.complete(); ev.detail.complete();
}); });
var groupItems = []; var groupItems = [];

View File

@ -1,10 +1,33 @@
// TODO(FW-6590): Remove this once the deprecated event is removed
export interface ItemReorderEventDetail { export interface ItemReorderEventDetail {
from: number; from: number;
to: number; to: number;
complete: (data?: boolean | any[]) => any; complete: (data?: boolean | any[]) => any;
} }
// TODO(FW-6590): Remove this once the deprecated event is removed
export interface ItemReorderCustomEvent extends CustomEvent { export interface ItemReorderCustomEvent extends CustomEvent {
detail: ItemReorderEventDetail; detail: ItemReorderEventDetail;
target: HTMLIonReorderGroupElement; target: HTMLIonReorderGroupElement;
} }
export interface ReorderMoveEventDetail {
from: number;
to: number;
}
export interface ReorderEndEventDetail {
from: number;
to: number;
complete: (data?: boolean | any[]) => any;
}
export interface ReorderMoveCustomEvent extends CustomEvent {
detail: ReorderMoveEventDetail;
target: HTMLIonReorderGroupElement;
}
export interface ReorderEndCustomEvent extends CustomEvent {
detail: ReorderEndEventDetail;
target: HTMLIonReorderGroupElement;
}

View File

@ -7,7 +7,7 @@ import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import type { Gesture, GestureDetail } from '../../interface'; import type { Gesture, GestureDetail } from '../../interface';
import type { ItemReorderEventDetail } from './reorder-group-interface'; import type { ItemReorderEventDetail, ReorderMoveEventDetail, ReorderEndEventDetail } from './reorder-group-interface';
// TODO(FW-2832): types // TODO(FW-2832): types
@ -51,12 +51,35 @@ export class ReorderGroup implements ComponentInterface {
} }
} }
// TODO(FW-6590): Remove this in a major release.
/** /**
* Event that needs to be listened to in order to complete the reorder action. * Event that needs to be listened to in order to complete the reorder action.
* @deprecated Use `ionReorderEnd` instead. If you are accessing
* `event.detail.from` or `event.detail.to` and relying on them
* being different you should now add checks as they are always emitted
* in `ionReorderEnd`, even when they are the same.
*/
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
/**
* Event that is emitted when the reorder gesture starts.
*/
@Event() ionReorderStart!: EventEmitter<void>;
/**
* Event that is emitted as the reorder gesture moves.
*/
@Event() ionReorderMove!: EventEmitter<ReorderMoveEventDetail>;
/**
* Event that is emitted when the reorder gesture ends.
* The from and to properties are always available, regardless of
* if the reorder gesture moved the item. If the item did not change
* from its start position, the from and to properties will be the same.
* Once the event has been emitted, the `complete()` method then needs * Once the event has been emitted, the `complete()` method then needs
* to be called in order to finalize the reorder action. * to be called in order to finalize the reorder action.
*/ */
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>; @Event() ionReorderEnd!: EventEmitter<ReorderEndEventDetail>;
async connectedCallback() { async connectedCallback() {
const contentEl = findClosestIonContent(this.el); const contentEl = findClosestIonContent(this.el);
@ -88,7 +111,8 @@ export class ReorderGroup implements ComponentInterface {
} }
/** /**
* Completes the reorder operation. Must be called by the `ionItemReorder` event. * Completes the reorder operation. Must be called by the `ionReorderEnd` event.
*
* If a list of items is passed, the list will be reordered and returned in the * If a list of items is passed, the list will be reordered and returned in the
* proper order. * proper order.
* *
@ -163,6 +187,8 @@ export class ReorderGroup implements ComponentInterface {
item.classList.add(ITEM_REORDER_SELECTED); item.classList.add(ITEM_REORDER_SELECTED);
hapticSelectionStart(); hapticSelectionStart();
this.ionReorderStart.emit();
} }
private onMove(ev: GestureDetail) { private onMove(ev: GestureDetail) {
@ -179,6 +205,7 @@ export class ReorderGroup implements ComponentInterface {
const currentY = Math.max(top, Math.min(ev.currentY, bottom)); const currentY = Math.max(top, Math.min(ev.currentY, bottom));
const deltaY = scroll + currentY - ev.startY; const deltaY = scroll + currentY - ev.startY;
const normalizedY = currentY - top; const normalizedY = currentY - top;
const fromIndex = this.lastToIndex;
const toIndex = this.itemIndexForTop(normalizedY); const toIndex = this.itemIndexForTop(normalizedY);
if (toIndex !== this.lastToIndex) { if (toIndex !== this.lastToIndex) {
const fromIndex = indexForItem(selectedItem); const fromIndex = indexForItem(selectedItem);
@ -190,6 +217,11 @@ export class ReorderGroup implements ComponentInterface {
// Update selected item position // Update selected item position
selectedItem.style.transform = `translateY(${deltaY}px)`; selectedItem.style.transform = `translateY(${deltaY}px)`;
this.ionReorderMove.emit({
from: fromIndex,
to: toIndex,
});
} }
private onEnd() { private onEnd() {
@ -206,6 +238,7 @@ export class ReorderGroup implements ComponentInterface {
if (toIndex === fromIndex) { if (toIndex === fromIndex) {
this.completeReorder(); this.completeReorder();
} else { } else {
// TODO(FW-6590): Remove this once the deprecated event is removed
this.ionItemReorder.emit({ this.ionItemReorder.emit({
from: fromIndex, from: fromIndex,
to: toIndex, to: toIndex,
@ -214,6 +247,12 @@ export class ReorderGroup implements ComponentInterface {
} }
hapticSelectionEnd(); hapticSelectionEnd();
this.ionReorderEnd.emit({
from: fromIndex,
to: toIndex,
complete: this.completeReorder.bind(this),
});
} }
private completeReorder(listOrReorder?: boolean | any[]): any { private completeReorder(listOrReorder?: boolean | any[]): any {

View File

@ -122,8 +122,25 @@
const reorderGroup = document.getElementById('reorder'); const reorderGroup = document.getElementById('reorder');
reorderGroup.disabled = !reorderGroup.disabled; reorderGroup.disabled = !reorderGroup.disabled;
// TODO(FW-6590): Remove this once the deprecated event is removed
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => { reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
console.log('Dragged from index', detail.from, 'to', detail.to); console.log('ionItemReorder: Dragged from index', detail.from, 'to', detail.to);
});
reorderGroup.addEventListener('ionReorderStart', () => {
console.log('ionReorderStart');
});
reorderGroup.addEventListener('ionReorderMove', ({ detail }) => {
console.log('ionReorderMove: Dragged from index', detail.from, 'to', detail.to);
});
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
if (detail.from !== detail.to) {
console.log('ionReorderEnd: Dragged from index', detail.from, 'to', detail.to);
} else {
console.log('ionReorderEnd: No position change occurred');
}
detail.complete(); detail.complete();
}); });

View File

@ -14,7 +14,7 @@
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script> <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head> </head>
<body onLoad="render()"> <body>
<ion-app> <ion-app>
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
@ -24,7 +24,7 @@
<ion-content id="content"> <ion-content id="content">
<ion-list> <ion-list>
<ion-reorder-group id="reorderGroup" disabled="false"> <ion-reorder-group disabled="false">
<!-- items will be inserted here --> <!-- items will be inserted here -->
</ion-reorder-group> </ion-reorder-group>
</ion-list> </ion-list>
@ -36,27 +36,44 @@
for (var i = 0; i < 30; i++) { for (var i = 0; i < 30; i++) {
items.push(i + 1); items.push(i + 1);
} }
const reorderGroup = document.getElementById('reorderGroup'); const reorderGroup = document.querySelector('ion-reorder-group');
reorderItems(items);
function render() {
let html = '';
for (let item of items) {
html += `
<ion-item>
<ion-label>${item}</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>`;
}
reorderGroup.innerHTML = html;
}
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
console.log('Dragged from index', detail.from, 'to', detail.to);
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
// Before complete is called with the items they will remain in the
// order before the drag
console.log('Before complete', items); console.log('Before complete', items);
// Finish the reorder and position the item in the DOM based on
// where the gesture ended. Update the items variable to the
// new order of items
items = detail.complete(items); items = detail.complete(items);
// Reorder the items in the DOM
reorderItems(items);
// After complete is called the items will be in the new order
console.log('After complete', items); console.log('After complete', items);
}); });
function reorderItems(items) {
reorderGroup.replaceChildren();
let reordered = '';
for (let i = 0; i < items.length; i++) {
reordered += `
<ion-item>
<ion-label>
Item ${items[i]}
</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
`;
}
reorderGroup.innerHTML = reordered;
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -37,9 +37,9 @@
</ion-reorder-group> </ion-reorder-group>
<script> <script>
const group = document.querySelector('ion-reorder-group'); const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionItemReorder', (ev) => { group.addEventListener('ionReorderEnd', (ev) => {
ev.detail.complete(); ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionItemReorderComplete')); window.dispatchEvent(new CustomEvent('ionReorderComplete'));
}); });
</script> </script>
</body> </body>

View File

@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
}); });
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => { test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('ion-item'); const items = page.locator('ion-item');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300); await dragElementBy(items.nth(1), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']); await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
}); });
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('ion-reorder'); const reorderHandle = page.locator('ion-reorder');
const items = page.locator('ion-item'); const items = page.locator('ion-item');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300); await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']); await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
}); });

View File

@ -68,9 +68,9 @@
customElements.define('app-reorder', AppReorder); customElements.define('app-reorder', AppReorder);
const group = document.querySelector('ion-reorder-group'); const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionItemReorder', (ev) => { group.addEventListener('ionReorderEnd', (ev) => {
ev.detail.complete(); ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionItemReorderComplete')); window.dispatchEvent(new CustomEvent('ionReorderComplete'));
}); });
</script> </script>
</ion-app> </ion-app>

View File

@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
}); });
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => { test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('app-reorder'); const items = page.locator('app-reorder');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300); await dragElementBy(items.nth(1), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']); await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
}); });
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('app-reorder ion-reorder'); const reorderHandle = page.locator('app-reorder ion-reorder');
const items = page.locator('app-reorder'); const items = page.locator('app-reorder');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300); await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']); await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
}); });

View File

@ -0,0 +1,289 @@
import { expect } from '@playwright/test';
import { configs, dragElementBy, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('reorder-group: events:'), () => {
test.describe('ionReorderStart', () => {
test('should emit when the reorder operation starts', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderStart = await page.spyOnEvent('ionReorderStart');
await expect(ionReorderStart).toHaveReceivedEventTimes(0);
// Start the drag to verify it emits the event without having to
// actually move the item. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0, undefined, undefined, false);
await page.waitForChanges();
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
// Drag the reorder item further to verify it does
// not emit the event again
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
});
});
test.describe('ionReorderMove', () => {
test('should emit when the reorder operation does not move the item position', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0);
await page.waitForChanges();
expect(ionReorderMove.events.length).toBeGreaterThan(0);
// Grab the last event to verify that it is emitting
// the correct from and to positions
const lastEvent = ionReorderMove.events[ionReorderMove.events.length - 1];
expect(lastEvent?.detail.from).toBe(0);
expect(lastEvent?.detail.to).toBe(0);
});
test('should emit when the reorder operation moves the item by multiple positions', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
// Drag the reorder item by a lot to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
expect(ionReorderMove.events.length).toBeGreaterThan(0);
// Grab the last event where the from and to are different to
// verify that it is not using the gesture start position as the from
const lastDifferentEvent = ionReorderMove.events
.reverse()
.find((event) => event.detail.from !== event.detail.to);
expect(lastDifferentEvent?.detail.from).toBe(1);
expect(lastDifferentEvent?.detail.to).toBe(2);
});
});
test.describe('ionReorderEnd', () => {
test('should emit without details when the reorder operation ends without moving the item position', async ({
page,
}) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
// Drag the reorder item a little bit but not enough to
// make it switch to a different position
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 0, complete: undefined });
});
test('should emit with details when the reorder operation ends and the item has moved', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
// Start the drag to verify it does not emit the event at the start
// of the drag or during the drag. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(0);
// Drag the reorder item further and release the drag to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
});
});
// TODO(FW-6590): Remove this once the deprecated event is removed
test.describe('ionItemReorder', () => {
test('should not emit when the reorder operation ends without moving the item position', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
// Drag the reorder item a little bit but not enough to
// make it switch to a different position
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
});
test('should emit when the reorder operation ends and the item has moved', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
// Start the drag to verify it does not emit the event at the start
// of the drag or during the drag. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
// Drag the reorder item further and release the drag to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(1);
await expect(ionItemReorder).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
});
});
});
});

View File

@ -57,9 +57,9 @@
<script> <script>
const group = document.querySelector('ion-reorder-group'); const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionItemReorder', (ev) => { group.addEventListener('ionReorderEnd', (ev) => {
ev.detail.complete(); ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionItemReorderComplete')); window.dispatchEvent(new CustomEvent('ionReorderComplete'));
}); });
</script> </script>
</body> </body>

View File

@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
}); });
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => { test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('ion-item'); const items = page.locator('ion-item');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300); await dragElementBy(items.nth(1), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']); await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
}); });
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('ion-reorder'); const reorderHandle = page.locator('ion-reorder');
const items = page.locator('ion-item'); const items = page.locator('ion-item');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete'); const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']); await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300); await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionItemReorderComplete.next(); await ionReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']); await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
}); });

View File

@ -25,7 +25,11 @@ export { RadioGroupCustomEvent } from './components/radio-group/radio-group-inte
export { RangeCustomEvent, PinFormatter } from './components/range/range-interface'; export { RangeCustomEvent, PinFormatter } from './components/range/range-interface';
export { HTMLStencilElement, RouterCustomEvent } from './components/router/utils/interface'; export { HTMLStencilElement, RouterCustomEvent } from './components/router/utils/interface';
export { RefresherCustomEvent } from './components/refresher/refresher-interface'; export { RefresherCustomEvent } from './components/refresher/refresher-interface';
export { ItemReorderCustomEvent } from './components/reorder-group/reorder-group-interface'; export {
ItemReorderCustomEvent,
ReorderEndCustomEvent,
ReorderMoveCustomEvent,
} from './components/reorder-group/reorder-group-interface';
export { SearchbarCustomEvent } from './components/searchbar/searchbar-interface'; export { SearchbarCustomEvent } from './components/searchbar/searchbar-interface';
export { SegmentCustomEvent } from './components/segment/segment-interface'; export { SegmentCustomEvent } from './components/segment/segment-interface';
export { SelectCustomEvent, SelectCompareFn } from './components/select/select-interface'; export { SelectCustomEvent, SelectCompareFn } from './components/select/select-interface';

View File

@ -1895,20 +1895,40 @@ export class IonReorderGroup {
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach(); c.detach();
this.el = r.nativeElement; this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionItemReorder']); proxyOutputs(this, this.el, ['ionItemReorder', 'ionReorderStart', 'ionReorderMove', 'ionReorderEnd']);
} }
} }
import type { ItemReorderEventDetail as IIonReorderGroupItemReorderEventDetail } from '@ionic/core'; import type { ItemReorderEventDetail as IIonReorderGroupItemReorderEventDetail } from '@ionic/core';
import type { ReorderMoveEventDetail as IIonReorderGroupReorderMoveEventDetail } from '@ionic/core';
import type { ReorderEndEventDetail as IIonReorderGroupReorderEndEventDetail } from '@ionic/core';
export declare interface IonReorderGroup extends Components.IonReorderGroup { export declare interface IonReorderGroup extends Components.IonReorderGroup {
/** /**
* Event that needs to be listened to in order to complete the reorder action. * Event that needs to be listened to in order to complete the reorder action. @deprecated Use `ionReorderEnd` instead. If you are accessing
`event.detail.from` or `event.detail.to` and relying on them
being different you should now add checks as they are always emitted
in `ionReorderEnd`, even when they are the same.
*/
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>;
/**
* Event that is emitted when the reorder gesture starts.
*/
ionReorderStart: EventEmitter<CustomEvent<void>>;
/**
* Event that is emitted as the reorder gesture moves.
*/
ionReorderMove: EventEmitter<CustomEvent<IIonReorderGroupReorderMoveEventDetail>>;
/**
* Event that is emitted when the reorder gesture ends.
The from and to properties are always available, regardless of
if the reorder gesture moved the item. If the item did not change
from its start position, the from and to properties will be the same.
Once the event has been emitted, the `complete()` method then needs Once the event has been emitted, the `complete()` method then needs
to be called in order to finalize the reorder action. to be called in order to finalize the reorder action.
*/ */
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>; ionReorderEnd: EventEmitter<CustomEvent<IIonReorderGroupReorderEndEventDetail>>;
} }

View File

@ -90,6 +90,7 @@ export {
InputOtpChangeEventDetail, InputOtpChangeEventDetail,
InputOtpCompleteEventDetail, InputOtpCompleteEventDetail,
InputOtpInputEventDetail, InputOtpInputEventDetail,
// TODO(FW-6590): Remove the next two lines once the deprecated event is removed
ItemReorderEventDetail, ItemReorderEventDetail,
ItemReorderCustomEvent, ItemReorderCustomEvent,
ItemSlidingCustomEvent, ItemSlidingCustomEvent,
@ -112,6 +113,10 @@ export {
RangeKnobMoveEndEventDetail, RangeKnobMoveEndEventDetail,
RefresherCustomEvent, RefresherCustomEvent,
RefresherEventDetail, RefresherEventDetail,
ReorderMoveCustomEvent,
ReorderMoveEventDetail,
ReorderEndCustomEvent,
ReorderEndEventDetail,
RouterEventDetail, RouterEventDetail,
RouterCustomEvent, RouterCustomEvent,
ScrollBaseCustomEvent, ScrollBaseCustomEvent,

View File

@ -1755,20 +1755,40 @@ export class IonReorderGroup {
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach(); c.detach();
this.el = r.nativeElement; this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionItemReorder']); proxyOutputs(this, this.el, ['ionItemReorder', 'ionReorderStart', 'ionReorderMove', 'ionReorderEnd']);
} }
} }
import type { ItemReorderEventDetail as IIonReorderGroupItemReorderEventDetail } from '@ionic/core/components'; import type { ItemReorderEventDetail as IIonReorderGroupItemReorderEventDetail } from '@ionic/core/components';
import type { ReorderMoveEventDetail as IIonReorderGroupReorderMoveEventDetail } from '@ionic/core/components';
import type { ReorderEndEventDetail as IIonReorderGroupReorderEndEventDetail } from '@ionic/core/components';
export declare interface IonReorderGroup extends Components.IonReorderGroup { export declare interface IonReorderGroup extends Components.IonReorderGroup {
/** /**
* Event that needs to be listened to in order to complete the reorder action. * Event that needs to be listened to in order to complete the reorder action. @deprecated Use `ionReorderEnd` instead. If you are accessing
`event.detail.from` or `event.detail.to` and relying on them
being different you should now add checks as they are always emitted
in `ionReorderEnd`, even when they are the same.
*/
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>;
/**
* Event that is emitted when the reorder gesture starts.
*/
ionReorderStart: EventEmitter<CustomEvent<void>>;
/**
* Event that is emitted as the reorder gesture moves.
*/
ionReorderMove: EventEmitter<CustomEvent<IIonReorderGroupReorderMoveEventDetail>>;
/**
* Event that is emitted when the reorder gesture ends.
The from and to properties are always available, regardless of
if the reorder gesture moved the item. If the item did not change
from its start position, the from and to properties will be the same.
Once the event has been emitted, the `complete()` method then needs Once the event has been emitted, the `complete()` method then needs
to be called in order to finalize the reorder action. to be called in order to finalize the reorder action.
*/ */
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>; ionReorderEnd: EventEmitter<CustomEvent<IIonReorderGroupReorderEndEventDetail>>;
} }

View File

@ -88,6 +88,7 @@ export {
InputOtpChangeEventDetail, InputOtpChangeEventDetail,
InputOtpCompleteEventDetail, InputOtpCompleteEventDetail,
InputOtpInputEventDetail, InputOtpInputEventDetail,
// TODO(FW-6590): Remove the next two lines once the deprecated event is removed
ItemReorderEventDetail, ItemReorderEventDetail,
ItemReorderCustomEvent, ItemReorderCustomEvent,
ItemSlidingCustomEvent, ItemSlidingCustomEvent,
@ -110,6 +111,10 @@ export {
RangeKnobMoveEndEventDetail, RangeKnobMoveEndEventDetail,
RefresherCustomEvent, RefresherCustomEvent,
RefresherEventDetail, RefresherEventDetail,
ReorderMoveCustomEvent,
ReorderMoveEventDetail,
ReorderEndCustomEvent,
ReorderEndEventDetail,
RouterEventDetail, RouterEventDetail,
RouterCustomEvent, RouterCustomEvent,
ScrollBaseCustomEvent, ScrollBaseCustomEvent,

View File

@ -47,6 +47,7 @@ export {
InputOtpChangeEventDetail, InputOtpChangeEventDetail,
InputOtpCompleteEventDetail, InputOtpCompleteEventDetail,
InputOtpInputEventDetail, InputOtpInputEventDetail,
// TODO(FW-6590): Remove the next two lines once the deprecated event is removed
ItemReorderEventDetail, ItemReorderEventDetail,
ItemReorderCustomEvent, ItemReorderCustomEvent,
ItemSlidingCustomEvent, ItemSlidingCustomEvent,
@ -68,6 +69,10 @@ export {
RangeKnobMoveEndEventDetail, RangeKnobMoveEndEventDetail,
RefresherCustomEvent, RefresherCustomEvent,
RefresherEventDetail, RefresherEventDetail,
ReorderMoveCustomEvent,
ReorderMoveEventDetail,
ReorderEndCustomEvent,
ReorderEndEventDetail,
RouterEventDetail, RouterEventDetail,
RouterCustomEvent, RouterCustomEvent,
ScrollBaseCustomEvent, ScrollBaseCustomEvent,

View File

@ -84,6 +84,7 @@ export {
InputOtpChangeEventDetail, InputOtpChangeEventDetail,
InputOtpCompleteEventDetail, InputOtpCompleteEventDetail,
InputOtpInputEventDetail, InputOtpInputEventDetail,
// TODO(FW-6590): Remove the next two lines once the deprecated event is removed
ItemReorderEventDetail, ItemReorderEventDetail,
ItemReorderCustomEvent, ItemReorderCustomEvent,
ItemSlidingCustomEvent, ItemSlidingCustomEvent,
@ -107,6 +108,10 @@ export {
RangeKnobMoveEndEventDetail, RangeKnobMoveEndEventDetail,
RefresherCustomEvent, RefresherCustomEvent,
RefresherEventDetail, RefresherEventDetail,
ReorderMoveCustomEvent,
ReorderMoveEventDetail,
ReorderEndCustomEvent,
ReorderEndEventDetail,
RouterEventDetail, RouterEventDetail,
RouterCustomEvent, RouterCustomEvent,
ScrollBaseCustomEvent, ScrollBaseCustomEvent,

View File

@ -804,9 +804,15 @@ export const IonReorder: StencilVueComponent<JSX.IonReorder> = /*@__PURE__*/ def
export const IonReorderGroup: StencilVueComponent<JSX.IonReorderGroup> = /*@__PURE__*/ defineContainer<JSX.IonReorderGroup>('ion-reorder-group', defineIonReorderGroup, [ export const IonReorderGroup: StencilVueComponent<JSX.IonReorderGroup> = /*@__PURE__*/ defineContainer<JSX.IonReorderGroup>('ion-reorder-group', defineIonReorderGroup, [
'disabled', 'disabled',
'ionItemReorder' 'ionItemReorder',
'ionReorderStart',
'ionReorderMove',
'ionReorderEnd'
], [ ], [
'ionItemReorder' 'ionItemReorder',
'ionReorderStart',
'ionReorderMove',
'ionReorderEnd'
]); ]);