diff --git a/core/api.txt b/core/api.txt index c792eb050a..745d82786a 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 6e0e240584..7721801eb6 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"; @@ -2783,7 +2783,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; @@ -4769,6 +4769,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; @@ -8053,9 +8056,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 22228cfd10..64a9bf0604 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 d76af54d6b..b400413d15 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 fe4d424ba0..b13c12cc93 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,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 * proper order. * @@ -163,6 +187,8 @@ export class ReorderGroup implements ComponentInterface { item.classList.add(ITEM_REORDER_SELECTED); hapticSelectionStart(); + + this.ionReorderStart.emit(); } private onMove(ev: GestureDetail) { @@ -179,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); @@ -190,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() { @@ -206,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, @@ -214,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 55e034c82a..84af0ed512 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 e30aa583ae..56cf7b67da 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 79150979ff..b213b0a1b4 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 c443275e43..c303c6c169 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 0f425dc881..7679dc3827 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 a86de620ce..8c7377bf4d 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 0000000000..d3324e7dbd --- /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 e286dba3be..eb147b0053 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 12a76299b5..0711c6022a 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 3dcfb6aec2..878d6c0846 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 12b3b7ba3b..c81ac8f857 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 cd2d82ea4b..3ee4a74ee1 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 c7d111f7be..93f9bf10c8 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 23debccc1c..db9a8a57da 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 417b826866..5355401a79 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 0a6aac1597..b189d4d937 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 0db034f746..a0c2323290 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' ]);