feat(modal): scroll at any breakpoint

This commit is contained in:
Maria Hutt
2025-01-13 16:42:50 -08:00
parent 05928e3877
commit ed0e7de898
5 changed files with 66 additions and 6 deletions

View File

@ -1085,6 +1085,7 @@ ion-modal,prop,keyboardClose,boolean,true,false,false
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,mode,"ios" | "md",undefined,false,false
ion-modal,prop,presentingElement,HTMLElement | undefined,undefined,false,false
ion-modal,prop,scrollAtEdge,boolean,true,false,false
ion-modal,prop,showBackdrop,boolean,true,false,false
ion-modal,prop,trigger,string | undefined,undefined,false,false
ion-modal,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>

View File

@ -1793,6 +1793,10 @@ export namespace Components {
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode.
*/
"presentingElement"?: HTMLElement;
/**
* Determines whether or not the sheet modal will only scroll when fully expanded. If the value is `true`, the modal will only scroll when fully expanded. If the value is `false`, the modal will scroll at any breakpoint.
*/
"scrollAtEdge": boolean;
/**
* Move a sheet style modal to a specific breakpoint. The breakpoint value must be a value defined in your `breakpoints` array.
*/
@ -6618,6 +6622,10 @@ declare namespace LocalJSX {
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode.
*/
"presentingElement"?: HTMLElement;
/**
* Determines whether or not the sheet modal will only scroll when fully expanded. If the value is `true`, the modal will only scroll when fully expanded. If the value is `false`, the modal will scroll at any breakpoint.
*/
"scrollAtEdge"?: boolean;
/**
* If `true`, a backdrop will be displayed behind the modal. This property controls whether or not the backdrop darkens the screen when the modal is presented. It does not control whether or not the backdrop is active or present in the DOM.
*/

View File

@ -1,3 +1,4 @@
import { createAnimation } from '@utils/animation/animation';
import { isIonContent, findClosestIonContent } from '@utils/content';
import { createGesture } from '@utils/gesture';
import { clamp, raf, getElementRoot } from '@utils/helpers';
@ -51,7 +52,8 @@ export const createSheetGesture = (
breakpoints: number[] = [],
getCurrentBreakpoint: () => number,
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
onBreakpointChange: (breakpoint: number) => void,
scrollAtEdge: boolean
) => {
// Defaults for the sheet swipe animation
const defaultBackdrop = [
@ -71,6 +73,10 @@ export const createSheetGesture = (
{ offset: 1, transform: 'translateY(100%)' },
],
BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
CONTENT_KEYFRAMES: [
{ offset: 0, maxHeight: '100%' },
{ offset: 1, maxHeight: '0%' },
],
};
const contentEl = baseEl.querySelector('ion-content');
@ -81,9 +87,20 @@ export const createSheetGesture = (
const canDismissMaxStep = 0.95;
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
let contentAnimation: Animation | undefined;
const maxBreakpoint = breakpoints[breakpoints.length - 1];
const minBreakpoint = breakpoints[0];
if (!scrollAtEdge) {
contentAnimation = animation
.addAnimation(
createAnimation('contentAnimation')
.addElement(contentEl!.parentElement!)
.keyframes(SheetDefaults.CONTENT_KEYFRAMES)
)
.childAnimations.find((ani) => ani.id === 'contentAnimation');
}
const enableBackdrop = () => {
baseEl.style.setProperty('pointer-events', 'auto');
backdropEl.style.setProperty('pointer-events', 'auto');
@ -138,7 +155,7 @@ export const createSheetGesture = (
}
}
if (contentEl && currentBreakpoint !== maxBreakpoint) {
if (contentEl && currentBreakpoint !== maxBreakpoint && scrollAtEdge) {
contentEl.scrollY = false;
}
@ -323,6 +340,20 @@ export const createSheetGesture = (
},
]);
if (contentAnimation) {
/**
* The modal content should scroll at any breakpoint.
* In order to do this, the content needs to be completely
* viewable so scrolling can access everything. Othewise,
* the default behavior would show the content off the screen
* and only allow scrolling when the sheet is fully expanded.
*/
contentAnimation.keyframes([
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
]);
}
animation.progressStep(0);
}
@ -339,13 +370,14 @@ export const createSheetGesture = (
}
/**
* If the sheet is going to be fully expanded then we should enable
* scrolling immediately. The sheet modal animation takes ~500ms to finish
* If the sheet is going to be fully expanded or if the sheet has toggled
* to scroll at any breakpoint then we should enable scrolling immediately.
* The sheet modal animation takes ~500ms to finish
* so if we wait until then there is a visible delay for when scrolling is
* re-enabled. Native iOS allows for scrolling on the sheet modal as soon
* as the gesture is released, so we align with that.
*/
if (contentEl && snapToBreakpoint === breakpoints[breakpoints.length - 1]) {
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !scrollAtEdge)) {
contentEl.scrollY = true;
}
@ -365,6 +397,7 @@ export const createSheetGesture = (
raf(() => {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
animation.progressStart(true, 1 - snapToBreakpoint);
currentBreakpoint = snapToBreakpoint;
onBreakpointChange(currentBreakpoint);

View File

@ -289,6 +289,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() canDismiss: boolean | ((data?: any, role?: string) => Promise<boolean>) = true;
/**
* Determines whether or not the sheet modal will only
* scroll when fully expanded.
*
* If the value is `true`, the modal will only scroll
* when fully expanded.
* If the value is `false`, the modal will scroll at
* any breakpoint.
*/
@Prop() scrollAtEdge = true;
/**
* Emitted after the modal has presented.
*/
@ -687,7 +698,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.currentBreakpoint = breakpoint;
this.ionBreakpointDidChange.emit({ breakpoint });
}
}
},
this.scrollAtEdge
);
this.gesture = gesture;

View File

@ -184,11 +184,17 @@
${items}
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
`;
let extraOptions = {
initialBreakpoint: 0.25,
breakpoints: [0, 0.25, 0.5, 0.75, 1],
prefersScrollingWhenScrolledToEdge: false,
};
if (options) {