From ea6898635434bd80c29a2ac6aa43c28f165b1a3b Mon Sep 17 00:00:00 2001 From: kumibrr Date: Mon, 23 Dec 2024 00:24:47 +0100 Subject: [PATCH 01/39] feat(modal): added snapBreakpoints to sheet modals --- core/src/components.d.ts | 8 ++++ core/src/components/modal/gestures/sheet.ts | 40 ++++++++++++++++--- core/src/components/modal/modal.tsx | 22 ++++++++++ .../components/modal/test/sheet/index.html | 6 +++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 1bdfaa88545..b6b953ea918 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1801,6 +1801,10 @@ export namespace Components { * 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. */ "showBackdrop": boolean; + /** + * The snapBreakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property and they must be a value in `breakpoints` property. The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints` allows the content to scroll, and the modal will only be draggable by the handle. + */ + "snapBreakpoints"?: number[]; /** * An ID corresponding to the trigger element that causes the modal to open when clicked. */ @@ -6622,6 +6626,10 @@ declare namespace LocalJSX { * 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. */ "showBackdrop"?: boolean; + /** + * The snapBreakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property and they must be a value in `breakpoints` property. The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints` allows the content to scroll, and the modal will only be draggable by the handle. + */ + "snapBreakpoints"?: number[]; /** * An ID corresponding to the trigger element that causes the modal to open when clicked. */ diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index a90eb5d99ea..a20550808da 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'; @@ -49,6 +50,7 @@ export const createSheetGesture = ( backdropBreakpoint: number, animation: Animation, breakpoints: number[] = [], + snapBreakpoints: number[] = [], getCurrentBreakpoint: () => number, onDismiss: () => void, onBreakpointChange: (breakpoint: number) => void @@ -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'); @@ -79,10 +85,19 @@ export const createSheetGesture = ( let offset = 0; let canDismissBlocksGesture = false; const canDismissMaxStep = 0.95; - const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation'); - const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation'); const maxBreakpoint = breakpoints[breakpoints.length - 1]; const minBreakpoint = breakpoints[0]; + const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation'); + const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation'); + let contentAnimation: Animation | undefined; + if (snapBreakpoints.length > 0) { + 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'); @@ -138,7 +153,7 @@ export const createSheetGesture = ( } } - if (contentEl && currentBreakpoint !== maxBreakpoint) { + if (contentEl && currentBreakpoint !== maxBreakpoint && !snapBreakpoints.includes(currentBreakpoint)) { contentEl.scrollY = false; } @@ -152,7 +167,14 @@ export const createSheetGesture = ( * and then swipe again. The target content will not be the same between swipes. */ const contentEl = findClosestIonContent(detail.event.target! as HTMLElement); - currentBreakpoint = getCurrentBreakpoint(); + currentBreakpoint = getCurrentBreakpoint();; + + /** + * If we are in a snap breakpoint, we should not allow the swipe to start. + */ + if (snapBreakpoints.includes(currentBreakpoint) && contentEl) { + return false; + } if (currentBreakpoint === 1 && contentEl) { /** @@ -323,6 +345,13 @@ export const createSheetGesture = ( }, ]); + if (contentAnimation) { + contentAnimation.keyframes([ + { offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` }, + { offset: 1, maxHeight: `${snapToBreakpoint * 100}%` }, + ]); + } + animation.progressStep(0); } @@ -345,7 +374,7 @@ export const createSheetGesture = ( * 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] || snapBreakpoints.includes(snapToBreakpoint))) { contentEl.scrollY = true; } @@ -365,6 +394,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 bc8f6184f9a..28354e73eda 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -75,6 +75,7 @@ export class Modal implements ComponentInterface, OverlayInterface { private wrapperEl?: HTMLElement; private backdropEl?: HTMLIonBackdropElement; private sortedBreakpoints?: number[]; + private sortedSnapBreakpoints?: number[]; private keyboardOpenCallback?: () => void; private moveSheetToBreakpoint?: (options: MoveSheetToBreakpointOptions) => Promise; private inheritedAttributes: Attributes = {}; @@ -130,6 +131,19 @@ export class Modal implements ComponentInterface, OverlayInterface { */ @Prop() breakpoints?: number[]; + /** + * The snapBreakpoints to use when creating a sheet modal. Each value in the + * array must be a decimal between 0 and 1 where 0 indicates the modal is fully + * closed and 1 indicates the modal is fully open. Values are relative + * to the height of the modal, not the height of the screen. One of the values in this + * array must be the value of the `initialBreakpoint` property and they must be a + * value in `breakpoints` property. + * + * The difference between `breakpoints` and `snapBreakpoints` is that `snapBreakpoints` + * allows the content to scroll, and the modal will only be draggable by the handle. + */ + @Prop() snapBreakpoints?: number[]; + /** * A decimal value between 0 and 1 that indicates the * initial point the modal will open at when creating a @@ -354,6 +368,12 @@ export class Modal implements ComponentInterface, OverlayInterface { } } + snapBreakpointsChanged(snapBreakpoints: number[] | undefined) { + if (snapBreakpoints !== undefined) { + this.sortedSnapBreakpoints = snapBreakpoints.sort((a, b) => a - b); + } + } + connectedCallback() { const { el } = this; prepareOverlay(el); @@ -429,6 +449,7 @@ export class Modal implements ComponentInterface, OverlayInterface { raf(() => this.present()); } this.breakpointsChanged(this.breakpoints); + this.snapBreakpointsChanged(this.snapBreakpoints); /** * When binding values in frameworks such as Angular @@ -680,6 +701,7 @@ export class Modal implements ComponentInterface, OverlayInterface { backdropBreakpoint, ani, this.sortedBreakpoints, + this.sortedSnapBreakpoints, () => this.currentBreakpoint ?? 0, () => this.sheetOnDismiss(), (breakpoint: number) => { diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index bc4ba338001..b570adf11f7 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -100,6 +100,12 @@ > Present Sheet Modal (Max breakpoint is not 1) + @@ -193,7 +193,9 @@ Footer + Add More Content +
`; @@ -220,6 +222,20 @@ button.addEventListener('click', () => { modalElement.dismiss(); }); + + const buttonAdd = element.querySelector('#add-content'); + buttonAdd.addEventListener('click', () => { + const dynamicContent = modalElement.shadowRoot.querySelector('#dynamic-content'); + console.log(modalElement.shadowRoot) + const newToolbar = document.createElement('ion-toolbar'); + newToolbar.innerHTML = ` + Additional Content + `; + dynamicContent.appendChild(newToolbar); + dynamicContent + }); + + document.body.appendChild(modalElement); return modalElement; } From 566d3dbbc036429ddaea6b805597cf5ba9d854e6 Mon Sep 17 00:00:00 2001 From: kumibrr Date: Mon, 27 Jan 2025 23:22:40 +0100 Subject: [PATCH 07/39] feat(modal): improved implementation --- .../components/modal/animations/ios.enter.ts | 25 +++++++++++++------ .../components/modal/animations/ios.leave.ts | 12 ++++++--- .../components/modal/animations/md.enter.ts | 25 +++++++++++++------ .../components/modal/animations/md.leave.ts | 16 +++++++++--- core/src/components/modal/animations/sheet.ts | 17 +++++++++---- core/src/components/modal/gestures/sheet.ts | 17 +++---------- core/src/components/modal/modal.scss | 7 ++++++ core/src/components/modal/modal.tsx | 16 ------------ 8 files changed, 79 insertions(+), 56 deletions(-) diff --git a/core/src/components/modal/animations/ios.enter.ts b/core/src/components/modal/animations/ios.enter.ts index d1f3db79f91..294b9c169fa 100644 --- a/core/src/components/modal/animations/ios.enter.ts +++ b/core/src/components/modal/animations/ios.enter.ts @@ -35,22 +35,33 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); - footerAnimation?.addElement(root.querySelector('ion-footer')!); - const baseAnimation = createAnimation('entering-base') .addElement(baseEl) .easing('cubic-bezier(0.32,0.72,0,1)') .duration(500) - .addAnimation([wrapperAnimation]); + .addAnimation([wrapperAnimation]) + .beforeAddWrite(() => { + const ionFooter = baseEl.querySelector('ion-footer'); + if (ionFooter) { + const footerHeight = ionFooter.clientHeight; + const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; + baseEl.shadowRoot!.appendChild(clonedFooter); + ionFooter.remove(); + + // add padding bottom to the .ion-page element to be + // the same as the cloned footer height + const page = baseEl.querySelector('.ion-page') as HTMLElement; + page.style.setProperty('padding-bottom', `${footerHeight}px`); + if (animateContentHeight && footerAnimation) { + footerAnimation.addElement(root.querySelector('ion-footer')!); + baseAnimation.addAnimation(footerAnimation); + } + }}); if (animateContentHeight && contentAnimation) { baseAnimation.addAnimation(contentAnimation); } - if (animateContentHeight && footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - if (presentingEl) { const isMobile = window.innerWidth < 768; const hasCardModal = diff --git a/core/src/components/modal/animations/ios.leave.ts b/core/src/components/modal/animations/ios.leave.ts index 914652878fa..dfb41210f26 100644 --- a/core/src/components/modal/animations/ios.leave.ts +++ b/core/src/components/modal/animations/ios.leave.ts @@ -12,7 +12,7 @@ const createLeaveAnimation = () => { const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(0vh)', 'translateY(100vh)'); - return { backdropAnimation, wrapperAnimation }; + return { backdropAnimation, wrapperAnimation, footerAnimation: undefined }; }; /** @@ -21,19 +21,25 @@ const createLeaveAnimation = () => { export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => { const { presentingEl, currentBreakpoint } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation } = - currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation(); + const { wrapperAnimation, backdropAnimation, footerAnimation } = + currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 }); + footerAnimation?.addElement(root.querySelector('ion-footer')!); + const baseAnimation = createAnimation('leaving-base') .addElement(baseEl) .easing('cubic-bezier(0.32,0.72,0,1)') .duration(duration) .addAnimation(wrapperAnimation); + if (footerAnimation) { + baseAnimation.addAnimation(footerAnimation); + } + if (presentingEl) { const isMobile = window.innerWidth < 768; const hasCardModal = diff --git a/core/src/components/modal/animations/md.enter.ts b/core/src/components/modal/animations/md.enter.ts index 002f6a78761..6f6b4ace6fc 100644 --- a/core/src/components/modal/animations/md.enter.ts +++ b/core/src/components/modal/animations/md.enter.ts @@ -37,21 +37,32 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); - footerAnimation?.addElement(root.querySelector('ion-footer')!); - const baseAnimation = createAnimation() .addElement(baseEl) .easing('cubic-bezier(0.36,0.66,0.04,1)') .duration(280) - .addAnimation([backdropAnimation, wrapperAnimation]); + .addAnimation([backdropAnimation, wrapperAnimation]) + .beforeAddWrite(() => { + const ionFooter = baseEl.querySelector('ion-footer'); + if (ionFooter) { + const footerHeight = ionFooter.clientHeight; + const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; + baseEl.shadowRoot!.appendChild(clonedFooter); + ionFooter.remove(); + + // add padding bottom to the .ion-page element to be + // the same as the cloned footer height + const page = baseEl.querySelector('.ion-page') as HTMLElement; + page.style.setProperty('padding-bottom', `${footerHeight}px`); + if (animateContentHeight && footerAnimation) { + footerAnimation.addElement(root.querySelector('ion-footer')!); + baseAnimation.addAnimation(footerAnimation); + } + }}); if (animateContentHeight && contentAnimation) { baseAnimation.addAnimation(contentAnimation); } - if (animateContentHeight && footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - return baseAnimation; }; diff --git a/core/src/components/modal/animations/md.leave.ts b/core/src/components/modal/animations/md.leave.ts index 9977c678d07..ed3f29d1716 100644 --- a/core/src/components/modal/animations/md.leave.ts +++ b/core/src/components/modal/animations/md.leave.ts @@ -14,7 +14,7 @@ const createLeaveAnimation = () => { { offset: 1, opacity: 0, transform: 'translateY(40px)' }, ]); - return { backdropAnimation, wrapperAnimation }; + return { backdropAnimation, wrapperAnimation, footerAnimation: undefined }; }; /** @@ -23,14 +23,22 @@ const createLeaveAnimation = () => { export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => { const { currentBreakpoint } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation } = - currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation(); + const { wrapperAnimation, backdropAnimation, footerAnimation } = + currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!); + footerAnimation?.addElement(root.querySelector('ion-footer')!); + console.log(footerAnimation); - return createAnimation() + const baseAnimation = createAnimation() .easing('cubic-bezier(0.47,0,0.745,0.715)') .duration(200) .addAnimation([backdropAnimation, wrapperAnimation]); + + if (footerAnimation) { + baseAnimation.addAnimation(footerAnimation); + } + + return baseAnimation; }; diff --git a/core/src/components/modal/animations/sheet.ts b/core/src/components/modal/animations/sheet.ts index 2f8027b6f27..99edda1b4d0 100644 --- a/core/src/components/modal/animations/sheet.ts +++ b/core/src/components/modal/animations/sheet.ts @@ -34,16 +34,16 @@ export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimat { offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` }, ]); - const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight; + const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight; const footerAnimation = createAnimation('footerAnimation').keyframes([ - { offset: 0, opacity: 1, transform: `translateY(-${wrapperHeight}px)` }, - { offset: 1, opacity: 1, transform: `translateY(-${wrapperHeight! * (1 - currentBreakpoint!)}px)` }, + { offset: 0, opacity: 1, transform: `translateY(${footerHeight}px)` }, + { offset: 0.2, opacity: 1, transform: `translateY(0)` }, ]); return { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation }; }; -export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => { +export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions) => { const { currentBreakpoint, backdropBreakpoint } = opts; /** @@ -75,5 +75,12 @@ export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => { { offset: 1, opacity: 1, transform: `translateY(100%)` }, ]); - return { wrapperAnimation, backdropAnimation }; + const footerHeight = baseEl.shadowRoot!.querySelector('ion-footer')?.clientHeight; + const footerAnimation = createAnimation('footerAnimation').keyframes([ + { offset: 0, opacity: 1, transform: `translateY(0)` }, + { offset: 0.7, opacity: 1, transform: `translateY(0)` }, + { offset: 1, opacity: 1, transform: `translateY(${footerHeight}px)` }, + ]); + + return { wrapperAnimation, backdropAnimation, footerAnimation }; }; diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 34377f37bf1..af2945cc787 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -75,11 +75,7 @@ export const createSheetGesture = ( CONTENT_KEYFRAMES: [ { offset: 0, maxHeight: '100%' }, { offset: 1, maxHeight: '0%' }, - ], - FOOTER_KEYFRAMES: [ - { offset: 0, transform: `translateY(0)` }, - { offset: 1, transform: `translateY(-${wrapperEl.clientHeight}px)` }, - ], + ] }; const contentEl = baseEl.querySelector('ion-content'); @@ -93,7 +89,7 @@ export const createSheetGesture = ( const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation'); const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation'); const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation'); - const footerAnimation = animation.childAnimations.find((ani) => ani.id === 'footerAnimation'); + animation.childAnimations.find((ani) => ani.id === 'footerAnimation')?.destroy(); const enableBackdrop = () => { baseEl.style.setProperty('pointer-events', 'auto'); @@ -133,7 +129,6 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); - footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); animation.progressStart(true, 1 - currentBreakpoint); @@ -345,7 +340,7 @@ export const createSheetGesture = ( }, ]); - if (contentAnimation && footerAnimation) { + if (contentAnimation) { /** * The modal content should scroll at any breakpoint when scrollAtEdge * is disabled. In order to do this, the content needs to be completely @@ -357,11 +352,6 @@ export const createSheetGesture = ( { offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` }, { offset: 1, maxHeight: `${snapToBreakpoint * 100}%` }, ]); - - footerAnimation.keyframes([ - { offset: 0, transform: `translateY(-${height * (breakpointOffset)}px)` }, - { offset: 1, transform: `translateY(-${height * (1 - snapToBreakpoint)}px)` }, - ]) } animation.progressStep(0); @@ -408,7 +398,6 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); - footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); animation.progressStart(true, 1 - snapToBreakpoint); currentBreakpoint = snapToBreakpoint; onBreakpointChange(currentBreakpoint); diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.scss index 02b7128c99c..7e1deca8225 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.scss @@ -166,3 +166,10 @@ ion-backdrop { position: absolute; bottom: 0; } + +:host(.modal-sheet) ion-footer { + position: absolute; + bottom: 0; + + width: var(--width); +} diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index e046a2d9df2..86ca4886166 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -532,22 +532,6 @@ export class Modal implements ComponentInterface, OverlayInterface { this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline); - /** - * If scrollAtEdge is false, footer must be fixed to - * the bottom of the modal and be always visible. - */ - if (this.scrollAtEdge === false){ - const footer = this.usersElement.querySelector('ion-footer'); - if (!footer) return; - footer.style.position = 'fixed'; - footer.style.bottom = '0'; - const fixedFooter = footer.cloneNode(true) as HTMLIonFooterElement; - footer.remove(); - this.usersElement.style.setProperty('padding-bottom', `${fixedFooter.clientHeight}px`); - el.shadowRoot!.querySelector('.modal-wrapper')!.appendChild(footer); - } - - /** * When using the lazy loaded build of Stencil, we need to wait * for every Stencil component instance to be ready before presenting From 9ab373307a06884b9edb7f6df469c3f1179eb13e Mon Sep 17 00:00:00 2001 From: kumibrr Date: Mon, 27 Jan 2025 23:27:09 +0100 Subject: [PATCH 08/39] fix: added back breakpoint: 0 --- core/src/components/modal/test/sheet/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index 1f85a8a35b4..ed848e550f1 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -102,7 +102,7 @@ From 556cb6a8d5ffe8b204bd68220b609ad268ae111c Mon Sep 17 00:00:00 2001 From: kumibrr Date: Tue, 28 Jan 2025 14:29:36 +0100 Subject: [PATCH 09/39] removed console.log --- core/src/components/modal/animations/md.leave.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/components/modal/animations/md.leave.ts b/core/src/components/modal/animations/md.leave.ts index ed3f29d1716..7a5d737ad14 100644 --- a/core/src/components/modal/animations/md.leave.ts +++ b/core/src/components/modal/animations/md.leave.ts @@ -29,7 +29,6 @@ export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!); footerAnimation?.addElement(root.querySelector('ion-footer')!); - console.log(footerAnimation); const baseAnimation = createAnimation() .easing('cubic-bezier(0.47,0,0.745,0.715)') From ad90cd9b20db5cf4d85056bf0a8f81700f635c43 Mon Sep 17 00:00:00 2001 From: kumibrr Date: Tue, 28 Jan 2025 14:30:45 +0100 Subject: [PATCH 10/39] feat: added footer slide-in and slide-out animation --- core/src/components/modal/animations/sheet.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/core/src/components/modal/animations/sheet.ts b/core/src/components/modal/animations/sheet.ts index 99edda1b4d0..5354e7398fc 100644 --- a/core/src/components/modal/animations/sheet.ts +++ b/core/src/components/modal/animations/sheet.ts @@ -34,10 +34,21 @@ export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimat { offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` }, ]); - const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight; + const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0; + + const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight + ?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0; + + const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100; + + const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2)); + + const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2)); + const footerAnimation = createAnimation('footerAnimation').keyframes([ { offset: 0, opacity: 1, transform: `translateY(${footerHeight}px)` }, - { offset: 0.2, opacity: 1, transform: `translateY(0)` }, + { offset: headerOffset, opacity: 1, transform: `translateY(${footerHeight}px)` }, + { offset: ((footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' }, ]); return { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation }; @@ -75,11 +86,21 @@ export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimat { offset: 1, opacity: 1, transform: `translateY(100%)` }, ]); - const footerHeight = baseEl.shadowRoot!.querySelector('ion-footer')?.clientHeight; + const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0; + + const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight + ?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0; + + const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100; + + const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2)); + + const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2)); + const footerAnimation = createAnimation('footerAnimation').keyframes([ - { offset: 0, opacity: 1, transform: `translateY(0)` }, - { offset: 0.7, opacity: 1, transform: `translateY(0)` }, - { offset: 1, opacity: 1, transform: `translateY(${footerHeight}px)` }, + { offset: 0, opacity: 1, transform: 'translateY(0)' }, + { offset: (1 - (footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' }, + { offset: (1), opacity: 1, transform: `translateY(${footerHeight}px)` }, ]); return { wrapperAnimation, backdropAnimation, footerAnimation }; From 396bf73b3f3e15a9457907c96f4152afdb22d2fe Mon Sep 17 00:00:00 2001 From: kumibrr Date: Tue, 28 Jan 2025 14:32:31 +0100 Subject: [PATCH 11/39] fix: footerAnimation did not exist at gestures initialization. --- core/src/components/modal/animations/ios.enter.ts | 14 +++++++++----- core/src/components/modal/animations/md.enter.ts | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/core/src/components/modal/animations/ios.enter.ts b/core/src/components/modal/animations/ios.enter.ts index 294b9c169fa..bbfb9633f3a 100644 --- a/core/src/components/modal/animations/ios.enter.ts +++ b/core/src/components/modal/animations/ios.enter.ts @@ -41,8 +41,10 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio .duration(500) .addAnimation([wrapperAnimation]) .beforeAddWrite(() => { + if (!animateContentHeight) return; + const ionFooter = baseEl.querySelector('ion-footer'); - if (ionFooter) { + if (ionFooter && footerAnimation) { const footerHeight = ionFooter.clientHeight; const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; baseEl.shadowRoot!.appendChild(clonedFooter); @@ -52,16 +54,18 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio // the same as the cloned footer height const page = baseEl.querySelector('.ion-page') as HTMLElement; page.style.setProperty('padding-bottom', `${footerHeight}px`); - if (animateContentHeight && footerAnimation) { footerAnimation.addElement(root.querySelector('ion-footer')!); - baseAnimation.addAnimation(footerAnimation); - } - }}); + } + }); if (animateContentHeight && contentAnimation) { baseAnimation.addAnimation(contentAnimation); } + if (animateContentHeight && footerAnimation) { + baseAnimation.addAnimation(footerAnimation); + } + if (presentingEl) { const isMobile = window.innerWidth < 768; const hasCardModal = diff --git a/core/src/components/modal/animations/md.enter.ts b/core/src/components/modal/animations/md.enter.ts index 6f6b4ace6fc..9fa8e142243 100644 --- a/core/src/components/modal/animations/md.enter.ts +++ b/core/src/components/modal/animations/md.enter.ts @@ -43,8 +43,10 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption .duration(280) .addAnimation([backdropAnimation, wrapperAnimation]) .beforeAddWrite(() => { + if (!animateContentHeight) return; + const ionFooter = baseEl.querySelector('ion-footer'); - if (ionFooter) { + if (ionFooter && footerAnimation) { const footerHeight = ionFooter.clientHeight; const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; baseEl.shadowRoot!.appendChild(clonedFooter); @@ -54,15 +56,17 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption // the same as the cloned footer height const page = baseEl.querySelector('.ion-page') as HTMLElement; page.style.setProperty('padding-bottom', `${footerHeight}px`); - if (animateContentHeight && footerAnimation) { footerAnimation.addElement(root.querySelector('ion-footer')!); - baseAnimation.addAnimation(footerAnimation); - } - }}); + } + }); if (animateContentHeight && contentAnimation) { baseAnimation.addAnimation(contentAnimation); } + if (animateContentHeight && footerAnimation) { + baseAnimation.addAnimation(footerAnimation); + } + return baseAnimation; }; From ac11277074828a09583745efff3e61022fcb0337 Mon Sep 17 00:00:00 2001 From: kumibrr Date: Tue, 28 Jan 2025 16:17:52 +0100 Subject: [PATCH 12/39] restored footer onMove and onEnd animations --- core/src/components/modal/gestures/sheet.ts | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index af2945cc787..c32ae3dfb70 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -54,6 +54,12 @@ export const createSheetGesture = ( onDismiss: () => void, onBreakpointChange: (breakpoint: number) => void ) => { + const contentEl = baseEl.querySelector('ion-content'); + const height = wrapperEl.clientHeight; + const footerEl = baseEl.shadowRoot?.querySelector('ion-footer'); + const footerHeight = footerEl?.clientHeight ?? 0; + const footerDismissalPercentage = parseFloat(( 1 - (footerHeight ? (footerHeight / height) : 0)).toFixed(2)); + // Defaults for the sheet swipe animation const defaultBackdrop = [ { offset: 0, opacity: 'var(--backdrop-opacity)' }, @@ -75,11 +81,14 @@ export const createSheetGesture = ( CONTENT_KEYFRAMES: [ { offset: 0, maxHeight: '100%' }, { offset: 1, maxHeight: '0%' }, - ] + ], + FOOTER_KEYFRAMES: [ + { offset: 0, transform: 'translateY(0)' }, + { offset: footerDismissalPercentage, transform: 'translateY(0)' }, + { offset: 1, transform: `translateY(${footerHeight}px)` }, + ], }; - const contentEl = baseEl.querySelector('ion-content'); - const height = wrapperEl.clientHeight; let currentBreakpoint = initialBreakpoint; let offset = 0; let canDismissBlocksGesture = false; @@ -89,7 +98,11 @@ export const createSheetGesture = ( const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation'); const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation'); const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation'); - animation.childAnimations.find((ani) => ani.id === 'footerAnimation')?.destroy(); + const footerAnimation = animation.childAnimations.find((ani) => ani.id === 'footerAnimation'); + + if (footerAnimation && footerEl) { + // footerAnimation.addElement(footerEl); + } const enableBackdrop = () => { baseEl.style.setProperty('pointer-events', 'auto'); @@ -129,6 +142,7 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); + footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); animation.progressStart(true, 1 - currentBreakpoint); @@ -354,6 +368,13 @@ export const createSheetGesture = ( ]); } + if (footerAnimation) { + footerAnimation.keyframes([ + { offset: 0, transform: 'translateY(0)' }, + { offset: 0.85, transform: 'translateY(0)' }, + { offset: footerDismissalPercentage, transform: `translateY(${!shouldRemainOpen ? footerHeight : 0}px)` }, + ]) + } animation.progressStep(0); } @@ -398,6 +419,7 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); + footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); animation.progressStart(true, 1 - snapToBreakpoint); currentBreakpoint = snapToBreakpoint; onBreakpointChange(currentBreakpoint); From 44b8152d94bd26a3787f0a6a1ed5d1e60e2cad05 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 28 Jan 2025 16:58:43 -0800 Subject: [PATCH 13/39] refactor(modal): use a clone footer to prevent flickering --- .../components/modal/animations/ios.enter.ts | 62 +++++++++----- .../components/modal/animations/ios.leave.ts | 12 +-- .../components/modal/animations/md.enter.ts | 80 ++++++++++++------- .../components/modal/animations/md.leave.ts | 11 +-- core/src/components/modal/animations/sheet.ts | 45 ++--------- core/src/components/modal/gestures/sheet.ts | 42 ++++------ core/src/components/modal/modal-interface.ts | 2 +- core/src/components/modal/modal.scss | 10 +-- core/src/components/modal/modal.tsx | 25 ++++-- .../components/modal/test/sheet/index.html | 19 +---- 10 files changed, 149 insertions(+), 159 deletions(-) diff --git a/core/src/components/modal/animations/ios.enter.ts b/core/src/components/modal/animations/ios.enter.ts index bbfb9633f3a..d9fcf1d8d13 100644 --- a/core/src/components/modal/animations/ios.enter.ts +++ b/core/src/components/modal/animations/ios.enter.ts @@ -17,23 +17,25 @@ const createEnterAnimation = () => { const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)'); - return { backdropAnimation, wrapperAnimation, contentAnimation: undefined, footerAnimation: undefined }; + return { backdropAnimation, wrapperAnimation, contentAnimation: undefined }; }; /** * iOS Modal Enter Animation for the Card presentation style */ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => { - const { presentingEl, currentBreakpoint, animateContentHeight } = opts; + const { presentingEl, currentBreakpoint, scrollAtEdge } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } = - currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation(); + const { wrapperAnimation, backdropAnimation, contentAnimation } = + currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 }); - contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); + // The content animation is only added if scrolling is enabled for + // all the breakpoints. + !scrollAtEdge && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); const baseAnimation = createAnimation('entering-base') .addElement(baseEl) @@ -41,31 +43,53 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio .duration(500) .addAnimation([wrapperAnimation]) .beforeAddWrite(() => { - if (!animateContentHeight) return; + if (scrollAtEdge) { + // Scroll can only be done when the modal is fully expanded. + return; + } + /** + * There are some browsers that causes flickering when + * dragging the content when scroll is enabled at every + * breakpoint. This is due to the wrapper element being + * transformed off the screen and having a snap animation. + * + * A workaround is to clone the footer element and append + * it outside of the wrapper element. This way, the footer + * is still visible and the drag can be done without + * flickering. The original footer is hidden until the modal + * is dismissed. This maintains the animation of the footer + * when the modal is dismissed. + * + * The workaround needs to be done before the animation starts + * so there are no flickering issues. + */ const ionFooter = baseEl.querySelector('ion-footer'); - if (ionFooter && footerAnimation) { + /** + * This check is needed to prevent more than one footer + * from being appended to the shadow root. + * Otherwise, iOS and MD enter animations would append + * the footer twice. + */ + const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer'); + if (ionFooter && !ionFooterAlreadyAppended) { const footerHeight = ionFooter.clientHeight; - const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; + const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement; + baseEl.shadowRoot!.appendChild(clonedFooter); - ionFooter.remove(); + ionFooter.style.setProperty('display', 'none'); + ionFooter.setAttribute('aria-hidden', 'true'); - // add padding bottom to the .ion-page element to be - // the same as the cloned footer height + // Padding is added to prevent some content from being hidden. const page = baseEl.querySelector('.ion-page') as HTMLElement; page.style.setProperty('padding-bottom', `${footerHeight}px`); - footerAnimation.addElement(root.querySelector('ion-footer')!); - } - }); + } + }); - if (animateContentHeight && contentAnimation) { + if (contentAnimation) { baseAnimation.addAnimation(contentAnimation); } - if (animateContentHeight && footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - if (presentingEl) { const isMobile = window.innerWidth < 768; const hasCardModal = diff --git a/core/src/components/modal/animations/ios.leave.ts b/core/src/components/modal/animations/ios.leave.ts index dfb41210f26..914652878fa 100644 --- a/core/src/components/modal/animations/ios.leave.ts +++ b/core/src/components/modal/animations/ios.leave.ts @@ -12,7 +12,7 @@ const createLeaveAnimation = () => { const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(0vh)', 'translateY(100vh)'); - return { backdropAnimation, wrapperAnimation, footerAnimation: undefined }; + return { backdropAnimation, wrapperAnimation }; }; /** @@ -21,25 +21,19 @@ const createLeaveAnimation = () => { export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => { const { presentingEl, currentBreakpoint } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation, footerAnimation } = - currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation(); + const { wrapperAnimation, backdropAnimation } = + currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 }); - footerAnimation?.addElement(root.querySelector('ion-footer')!); - const baseAnimation = createAnimation('leaving-base') .addElement(baseEl) .easing('cubic-bezier(0.32,0.72,0,1)') .duration(duration) .addAnimation(wrapperAnimation); - if (footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - if (presentingEl) { const isMobile = window.innerWidth < 768; const hasCardModal = diff --git a/core/src/components/modal/animations/md.enter.ts b/core/src/components/modal/animations/md.enter.ts index 9fa8e142243..72a446c80f8 100644 --- a/core/src/components/modal/animations/md.enter.ts +++ b/core/src/components/modal/animations/md.enter.ts @@ -19,54 +19,78 @@ const createEnterAnimation = () => { { offset: 1, opacity: 1, transform: `translateY(0px)` }, ]); - return { backdropAnimation, wrapperAnimation, contentAnimation: undefined, footerAnimation: undefined }; + return { backdropAnimation, wrapperAnimation, contentAnimation: undefined }; }; /** * Md Modal Enter Animation */ export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => { - const { currentBreakpoint, animateContentHeight } = opts; + const { currentBreakpoint, scrollAtEdge } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } = - currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation(); + const { wrapperAnimation, backdropAnimation, contentAnimation } = + currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!); - contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); + // The content animation is only added if scrolling is enabled for + // all the breakpoints. + scrollAtEdge && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!); const baseAnimation = createAnimation() - .addElement(baseEl) - .easing('cubic-bezier(0.36,0.66,0.04,1)') - .duration(280) - .addAnimation([backdropAnimation, wrapperAnimation]) - .beforeAddWrite(() => { - if (!animateContentHeight) return; + .addElement(baseEl) + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .duration(280) + .addAnimation([backdropAnimation, wrapperAnimation]) + .beforeAddWrite(() => { + if (scrollAtEdge) { + // Scroll can only be done when the modal is fully expanded. + return; + } + + /** + * There are some browsers that causes flickering when + * dragging the content when scroll is enabled at every + * breakpoint. This is due to the wrapper element being + * transformed off the screen and having a snap animation. + * + * A workaround is to clone the footer element and append + * it outside of the wrapper element. This way, the footer + * is still visible and the drag can be done without + * flickering. The original footer is hidden until the modal + * is dismissed. This maintains the animation of the footer + * when the modal is dismissed. + * + * The workaround needs to be done before the animation starts + * so there are no flickering issues. + */ + const ionFooter = baseEl.querySelector('ion-footer'); + /** + * This check is needed to prevent more than one footer + * from being appended to the shadow root. + * Otherwise, iOS and MD enter animations would append + * the footer twice. + */ + const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer'); + if (ionFooter && !ionFooterAlreadyAppended) { + const footerHeight = ionFooter.clientHeight; + const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement; - const ionFooter = baseEl.querySelector('ion-footer'); - if (ionFooter && footerAnimation) { - const footerHeight = ionFooter.clientHeight; - const clonedFooter = ionFooter.cloneNode(true) as HTMLElement; - baseEl.shadowRoot!.appendChild(clonedFooter); - ionFooter.remove(); + baseEl.shadowRoot!.appendChild(clonedFooter); + ionFooter.style.setProperty('display', 'none'); + ionFooter.setAttribute('aria-hidden', 'true'); - // add padding bottom to the .ion-page element to be - // the same as the cloned footer height - const page = baseEl.querySelector('.ion-page') as HTMLElement; - page.style.setProperty('padding-bottom', `${footerHeight}px`); - footerAnimation.addElement(root.querySelector('ion-footer')!); + // Padding is added to prevent some content from being hidden. + const page = baseEl.querySelector('.ion-page') as HTMLElement; + page.style.setProperty('padding-bottom', `${footerHeight}px`); } - }); + }); - if (animateContentHeight && contentAnimation) { + if (contentAnimation) { baseAnimation.addAnimation(contentAnimation); } - if (animateContentHeight && footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - return baseAnimation; }; diff --git a/core/src/components/modal/animations/md.leave.ts b/core/src/components/modal/animations/md.leave.ts index 7a5d737ad14..0caa73e0e84 100644 --- a/core/src/components/modal/animations/md.leave.ts +++ b/core/src/components/modal/animations/md.leave.ts @@ -14,7 +14,7 @@ const createLeaveAnimation = () => { { offset: 1, opacity: 0, transform: 'translateY(40px)' }, ]); - return { backdropAnimation, wrapperAnimation, footerAnimation: undefined }; + return { backdropAnimation, wrapperAnimation }; }; /** @@ -23,21 +23,16 @@ const createLeaveAnimation = () => { export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => { const { currentBreakpoint } = opts; const root = getElementRoot(baseEl); - const { wrapperAnimation, backdropAnimation, footerAnimation } = - currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation(); + const { wrapperAnimation, backdropAnimation } = + currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation(); backdropAnimation.addElement(root.querySelector('ion-backdrop')!); wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!); - footerAnimation?.addElement(root.querySelector('ion-footer')!); const baseAnimation = createAnimation() .easing('cubic-bezier(0.47,0,0.745,0.715)') .duration(200) .addAnimation([backdropAnimation, wrapperAnimation]); - if (footerAnimation) { - baseAnimation.addAnimation(footerAnimation); - } - return baseAnimation; }; diff --git a/core/src/components/modal/animations/sheet.ts b/core/src/components/modal/animations/sheet.ts index 5354e7398fc..c5fa572adda 100644 --- a/core/src/components/modal/animations/sheet.ts +++ b/core/src/components/modal/animations/sheet.ts @@ -3,7 +3,7 @@ import { createAnimation } from '@utils/animation/animation'; import type { ModalAnimationOptions } from '../modal-interface'; import { getBackdropValueForSheet } from '../utils'; -export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions) => { +export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => { const { currentBreakpoint, backdropBreakpoint } = opts; /** @@ -29,32 +29,18 @@ export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimat { offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` }, ]); + /** + * This allows the content to be scrollable at any breakpoint. + */ const contentAnimation = createAnimation('contentAnimation').keyframes([ { offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` }, { offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` }, ]); - const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0; - - const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight - ?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0; - - const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100; - - const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2)); - - const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2)); - - const footerAnimation = createAnimation('footerAnimation').keyframes([ - { offset: 0, opacity: 1, transform: `translateY(${footerHeight}px)` }, - { offset: headerOffset, opacity: 1, transform: `translateY(${footerHeight}px)` }, - { offset: ((footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' }, - ]); - - return { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation }; + return { wrapperAnimation, backdropAnimation, contentAnimation }; }; -export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions) => { +export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => { const { currentBreakpoint, backdropBreakpoint } = opts; /** @@ -86,22 +72,5 @@ export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimat { offset: 1, opacity: 1, transform: `translateY(100%)` }, ]); - const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0; - - const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight - ?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0; - - const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100; - - const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2)); - - const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2)); - - const footerAnimation = createAnimation('footerAnimation').keyframes([ - { offset: 0, opacity: 1, transform: 'translateY(0)' }, - { offset: (1 - (footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' }, - { offset: (1), opacity: 1, transform: `translateY(${footerHeight}px)` }, - ]); - - return { wrapperAnimation, backdropAnimation, footerAnimation }; + return { wrapperAnimation, backdropAnimation }; }; diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index c32ae3dfb70..04f565562ee 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -54,12 +54,6 @@ export const createSheetGesture = ( onDismiss: () => void, onBreakpointChange: (breakpoint: number) => void ) => { - const contentEl = baseEl.querySelector('ion-content'); - const height = wrapperEl.clientHeight; - const footerEl = baseEl.shadowRoot?.querySelector('ion-footer'); - const footerHeight = footerEl?.clientHeight ?? 0; - const footerDismissalPercentage = parseFloat(( 1 - (footerHeight ? (footerHeight / height) : 0)).toFixed(2)); - // Defaults for the sheet swipe animation const defaultBackdrop = [ { offset: 0, opacity: 'var(--backdrop-opacity)' }, @@ -82,13 +76,10 @@ export const createSheetGesture = ( { offset: 0, maxHeight: '100%' }, { offset: 1, maxHeight: '0%' }, ], - FOOTER_KEYFRAMES: [ - { offset: 0, transform: 'translateY(0)' }, - { offset: footerDismissalPercentage, transform: 'translateY(0)' }, - { offset: 1, transform: `translateY(${footerHeight}px)` }, - ], }; + const contentEl = baseEl.querySelector('ion-content'); + const height = wrapperEl.clientHeight; let currentBreakpoint = initialBreakpoint; let offset = 0; let canDismissBlocksGesture = false; @@ -98,11 +89,6 @@ export const createSheetGesture = ( const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation'); const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation'); const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation'); - const footerAnimation = animation.childAnimations.find((ani) => ani.id === 'footerAnimation'); - - if (footerAnimation && footerEl) { - // footerAnimation.addElement(footerEl); - } const enableBackdrop = () => { baseEl.style.setProperty('pointer-events', 'auto'); @@ -142,8 +128,6 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); - footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); - animation.progressStart(true, 1 - currentBreakpoint); /** @@ -368,13 +352,6 @@ export const createSheetGesture = ( ]); } - if (footerAnimation) { - footerAnimation.keyframes([ - { offset: 0, transform: 'translateY(0)' }, - { offset: 0.85, transform: 'translateY(0)' }, - { offset: footerDismissalPercentage, transform: `translateY(${!shouldRemainOpen ? footerHeight : 0}px)` }, - ]) - } animation.progressStep(0); } @@ -387,6 +364,20 @@ export const createSheetGesture = ( if (shouldPreventDismiss) { handleCanDismiss(baseEl, animation); } else if (!shouldRemainOpen) { + if (!scrollAtEdge) { + const clonedFooter = wrapperEl.nextElementSibling as HTMLIonFooterElement; + // get the original footer, which is a child of + const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement; + // hide the cloned footer + clonedFooter.style.setProperty('display', 'none'); + // add the aria-hidden attribute to the cloned footer + clonedFooter.setAttribute('aria-hidden', 'true'); + // display the original footer, remove the inline style display: none + originalFooter.style.removeProperty('display'); + // remove the aria-hidden attribute + originalFooter.removeAttribute('aria-hidden'); + } + onDismiss(); } @@ -419,7 +410,6 @@ export const createSheetGesture = ( wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]); backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]); contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]); - footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]); animation.progressStart(true, 1 - snapToBreakpoint); currentBreakpoint = snapToBreakpoint; onBreakpointChange(currentBreakpoint); diff --git a/core/src/components/modal/modal-interface.ts b/core/src/components/modal/modal-interface.ts index bf83a194c89..6d0c745785a 100644 --- a/core/src/components/modal/modal-interface.ts +++ b/core/src/components/modal/modal-interface.ts @@ -31,7 +31,7 @@ export interface ModalAnimationOptions { presentingEl?: HTMLElement; currentBreakpoint?: number; backdropBreakpoint?: number; - animateContentHeight?: boolean; + scrollAtEdge?: boolean; } export interface ModalBreakpointChangeEventDetail { diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.scss index 7e1deca8225..43f9d7f7993 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.scss @@ -167,9 +167,9 @@ ion-backdrop { bottom: 0; } -:host(.modal-sheet) ion-footer { - position: absolute; - bottom: 0; +// :host(.modal-sheet.modal-scroll-all) ion-footer { +// position: absolute; +// bottom: 0; - width: var(--width); -} +// width: var(--width); +// } diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 86ca4886166..0c09d8de195 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -573,7 +573,7 @@ export class Modal implements ComponentInterface, OverlayInterface { presentingEl: presentingElement, currentBreakpoint: this.initialBreakpoint, backdropBreakpoint: this.backdropBreakpoint, - animateContentHeight: !this.scrollAtEdge + scrollAtEdge: this.scrollAtEdge, }); /* tslint:disable-next-line */ @@ -680,7 +680,7 @@ export class Modal implements ComponentInterface, OverlayInterface { presentingEl: this.presentingElement, currentBreakpoint: initialBreakpoint, backdropBreakpoint, - animateContentHeight: !this.scrollAtEdge, + scrollAtEdge: this.scrollAtEdge, })); ani.progressStart(true, 1); @@ -941,9 +941,17 @@ export class Modal implements ComponentInterface, OverlayInterface { }; render() { - const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } = - this; - + const { + handle, + isSheetModal, + presentingElement, + htmlAttributes, + handleBehavior, + inheritedAttributes, + focusTrap, + scrollAtEdge, + } = this; + console.log('isSheetModal', isSheetModal, 'scrollAtEdge', scrollAtEdge, 'both', isSheetModal && !scrollAtEdge); const showHandle = handle !== false && isSheetModal; const mode = getIonMode(this); const isCardModal = presentingElement !== undefined && mode === 'ios'; @@ -962,6 +970,7 @@ export class Modal implements ComponentInterface, OverlayInterface { ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, + [`modal-scroll-all`]: isSheetModal && !scrollAtEdge, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false, ...getClassMap(this.cssClass), @@ -1035,10 +1044,10 @@ interface ModalOverlayOptions { backdropBreakpoint: number; /** - * Whether or not the modal should animate - * content's max-height. + * Whether or not the modal should scroll + * only when fully expanded. */ - animateContentHeight?: boolean; + scrollAtEdge?: boolean; } type ModalPresentOptions = ModalOverlayOptions; diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index ed848e550f1..3d43d83407e 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -101,10 +101,10 @@ Present Sheet Modal (Max breakpoint is not 1) diff --git a/core/src/css/core.scss b/core/src/css/core.scss index 88a3cf0549d..1cf0e8bfb7d 100644 --- a/core/src/css/core.scss +++ b/core/src/css/core.scss @@ -2,6 +2,7 @@ @import "../themes/ionic.globals"; @import "../components/menu/menu.ios.vars"; @import "../components/menu/menu.md.vars"; +@import "../components/modal/modal.vars"; :root { /** @@ -51,18 +52,11 @@ body.backdrop-no-scroll { * the first toolbar in the header. * Footer also needs this. We do not adjust the bottom * padding though because of the safe area. - * - * There is a workaround to allow sheet modals to scroll - * at any breakpoint. This requires the footer to be - * cloned. However, the following styles are not applied. - * This is fixed by adding the styles directly on the - * modal iOS file. If this padding value is updated, then - * the value also needs to be updated in the modal iOS file. */ html.ios ion-modal.modal-card ion-header ion-toolbar:first-of-type, html.ios ion-modal.modal-sheet ion-header ion-toolbar:first-of-type, html.ios ion-modal ion-footer ion-toolbar:first-of-type { - padding-top: 6px; + padding-top: $modal-sheet-padding-top; } /** @@ -72,7 +66,7 @@ html.ios ion-modal ion-footer ion-toolbar:first-of-type { */ html.ios ion-modal.modal-card ion-header ion-toolbar:last-of-type, html.ios ion-modal.modal-sheet ion-header ion-toolbar:last-of-type { - padding-bottom: 6px; + padding-bottom: $modal-sheet-padding-bottom; } /** diff --git a/packages/angular/common/src/overlays/modal.ts b/packages/angular/common/src/overlays/modal.ts index 7154ccaac44..de884845cc1 100644 --- a/packages/angular/common/src/overlays/modal.ts +++ b/packages/angular/common/src/overlays/modal.ts @@ -59,6 +59,7 @@ const MODAL_INPUTS = [ 'canDismiss', 'cssClass', 'enterAnimation', + 'expandToScroll', 'event', 'focusTrap', 'handle', From df2c331d2f0f5b1f903c347aaf1f0b21b537f743 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 12:36:43 -0800 Subject: [PATCH 29/39] chore(vue): run build --- packages/vue/src/components/Overlays.ts | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/vue/src/components/Overlays.ts diff --git a/packages/vue/src/components/Overlays.ts b/packages/vue/src/components/Overlays.ts new file mode 100644 index 00000000000..5b3a4191e87 --- /dev/null +++ b/packages/vue/src/components/Overlays.ts @@ -0,0 +1,32 @@ +/** + * This is an autogenerated file created by 'scripts/copy-overlays.js'. + * Changes made to this file will be overwritten on build. + */ + +import type { + JSX, +} from '@ionic/core/components'; +import { defineCustomElement as defineIonActionSheetCustomElement } from '@ionic/core/components/ion-action-sheet.js' +import { defineCustomElement as defineIonAlertCustomElement } from '@ionic/core/components/ion-alert.js' +import { defineCustomElement as defineIonLoadingCustomElement } from '@ionic/core/components/ion-loading.js' +import { defineCustomElement as defineIonModalCustomElement } from '@ionic/core/components/ion-modal.js' +import { defineCustomElement as defineIonPickerLegacyCustomElement } from '@ionic/core/components/ion-picker-legacy.js' +import { defineCustomElement as defineIonPopoverCustomElement } from '@ionic/core/components/ion-popover.js' +import { defineCustomElement as defineIonToastCustomElement } from '@ionic/core/components/ion-toast.js' + +import { defineOverlayContainer } from '../utils/overlays'; + +export const IonActionSheet = /*@__PURE__*/ defineOverlayContainer('ion-action-sheet', defineIonActionSheetCustomElement, ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'subHeader', 'translucent', 'trigger']); + +export const IonAlert = /*@__PURE__*/ defineOverlayContainer('ion-alert', defineIonAlertCustomElement, ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger']); + +export const IonLoading = /*@__PURE__*/ defineOverlayContainer('ion-loading', defineIonLoadingCustomElement, ['animated', 'backdropDismiss', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'showBackdrop', 'spinner', 'translucent', 'trigger']); + +export const IonModal = /*@__PURE__*/ defineOverlayContainer('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'expandToScroll', 'focusTrap', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger'], true); + +export const IonPickerLegacy = /*@__PURE__*/ defineOverlayContainer('ion-picker-legacy', defineIonPickerLegacyCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'trigger']); + +export const IonPopover = /*@__PURE__*/ defineOverlayContainer('ion-popover', defineIonPopoverCustomElement, ['alignment', 'animated', 'arrow', 'backdropDismiss', 'component', 'componentProps', 'dismissOnSelect', 'enterAnimation', 'event', 'focusTrap', 'htmlAttributes', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'reference', 'showBackdrop', 'side', 'size', 'translucent', 'trigger', 'triggerAction']); + +export const IonToast = /*@__PURE__*/ defineOverlayContainer('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'translucent', 'trigger']); + \ No newline at end of file From e15ae91680d3a19065a293b9071f4277601210fd Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 12:38:05 -0800 Subject: [PATCH 30/39] fix(vue): add missing new line --- packages/vue/src/components/Overlays.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/src/components/Overlays.ts b/packages/vue/src/components/Overlays.ts index 5b3a4191e87..25345043e6f 100644 --- a/packages/vue/src/components/Overlays.ts +++ b/packages/vue/src/components/Overlays.ts @@ -29,4 +29,4 @@ export const IonPickerLegacy = /*@__PURE__*/ defineOverlayContainer('ion-popover', defineIonPopoverCustomElement, ['alignment', 'animated', 'arrow', 'backdropDismiss', 'component', 'componentProps', 'dismissOnSelect', 'enterAnimation', 'event', 'focusTrap', 'htmlAttributes', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'reference', 'showBackdrop', 'side', 'size', 'translucent', 'trigger', 'triggerAction']); export const IonToast = /*@__PURE__*/ defineOverlayContainer('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'swipeGesture', 'translucent', 'trigger']); - \ No newline at end of file + From 4ffa5f6b8419ceccd98f6cc8590b3793dc037cd3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 12:57:53 -0800 Subject: [PATCH 31/39] fix(modal): add footer check --- core/src/components/modal/gestures/sheet.ts | 7 ++++++- core/src/components/modal/modal.tsx | 15 +++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index eb2af0d6367..5bbc5b45c97 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -122,7 +122,12 @@ export const createSheetGesture = ( * @param footer - The footer to show */ const swapFooterVisibility = (footer: 'original' | 'cloned') => { - const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement; + const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null; + + if (!originalFooter) { + return; + } + const clonedFooter = wrapperEl.nextElementSibling as HTMLIonFooterElement; const footerToHide = footer === 'original' ? clonedFooter : originalFooter; const footerToShow = footer === 'original' ? originalFooter : clonedFooter; diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 07d5c57e74f..682f602121e 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -131,15 +131,14 @@ export class Modal implements ComponentInterface, OverlayInterface { @Prop() breakpoints?: number[]; /** - * Determines whether or not the sheet modal will only - * scroll/drag the content when fully expanded. This - * will only take effect when the `breakpoints` and - * `initialBreakpoint` properties are set. + * Controls whether scrolling or dragging within the sheet modal expands + * it to a larger breakpoint. This only takes effect when `breakpoints` + * and `initialBreakpoint` are set. * - * 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. + * If `true`, scrolling or dragging anywhere in the modal will first expand + * it to the next breakpoint. Once fully expanded, scrolling will affect the content. + * If `false`, scrolling will always affect the content, and the modal will only expand + * when dragging the header or handle. */ @Prop() expandToScroll = true; From bbbdb87e4df6aea56beabb7e5b099a2493150244 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 13:07:32 -0800 Subject: [PATCH 32/39] chore(core): run build --- core/src/components.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7d7c3fc2c40..5656510c471 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1732,7 +1732,7 @@ export namespace Components { */ "enterAnimation"?: AnimationBuilder; /** - * Determines whether or not the sheet modal will only scroll/drag the content when fully expanded. This will only take effect when the `breakpoints` and `initialBreakpoint` properties are set. 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. + * Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle. */ "expandToScroll": boolean; /** @@ -6537,7 +6537,7 @@ declare namespace LocalJSX { */ "enterAnimation"?: AnimationBuilder; /** - * Determines whether or not the sheet modal will only scroll/drag the content when fully expanded. This will only take effect when the `breakpoints` and `initialBreakpoint` properties are set. 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. + * Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle. */ "expandToScroll"?: boolean; /** From 433f563ab425e72b7633297fb3ad7313f58df550 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:01:56 -0800 Subject: [PATCH 33/39] Update core/src/components/modal/animations/md.leave.ts Co-authored-by: Brandy Smith --- core/src/components/modal/animations/md.leave.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components/modal/animations/md.leave.ts b/core/src/components/modal/animations/md.leave.ts index 56a7fe797ea..e453e9339cd 100644 --- a/core/src/components/modal/animations/md.leave.ts +++ b/core/src/components/modal/animations/md.leave.ts @@ -42,8 +42,8 @@ export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption /** * If expandToScroll is disabled, we need to swap * the visibility to the original, so the footer - * dismisses with the modal and doesn't stay - * until the modal is removed from the DOM. + * dismisses with the modal and doesn't stay + * until the modal is removed from the DOM. */ const ionFooter = baseEl.querySelector('ion-footer'); if (ionFooter) { From a9ea2f4f215daeac48fa89d23b88d37df18cb25b Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:02:06 -0800 Subject: [PATCH 34/39] Update core/src/components/modal/animations/ios.leave.ts Co-authored-by: Brandy Smith --- core/src/components/modal/animations/ios.leave.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components/modal/animations/ios.leave.ts b/core/src/components/modal/animations/ios.leave.ts index 13325ab75e8..89ba3ce8427 100644 --- a/core/src/components/modal/animations/ios.leave.ts +++ b/core/src/components/modal/animations/ios.leave.ts @@ -42,8 +42,8 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio /** * If expandToScroll is disabled, we need to swap * the visibility to the original, so the footer - * dismisses with the modal and doesn't stay - * until the modal is removed from the DOM. + * dismisses with the modal and doesn't stay + * until the modal is removed from the DOM. */ const ionFooter = baseEl.querySelector('ion-footer'); if (ionFooter) { From c099027784a95fb6ce4a3318df0c87b6fb5dcc1b Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:02:31 -0800 Subject: [PATCH 35/39] Update core/src/components/modal/gestures/sheet.ts Co-authored-by: Brandy Smith --- core/src/components/modal/gestures/sheet.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 5bbc5b45c97..50d0f3c53be 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -117,9 +117,8 @@ export const createSheetGesture = ( }; /** - * Used when `expandToScroll` is disabled. - * Changes the footer that is currently visible - * @param footer - The footer to show + * Toggles the visible modal footer when `expandToScroll` is disabled. + * @param footer The footer to show. */ const swapFooterVisibility = (footer: 'original' | 'cloned') => { const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null; From ce909b03627739e3529dbad8ccf081dea646cf1e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:02:47 -0800 Subject: [PATCH 36/39] Update core/src/components/modal/gestures/sheet.ts Co-authored-by: Brandy Smith --- core/src/components/modal/gestures/sheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 50d0f3c53be..de83aaf7169 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -234,7 +234,7 @@ export const createSheetGesture = ( /** * If expandToScroll is disabled, we need to swap - * the visibility to the original, so if the modal + * the footer visibility to the original, so if the modal * is dismissed, the footer dismisses with the modal * and doesn't stay on the screen after the modal is gone. */ From c0d75b560ddaac5ab2b3c15d3b2e60b5e297b9a7 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:03:02 -0800 Subject: [PATCH 37/39] Update core/src/components/modal/gestures/sheet.ts Co-authored-by: Brandy Smith --- core/src/components/modal/gestures/sheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index de83aaf7169..1fc10b21cc7 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -382,7 +382,7 @@ export const createSheetGesture = ( /** * The modal content should scroll at any breakpoint when expandToScroll * is disabled. In order to do this, the content needs to be completely - * viewable so scrolling can access everything. Othewise, the default + * viewable so scrolling can access everything. Otherwise, the default * behavior would show the content off the screen and only allow * scrolling when the sheet is fully expanded. */ From 6416ebe933ae47117ad6c9fc13016bd9f148bc4c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:03:16 -0800 Subject: [PATCH 38/39] Update core/src/components/modal/gestures/sheet.ts Co-authored-by: Brandy Smith --- core/src/components/modal/gestures/sheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 1fc10b21cc7..7191f642e9e 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -403,7 +403,7 @@ export const createSheetGesture = ( /** * If expandToScroll is disabled, we need to swap - * the visibility to the cloned one so the footer + * the footer visibility to the cloned one so the footer * doesn't flicker when the sheet's height is animated. */ if (!expandToScroll && shouldRemainOpen) { From f6cc5395b6733caad06fb90019dd9b6dc7c50d09 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 31 Jan 2025 16:03:51 -0800 Subject: [PATCH 39/39] Update core/src/components/modal/gestures/sheet.ts Co-authored-by: Brandy Smith --- core/src/components/modal/gestures/sheet.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 7191f642e9e..5c95481ca0c 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -417,12 +417,11 @@ export const createSheetGesture = ( } /** - * 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. - * 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. + * Enables scrolling immediately if the sheet is about to fully expand + * or if it allows scrolling at any breakpoint. Without this, there would + * be a ~500ms delay while the modal animation completes, causing a + * noticeable lag. Native iOS allows scrolling as soon as the gesture is + * released, so we align with that behavior. */ if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !expandToScroll)) { contentEl.scrollY = true;