Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: feature-8.7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>
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
Expand Down
24 changes: 20 additions & 4 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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<any>;
Expand Down Expand Up @@ -4755,6 +4755,9 @@ declare global {
};
interface HTMLIonReorderGroupElementEventMap {
"ionItemReorder": ItemReorderEventDetail;
"ionReorderStart": void;
"ionReorderMove": ReorderMoveEventDetail;
"ionReorderEnd": ReorderEndEventDetail;
}
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;
Expand Down Expand Up @@ -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<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 {
/**
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/item/test/reorder/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
23 changes: 23 additions & 0 deletions core/src/components/reorder-group/reorder-group-interface.ts
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 41 additions & 3 deletions core/src/components/reorder-group/reorder-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<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
* to be called in order to finalize the reorder action.
*/
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
@Event() ionReorderEnd!: EventEmitter<ReorderEndEventDetail>;

async connectedCallback() {
const contentEl = findClosestIonContent(this.el);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -164,6 +187,8 @@ export class ReorderGroup implements ComponentInterface {
item.classList.add(ITEM_REORDER_SELECTED);

hapticSelectionStart();

this.ionReorderStart.emit();
}

private onMove(ev: GestureDetail) {
Expand All @@ -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);
Expand All @@ -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() {
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
19 changes: 18 additions & 1 deletion core/src/components/reorder-group/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
53 changes: 35 additions & 18 deletions core/src/components/reorder-group/test/data/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>

<body onLoad="render()">
<body>
<ion-app>
<ion-header>
<ion-toolbar>
Expand All @@ -24,7 +24,7 @@

<ion-content id="content">
<ion-list>
<ion-reorder-group id="reorderGroup" disabled="false">
<ion-reorder-group disabled="false">
<!-- items will be inserted here -->
</ion-reorder-group>
</ion-list>
Expand All @@ -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 += `
<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);
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 += `
<ion-item>
<ion-label>
Item ${items[i]}
</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
`;
}

reorderGroup.innerHTML = reordered;
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
</ion-reorder-group>
<script>
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'));
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand Down
4 changes: 2 additions & 2 deletions core/src/components/reorder-group/test/nested/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
</script>
</ion-app>
Expand Down
Loading
Loading