Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modal): add expandToScroll property to allow scrolling at all breakpoints #30097

Merged
merged 45 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ea68986
feat(modal): added snapBreakpoints to sheet modals
kumibrr Dec 22, 2024
4970471
fix: removed trailing semicolon
kumibrr Dec 22, 2024
95c5314
chore: ran build
kumibrr Dec 26, 2024
1dcf5c9
refactor(modal): replaced snapBreakpoints array implementation with s…
kumibrr Jan 14, 2025
96c5e41
feat(modal): add contentAnimation support and animateContentHeight op…
kumibrr Jan 14, 2025
4d0d56f
Merge branch 'ionic-team:main' into main
kumibrr Jan 14, 2025
37c5832
feat(modal): added footer animation.
kumibrr Jan 27, 2025
566d3db
feat(modal): improved implementation
kumibrr Jan 27, 2025
9ab3733
fix: added back breakpoint: 0
kumibrr Jan 27, 2025
556cb6a
removed console.log
kumibrr Jan 28, 2025
ad90cd9
feat: added footer slide-in and slide-out animation
kumibrr Jan 28, 2025
396bf73
fix: footerAnimation did not exist at gestures initialization.
kumibrr Jan 28, 2025
ac11277
restored footer onMove and onEnd animations
kumibrr Jan 28, 2025
44b8152
refactor(modal): use a clone footer to prevent flickering
thetaPC Jan 29, 2025
804d043
fix(modal): add animation when scrollAtEdge is false
thetaPC Jan 29, 2025
59dd5b0
Merge pull request #1 from thetaPC/scroll
kumibrr Jan 29, 2025
4052a86
feat(modal): implement footer visibility swap for scrollAtEdge handling
kumibrr Jan 29, 2025
f4cf4c1
chore: added comments explaining footer visibility swaps
kumibrr Jan 29, 2025
df96686
feat(modal): added footer visibility swap based on scrollAtEdge in le…
kumibrr Jan 29, 2025
991e02a
fix(modal): padding value was always zero
kumibrr Jan 29, 2025
7c24b1f
chore(modal): minor fixes
kumibrr Jan 29, 2025
6d3473f
Merge remote-tracking branch 'upstream/main'
thetaPC Jan 29, 2025
6778e42
feat(modal): add padding to the first toolbar in modal sheets
kumibrr Jan 29, 2025
cef6fd7
fix(modal): limited padding-top to only apply on ios styles
kumibrr Jan 29, 2025
c8392bb
chore(modal): added explanation for padding-top
kumibrr Jan 29, 2025
dc4caaf
chore(modal): added section for sheet modal
kumibrr Jan 29, 2025
f6702a7
chore(vue): remove file
thetaPC Jan 29, 2025
b1726ec
Merge branch 'main' of github.com:kumibrr/ionic-framework
thetaPC Jan 29, 2025
42aa703
fix(modal): add missing parameter
thetaPC Jan 30, 2025
30e2d8b
test(modal): update snapshots
thetaPC Jan 30, 2025
3a79743
refactor(modal): add comments
thetaPC Jan 30, 2025
aca5855
Merge branch 'feature-8.5' into main
brandyscarney Jan 30, 2025
6e07a6f
refactor(modal): add requested changes
thetaPC Jan 31, 2025
80a826e
Merge branch 'main' of github.com:kumibrr/ionic-framework
thetaPC Jan 31, 2025
df2c331
chore(vue): run build
thetaPC Jan 31, 2025
e15ae91
fix(vue): add missing new line
thetaPC Jan 31, 2025
4ffa5f6
fix(modal): add footer check
thetaPC Jan 31, 2025
bbbdb87
chore(core): run build
thetaPC Jan 31, 2025
433f563
Update core/src/components/modal/animations/md.leave.ts
thetaPC Feb 1, 2025
a9ea2f4
Update core/src/components/modal/animations/ios.leave.ts
thetaPC Feb 1, 2025
c099027
Update core/src/components/modal/gestures/sheet.ts
thetaPC Feb 1, 2025
ce909b0
Update core/src/components/modal/gestures/sheet.ts
thetaPC Feb 1, 2025
c0d75b5
Update core/src/components/modal/gestures/sheet.ts
thetaPC Feb 1, 2025
6416ebe
Update core/src/components/modal/gestures/sheet.ts
thetaPC Feb 1, 2025
f6cc539
Update core/src/components/modal/gestures/sheet.ts
thetaPC Feb 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
59 changes: 55 additions & 4 deletions core/src/components/modal/animations/ios.enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,78 @@ const createEnterAnimation = () => {

const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');

return { backdropAnimation, wrapperAnimation };
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 } = opts;
const { presentingEl, currentBreakpoint, scrollAtEdge } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
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 });

// 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)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
.addAnimation(wrapperAnimation);
.addAnimation([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;

baseEl.shadowRoot!.appendChild(clonedFooter);
ionFooter.style.setProperty('display', 'none');
ionFooter.setAttribute('aria-hidden', 'true');

// 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 (contentAnimation) {
baseAnimation.addAnimation(contentAnimation);
}

if (presentingEl) {
const isMobile = window.innerWidth < 768;
Expand Down
30 changes: 28 additions & 2 deletions core/src/components/modal/animations/ios.leave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const createLeaveAnimation = () => {
* iOS Modal Leave Animation
*/
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => {
const { presentingEl, currentBreakpoint } = opts;
const { presentingEl, currentBreakpoint, scrollAtEdge } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
Expand All @@ -32,7 +32,33 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration)
.addAnimation(wrapperAnimation);
.addAnimation(wrapperAnimation)
.beforeAddWrite(() => {
if (scrollAtEdge) {
// Scroll can only be done when the modal is fully expanded.
return;
}

/**
* If scrollAtEdge 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.
thetaPC marked this conversation as resolved.
Show resolved Hide resolved
*/
const ionFooter = baseEl.querySelector('ion-footer');
if (ionFooter) {
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;

ionFooter.style.removeProperty('display');
ionFooter.removeAttribute('aria-hidden');

clonedFooter.style.setProperty('display', 'none');
clonedFooter.setAttribute('aria-hidden', 'true');

const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.removeProperty('padding-bottom');
}
});

if (presentingEl) {
const isMobile = window.innerWidth < 768;
Expand Down
63 changes: 58 additions & 5 deletions core/src/components/modal/animations/md.enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,78 @@ const createEnterAnimation = () => {
{ offset: 1, opacity: 1, transform: `translateY(0px)` },
]);

return { backdropAnimation, wrapperAnimation };
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
};

/**
* Md Modal Enter Animation
*/
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
const { currentBreakpoint } = opts;
const { currentBreakpoint, scrollAtEdge } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
const { wrapperAnimation, backdropAnimation, contentAnimation } =
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();

backdropAnimation.addElement(root.querySelector('ion-backdrop')!);

wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);

return createAnimation()
// 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]);
.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;

baseEl.shadowRoot!.appendChild(clonedFooter);
ionFooter.style.setProperty('display', 'none');
ionFooter.setAttribute('aria-hidden', 'true');

// 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 (contentAnimation) {
baseAnimation.addAnimation(contentAnimation);
}

return baseAnimation;
};
34 changes: 31 additions & 3 deletions core/src/components/modal/animations/md.leave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,44 @@ const createLeaveAnimation = () => {
* Md Modal Leave Animation
*/
export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
const { currentBreakpoint } = opts;
const { currentBreakpoint, scrollAtEdge } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();

backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);

return createAnimation()
const baseAnimation = createAnimation()
.easing('cubic-bezier(0.47,0,0.745,0.715)')
.duration(200)
.addAnimation([backdropAnimation, wrapperAnimation]);
.addAnimation([backdropAnimation, wrapperAnimation])
.beforeAddWrite(() => {
if (scrollAtEdge) {
// Scroll can only be done when the modal is fully expanded.
return;
}

/**
* If scrollAtEdge 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.
thetaPC marked this conversation as resolved.
Show resolved Hide resolved
*/
const ionFooter = baseEl.querySelector('ion-footer');
if (ionFooter) {
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;

ionFooter.style.removeProperty('display');
ionFooter.removeAttribute('aria-hidden');

clonedFooter.style.setProperty('display', 'none');
clonedFooter.setAttribute('aria-hidden', 'true');

const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.removeProperty('padding-bottom');
}
});

return baseAnimation;
};
14 changes: 12 additions & 2 deletions core/src/components/modal/animations/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ModalAnimationOptions } from '../modal-interface';
import { getBackdropValueForSheet } from '../utils';

export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
const { currentBreakpoint, backdropBreakpoint } = opts;
const { currentBreakpoint, backdropBreakpoint, scrollAtEdge } = opts;

/**
* If the backdropBreakpoint is undefined, then the backdrop
Expand All @@ -29,7 +29,17 @@ export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
{ offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` },
]);

return { wrapperAnimation, backdropAnimation };
/**
* This allows the content to be scrollable at any breakpoint.
*/
const contentAnimation = !scrollAtEdge
? createAnimation('contentAnimation').keyframes([
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
])
: undefined;

return { wrapperAnimation, backdropAnimation, contentAnimation };
};

export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {
Expand Down
Loading