feat(modal): add expandToScroll property to allow scrolling at all breakpoints (#30097)
Issue number: resolves #24631 Co-authored-by: Maria Hutt <13530427+thetaPC@users.noreply.github.com> Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
@ -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;
|
||||
|
||||
@ -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();
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ 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();
|
||||
@ -29,8 +29,36 @@ export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
@ -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) => {
|
||||
|
||||
@ -49,6 +49,7 @@ export const createSheetGesture = (
|
||||
backdropBreakpoint: number,
|
||||
animation: Animation,
|
||||
breakpoints: number[] = [],
|
||||
expandToScroll: boolean,
|
||||
getCurrentBreakpoint: () => number,
|
||||
onDismiss: () => void,
|
||||
onBreakpointChange: (breakpoint: number) => void
|
||||
@ -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');
|
||||
@ -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');
|
||||
@ -110,6 +116,36 @@ export const createSheetGesture = (
|
||||
baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (!originalFooter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedFooter = wrapperEl.nextElementSibling as HTMLIonFooterElement;
|
||||
const footerToHide = footer === 'original' ? clonedFooter : originalFooter;
|
||||
const footerToShow = footer === 'original' ? originalFooter : clonedFooter;
|
||||
|
||||
footerToShow.style.removeProperty('display');
|
||||
footerToShow.removeAttribute('aria-hidden');
|
||||
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
if (footer === 'original') {
|
||||
page.style.removeProperty('padding-bottom');
|
||||
} else {
|
||||
const pagePadding = footerToShow.clientHeight;
|
||||
page.style.setProperty('padding-bottom', `${pagePadding}px`);
|
||||
}
|
||||
|
||||
footerToHide.style.setProperty('display', 'none');
|
||||
footerToHide.setAttribute('aria-hidden', 'true');
|
||||
};
|
||||
|
||||
/**
|
||||
* After the entering animation completes,
|
||||
* we need to set the animation to go from
|
||||
@ -121,6 +157,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);
|
||||
|
||||
/**
|
||||
@ -138,7 +175,7 @@ export const createSheetGesture = (
|
||||
}
|
||||
}
|
||||
|
||||
if (contentEl && currentBreakpoint !== maxBreakpoint) {
|
||||
if (contentEl && currentBreakpoint !== maxBreakpoint && expandToScroll) {
|
||||
contentEl.scrollY = false;
|
||||
}
|
||||
|
||||
@ -154,6 +191,14 @@ export const createSheetGesture = (
|
||||
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
|
||||
currentBreakpoint = getCurrentBreakpoint();
|
||||
|
||||
/**
|
||||
* If we have expandToScroll disabled, we should not allow the swipe gesture to start
|
||||
* if the content is being swiped.
|
||||
*/
|
||||
if (!expandToScroll && contentEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentBreakpoint === 1 && contentEl) {
|
||||
/**
|
||||
* The modal should never swipe to close on the content with a refresher.
|
||||
@ -187,6 +232,16 @@ export const createSheetGesture = (
|
||||
*/
|
||||
canDismissBlocksGesture = baseEl.canDismiss !== undefined && baseEl.canDismiss !== true && minBreakpoint === 0;
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* 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.
|
||||
*/
|
||||
if (!expandToScroll) {
|
||||
swapFooterVisibility('original');
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are pulling down, then it is possible we are pulling on the content.
|
||||
* We do not want scrolling to happen at the same time as the gesture.
|
||||
@ -323,6 +378,20 @@ export const createSheetGesture = (
|
||||
},
|
||||
]);
|
||||
|
||||
if (contentAnimation) {
|
||||
/**
|
||||
* 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. Otherwise, the default
|
||||
* behavior would show the content off the screen and only allow
|
||||
* scrolling when the sheet is fully expanded.
|
||||
*/
|
||||
contentAnimation.keyframes([
|
||||
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
|
||||
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
|
||||
]);
|
||||
}
|
||||
|
||||
animation.progressStep(0);
|
||||
}
|
||||
|
||||
@ -332,6 +401,15 @@ export const createSheetGesture = (
|
||||
*/
|
||||
gesture.enable(false);
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* the footer visibility to the cloned one so the footer
|
||||
* doesn't flicker when the sheet's height is animated.
|
||||
*/
|
||||
if (!expandToScroll && shouldRemainOpen) {
|
||||
swapFooterVisibility('cloned');
|
||||
}
|
||||
|
||||
if (shouldPreventDismiss) {
|
||||
handleCanDismiss(baseEl, animation);
|
||||
} else if (!shouldRemainOpen) {
|
||||
@ -339,13 +417,13 @@ 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.
|
||||
* 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]) {
|
||||
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !expandToScroll)) {
|
||||
contentEl.scrollY = true;
|
||||
}
|
||||
|
||||
@ -365,6 +443,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);
|
||||
|
||||
@ -31,6 +31,7 @@ export interface ModalAnimationOptions {
|
||||
presentingEl?: HTMLElement;
|
||||
currentBreakpoint?: number;
|
||||
backdropBreakpoint?: number;
|
||||
expandToScroll: boolean;
|
||||
}
|
||||
|
||||
export interface ModalBreakpointChangeEventDetail {
|
||||
|
||||
@ -87,3 +87,16 @@
|
||||
:host(.modal-sheet) .modal-wrapper {
|
||||
@include border-radius(var(--border-radius), var(--border-radius), 0, 0);
|
||||
}
|
||||
|
||||
// iOS Sheet Modal - Scroll at all breakpoints
|
||||
// --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sheet modals require an additional padding as mentioned in the
|
||||
* `core.scss` file. However, there's a workaround that requires
|
||||
* a cloned footer to be added to the modal. This is only necessary
|
||||
* because the core styles are not being applied to the cloned footer.
|
||||
*/
|
||||
:host(.modal-sheet.modal-no-expand-scroll) ion-footer ion-toolbar:first-of-type {
|
||||
padding-top: $modal-sheet-padding-top;
|
||||
}
|
||||
|
||||
@ -166,3 +166,13 @@ ion-backdrop {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
// Sheet Modal - Scroll at all breakpoints
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.modal-sheet.modal-no-expand-scroll) ion-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
width: var(--width);
|
||||
}
|
||||
|
||||
@ -130,6 +130,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() breakpoints?: number[];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Prop() expandToScroll = true;
|
||||
|
||||
/**
|
||||
* A decimal value between 0 and 1 that indicates the
|
||||
* initial point the modal will open at when creating a
|
||||
@ -562,6 +574,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: presentingElement,
|
||||
currentBreakpoint: this.initialBreakpoint,
|
||||
backdropBreakpoint: this.backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
});
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
@ -616,7 +629,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
// should be in the DOM and referenced by now, except
|
||||
// for the presenting el
|
||||
const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
|
||||
const ani = (this.animation = animationBuilder(el, { presentingEl: this.presentingElement }));
|
||||
const ani = (this.animation = animationBuilder(el, {
|
||||
presentingEl: this.presentingElement,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}));
|
||||
|
||||
const contentEl = findIonContent(el);
|
||||
if (!contentEl) {
|
||||
@ -668,6 +684,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: this.presentingElement,
|
||||
currentBreakpoint: initialBreakpoint,
|
||||
backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}));
|
||||
|
||||
ani.progressStart(true, 1);
|
||||
@ -680,6 +697,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
backdropBreakpoint,
|
||||
ani,
|
||||
this.sortedBreakpoints,
|
||||
this.expandToScroll,
|
||||
() => this.currentBreakpoint ?? 0,
|
||||
() => this.sheetOnDismiss(),
|
||||
(breakpoint: number) => {
|
||||
@ -778,6 +796,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: presentingElement,
|
||||
currentBreakpoint: this.currentBreakpoint ?? this.initialBreakpoint,
|
||||
backdropBreakpoint: this.backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}
|
||||
);
|
||||
|
||||
@ -927,9 +946,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } =
|
||||
this;
|
||||
|
||||
const {
|
||||
handle,
|
||||
isSheetModal,
|
||||
presentingElement,
|
||||
htmlAttributes,
|
||||
handleBehavior,
|
||||
inheritedAttributes,
|
||||
focusTrap,
|
||||
expandToScroll,
|
||||
} = this;
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
||||
@ -948,6 +974,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
['modal-default']: !isCardModal && !isSheetModal,
|
||||
[`modal-card`]: isCardModal,
|
||||
[`modal-sheet`]: isSheetModal,
|
||||
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
|
||||
'overlay-hidden': true,
|
||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
||||
...getClassMap(this.cssClass),
|
||||
@ -1019,6 +1046,12 @@ interface ModalOverlayOptions {
|
||||
* to fade in when using a sheet modal.
|
||||
*/
|
||||
backdropBreakpoint: number;
|
||||
|
||||
/**
|
||||
* Whether or not the modal should scroll/drag
|
||||
* the content only when fully expanded.
|
||||
*/
|
||||
expandToScroll?: boolean;
|
||||
}
|
||||
|
||||
type ModalPresentOptions = ModalOverlayOptions;
|
||||
|
||||
@ -23,3 +23,9 @@ $modal-inset-height-large: 600px;
|
||||
|
||||
/// @prop - Text color of the modal content
|
||||
$modal-text-color: $text-color;
|
||||
|
||||
/// @prop - Padding top of the sheet modal
|
||||
$modal-sheet-padding-top: 6px;
|
||||
|
||||
/// @prop - Padding bottom of the sheet modal
|
||||
$modal-sheet-padding-bottom: 6px;
|
||||
|
||||
@ -100,6 +100,12 @@
|
||||
>
|
||||
Present Sheet Modal (Max breakpoint is not 1)
|
||||
</button>
|
||||
<button
|
||||
id="scroll-at-edge-modal"
|
||||
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.25, 0.5, 0.75, 1], expandToScroll: false })"
|
||||
>
|
||||
Present Sheet Modal (Scroll at any breakpoint)
|
||||
</button>
|
||||
<button
|
||||
id="custom-backdrop-modal"
|
||||
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"
|
||||
@ -184,6 +190,11 @@
|
||||
${items}
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`;
|
||||
|
||||
let extraOptions = {
|
||||
@ -209,6 +220,7 @@
|
||||
button.addEventListener('click', () => {
|
||||
modalElement.dismiss();
|
||||
});
|
||||
|
||||
document.body.appendChild(modalElement);
|
||||
return modalElement;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |