Skip to content

feat(modal): expose dismiss intent on card modal ionDragEnd - #31002

Open
aeharding wants to merge 3 commits into
ionic-team:major-9.0from
aeharding:should-complete
Open

feat(modal): expose dismiss intent on card modal ionDragEnd#31002
aeharding wants to merge 3 commits into
ionic-team:major-9.0from
aeharding:should-complete

Conversation

@aeharding

@aeharding aeharding commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Issue number: resolves #31001


What is the current behavior?

The ionDragEnd event on card modals emits currentY, deltaY, velocityY and progress. None of these indicate whether the modal is about to dismiss, so consumers cannot react to the outcome of the gesture when the user lifts their finger.

What is the new behavior?

  • ionDragEnd now includes isDismissing on card modals. It is true when the gesture passed the dismiss threshold and the modal will animate away, and false when the modal will settle back open.
  • Updated tests.

Sheet modals are unchanged and continue to report snapBreakpoint.

Does this introduce a breaking change?

  • Yes
  • No

Other information

Testing with Voyager and works great! aeharding/voyager#2227

Preview

@aeharding
aeharding requested a review from a team as a code owner March 8, 2026 20:41
@aeharding
aeharding requested a review from thetaPC March 8, 2026 20:41
@vercel

vercel Bot commented Mar 8, 2026

Copy link
Copy Markdown

@aeharding is attempting to deploy a commit to the Ionic Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the package: core @ionic/core package label Mar 8, 2026
@brandyscarney brandyscarney changed the title feat: expose snapBreakpoint on swipe-to-close modals feat(modal): expose snapBreakpoint on swipe-to-close modals Mar 9, 2026
@aeharding
aeharding changed the base branch from main to major-9.0 March 17, 2026 20:59
deltaY: detail.deltaY,
velocityY: detail.velocityY,
progress: calculateProgress(el, detail.deltaY),
snapBreakpoint: shouldComplete ? 0 : 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snapBreakpoint is specific to sheet modals, and card modals have no breakpoint concept, so exposing a "breakpoint" here is a bit misleading. Let's call it isDismissing and return a plain boolean instead of 0/1.

Suggested change
snapBreakpoint: shouldComplete ? 0 : 1,
isDismissing: shouldComplete,

This also follows the convention we use for other booleans on public event details, such as isScrolling on ion-content's ScrollDetail and isAnimating on the menu interface.

One more change to go with it: please add the field to ModalDragEventDetail in modal-interface.ts so it's part of the public type:

/**
   * 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this variable changes the ionDragEnd detail length, so the existing assertion in modal-card.e2e.ts will fail. Please update the expected length there.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview Jul 30, 2026 5:49pm

Request Review

@thetaPC thetaPC changed the title feat(modal): expose snapBreakpoint on swipe-to-close modals feat(modal): expose dismiss intent on card modal ionDragEnd Jul 30, 2026

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! I did catch an issue that I believe is blocking and some others that aren't necessarily but might be nice to fix now

deltaY: detail.deltaY,
velocityY: detail.velocityY,
progress: calculateProgress(el, detail.deltaY),
isDismissing: shouldComplete,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a problem here with canDismiss. shouldComplete gets forced to false whenever canDismiss is a function or false, so this reports isDismissing: false on a modal that's about to dismiss. I built the branch locally to check: card modal with canDismiss: () => Promise.resolve(true), swipe the header down 450px, and ionDragEnd fires with {"deltaY":450,"velocityY":3.94,"progress":0.322,"isDismissing":false} immediately before ionModalDidDismiss.

I'd want this sorted before approving, because "confirm before dismiss" is the most common reason to set canDismiss in the first place, and there's no later drag event to correct the value.


expect(ionDragEnd.length).toBe(1);
expect(Object.keys(dragEndEvent.detail).length).toBe(4);
expect(Object.keys(dragEndEvent.detail).sort()).toEqual([

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only checks the key set, so it passes whether the implementation emits true, false, null, or a hardcoded constant. The value is the entire feature, and the canDismiss problem I flagged over in swipe-to-close.ts passes like this.

The fixture already handles both outcomes, and the suite already proves which distances land either side of the threshold: swipeToCloseModal(selector) defaults to 300px and asserts dismissal, and the test further up drags 20px with the modal staying open. So something like:

test('should report isDismissing true when the gesture dismisses the modal', async ({ page }) => {
  await dragElementBy(page.locator('.modal-card ion-header'), page, 0, 300);
  const ev = await ionDragEnd.next();
  expect(ev.detail.isDismissing).toBe(true);
});

test('should report isDismissing false when the modal settles back open', async ({ page }) => {
  await dragElementBy(page.locator('.modal-card ion-header'), page, 0, 20);
  const ev = await ionDragEnd.next();
  expect(ev.detail.isDismissing).toBe(false);
  await expect(page.locator('ion-modal')).toBeVisible();
});

I ran both against a local build with this patch applied and they pass. Worth pairing each with the visibility check so the test proves the field matches reality instead of matching itself. A third one for the canDismiss path would be good too once that's settled, #card-can-dismiss-promise-true in test/can-dismiss/index.html is already there for it.

Swapping .length).toBe(4) for Object.keys(...).sort() is a nice improvement by the way, a count wouldn't have caught a rename.

* `true` if the gesture passed the dismiss threshold, `false` if the
* modal will remain open.
*/
isDismissing?: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sheet modals never set this, so a consumer writing the obvious if (ev.detail.isDismissing) gets undefined on a sheet that is dismissing. I dragged one down to breakpoint 0 locally and got {"deltaY":180,"velocityY":10.76,"progress":0,"snapBreakpoint":0} with typeof isDismissing === 'undefined'. That's falsy, so the check quietly does nothing.

Both gestures publish through the same ionDragEnd, and sheet.ts already has what it needs in scope where it builds the detail:

isDismissing: snapBreakpoint === 0 && !canDismissBlocksGesture,

Same two values moveSheetToBreakpoint derives dismissal from. The early-return onDragEnd a bit further down would need an explicit false, since it doesn't move the sheet at all.

Card ionDragMove is the other half of this, it gets nothing while sheet emits snapBreakpoint on both move and end, where the move-time meaning is exactly "what happens if you let go now". I think both are worth deciding now rather than after release, because the asymmetry is permanent once it ships. Up to you though, the issue only asked about card modals so I won't block on it.

@@ -72,4 +72,10 @@ export interface ModalDragEventDetail {
* the gesture.
*/
snapBreakpoint?: number;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ModalDragEventDetail block in ionic-docs at docs/api/modal.md also needs to be updated, nothing over there picks this up automatically, so as it stands this ships undocumented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: swipe-to-close should support e.detail.snapBreakpoint

3 participants