diff --git a/core/api.txt b/core/api.txt index 67d4941755..3d8dff2993 100644 --- a/core/api.txt +++ b/core/api.txt @@ -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 diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 1bdfaa8854..4dd337766b 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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. */ diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index a90eb5d99e..9eba5ab7fb 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -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); diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index bc8f6184f9..3c5351d4dc 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -289,6 +289,17 @@ export class Modal implements ComponentInterface, OverlayInterface { */ @Prop() canDismiss: boolean | ((data?: any, role?: string) => Promise) = 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; diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index bc4ba33800..a2fa19f44a 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -184,11 +184,17 @@ ${items} + + + Footer + + `; let extraOptions = { initialBreakpoint: 0.25, breakpoints: [0, 0.25, 0.5, 0.75, 1], + prefersScrollingWhenScrolledToEdge: false, }; if (options) {