Skip to content

Commit 5583717

Browse files
docs(modal): add playgrounds for sheet and card modal drag events (#4415)
Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
1 parent e360ac8 commit 5583717

15 files changed

Lines changed: 849 additions & 0 deletions

File tree

docs/api/modal.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,26 @@ A few things to keep in mind when creating custom dialogs:
210210
* `ion-content` is intended to be used in full-page modals, cards, and sheets. If your custom dialog has a dynamic or unknown size, `ion-content` should not be used.
211211
* Creating custom dialogs provides a way of ejecting from the default modal experience. As a result, custom dialogs should not be used with card or sheet modals.
212212

213+
## Event Handling
214+
215+
### Using `ionDragStart` and `ionDragEnd`
216+
217+
The `ionDragStart` event is emitted as soon as the user begins a dragging gesture on the modal. This event fires at the moment the user initiates contact with the handle or modal surface, before any actual displacement occurs. It is particularly useful for preparing the interface for a transition, such as hiding certain interactive elements (like headers or buttons) to ensure a smooth dragging experience.
218+
219+
The `ionDragEnd` event is emitted when the user completes the dragging gesture by releasing the modal. Like the move event, it includes the final [`ModalDragEventDetail`](#modaldrageventdetail) object. This event is commonly used to finalize state changes once the modal has come to a rest.
220+
221+
import DragStartEndEvents from '@site/static/usage/v8/modal/drag-start-end-events/index.md';
222+
223+
<DragStartEndEvents />
224+
225+
### Using `ionDragMove`
226+
227+
The `ionDragMove` event is emitted continuously while the user is actively dragging the modal. This event provides a [`ModalDragEventDetail`](#modaldrageventdetail) object containing real-time data, essential for creating highly responsive UI updates that react instantly to the user's touch. For example, the `progress` value can be used to dynamically darken a header's opacity as the modal is dragged upward.
228+
229+
import DragMoveEvent from '@site/static/usage/v8/modal/drag-move-event/index.md';
230+
231+
<DragMoveEvent />
232+
213233
## Interfaces
214234

215235
### ModalOptions
@@ -251,6 +271,59 @@ interface ModalCustomEvent extends CustomEvent {
251271
}
252272
```
253273

274+
### ModalDragEventDetail
275+
276+
When using the `ionDragMove` and `ionDragEnd` events, the event detail contains the following properties:
277+
278+
```typescript
279+
interface ModalDragEventDetail {
280+
/**
281+
* The current Y position of the modal.
282+
*
283+
* This can be used to determine how far the modal has been dragged.
284+
*/
285+
currentY: number;
286+
/**
287+
* The change in Y position since the gesture started.
288+
*
289+
* This can be used to determine the direction of the drag.
290+
*/
291+
deltaY: number;
292+
/**
293+
* The velocity of the drag in the Y direction.
294+
*
295+
* This can be used to determine how fast the modal is being dragged.
296+
*/
297+
velocityY: number;
298+
/**
299+
* A number between 0 and 1.
300+
*
301+
* In a sheet modal, progress represents the relative position between
302+
* the lowest and highest defined breakpoints.
303+
*
304+
* In a card modal, it measures the relative position between the
305+
* bottom of the screen and the top of the modal when it is fully
306+
* open.
307+
*
308+
* This can be used to style content based on how far the modal has
309+
* been dragged.
310+
*/
311+
progress: number;
312+
/**
313+
* If the modal is a sheet modal, this will be the breakpoint that
314+
* the modal will snap to if the user lets go of the modal at the
315+
* current moment.
316+
*
317+
* If it's a card modal, this property will not be included in the
318+
* event payload.
319+
*
320+
* This can be used to style content based on where the modal will
321+
* snap to upon release.
322+
*/
323+
snapBreakpoint?: number;
324+
}
325+
```
326+
254327
## Accessibility
255328

256329
### Keyboard Interactions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
```html
2+
<ion-header #header>
3+
<ion-toolbar>
4+
<ion-title>App</ion-title>
5+
</ion-toolbar>
6+
</ion-header>
7+
<ion-content class="ion-padding">
8+
<ion-button id="open-modal" expand="block">Open Sheet Modal</ion-button>
9+
10+
<ion-modal
11+
trigger="open-modal"
12+
[initialBreakpoint]="0.25"
13+
[breakpoints]="[0, 0.25, 0.5, 0.75, 1]"
14+
(ionDragMove)="onDragMove($event)"
15+
(ionDragEnd)="onDragEnd($event)"
16+
(willDismiss)="onWillDismiss()"
17+
>
18+
<ng-template>
19+
<ion-content class="ion-padding">
20+
<div class="ion-margin-top">
21+
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
22+
</div>
23+
</ion-content>
24+
</ng-template>
25+
</ion-modal>
26+
</ion-content>
27+
```
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
```ts
2+
import { Component, ElementRef, ViewChild } from '@angular/core';
3+
import { IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar } from '@ionic/angular/standalone';
4+
import type { ModalDragEventDetail } from '@ionic/angular/standalone';
5+
6+
@Component({
7+
selector: 'app-example',
8+
templateUrl: 'example.component.html',
9+
standalone: true,
10+
imports: [IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar],
11+
})
12+
export class ExampleComponent {
13+
@ViewChild('header', { read: ElementRef })
14+
header!: ElementRef<HTMLIonHeaderElement>;
15+
// Assign the current snap breakpoint to the initial breakpoint so
16+
// that we can track changes during the drag
17+
currentSnap = 0.25;
18+
19+
onDragMove(event: CustomEvent<ModalDragEventDetail>) {
20+
// `progress` is a value from 1 (top) to 0 (bottom)
21+
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
22+
const { progress, snapBreakpoint } = event.detail;
23+
24+
if (this.currentSnap !== snapBreakpoint) {
25+
this.currentSnap = snapBreakpoint as number;
26+
27+
console.log('Current snap breakpoint:', snapBreakpoint);
28+
}
29+
30+
const headerEl = this.header.nativeElement;
31+
/**
32+
* Inverse relationship:
33+
* 1.0 progress = 0 opacity
34+
* 0 progress = 1.0 opacity
35+
*/
36+
const currentOpacity = 1 - progress;
37+
38+
headerEl.style.opacity = currentOpacity.toString();
39+
}
40+
41+
onDragEnd(event: CustomEvent<ModalDragEventDetail>) {
42+
// `progress` is a value from 1 (top) to 0 (bottom)
43+
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
44+
const { progress, snapBreakpoint } = event.detail;
45+
const headerEl = this.header.nativeElement;
46+
47+
/**
48+
* If the modal is snapping to the closed state (0), reset the
49+
* styles.
50+
*/
51+
if (snapBreakpoint === 0) {
52+
headerEl.style.removeProperty('opacity');
53+
headerEl.style.removeProperty('transition');
54+
return;
55+
}
56+
57+
// Smooth transition to the final resting opacity
58+
headerEl.style.transition = 'opacity 0.4s ease';
59+
// The final opacity matches the inverse of the resting progress
60+
headerEl.style.opacity = (1 - progress).toString();
61+
}
62+
63+
/**
64+
* If the user dismisses the modal (e.g. tapping the backdrop),
65+
* reset the styles.
66+
*/
67+
onWillDismiss() {
68+
const headerEl = this.header.nativeElement;
69+
70+
// Reset styles when the modal is dismissed
71+
headerEl.style.removeProperty('opacity');
72+
headerEl.style.removeProperty('transition');
73+
}
74+
}
75+
```
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Modal</title>
7+
<link rel="stylesheet" href="../../common.css" />
8+
<script src="../../common.js"></script>
9+
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core@8/dist/ionic/ionic.esm.js"></script>
10+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@8/css/ionic.bundle.css" />
11+
</head>
12+
13+
<body>
14+
<ion-app>
15+
<ion-header>
16+
<ion-toolbar>
17+
<ion-title>App</ion-title>
18+
</ion-toolbar>
19+
</ion-header>
20+
<ion-content class="ion-padding">
21+
<ion-button id="open-modal" expand="block">Open Sheet Modal</ion-button>
22+
23+
<ion-modal trigger="open-modal" initial-breakpoint="0.25">
24+
<ion-content class="ion-padding">
25+
<div class="ion-margin-top">
26+
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
27+
</div>
28+
</ion-content>
29+
</ion-modal>
30+
</ion-content>
31+
</ion-app>
32+
33+
<script>
34+
const modal = document.querySelector('ion-modal');
35+
const header = document.querySelector('ion-header');
36+
// Assign the current snap breakpoint to the initial breakpoint so
37+
// that we can track changes during the drag
38+
let currentSnap = 0.25;
39+
modal.breakpoints = [0, 0.25, 0.5, 0.75, 1];
40+
41+
modal.addEventListener('ionDragMove', (event) => {
42+
// `progress` is a value from 1 (top) to 0 (bottom)
43+
const { progress, snapBreakpoint } = event.detail;
44+
45+
if (currentSnap !== snapBreakpoint) {
46+
currentSnap = snapBreakpoint;
47+
48+
console.log('Current snap breakpoint:', snapBreakpoint);
49+
}
50+
51+
/**
52+
* Inverse relationship:
53+
* 1.0 progress = 0 opacity
54+
* 0 progress = 1.0 opacity
55+
*/
56+
const currentOpacity = 1 - progress;
57+
58+
header.style.opacity = currentOpacity;
59+
});
60+
61+
modal.addEventListener('ionDragEnd', (event) => {
62+
// `progress` is a value from 1 (top) to 0 (bottom)
63+
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
64+
const { progress, snapBreakpoint } = event.detail;
65+
66+
/**
67+
* If the modal is snapping to the closed state (0), reset the
68+
* styles.
69+
*/
70+
if (snapBreakpoint === 0) {
71+
header.style.removeProperty('opacity');
72+
header.style.removeProperty('transition');
73+
return;
74+
}
75+
76+
// Smooth transition to the final resting opacity
77+
header.style.transition = 'opacity 0.4s ease';
78+
// The final opacity matches the inverse of the resting progress
79+
header.style.opacity = 1 - progress;
80+
});
81+
82+
/**
83+
* If the user dismisses the modal (e.g. tapping the backdrop),
84+
* reset the styles.
85+
*/
86+
modal.addEventListener('willDismiss', (event) => {
87+
// Reset styles when the modal is dismissed
88+
header.style.removeProperty('opacity');
89+
header.style.removeProperty('transition');
90+
});
91+
</script>
92+
</body>
93+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Playground from '@site/src/components/global/Playground';
2+
3+
import javascript from './javascript.md';
4+
5+
import react from './react.md';
6+
7+
import vue from './vue.md';
8+
9+
import angular_example_component_html from './angular/example_component_html.md';
10+
import angular_example_component_ts from './angular/example_component_ts.md';
11+
12+
<Playground
13+
version="8"
14+
code={{
15+
javascript,
16+
react,
17+
vue,
18+
angular: {
19+
files: {
20+
'src/app/example.component.html': angular_example_component_html,
21+
'src/app/example.component.ts': angular_example_component_ts,
22+
},
23+
},
24+
}}
25+
src="usage/v8/modal/drag-move-event/demo.html"
26+
devicePreview
27+
includeIonContent={false}
28+
showConsole={true}
29+
/>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
```html
2+
<ion-header>
3+
<ion-toolbar>
4+
<ion-title>App</ion-title>
5+
</ion-toolbar>
6+
</ion-header>
7+
<ion-content class="ion-padding">
8+
<ion-button id="open-modal" expand="block">Open Sheet Modal</ion-button>
9+
10+
<ion-modal trigger="open-modal" initial-breakpoint="0.25">
11+
<ion-content class="ion-padding">
12+
<div class="ion-margin-top">
13+
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
14+
</div>
15+
</ion-content>
16+
</ion-modal>
17+
</ion-content>
18+
19+
<script>
20+
const modal = document.querySelector('ion-modal');
21+
const header = document.querySelector('ion-header');
22+
// Assign the current snap breakpoint to the initial breakpoint so
23+
// that we can track changes during the drag
24+
let currentSnap = 0.25;
25+
modal.breakpoints = [0, 0.25, 0.5, 0.75, 1];
26+
27+
modal.addEventListener('ionDragMove', (event) => {
28+
// `progress` is a value from 1 (top) to 0 (bottom)
29+
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
30+
const { progress, snapBreakpoint } = event.detail;
31+
32+
if (currentSnap !== snapBreakpoint) {
33+
currentSnap = snapBreakpoint;
34+
35+
console.log('Current snap breakpoint:', snapBreakpoint);
36+
}
37+
38+
/**
39+
* Inverse relationship:
40+
* 1.0 progress = 0 opacity
41+
* 0 progress = 1.0 opacity
42+
*/
43+
const currentOpacity = 1 - progress;
44+
45+
header.style.opacity = currentOpacity;
46+
});
47+
48+
modal.addEventListener('ionDragEnd', (event) => {
49+
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
50+
const { progress, snapBreakpoint } = event.detail;
51+
52+
/**
53+
* If the modal is snapping to the closed state (0), reset the
54+
* styles.
55+
*/
56+
if (snapBreakpoint === 0) {
57+
header.style.removeProperty('opacity');
58+
return;
59+
}
60+
61+
// Smooth transition to the final resting opacity
62+
header.style.transition = 'opacity 0.4s ease';
63+
// The final opacity matches the inverse of the resting progress
64+
header.style.opacity = 1 - progress;
65+
});
66+
67+
/**
68+
* If the user dismisses the modal (e.g. tapping the backdrop),
69+
* reset the styles.
70+
*/
71+
modal.addEventListener('willDismiss', (event) => {
72+
// Reset styles when the modal is dismissed
73+
header.style.removeProperty('opacity');
74+
header.style.removeProperty('transition');
75+
});
76+
</script>
77+
```

0 commit comments

Comments
 (0)