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 all 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 @@ -1074,6 +1074,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise<boolean>) | boolean,true,false,false
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,expandToScroll,boolean,true,false,false
ion-modal,prop,focusTrap,boolean,true,false,false
ion-modal,prop,handle,boolean | undefined,undefined,false,false
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
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 @@ -1731,6 +1731,10 @@ export namespace Components {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* 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;
/**
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
*/
Expand Down Expand Up @@ -6532,6 +6536,10 @@ declare namespace LocalJSX {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* 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;
/**
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
*/
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, expandToScroll } = 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.
!expandToScroll && 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 (expandToScroll) {
// 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, expandToScroll } = 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 (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}

/**
* 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.
*/
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, expandToScroll } = 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.
expandToScroll && 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 (expandToScroll) {
// 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, expandToScroll } = 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 (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}

/**
* 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.
*/
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, expandToScroll } = 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 = !expandToScroll
? 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
Loading