diff --git a/core/api.txt b/core/api.txt index c792eb050a1..745d82786af 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1508,6 +1508,9 @@ ion-reorder-group,none ion-reorder-group,prop,disabled,boolean,true,false,false ion-reorder-group,method,complete,complete(listOrReorder?: boolean | any[]) => Promise 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,prop,type,"bounded" | "unbounded",'bounded',false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 3fc70d62b02..66ab40e747a 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -30,7 +30,7 @@ import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct 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 { 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 { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-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 { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-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 { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface"; export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface"; @@ -2770,7 +2770,7 @@ export namespace Components { } 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. */ "complete": (listOrReorder?: boolean | any[]) => Promise; @@ -4755,6 +4755,9 @@ declare global { }; interface HTMLIonReorderGroupElementEventMap { "ionItemReorder": ItemReorderEventDetail; + "ionReorderStart": void; + "ionReorderMove": ReorderMoveEventDetail; + "ionReorderEnd": ReorderEndEventDetail; } interface HTMLIonReorderGroupElement extends Components.IonReorderGroup, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLIonReorderGroupElement, ev: IonReorderGroupCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -8039,9 +8042,22 @@ declare namespace LocalJSX { */ "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) => 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) => void; + /** + * Event that is emitted as the reorder gesture moves. + */ + "onIonReorderMove"?: (event: IonReorderGroupCustomEvent) => void; + /** + * Event that is emitted when the reorder gesture starts. + */ + "onIonReorderStart"?: (event: IonReorderGroupCustomEvent) => void; } interface IonRippleEffect { /** diff --git a/core/src/components/item/test/reorder/index.html b/core/src/components/item/test/reorder/index.html index 22228cfd10a..64a9bf06042 100644 --- a/core/src/components/item/test/reorder/index.html +++ b/core/src/components/item/test/reorder/index.html @@ -84,7 +84,7 @@ } function initGroup(group) { var groupEl = document.getElementById(group.id); - groupEl.addEventListener('ionItemReorder', function (ev) { + groupEl.addEventListener('ionReorderEnd', function (ev) { ev.detail.complete(); }); var groupItems = []; diff --git a/core/src/components/reorder-group/reorder-group-interface.ts b/core/src/components/reorder-group/reorder-group-interface.ts index d76af54d6bd..b400413d15e 100644 --- a/core/src/components/reorder-group/reorder-group-interface.ts +++ b/core/src/components/reorder-group/reorder-group-interface.ts @@ -1,10 +1,33 @@ +// TODO(FW-6590): Remove this once the deprecated event is removed export interface ItemReorderEventDetail { from: number; to: number; complete: (data?: boolean | any[]) => any; } +// TODO(FW-6590): Remove this once the deprecated event is removed export interface ItemReorderCustomEvent extends CustomEvent { detail: ItemReorderEventDetail; 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; +} diff --git a/core/src/components/reorder-group/reorder-group.tsx b/core/src/components/reorder-group/reorder-group.tsx index 3dba0535b89..b13c12cc931 100644 --- a/core/src/components/reorder-group/reorder-group.tsx +++ b/core/src/components/reorder-group/reorder-group.tsx @@ -7,7 +7,7 @@ import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from import { getIonMode } from '../../global/ionic-global'; 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 @@ -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. + * @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; + + /** + * Event that is emitted when the reorder gesture starts. + */ + @Event() ionReorderStart!: EventEmitter; + + /** + * Event that is emitted as the reorder gesture moves. + */ + @Event() ionReorderMove!: EventEmitter; + + /** + * 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. */ - @Event() ionItemReorder!: EventEmitter; + @Event() ionReorderEnd!: EventEmitter; async connectedCallback() { const contentEl = findClosestIonContent(this.el); @@ -88,7 +111,7 @@ 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 * proper order. @@ -164,6 +187,8 @@ export class ReorderGroup implements ComponentInterface { item.classList.add(ITEM_REORDER_SELECTED); hapticSelectionStart(); + + this.ionReorderStart.emit(); } private onMove(ev: GestureDetail) { @@ -180,6 +205,7 @@ export class ReorderGroup implements ComponentInterface { const currentY = Math.max(top, Math.min(ev.currentY, bottom)); const deltaY = scroll + currentY - ev.startY; const normalizedY = currentY - top; + const fromIndex = this.lastToIndex; const toIndex = this.itemIndexForTop(normalizedY); if (toIndex !== this.lastToIndex) { const fromIndex = indexForItem(selectedItem); @@ -191,6 +217,11 @@ export class ReorderGroup implements ComponentInterface { // Update selected item position selectedItem.style.transform = `translateY(${deltaY}px)`; + + this.ionReorderMove.emit({ + from: fromIndex, + to: toIndex, + }); } private onEnd() { @@ -207,6 +238,7 @@ export class ReorderGroup implements ComponentInterface { if (toIndex === fromIndex) { this.completeReorder(); } else { + // TODO(FW-6590): Remove this once the deprecated event is removed this.ionItemReorder.emit({ from: fromIndex, to: toIndex, @@ -215,6 +247,12 @@ export class ReorderGroup implements ComponentInterface { } hapticSelectionEnd(); + + this.ionReorderEnd.emit({ + from: fromIndex, + to: toIndex, + complete: this.completeReorder.bind(this), + }); } private completeReorder(listOrReorder?: boolean | any[]): any { diff --git a/core/src/components/reorder-group/test/basic/index.html b/core/src/components/reorder-group/test/basic/index.html index 55e034c82ab..84af0ed5120 100644 --- a/core/src/components/reorder-group/test/basic/index.html +++ b/core/src/components/reorder-group/test/basic/index.html @@ -122,8 +122,25 @@ const reorderGroup = document.getElementById('reorder'); reorderGroup.disabled = !reorderGroup.disabled; + // TODO(FW-6590): Remove this once the deprecated event is removed 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(); }); diff --git a/core/src/components/reorder-group/test/data/index.html b/core/src/components/reorder-group/test/data/index.html index e30aa583ae6..56cf7b67da1 100644 --- a/core/src/components/reorder-group/test/data/index.html +++ b/core/src/components/reorder-group/test/data/index.html @@ -14,7 +14,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -36,27 +36,44 @@ for (var i = 0; i < 30; i++) { items.push(i + 1); } - const reorderGroup = document.getElementById('reorderGroup'); - - function render() { - let html = ''; - for (let item of items) { - html += ` - - ${item} - - `; - } - reorderGroup.innerHTML = html; - } - - reorderGroup.addEventListener('ionItemReorder', ({ detail }) => { - console.log('Dragged from index', detail.from, 'to', detail.to); + const reorderGroup = document.querySelector('ion-reorder-group'); + reorderItems(items); + 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); + + // 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); + + // 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); }); + + function reorderItems(items) { + reorderGroup.replaceChildren(); + + let reordered = ''; + + for (let i = 0; i < items.length; i++) { + reordered += ` + + + Item ${items[i]} + + + + `; + } + + reorderGroup.innerHTML = reordered; + } diff --git a/core/src/components/reorder-group/test/interactive/index.html b/core/src/components/reorder-group/test/interactive/index.html index 79150979ff3..b213b0a1b49 100644 --- a/core/src/components/reorder-group/test/interactive/index.html +++ b/core/src/components/reorder-group/test/interactive/index.html @@ -37,9 +37,9 @@ diff --git a/core/src/components/reorder-group/test/interactive/reorder-group.e2e.ts b/core/src/components/reorder-group/test/interactive/reorder-group.e2e.ts index c443275e43b..c303c6c169e 100644 --- a/core/src/components/reorder-group/test/interactive/reorder-group.e2e.ts +++ b/core/src/components/reorder-group/test/interactive/reorder-group.e2e.ts @@ -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 }) => { 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 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']); }); test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { const reorderHandle = page.locator('ion-reorder'); 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 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']); }); diff --git a/core/src/components/reorder-group/test/nested/index.html b/core/src/components/reorder-group/test/nested/index.html index 0f425dc8812..7679dc38276 100644 --- a/core/src/components/reorder-group/test/nested/index.html +++ b/core/src/components/reorder-group/test/nested/index.html @@ -68,9 +68,9 @@ customElements.define('app-reorder', AppReorder); const group = document.querySelector('ion-reorder-group'); - group.addEventListener('ionItemReorder', (ev) => { + group.addEventListener('ionReorderEnd', (ev) => { ev.detail.complete(); - window.dispatchEvent(new CustomEvent('ionItemReorderComplete')); + window.dispatchEvent(new CustomEvent('ionReorderComplete')); }); diff --git a/core/src/components/reorder-group/test/nested/reorder-group.e2e.ts b/core/src/components/reorder-group/test/nested/reorder-group.e2e.ts index a86de620cee..8c7377bf4d7 100644 --- a/core/src/components/reorder-group/test/nested/reorder-group.e2e.ts +++ b/core/src/components/reorder-group/test/nested/reorder-group.e2e.ts @@ -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 }) => { 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 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']); }); test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { const reorderHandle = page.locator('app-reorder ion-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 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']); }); diff --git a/core/src/components/reorder-group/test/reorder-group-events.e2e.ts b/core/src/components/reorder-group/test/reorder-group-events.e2e.ts new file mode 100644 index 00000000000..d3324e7dbdf --- /dev/null +++ b/core/src/components/reorder-group/test/reorder-group-events.e2e.ts @@ -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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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( + ` + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + `, + 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 }); + }); + }); + }); +}); diff --git a/core/src/components/reorder-group/test/scroll-target/index.html b/core/src/components/reorder-group/test/scroll-target/index.html index e286dba3bec..eb147b0053e 100644 --- a/core/src/components/reorder-group/test/scroll-target/index.html +++ b/core/src/components/reorder-group/test/scroll-target/index.html @@ -57,9 +57,9 @@ diff --git a/core/src/components/reorder-group/test/scroll-target/reorder-group.e2e.ts b/core/src/components/reorder-group/test/scroll-target/reorder-group.e2e.ts index 12a76299b53..0711c6022a9 100644 --- a/core/src/components/reorder-group/test/scroll-target/reorder-group.e2e.ts +++ b/core/src/components/reorder-group/test/scroll-target/reorder-group.e2e.ts @@ -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 }) => { 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 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']); }); test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => { const reorderHandle = page.locator('ion-reorder'); 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 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']); }); diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 3dcfb6aec2f..878d6c08467 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -25,7 +25,11 @@ export { RadioGroupCustomEvent } from './components/radio-group/radio-group-inte export { RangeCustomEvent, PinFormatter } from './components/range/range-interface'; export { HTMLStencilElement, RouterCustomEvent } from './components/router/utils/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 { SegmentCustomEvent } from './components/segment/segment-interface'; export { SelectCustomEvent, SelectCompareFn } from './components/select/select-interface'; diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 12b3b7ba3b2..c81ac8f8571 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -1895,20 +1895,40 @@ export class IonReorderGroup { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); 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 { ReorderMoveEventDetail as IIonReorderGroupReorderMoveEventDetail } from '@ionic/core'; +import type { ReorderEndEventDetail as IIonReorderGroupReorderEndEventDetail } from '@ionic/core'; 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>; + /** + * Event that is emitted when the reorder gesture starts. + */ + ionReorderStart: EventEmitter>; + /** + * Event that is emitted as the reorder gesture moves. + */ + ionReorderMove: EventEmitter>; + /** + * 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. */ - ionItemReorder: EventEmitter>; + ionReorderEnd: EventEmitter>; } diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index cd2d82ea4b0..3ee4a74ee1e 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -90,6 +90,7 @@ export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, + // TODO(FW-6590): Remove the next two lines once the deprecated event is removed ItemReorderEventDetail, ItemReorderCustomEvent, ItemSlidingCustomEvent, @@ -112,6 +113,10 @@ export { RangeKnobMoveEndEventDetail, RefresherCustomEvent, RefresherEventDetail, + ReorderMoveCustomEvent, + ReorderMoveEventDetail, + ReorderEndCustomEvent, + ReorderEndEventDetail, RouterEventDetail, RouterCustomEvent, ScrollBaseCustomEvent, diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index c7d111f7bef..93f9bf10c86 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -1755,20 +1755,40 @@ export class IonReorderGroup { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); 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 { ReorderMoveEventDetail as IIonReorderGroupReorderMoveEventDetail } from '@ionic/core/components'; +import type { ReorderEndEventDetail as IIonReorderGroupReorderEndEventDetail } from '@ionic/core/components'; 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>; + /** + * Event that is emitted when the reorder gesture starts. + */ + ionReorderStart: EventEmitter>; + /** + * Event that is emitted as the reorder gesture moves. + */ + ionReorderMove: EventEmitter>; + /** + * 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. */ - ionItemReorder: EventEmitter>; + ionReorderEnd: EventEmitter>; } diff --git a/packages/angular/standalone/src/index.ts b/packages/angular/standalone/src/index.ts index 23debccc1cd..db9a8a57da7 100644 --- a/packages/angular/standalone/src/index.ts +++ b/packages/angular/standalone/src/index.ts @@ -88,6 +88,7 @@ export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, + // TODO(FW-6590): Remove the next two lines once the deprecated event is removed ItemReorderEventDetail, ItemReorderCustomEvent, ItemSlidingCustomEvent, @@ -110,6 +111,10 @@ export { RangeKnobMoveEndEventDetail, RefresherCustomEvent, RefresherEventDetail, + ReorderMoveCustomEvent, + ReorderMoveEventDetail, + ReorderEndCustomEvent, + ReorderEndEventDetail, RouterEventDetail, RouterCustomEvent, ScrollBaseCustomEvent, diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 417b826866a..5355401a797 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -47,6 +47,7 @@ export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, + // TODO(FW-6590): Remove the next two lines once the deprecated event is removed ItemReorderEventDetail, ItemReorderCustomEvent, ItemSlidingCustomEvent, @@ -68,6 +69,10 @@ export { RangeKnobMoveEndEventDetail, RefresherCustomEvent, RefresherEventDetail, + ReorderMoveCustomEvent, + ReorderMoveEventDetail, + ReorderEndCustomEvent, + ReorderEndEventDetail, RouterEventDetail, RouterCustomEvent, ScrollBaseCustomEvent, diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 0a6aac15970..b189d4d9370 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -84,6 +84,7 @@ export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, + // TODO(FW-6590): Remove the next two lines once the deprecated event is removed ItemReorderEventDetail, ItemReorderCustomEvent, ItemSlidingCustomEvent, @@ -107,6 +108,10 @@ export { RangeKnobMoveEndEventDetail, RefresherCustomEvent, RefresherEventDetail, + ReorderMoveCustomEvent, + ReorderMoveEventDetail, + ReorderEndCustomEvent, + ReorderEndEventDetail, RouterEventDetail, RouterCustomEvent, ScrollBaseCustomEvent, diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 0db034f746d..a0c23232908 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -804,9 +804,15 @@ export const IonReorder: StencilVueComponent = /*@__PURE__*/ def export const IonReorderGroup: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-reorder-group', defineIonReorderGroup, [ 'disabled', - 'ionItemReorder' + 'ionItemReorder', + 'ionReorderStart', + 'ionReorderMove', + 'ionReorderEnd' ], [ - 'ionItemReorder' + 'ionItemReorder', + 'ionReorderStart', + 'ionReorderMove', + 'ionReorderEnd' ]);