Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dafb0b9005 | ||
|
|
6b164c8f44 | ||
|
|
5546e95288 | ||
|
|
52d6e09b27 | ||
|
|
a6e734cf1a | ||
|
|
ed77d81eaa | ||
|
|
e4957fa4d2 | ||
|
|
d9f8c1352f | ||
|
|
41da4c3565 | ||
|
|
0b549835b6 | ||
|
|
0bbb9f37b4 | ||
|
|
166e43554e |
@@ -1075,6 +1075,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
|
||||
@@ -1347,7 +1348,6 @@ ion-radio-group,none
|
||||
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
|
||||
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
||||
ion-radio-group,prop,name,string,this.inputId,false,false
|
||||
ion-radio-group,prop,required,boolean,false,false,false
|
||||
ion-radio-group,prop,value,any,undefined,false,false
|
||||
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||
|
||||
|
||||
16
core/src/components.d.ts
vendored
@@ -1735,6 +1735,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.
|
||||
*/
|
||||
@@ -2307,10 +2311,6 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required": boolean;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
@@ -6552,6 +6552,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.
|
||||
*/
|
||||
@@ -7087,10 +7091,6 @@ declare namespace LocalJSX {
|
||||
* Emitted when the `value` property has changed. This is used to ensure that `ion-radio` can respond to any value property changes from the group.
|
||||
*/
|
||||
"onIonValueChange"?: (event: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => void;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
|
||||
@@ -455,6 +455,7 @@ export class Content implements ComponentInterface {
|
||||
overscroll: forceOverscroll,
|
||||
[`content-${rtl}`]: true,
|
||||
})}
|
||||
tabIndex={'0'}
|
||||
style={{
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
'--offset-bottom': `${this.cBottom}px`,
|
||||
|
||||
@@ -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 |
@@ -34,11 +34,6 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Prop() name: string = this.inputId;
|
||||
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -230,12 +225,12 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, labelId, el, name, value, required } = this;
|
||||
const { label, labelId, el, name, value } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
renderHiddenInput(true, el, name, value, false);
|
||||
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode} aria-required={`${required}`}></Host>;
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode}></Host>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ export class TabButton implements ComponentInterface, AnchorInterface {
|
||||
role="tab"
|
||||
aria-selected={selected ? 'true' : null}
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
tabindex={disabled ? '-1' : undefined}
|
||||
tabindex={disabled ? '-1' : '0'}
|
||||
{...inheritedAttributes}
|
||||
>
|
||||
<span class="button-inner">
|
||||
|
||||
@@ -55,7 +55,7 @@ export class Tab implements ComponentInterface {
|
||||
|
||||
@Watch('active')
|
||||
changeActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
if (isActive) {
|
||||
this.prepareLazyLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,9 @@ export class Tabs implements NavOutlet {
|
||||
}
|
||||
|
||||
private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => {
|
||||
|
||||
console.log("onTabClicked");
|
||||
|
||||
const { href, tab } = ev.detail;
|
||||
if (this.useRouter && href !== undefined) {
|
||||
const router = document.querySelector('ion-router');
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -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 {
|
||||
/**
|
||||
@@ -55,7 +56,7 @@ body.backdrop-no-scroll {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,6 +59,7 @@ const MODAL_INPUTS = [
|
||||
'canDismiss',
|
||||
'cssClass',
|
||||
'enterAnimation',
|
||||
'expandToScroll',
|
||||
'event',
|
||||
'focusTrap',
|
||||
'handle',
|
||||
|
||||
@@ -1639,14 +1639,14 @@ export declare interface IonRadio extends Components.IonRadio {
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'required', 'value']
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-radio-group',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'required', 'value'],
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value'],
|
||||
})
|
||||
export class IonRadioGroup {
|
||||
protected el: HTMLIonRadioGroupElement;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const IonAlert = /*@__PURE__*/ defineOverlayContainer<JSX.IonAlert>('ion-
|
||||
|
||||
export const IonLoading = /*@__PURE__*/ defineOverlayContainer<JSX.IonLoading>('ion-loading', defineIonLoadingCustomElement, ['animated', 'backdropDismiss', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'showBackdrop', 'spinner', 'translucent', 'trigger']);
|
||||
|
||||
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'focusTrap', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger'], true);
|
||||
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('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<JSX.IonPickerLegacy>('ion-picker-legacy', defineIonPickerLegacyCustomElement, ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'trigger']);
|
||||
|
||||
|
||||
@@ -695,7 +695,6 @@ export const IonRadioGroup = /*@__PURE__*/ defineContainer<JSX.IonRadioGroup, JS
|
||||
'allowEmptySelection',
|
||||
'compareWith',
|
||||
'name',
|
||||
'required',
|
||||
'value',
|
||||
'ionChange',
|
||||
'ionValueChange'
|
||||
|
||||