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): added snapBreakpoints to sheet modals #30097

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 10 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,33 @@ 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, animateContentHeight } = 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 });

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]);

if (contentAnimation && animateContentHeight) {
baseAnimation.addAnimation(contentAnimation);
}

if (presentingEl) {
const isMobile = window.innerWidth < 768;
Expand Down
24 changes: 16 additions & 8 deletions core/src/components/modal/animations/md.enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,33 @@ 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, animateContentHeight } = 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()
.addElement(baseEl)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(280)
.addAnimation([backdropAnimation, wrapperAnimation]);
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]);

if (contentAnimation && animateContentHeight) {
baseAnimation.addAnimation(contentAnimation);
}

return baseAnimation;
};
7 changes: 6 additions & 1 deletion core/src/components/modal/animations/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
{ offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` },
]);

return { wrapperAnimation, backdropAnimation };
const contentAnimation = createAnimation('contentAnimation').keyframes([
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
]);

return { wrapperAnimation, backdropAnimation, contentAnimation };
};

export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {
Expand Down
49 changes: 40 additions & 9 deletions core/src/components/modal/gestures/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const createSheetGesture = (
backdropBreakpoint: number,
animation: Animation,
breakpoints: number[] = [],
scrollAtEdge: boolean,
getCurrentBreakpoint: () => number,
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
Expand All @@ -71,6 +72,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');
Expand All @@ -79,10 +84,11 @@ 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');
const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation');

const enableBackdrop = () => {
baseEl.style.setProperty('pointer-events', 'auto');
Expand Down Expand Up @@ -121,6 +127,7 @@ export const createSheetGesture = (
if (wrapperAnimation && backdropAnimation) {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
animation.progressStart(true, 1 - currentBreakpoint);

/**
Expand All @@ -138,7 +145,7 @@ export const createSheetGesture = (
}
}

if (contentEl && currentBreakpoint !== maxBreakpoint) {
if (contentEl && currentBreakpoint !== maxBreakpoint && scrollAtEdge) {
contentEl.scrollY = false;
}

Expand All @@ -154,6 +161,14 @@ export const createSheetGesture = (
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
currentBreakpoint = getCurrentBreakpoint();

/**
* If we have scrollAtEdge disabled, we should not allow the swipe gesture to start
* if the content is being swiped.
*/
if (!scrollAtEdge && contentEl) {
return false;
}

if (currentBreakpoint === 1 && contentEl) {
/**
* The modal should never swipe to close on the content with a refresher.
Expand Down Expand Up @@ -323,6 +338,20 @@ export const createSheetGesture = (
},
]);

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
* viewable so scrolling can access everything. Othewise, the default
* behavior would show the content off the screen and only allow
* scrolling when the sheet is fully expanded.
*/
contentAnimation.keyframes([
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
]);
kumibrr marked this conversation as resolved.
Show resolved Hide resolved
}

animation.progressStep(0);
}

Expand All @@ -339,13 +368,14 @@ export const createSheetGesture = (
}

/**
* If the sheet is going to be fully expanded then we should enable
* scrolling immediately. The sheet modal animation takes ~500ms to finish
* so if we wait until then there is a visible delay for when scrolling is
* re-enabled. Native iOS allows for scrolling on the sheet modal as soon
* as the gesture is released, so we align with that.
* If 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.
*/
if (contentEl && snapToBreakpoint === breakpoints[breakpoints.length - 1]) {
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !scrollAtEdge)) {
contentEl.scrollY = true;
}

Expand All @@ -365,6 +395,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);
Expand Down
1 change: 1 addition & 0 deletions core/src/components/modal/modal-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface ModalAnimationOptions {
presentingEl?: HTMLElement;
currentBreakpoint?: number;
backdropBreakpoint?: number;
animateContentHeight?: boolean;
}

export interface ModalBreakpointChangeEventDetail {
Expand Down
20 changes: 20 additions & 0 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() breakpoints?: number[];

/**
* Determines whether or not the sheet modal will only
* scroll when fully expanded.
*
* If the value is `true`, the modal will only scroll
* when fully expanded.
* If the value is `false`, the modal will scroll at
* any breakpoint.
*/
@Prop() scrollAtEdge = true;

/**
* A decimal value between 0 and 1 that indicates the
* initial point the modal will open at when creating a
Expand Down Expand Up @@ -562,6 +573,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
presentingEl: presentingElement,
currentBreakpoint: this.initialBreakpoint,
backdropBreakpoint: this.backdropBreakpoint,
animateContentHeight: !this.scrollAtEdge
});

/* tslint:disable-next-line */
Expand Down Expand Up @@ -668,6 +680,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
presentingEl: this.presentingElement,
currentBreakpoint: initialBreakpoint,
backdropBreakpoint,
animateContentHeight: !this.scrollAtEdge,
}));

ani.progressStart(true, 1);
Expand All @@ -680,6 +693,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
backdropBreakpoint,
ani,
this.sortedBreakpoints,
this.scrollAtEdge,
() => this.currentBreakpoint ?? 0,
() => this.sheetOnDismiss(),
(breakpoint: number) => {
Expand Down Expand Up @@ -1019,6 +1033,12 @@ interface ModalOverlayOptions {
* to fade in when using a sheet modal.
*/
backdropBreakpoint: number;

/**
* Whether or not the modal should animate
* content's max-height.
*/
animateContentHeight?: boolean;
}

type ModalPresentOptions = ModalOverlayOptions;
Expand Down
11 changes: 11 additions & 0 deletions core/src/components/modal/test/sheet/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
>
Present Sheet Modal (Max breakpoint is not 1)
</button>
<button
id="custom-breakpoint-modal"
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0,0.25, 0.5, 0.75], scrollAtEdge: false })"
>
Present Sheet Modal (scrollAtEdge)
</button>
<button
id="custom-backdrop-modal"
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"
Expand Down Expand Up @@ -184,6 +190,11 @@
${items}
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
`;

let extraOptions = {
Expand Down
Loading