From c200d782a0420a6480433afb9bd49b0653c3b34c Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Sun, 8 Mar 2026 15:39:13 -0500 Subject: [PATCH 1/5] feat: expose snapBreakpoint on swipe-to-close modals Resolves #31001 --- core/src/components/modal/gestures/swipe-to-close.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/components/modal/gestures/swipe-to-close.ts b/core/src/components/modal/gestures/swipe-to-close.ts index 2b66e7eac24..e743f043f0d 100644 --- a/core/src/components/modal/gestures/swipe-to-close.ts +++ b/core/src/components/modal/gestures/swipe-to-close.ts @@ -308,6 +308,7 @@ export const createSwipeToCloseGesture = ( deltaY: detail.deltaY, velocityY: detail.velocityY, progress: calculateProgress(el, detail.deltaY), + snapBreakpoint: shouldComplete ? 0 : 1, }; onDragEnd(eventDetail); From 4d87d5e47d2cca7407e6846d670ea3db3cc3886c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Jul 2026 09:59:42 -0700 Subject: [PATCH 2/5] feat(modal): rename drag end detail to isDismissing --- core/src/components/modal/gestures/swipe-to-close.ts | 2 +- core/src/components/modal/modal-interface.ts | 6 ++++++ core/src/components/modal/test/card/modal-card.e2e.ts | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/src/components/modal/gestures/swipe-to-close.ts b/core/src/components/modal/gestures/swipe-to-close.ts index e743f043f0d..541d8bb2bb0 100644 --- a/core/src/components/modal/gestures/swipe-to-close.ts +++ b/core/src/components/modal/gestures/swipe-to-close.ts @@ -308,7 +308,7 @@ export const createSwipeToCloseGesture = ( deltaY: detail.deltaY, velocityY: detail.velocityY, progress: calculateProgress(el, detail.deltaY), - snapBreakpoint: shouldComplete ? 0 : 1, + isDismissing: shouldComplete, }; onDragEnd(eventDetail); diff --git a/core/src/components/modal/modal-interface.ts b/core/src/components/modal/modal-interface.ts index 97831de0b04..e3383edf132 100644 --- a/core/src/components/modal/modal-interface.ts +++ b/core/src/components/modal/modal-interface.ts @@ -72,4 +72,10 @@ export interface ModalDragEventDetail { * the gesture. */ snapBreakpoint?: number; + /** + * Whether the card modal will dismiss when the drag gesture ends. + * `true` if the gesture passed the dismiss threshold, `false` if the + * modal will remain open. + */ + isDismissing?: boolean; } diff --git a/core/src/components/modal/test/card/modal-card.e2e.ts b/core/src/components/modal/test/card/modal-card.e2e.ts index 8df6161ec70..bc9d2b5dcec 100644 --- a/core/src/components/modal/test/card/modal-card.e2e.ts +++ b/core/src/components/modal/test/card/modal-card.e2e.ts @@ -120,7 +120,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c expect(ionDragStart.length).toBe(1); expect(ionDragMove.length).toBeGreaterThan(0); - expect(Object.keys(dragMoveEvent.detail).length).toBe(4); + expect(Object.keys(dragMoveEvent.detail).sort()).toEqual(['currentY', 'deltaY', 'progress', 'velocityY']); expect(ionDragEnd.length).toBe(0); @@ -138,7 +138,13 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c expect(ionDragMove.length).toBeGreaterThan(0); expect(ionDragEnd.length).toBe(1); - expect(Object.keys(dragEndEvent.detail).length).toBe(4); + expect(Object.keys(dragEndEvent.detail).sort()).toEqual([ + 'currentY', + 'deltaY', + 'isDismissing', + 'progress', + 'velocityY', + ]); }); }); }); From ae4023f7426ded224243070daa9fc025f4f22314 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jul 2026 14:18:27 -0700 Subject: [PATCH 3/5] fix(modal): report isDismissing when canDismiss is set --- .../modal/gestures/swipe-to-close.ts | 18 ++++++++++++++++-- core/src/components/modal/modal-interface.ts | 7 ++++--- core/src/components/modal/test/card/index.html | 11 +++++++++-- .../modal/test/card/modal-card.e2e.ts | 17 +++++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/core/src/components/modal/gestures/swipe-to-close.ts b/core/src/components/modal/gestures/swipe-to-close.ts index 36a7a009005..4a8d938aa34 100644 --- a/core/src/components/modal/gestures/swipe-to-close.ts +++ b/core/src/components/modal/gestures/swipe-to-close.ts @@ -304,18 +304,32 @@ export const createSwipeToCloseGesture = ( * check canDismiss. 25% was chosen * to avoid accidental swipes. */ - if (isAttemptingDismissWithCanDismiss && clampedStep > maxStep / 4) { + const isAttemptingCanDismiss = isAttemptingDismissWithCanDismiss && clampedStep > maxStep / 4; + + if (isAttemptingCanDismiss) { handleCanDismiss(el, animation); } else if (shouldComplete) { onDismiss(); } + /** + * `shouldComplete` is always `false` when canDismiss blocks the + * gesture, since it only describes a dismiss that happens right + * away. In that case the modal dismisses through `handleCanDismiss` + * instead, which only dismisses when canDismiss is a function. + * A canDismiss function can still cancel the dismiss after this + * event is emitted. + */ + const isDismissing = isAttemptingDismissWithCanDismiss + ? isAttemptingCanDismiss && typeof el.canDismiss === 'function' + : shouldComplete; + const eventDetail: ModalDragEventDetail = { currentY: detail.currentY, deltaY: detail.deltaY, velocityY: detail.velocityY, progress: calculateProgress(el, detail.deltaY), - isDismissing: shouldComplete, + isDismissing, }; onDragEnd(eventDetail); diff --git a/core/src/components/modal/modal-interface.ts b/core/src/components/modal/modal-interface.ts index e3383edf132..f1e60be113d 100644 --- a/core/src/components/modal/modal-interface.ts +++ b/core/src/components/modal/modal-interface.ts @@ -73,9 +73,10 @@ export interface ModalDragEventDetail { */ snapBreakpoint?: number; /** - * Whether the card modal will dismiss when the drag gesture ends. - * `true` if the gesture passed the dismiss threshold, `false` if the - * modal will remain open. + * Whether the card modal is attempting to dismiss when the drag + * gesture ends. A `canDismiss` callback can still cancel the dismiss + * after this event is emitted, so use `ionModalDidDismiss` to confirm + * that the modal actually closed. */ isDismissing?: boolean; } diff --git a/core/src/components/modal/test/card/index.html b/core/src/components/modal/test/card/index.html index d064d0030ae..3f232925d59 100644 --- a/core/src/components/modal/test/card/index.html +++ b/core/src/components/modal/test/card/index.html @@ -52,6 +52,13 @@

iOS only

Card Modal Custom Radius + @@ -165,8 +172,8 @@

iOS only

await modal.present(); } - async function dragEvents() { - const modal = await createModal(document.querySelectorAll('.ion-page')[1], { id: 'drag-events' }); + async function dragEvents(opts) { + const modal = await createModal(document.querySelectorAll('.ion-page')[1], { id: 'drag-events', ...opts }); modal.addEventListener('ionDragStart', (event) => { console.log('Drag started'); diff --git a/core/src/components/modal/test/card/modal-card.e2e.ts b/core/src/components/modal/test/card/modal-card.e2e.ts index fc42016cfac..c37cba0c48b 100644 --- a/core/src/components/modal/test/card/modal-card.e2e.ts +++ b/core/src/components/modal/test/card/modal-card.e2e.ts @@ -156,5 +156,22 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c 'velocityY', ]); }); + test('should report isDismissing when canDismiss allows the dismiss', async ({ page }) => { + const cardModalPage = new CardModalPage(page); + await cardModalPage.navigate('/src/components/modal/test/card', config); + await cardModalPage.openModalByTrigger('#drag-events-can-dismiss'); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + /** + * `swipeToCloseModal` waits for `ionModalDidDismiss`, so this also + * verifies that the modal dismissed after canDismiss resolved. + */ + await cardModalPage.swipeToCloseModal('.modal-card ion-header'); + + const dragEndEvent = await ionDragEnd.next(); + + expect(dragEndEvent.detail.isDismissing).toBe(true); + }); }); }); From ed90f22d5a95b4e9e0a4146f9814fee3c53a283c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jul 2026 15:23:49 -0700 Subject: [PATCH 4/5] test(modal): cover isDismissing across the swipe and canDismiss paths --- .../modal/test/can-dismiss/modal-card.e2e.ts | 52 +++++++++++++++++++ .../src/components/modal/test/card/index.html | 11 +--- .../modal/test/card/modal-card.e2e.ts | 40 +++++++++----- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/core/src/components/modal/test/can-dismiss/modal-card.e2e.ts b/core/src/components/modal/test/can-dismiss/modal-card.e2e.ts index 383e70d6cbc..7f8c8da2e62 100644 --- a/core/src/components/modal/test/can-dismiss/modal-card.e2e.ts +++ b/core/src/components/modal/test/can-dismiss/modal-card.e2e.ts @@ -50,6 +50,58 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await ionModalDidDismiss.next(); }); + test('should report isDismissing as true when canDismiss is Promise', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await page.click('#card-can-dismiss-promise-true'); + + await ionModalDidPresent.next(); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + const modalHeader = page.locator('#modal-header'); + await dragElementBy(modalHeader, page, 0, 500); + + const dragEndEvent = await ionDragEnd.next(); + + /** + * canDismiss forces the swipe animation to stop before it completes, + * so the event has to report the dismiss that canDismiss goes on to + * perform. Waiting for `ionModalDidDismiss` proves the modal really + * did close. + */ + expect(dragEndEvent.detail.isDismissing).toBe(true); + + await ionModalDidDismiss.next(); + }); + test('should report isDismissing as true when canDismiss cancels the dismiss', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionHandlerDone = await page.spyOnEvent('ionHandlerDone'); + + await page.click('#card-can-dismiss-promise-false'); + + await ionModalDidPresent.next(); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + const modalHeader = page.locator('#modal-header'); + await dragElementBy(modalHeader, page, 0, 500); + + const dragEndEvent = await ionDragEnd.next(); + + /** + * `ionDragEnd` fires before canDismiss resolves, so it reports the + * dismiss the gesture is attempting rather than the outcome. This is + * the one case where the two differ: canDismiss goes on to cancel the + * dismiss and the modal stays open. + */ + expect(dragEndEvent.detail.isDismissing).toBe(true); + + await ionHandlerDone.next(); + + await expect(page.locator('ion-modal')).toBeVisible(); + }); test('should not dismiss on swipe when canDismiss is Promise', async ({ page }) => { const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); const ionHandlerDone = await page.spyOnEvent('ionHandlerDone'); diff --git a/core/src/components/modal/test/card/index.html b/core/src/components/modal/test/card/index.html index 3f232925d59..d064d0030ae 100644 --- a/core/src/components/modal/test/card/index.html +++ b/core/src/components/modal/test/card/index.html @@ -52,13 +52,6 @@

iOS only

Card Modal Custom Radius - @@ -172,8 +165,8 @@

iOS only

await modal.present(); } - async function dragEvents(opts) { - const modal = await createModal(document.querySelectorAll('.ion-page')[1], { id: 'drag-events', ...opts }); + async function dragEvents() { + const modal = await createModal(document.querySelectorAll('.ion-page')[1], { id: 'drag-events' }); modal.addEventListener('ionDragStart', (event) => { console.log('Drag started'); diff --git a/core/src/components/modal/test/card/modal-card.e2e.ts b/core/src/components/modal/test/card/modal-card.e2e.ts index c37cba0c48b..ade6979c2f7 100644 --- a/core/src/components/modal/test/card/modal-card.e2e.ts +++ b/core/src/components/modal/test/card/modal-card.e2e.ts @@ -107,13 +107,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c }); test.describe(title('card modal: drag events'), () => { - test('should emit ionDragStart, ionDragMove, and ionDragEnd events', async ({ page }) => { - await page.goto('/src/components/modal/test/card', config); + let cardModalPage: CardModalPage; - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + test.beforeEach(async ({ page }) => { + cardModalPage = new CardModalPage(page); + await cardModalPage.navigate('/src/components/modal/test/card', config); + }); - await page.click('#drag-events'); - await ionModalDidPresent.next(); + test('should emit ionDragStart, ionDragMove, and ionDragEnd events', async ({ page }) => { + await cardModalPage.openModalByTrigger('#drag-events'); const ionDragStart = await page.spyOnEvent('ionDragStart'); const ionDragMove = await page.spyOnEvent('ionDragMove'); @@ -156,22 +158,36 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c 'velocityY', ]); }); - test('should report isDismissing when canDismiss allows the dismiss', async ({ page }) => { - const cardModalPage = new CardModalPage(page); - await cardModalPage.navigate('/src/components/modal/test/card', config); - await cardModalPage.openModalByTrigger('#drag-events-can-dismiss'); + + test('should report isDismissing as true when the gesture dismisses the modal', async ({ page }) => { + await cardModalPage.openModalByTrigger('#card'); const ionDragEnd = await page.spyOnEvent('ionDragEnd'); /** - * `swipeToCloseModal` waits for `ionModalDidDismiss`, so this also - * verifies that the modal dismissed after canDismiss resolved. + * `swipeToCloseModal` drags 300px and waits for `ionModalDidDismiss`, + * so the modal is known to have dismissed by the time the event + * detail is checked. */ - await cardModalPage.swipeToCloseModal('.modal-card ion-header'); + await cardModalPage.swipeToCloseModal('ion-modal ion-header'); const dragEndEvent = await ionDragEnd.next(); expect(dragEndEvent.detail.isDismissing).toBe(true); }); + + test('should report isDismissing as false when the modal settles back open', async ({ page }) => { + const modal = await cardModalPage.openModalByTrigger('#card'); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + // 20px is short enough that the gesture never passes the dismiss threshold + await cardModalPage.swipeToCloseModal('ion-modal ion-header', false, 20); + + const dragEndEvent = await ionDragEnd.next(); + + expect(dragEndEvent.detail.isDismissing).toBe(false); + await expect(modal).toBeVisible(); + }); }); }); From 98fd35858546ed397fc57d2941b752c6abd0dfa5 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jul 2026 15:47:37 -0700 Subject: [PATCH 5/5] feat(modal): emit isDismissing on sheet modal drag end --- core/src/components/modal/gestures/sheet.ts | 18 +++++- core/src/components/modal/modal-interface.ts | 10 ++-- .../modal/test/can-dismiss/modal-sheet.e2e.ts | 50 ++++++++++++++++ .../components/modal/test/sheet/modal.e2e.ts | 59 +++++++++++++++++-- 4 files changed, 128 insertions(+), 9 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index cacf61c5b43..7d4d434488c 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -452,12 +452,23 @@ export const createSheetGesture = ( const onEnd = (detail: GestureDetail) => { const snapBreakpoint = calculateSnapBreakpoint(detail.deltaY); + /** + * `moveSheetToBreakpoint` only dismisses when the sheet snaps to + * breakpoint 0. When canDismiss blocks the gesture it keeps the sheet + * from snapping there and dismisses through `handleCanDismiss` instead, + * which only dismisses when canDismiss is a function. A canDismiss + * function can still cancel the dismiss after this event is emitted. + */ + const shouldPreventDismiss = canDismissBlocksGesture && snapBreakpoint === 0; + const isDismissing = shouldPreventDismiss ? typeof baseEl.canDismiss === 'function' : snapBreakpoint === 0; + const eventDetail: ModalDragEventDetail = { currentY: detail.currentY, deltaY: detail.deltaY, velocityY: detail.velocityY, progress: calculateProgress(detail.currentY), snapBreakpoint, + isDismissing, }; /** @@ -474,7 +485,12 @@ export const createSheetGesture = ( * swap to moving on drag and if we don't swap back here then the footer will get stuck. */ swapFooterPosition('stationary'); - onDragEnd(eventDetail); + + /** + * The sheet does not move in this case, so the gesture + * never dismisses the modal. + */ + onDragEnd({ ...eventDetail, isDismissing: false }); return; } diff --git a/core/src/components/modal/modal-interface.ts b/core/src/components/modal/modal-interface.ts index f1e60be113d..f31d911ad60 100644 --- a/core/src/components/modal/modal-interface.ts +++ b/core/src/components/modal/modal-interface.ts @@ -73,10 +73,12 @@ export interface ModalDragEventDetail { */ snapBreakpoint?: number; /** - * Whether the card modal is attempting to dismiss when the drag - * gesture ends. A `canDismiss` callback can still cancel the dismiss - * after this event is emitted, so use `ionModalDidDismiss` to confirm - * that the modal actually closed. + * Whether the modal is attempting to dismiss when the drag gesture + * ends. A `canDismiss` callback can still cancel the dismiss after + * this event is emitted, so use `ionModalDidDismiss` to confirm that + * the modal actually closed. + * + * This is only set on `ionDragEnd`. */ isDismissing?: boolean; } diff --git a/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts b/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts index b282645740d..865b5b5a8de 100644 --- a/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts +++ b/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts @@ -49,6 +49,56 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await ionModalDidDismiss.next(); }); + test('should report isDismissing as true when canDismiss is Promise', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await page.click('#sheet-can-dismiss-promise-true'); + + await ionModalDidPresent.next(); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + const modalHeader = page.locator('#modal-header'); + await dragElementBy(modalHeader, page, 0, 500); + + const dragEndEvent = await ionDragEnd.next(); + + /** + * canDismiss keeps the sheet from snapping to breakpoint 0, so the + * event has to report the dismiss that canDismiss goes on to perform. + */ + expect(dragEndEvent.detail.isDismissing).toBe(true); + + await ionModalDidDismiss.next(); + }); + test('should report isDismissing as true when canDismiss cancels the dismiss', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionHandlerDone = await page.spyOnEvent('ionHandlerDone'); + + await page.click('#sheet-can-dismiss-promise-false'); + + await ionModalDidPresent.next(); + + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + const modalHeader = page.locator('#modal-header'); + await dragElementBy(modalHeader, page, 0, 500); + + const dragEndEvent = await ionDragEnd.next(); + + /** + * `ionDragEnd` fires before canDismiss resolves, so it reports the + * dismiss the gesture is attempting rather than the outcome. This is + * the one case where the two differ: canDismiss goes on to cancel the + * dismiss and the sheet stays open. + */ + expect(dragEndEvent.detail.isDismissing).toBe(true); + + await ionHandlerDone.next(); + + await expect(page.locator('ion-modal')).toBeVisible(); + }); test('should not dismiss on swipe when canDismiss is Promise', async ({ page }) => { const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); const ionHandlerDone = await page.spyOnEvent('ionHandlerDone'); diff --git a/core/src/components/modal/test/sheet/modal.e2e.ts b/core/src/components/modal/test/sheet/modal.e2e.ts index dd47cb4fb54..7e127bc39ab 100644 --- a/core/src/components/modal/test/sheet/modal.e2e.ts +++ b/core/src/components/modal/test/sheet/modal.e2e.ts @@ -355,14 +355,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => }); test.describe(title('sheet modal: drag events'), () => { - test('should emit ionDragStart, ionDragMove, and ionDragEnd events', async ({ page }) => { + test.beforeEach(async ({ page }) => { await page.goto('/src/components/modal/test/sheet', config); const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); await page.click('#drag-events'); await ionModalDidPresent.next(); - + }); + test('should emit ionDragStart, ionDragMove, and ionDragEnd events', async ({ page }) => { const ionDragStart = await page.spyOnEvent('ionDragStart'); const ionDragMove = await page.spyOnEvent('ionDragMove'); const ionDragEnd = await page.spyOnEvent('ionDragEnd'); @@ -378,7 +379,13 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(ionDragStart.length).toBe(1); expect(ionDragMove.length).toBeGreaterThan(0); - expect(Object.keys(dragMoveEvent.detail).length).toBe(5); + expect(Object.keys(dragMoveEvent.detail).sort()).toEqual([ + 'currentY', + 'deltaY', + 'progress', + 'snapBreakpoint', + 'velocityY', + ]); expect(ionDragEnd.length).toBe(0); @@ -396,7 +403,51 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(ionDragMove.length).toBeGreaterThan(0); expect(ionDragEnd.length).toBe(1); - expect(Object.keys(dragEndEvent.detail).length).toBe(5); + expect(Object.keys(dragEndEvent.detail).sort()).toEqual([ + 'currentY', + 'deltaY', + 'isDismissing', + 'progress', + 'snapBreakpoint', + 'velocityY', + ]); + }); + test('should report isDismissing as true when the gesture dismisses the modal', async ({ page }) => { + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + const header = page.locator('.modal-sheet ion-header'); + const headerBox = (await header.boundingBox())!; + const viewport = page.viewportSize()!; + + /** + * The sheet starts at breakpoint 0.5, so its header sits near the middle + * of the viewport and the drag has to stay inside the bottom half. + * Dragging as far as the viewport allows moves the sheet well past the + * midpoint between breakpoints 0 and 0.25, so it snaps to 0 and dismisses. + */ + const dragByY = viewport.height - (headerBox.y + headerBox.height / 2) - 10; + + await dragElementBy(header, page, 0, dragByY); + + const dragEndEvent = await ionDragEnd.next(); + + expect(dragEndEvent.detail.isDismissing).toBe(true); + + await ionModalDidDismiss.next(); + }); + test('should report isDismissing as false when the sheet snaps to another breakpoint', async ({ page }) => { + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); + + const header = page.locator('.modal-sheet ion-header'); + + // Dragging this little keeps the sheet above breakpoint 0 + await dragElementBy(header, page, 0, 50); + + const dragEndEvent = await ionDragEnd.next(); + + expect(dragEndEvent.detail.isDismissing).toBe(false); + await expect(page.locator('ion-modal')).toBeVisible(); }); });