diff --git a/docs/api/modal.md b/docs/api/modal.md index 1249e2a813b..f8c4ce532e4 100644 --- a/docs/api/modal.md +++ b/docs/api/modal.md @@ -7,6 +7,8 @@ import Methods from '@ionic-internal/component-api/v8/modal/methods.md'; import Parts from '@ionic-internal/component-api/v8/modal/parts.md'; import CustomProps from '@ionic-internal/component-api/v8/modal/custom-props.mdx'; import Slots from '@ionic-internal/component-api/v8/modal/slots.md'; +import SheetDragEvents from '@site/static/usage/v8/modal/sheet/drag-events/index.md'; +import CardDragEvents from '@site/static/usage/v8/modal/card/drag-events/index.md'; ion-modal: Ionic Mobile App Custom Modal API Component @@ -115,6 +117,12 @@ import CardExample from '@site/static/usage/v8/modal/card/basic/index.md'; +### Drag Events for Card Modals + +When using a card modal, you may want to perform certain actions based on the dragging of the card. Ionic emits several events related to dragging that can be used for this purpose, such as `ionDragStart`, `ionDragMove`, and `ionDragEnd`. + + + ## Sheet Modal :::info @@ -165,6 +173,12 @@ import SheetScrollingContentExample from '@site/static/usage/v8/modal/sheet/expa +### Drag Events for Sheet Modals + +When using a sheet modal, you may want to perform certain actions based on the dragging of the sheet. Ionic emits several events related to dragging that can be used for this purpose, such as `ionDragStart`, `ionDragMove`, and `ionDragEnd`. + + + ## Styling Modals are presented at the root of your application so they overlay your entire app. This behavior applies to both inline modals and modals presented from a controller. As a result, custom modal styles can not be scoped to a particular component as they will not apply to the modal. Instead, styles must be applied globally. For most developers, placing the custom styles in `global.css` is sufficient. @@ -251,6 +265,59 @@ interface ModalCustomEvent extends CustomEvent { } ``` +### ModalDragEventDetail + +When using the `ionDragMove` and `ionDragEnd` events, the event detail contains the following properties: + +```typescript +interface ModalDragEventDetail { + /** + * The current Y position of the modal. + * + * This can be used to determine how far the modal has been dragged. + */ + currentY: number; + /** + * The change in Y position since the last event. + * + * This can be used to determine the direction of the drag. + */ + deltaY: number; + /** + * The velocity of the drag in the Y direction. + * + * This can be used to determine how fast the modal is being dragged. + */ + velocityY: number; + /** + * A number between 0 and 1. + * + * In a sheet modal, progress represents the relative position between + * the lowest and highest defined breakpoints. + * + * In a card modal, it measures the relative position between the + * bottom of the screen and the top of the modal when it is fully + * open. + * + * This can be used to style content based on how far the modal has + * been dragged. + */ + progress: number; + /** + * If the modal is a sheet modal, this will be the breakpoint that + * the modal will snap to if the user lets go of the modal at the + * current moment. + * + * If it's a card modal, this property will not be included in the + * event payload. + * + * This can be used to style content based on where the modal will + * snap to upon release. + */ + currentBreakpoint?: number; +} +``` + ## Accessibility ### Keyboard Interactions diff --git a/static/code/stackblitz/v8/angular/package.json b/static/code/stackblitz/v8/angular/package.json index 1b5bd920983..82814bfdfeb 100644 --- a/static/code/stackblitz/v8/angular/package.json +++ b/static/code/stackblitz/v8/angular/package.json @@ -15,8 +15,8 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-browser-dynamic": "^20.0.0", "@angular/router": "^20.0.0", - "@ionic/angular": "8.7.14", - "@ionic/core": "8.7.14", + "@ionic/angular": "8.7.17-dev.11772118942.181221d4", + "@ionic/core": "8.7.17-dev.11772118942.181221d4", "ionicons": "8.0.13", "rxjs": "^7.8.1", "tslib": "^2.5.0", diff --git a/static/code/stackblitz/v8/html/package.json b/static/code/stackblitz/v8/html/package.json index e0c5262152f..1122bd961da 100644 --- a/static/code/stackblitz/v8/html/package.json +++ b/static/code/stackblitz/v8/html/package.json @@ -9,7 +9,7 @@ "start": "vite preview" }, "dependencies": { - "@ionic/core": "8.7.14", + "@ionic/core": "8.7.17-dev.11772118942.181221d4", "ionicons": "8.0.13" }, "devDependencies": { diff --git a/static/code/stackblitz/v8/react/package.json b/static/code/stackblitz/v8/react/package.json index bbbf6308c01..41436604bc0 100644 --- a/static/code/stackblitz/v8/react/package.json +++ b/static/code/stackblitz/v8/react/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@ionic/react": "8.7.14", - "@ionic/react-router": "8.7.14", + "@ionic/react": "8.7.17-dev.11772118942.181221d4", + "@ionic/react-router": "8.7.17-dev.11772118942.181221d4", "@types/node": "^24.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/static/code/stackblitz/v8/vue/package.json b/static/code/stackblitz/v8/vue/package.json index dfe2750877e..421a8a70e32 100644 --- a/static/code/stackblitz/v8/vue/package.json +++ b/static/code/stackblitz/v8/vue/package.json @@ -8,8 +8,8 @@ "preview": "vite preview" }, "dependencies": { - "@ionic/vue": "8.7.14", - "@ionic/vue-router": "8.7.14", + "@ionic/vue": "8.7.17-dev.11772118942.181221d4", + "@ionic/vue-router": "8.7.17-dev.11772118942.181221d4", "vue": "^3.2.25", "vue-router": "5.0.1" }, diff --git a/static/usage/v8/modal/card/drag-events/angular/example_component_html.md b/static/usage/v8/modal/card/drag-events/angular/example_component_html.md new file mode 100644 index 00000000000..0327526a1c0 --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/angular/example_component_html.md @@ -0,0 +1,37 @@ +```html +
+ + + App + + + + + Open Card Modal + + + + + + Modal + + + +
+ Drag the handle to adjust the background brightness based on a custom brightness. +
+
+
+
+
+
+``` diff --git a/static/usage/v8/modal/card/drag-events/angular/example_component_ts.md b/static/usage/v8/modal/card/drag-events/angular/example_component_ts.md new file mode 100644 index 00000000000..4c999605489 --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/angular/example_component_ts.md @@ -0,0 +1,85 @@ +```ts +import { Component, ElementRef, ViewChild, OnInit } from '@angular/core'; +import { IonHeader, IonToolbar, IonTitle, IonContent, IonButton, IonModal, IonLabel } from '@ionic/angular/standalone'; +import type { ModalDragEventDetail } from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-example', + templateUrl: 'example.component.html', + standalone: true, + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonButton, IonModal, IonLabel], +}) +export class ExampleComponent implements OnInit { + @ViewChild('modal', { static: true }) modal!: IonModal; + @ViewChild('appPage', { static: true }) appPage!: ElementRef; + + presentingElement: HTMLElement | undefined; + + private readonly DARKEST_PERCENT = 50; + private readonly BRIGHTNESS_RANGE = 100 - this.DARKEST_PERCENT; + + ngOnInit() { + this.presentingElement = this.appPage.nativeElement; + } + + onModalWillPresent() { + const appEl = this.appPage.nativeElement; + + appEl.style.transition = 'filter 0.4s ease'; + // Set to max darkness immediately + appEl.style.setProperty('filter', `brightness(${this.DARKEST_PERCENT}%)`, 'important'); + } + + onDragStart() { + const appEl = this.appPage.nativeElement; + + // Ensure transitions are off during the active drag + appEl.style.transition = 'none'; + } + + onDragMove(event: CustomEvent) { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + + const appEl = this.appPage.nativeElement; + /** + * Calculate the current brightness based on how far the user has + * dragged. + * + * When dragging up, the background should become darker, + * and when dragging down, it should become lighter. + */ + const brightnessValue = 100 - progress * this.BRIGHTNESS_RANGE; + + // Update the brightness in real-time as the user drags + appEl.style.setProperty('filter', `brightness(${brightnessValue}%)`, 'important'); + } + + onDragEnd(event: CustomEvent) { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + + const appEl = this.appPage.nativeElement; + /** + * Snap the background brightness based on the user's drag intent. + * Progress > 0.4 implies an intent to open (snap dark), + * while < 0.4 implies a dismissal (snap bright). + */ + const brightnessValue = progress > 0.4 ? this.DARKEST_PERCENT : 100; + + // Reset to max darkness on snap-back for a nice visual effect + appEl.style.setProperty('filter', `brightness(${brightnessValue}%)`, 'important'); + + // Re-enable transition for a smooth snap-back + appEl.style.transition = 'filter 0.4s ease'; + } + + onModalWillDismiss() { + const appEl = this.appPage.nativeElement; + + // Clean up styles when the modal is dismissed + appEl.style.removeProperty('filter'); + appEl.style.removeProperty('transition'); + } +} +``` diff --git a/static/usage/v8/modal/card/drag-events/demo.html b/static/usage/v8/modal/card/drag-events/demo.html new file mode 100644 index 00000000000..5b41b41f23d --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/demo.html @@ -0,0 +1,78 @@ + + + + + + Modal + + + + + + + + +
+ + + App + + + + Open Card Modal + + + + + Modal + + + +
+ Drag the handle to adjust the background brightness based on a custom brightness. +
+
+
+
+
+
+ + + + diff --git a/static/usage/v8/modal/card/drag-events/index.md b/static/usage/v8/modal/card/drag-events/index.md new file mode 100644 index 00000000000..045bfe9f151 --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/index.md @@ -0,0 +1,28 @@ +import Playground from '@site/src/components/global/Playground'; + +import javascript from './javascript.md'; + +import react from './react.md'; + +import vue from './vue.md'; + +import angular_example_component_html from './angular/example_component_html.md'; +import angular_example_component_ts from './angular/example_component_ts.md'; + + diff --git a/static/usage/v8/modal/card/drag-events/javascript.md b/static/usage/v8/modal/card/drag-events/javascript.md new file mode 100644 index 00000000000..cbb63f7684d --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/javascript.md @@ -0,0 +1,87 @@ +```html +
+ + + App + + + + Open Card Modal + + + + + Modal + + + +
+ Drag the handle to adjust the background brightness based on a custom brightness. +
+
+
+
+
+ + +``` diff --git a/static/usage/v8/modal/card/drag-events/react.md b/static/usage/v8/modal/card/drag-events/react.md new file mode 100644 index 00000000000..77ce43c134e --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/react.md @@ -0,0 +1,110 @@ +```tsx +import React, { useRef, useState, useEffect } from 'react'; +import { IonButton, IonModal, IonHeader, IonContent, IonToolbar, IonTitle, IonPage, IonLabel } from '@ionic/react'; +import type { ModalDragEventDetail } from '@ionic/react'; + +function Example() { + const modal = useRef(null); + const page = useRef(null); + + const [presentingElement, setPresentingElement] = useState(null); + + const DARKEST_PERCENT = 50; + const BRIGHTNESS_RANGE = 100 - DARKEST_PERCENT; + + useEffect(() => { + setPresentingElement(page.current); + }, []); + + const onModalWillPresent = () => { + page.current!.style.transition = 'filter 0.4s ease'; + // Set to max darkness immediately + page.current!.style.setProperty('filter', `brightness(${DARKEST_PERCENT}%)`, 'important'); + }; + + const onDragStart = () => { + // Ensure transitions are off during the active drag + page.current!.style.transition = 'none'; + }; + + const onDragMove = (event: CustomEvent) => { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + + /** + * Calculate the current brightness based on how far the user has + * dragged. + * + * When dragging up, the background should become darker, + * and when dragging down, it should become lighter. + */ + const brightnessValue = 100 - progress * BRIGHTNESS_RANGE; + + // Update the brightness in real-time as the user drags + page.current!.style.setProperty('filter', `brightness(${brightnessValue}%)`, 'important'); + }; + + const onDragEnd = (event: CustomEvent) => { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + + /** + * Snap the background brightness based on the user's drag intent. + * Progress > 0.4 implies an intent to open (snap dark), + * while < 0.4 implies a dismissal (snap bright). + */ + const brightnessValue = progress > 0.4 ? DARKEST_PERCENT : 100; + + // Reset to max darkness on snap-back for a nice visual effect + page.current!.style.setProperty('filter', `brightness(${brightnessValue}%)`, 'important'); + + // Re-enable transition for a smooth snap-back + page.current!.style.transition = 'filter 0.4s ease'; + }; + + const onModalWillDismiss = () => { + // Clean up styles when the modal is dismissed + page.current!.style.removeProperty('filter'); + page.current!.style.removeProperty('transition'); + }; + + return ( + + + + App + + + + + Open Card Modal + + + + + + Modal + + + +
+ Drag the handle to adjust the background brightness based on a custom brightness. +
+
+
+
+
+ ); +} + +export default Example; +``` diff --git a/static/usage/v8/modal/card/drag-events/vue.md b/static/usage/v8/modal/card/drag-events/vue.md new file mode 100644 index 00000000000..a23a27cc8cf --- /dev/null +++ b/static/usage/v8/modal/card/drag-events/vue.md @@ -0,0 +1,114 @@ +```vue + + + +``` diff --git a/static/usage/v8/modal/sheet/drag-events/angular/example_component_html.md b/static/usage/v8/modal/sheet/drag-events/angular/example_component_html.md new file mode 100644 index 00000000000..491f8d5e215 --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/angular/example_component_html.md @@ -0,0 +1,28 @@ +```html + + + App + + + + Open Sheet Modal + + + + +
+ Drag the handle to adjust the modal's opacity based on a custom max opacity. +
+
+
+
+
+``` diff --git a/static/usage/v8/modal/sheet/drag-events/angular/example_component_ts.md b/static/usage/v8/modal/sheet/drag-events/angular/example_component_ts.md new file mode 100644 index 00000000000..98fa60ff5cd --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/angular/example_component_ts.md @@ -0,0 +1,108 @@ +```ts +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar } from '@ionic/angular/standalone'; +import type { ModalDragEventDetail } from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-example', + templateUrl: 'example.component.html', + standalone: true, + imports: [IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar], +}) +export class ExampleComponent { + @ViewChild('modal', { read: ElementRef }) + modal!: ElementRef; + + private baseOpacity!: number; + private readonly MAX_OPACITY = 0.8; + + onDragStart() { + const modalEl = this.modal.nativeElement; + const style = getComputedStyle(modalEl); + + // Fetch the current variable value + this.baseOpacity = parseFloat(style.getPropertyValue('--backdrop-opacity')); + + // Ensure transitions are off during the active drag + modalEl.style.transition = 'none'; + } + + onDragMove(event: CustomEvent) { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + const modalEl = this.modal.nativeElement; + const initialBreakpoint = modalEl.initialBreakpoint!; + let dynamicOpacity: number; + + /** + * Dragging Down: Progress is between 0 and the initial breakpoint + */ + if (progress <= initialBreakpoint) { + /** + * Calculate how far down the user has dragged from the initial + * breakpoint as a ratio + */ + const downwardProgressRatio = progress / initialBreakpoint; + /** + * Multiplying this by `baseOpacity` ensures that as progress hits 0, + * the opacity also hits 0 without a sudden jump + */ + dynamicOpacity = downwardProgressRatio * this.baseOpacity; + } else { + /** + * Dragging Up: Progress is between the initial breakpoint and 1.0 + */ + + /** + * Calculate how far up the user has dragged from the initial + * breakpoint as a ratio + */ + const distanceFromStart = progress - initialBreakpoint; + /** + * Calculate the total available space to drag up from the initial + * breakpoint to the top (1.0) + */ + const range = 1 - initialBreakpoint; + /** + * Normalizes that distance into a 0.0 to 1.0 value relative to + * the available upward space + */ + const currentGain = distanceFromStart / range; + + // Scale from `baseOpacity` up to `MAX_OPACITY` + dynamicOpacity = this.baseOpacity + currentGain * (this.MAX_OPACITY - this.baseOpacity); + } + + modalEl.style.setProperty('--backdrop-opacity', dynamicOpacity.toString()); + } + + onDragEnd(event: CustomEvent) { + // `currentBreakpoint` tells us which snap point the modal will animate to after the drag ends + const { currentBreakpoint } = event.detail; + const modalEl = this.modal.nativeElement; + + /** + * If the modal is snapping to the closed state (0), reset the + * styles. + */ + if (currentBreakpoint === 0) { + modalEl.style.removeProperty('--backdrop-opacity'); + return; + } + + // Determine the target opacity for the snap-back animation + let targetOpacity = this.baseOpacity; + /** + * If snapping to the top (1), set opacity to MAX_OPACITY for a nice + * visual effect. + */ + if (currentBreakpoint === 1) { + targetOpacity = this.MAX_OPACITY; + } + + // Re-enable transition for a smooth snap-back + modalEl.style.transition = '--backdrop-opacity 0.3s ease'; + modalEl.style.setProperty('--backdrop-opacity', targetOpacity.toString()); + } +} +``` diff --git a/static/usage/v8/modal/sheet/drag-events/demo.html b/static/usage/v8/modal/sheet/drag-events/demo.html new file mode 100644 index 00000000000..2835d936bc4 --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/demo.html @@ -0,0 +1,127 @@ + + + + + + Modal + + + + + + + + + + + App + + + + Open Sheet Modal + + + +
+ Drag the handle to adjust the modal's opacity based on a custom max opacity. +
+
+
+
+
+ + + + diff --git a/static/usage/v8/modal/sheet/drag-events/index.md b/static/usage/v8/modal/sheet/drag-events/index.md new file mode 100644 index 00000000000..d947355b9d7 --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/index.md @@ -0,0 +1,28 @@ +import Playground from '@site/src/components/global/Playground'; + +import javascript from './javascript.md'; + +import react from './react.md'; + +import vue from './vue.md'; + +import angular_example_component_html from './angular/example_component_html.md'; +import angular_example_component_ts from './angular/example_component_ts.md'; + + diff --git a/static/usage/v8/modal/sheet/drag-events/javascript.md b/static/usage/v8/modal/sheet/drag-events/javascript.md new file mode 100644 index 00000000000..00270f7654e --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/javascript.md @@ -0,0 +1,113 @@ +```html + + + App + + + + Open Sheet Modal + + + +
+ Drag the handle to adjust the modal's opacity based on a custom max opacity. +
+
+
+
+ + +``` diff --git a/static/usage/v8/modal/sheet/drag-events/react.md b/static/usage/v8/modal/sheet/drag-events/react.md new file mode 100644 index 00000000000..34e62d7d652 --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/react.md @@ -0,0 +1,132 @@ +```tsx +import React, { useRef } from 'react'; +import { IonButton, IonModal, IonHeader, IonContent, IonToolbar, IonTitle, IonPage, IonLabel } from '@ionic/react'; +import type { ModalDragEventDetail } from '@ionic/react'; + +function Example() { + const modal = useRef(null); + const baseOpacity = useRef(undefined); + const MAX_OPACITY = 0.8; + + const onDragStart = () => { + const modalEl = modal.current!; + const style = getComputedStyle(modalEl); + + // Fetch the current variable value + baseOpacity.current = parseFloat(style.getPropertyValue('--backdrop-opacity')); + + // Ensure transitions are off during the active drag + modalEl.style.transition = 'none'; + }; + + const onDragMove = (event: CustomEvent) => { + // `progress` is a value from 1 (top) to 0 (bottom) + const { progress } = event.detail; + const modalEl = modal.current!; + const initialBreakpoint = modalEl.initialBreakpoint!; + let dynamicOpacity: number; + + /** + * Dragging Down: Progress is between 0 and the initial breakpoint + */ + if (progress <= initialBreakpoint) { + /** + * Calculate how far down the user has dragged from the initial + * breakpoint as a ratio + */ + const downwardProgressRatio = progress / initialBreakpoint; + /** + * Multiplying this by `baseOpacity` ensures that as progress hits 0, + * the opacity also hits 0 without a sudden jump + */ + dynamicOpacity = downwardProgressRatio * baseOpacity.current!; + } else { + /** + * Dragging Up: Progress is between the initial breakpoint and 1.0 + */ + + /** + * Calculate how far up the user has dragged from the initial + * breakpoint as a ratio + */ + const distanceFromStart = progress - initialBreakpoint; + /** + * Calculate the total available space to drag up from the initial + * breakpoint to the top (1.0) + */ + const range = 1 - initialBreakpoint; + /** + * Normalizes that distance into a 0.0 to 1.0 value relative to + * the available upward space + */ + const currentGain = distanceFromStart / range; + + // Scale from `baseOpacity` up to `MAX_OPACITY` + dynamicOpacity = baseOpacity.current! + currentGain * (MAX_OPACITY - baseOpacity.current!); + } + + modalEl.style.setProperty('--backdrop-opacity', dynamicOpacity.toString()); + }; + + const onDragEnd = (event: CustomEvent) => { + // `currentBreakpoint` tells us which snap point the modal will animate to after the drag ends + const { currentBreakpoint } = event.detail; + const modalEl = modal.current!; + + /** + * If the modal is snapping to the closed state (0), reset the + * styles. + */ + if (currentBreakpoint === 0) { + modalEl.style.removeProperty('--backdrop-opacity'); + return; + } + + // Determine the target opacity for the snap-back animation + let targetOpacity = baseOpacity.current; + /** + * If snapping to the top (1), set opacity to MAX_OPACITY for a nice + * visual effect. + */ + if (currentBreakpoint === 1) { + targetOpacity = MAX_OPACITY; + } + + // Re-enable transition for a smooth snap-back + modalEl.style.transition = '--backdrop-opacity 0.3s ease'; + modalEl.style.setProperty('--backdrop-opacity', targetOpacity!.toString()); + }; + + return ( + + + + App + + + + + Open Sheet Modal + + + +
+ Drag the handle to adjust the modal's opacity based on a custom max opacity. +
+
+
+
+
+ ); +} + +export default Example; +``` diff --git a/static/usage/v8/modal/sheet/drag-events/vue.md b/static/usage/v8/modal/sheet/drag-events/vue.md new file mode 100644 index 00000000000..ee686e76383 --- /dev/null +++ b/static/usage/v8/modal/sheet/drag-events/vue.md @@ -0,0 +1,128 @@ +```vue + + + +```