fix(modal): respect card-style modal spec for iPadOS (#20750)

fixes #20700
This commit is contained in:
Liam DeBeasi
2020-03-25 13:36:54 -04:00
committed by GitHub
parent 794c3d4e96
commit ae7fe543fe
6 changed files with 186 additions and 61 deletions

View File

@ -9,7 +9,6 @@ export const iosEnterAnimation = (
baseEl: HTMLElement, baseEl: HTMLElement,
presentingEl?: HTMLElement, presentingEl?: HTMLElement,
): Animation => { ): Animation => {
// The top translate Y for the presenting element
const backdropAnimation = createAnimation() const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!) .addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)') .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
@ -19,7 +18,7 @@ export const iosEnterAnimation = (
.afterClearStyles(['pointer-events']); .afterClearStyles(['pointer-events']);
const wrapperAnimation = createAnimation() const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!) .addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 }) .beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)'); .fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
@ -27,26 +26,33 @@ export const iosEnterAnimation = (
.addElement(baseEl) .addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)') .easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500) .duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]); .addAnimation(wrapperAnimation);
if (presentingEl) { if (presentingEl) {
/** const isMobile = window.innerWidth < 768;
* Fallback for browsers that does not support `max()` (ex: Firefox) const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);
* No need to wrry about statusbar padding since engines like Gecko
* are not used as the engine for standlone Cordova/Capacitor apps
*/
const transformOffset = (!CSS.supports('width', 'max(0px, 1px)')) ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const modalTransform = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined) ? '-10px' : transformOffset;
const bodyEl = document.body;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const finalTransform = `translateY(${modalTransform}) scale(${toPresentingScale})`;
const presentingAnimation = createAnimation() const presentingAnimation = createAnimation()
.beforeStyles({ .beforeStyles({
'transform': 'translateY(0)', 'transform': 'translateY(0)',
'transform-origin': 'top center', 'transform-origin': 'top center',
'overflow': 'hidden' 'overflow': 'hidden'
}) });
const bodyEl = document.body;
if (isMobile) {
/**
* Fallback for browsers that does not support `max()` (ex: Firefox)
* No need to worry about statusbar padding since engines like Gecko
* are not used as the engine for standlone Cordova/Capacitor apps
*/
const transformOffset = (!CSS.supports('width', 'max(0px, 1px)')) ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const modalTransform = hasCardModal ? '-10px' : transformOffset;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const finalTransform = `translateY(${modalTransform}) scale(${toPresentingScale})`;
presentingAnimation
.afterStyles({ .afterStyles({
'transform': finalTransform 'transform': finalTransform
}) })
@ -58,6 +64,40 @@ export const iosEnterAnimation = (
]); ]);
baseAnimation.addAnimation(presentingAnimation); baseAnimation.addAnimation(presentingAnimation);
} else {
baseAnimation.addAnimation(backdropAnimation);
if (!hasCardModal) {
wrapperAnimation.fromTo('opacity', '0', '1');
} else {
const toPresentingScale = (hasCardModal) ? SwipeToCloseDefaults.MIN_PRESENTING_SCALE : 1;
const finalTransform = `translateY(-10px) scale(${toPresentingScale})`;
presentingAnimation
.afterStyles({
'transform': finalTransform
})
.addElement(presentingEl.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, filter: 'contrast(1)', transform: 'translateY(0) scale(1)' },
{ offset: 1, filter: 'contrast(0.85)', transform: finalTransform }
]);
const shadowAnimation = createAnimation()
.afterStyles({
'transform': finalTransform
})
.addElement(presentingEl.querySelector('.modal-shadow')!)
.keyframes([
{ offset: 0, opacity: '1', transform: 'translateY(0) scale(1)' },
{ offset: 1, opacity: '0', transform: finalTransform }
]);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
}
} else {
baseAnimation.addAnimation(backdropAnimation);
} }
return baseAnimation; return baseAnimation;

View File

@ -10,13 +10,12 @@ export const iosLeaveAnimation = (
presentingEl?: HTMLElement, presentingEl?: HTMLElement,
duration = 500 duration = 500
): Animation => { ): Animation => {
const backdropAnimation = createAnimation() const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!) .addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0); .fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
const wrapperAnimation = createAnimation() const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!) .addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 }) .beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(0vh)', 'translateY(100vh)'); .fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');
@ -24,15 +23,13 @@ export const iosLeaveAnimation = (
.addElement(baseEl) .addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)') .easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration) .duration(duration)
.addAnimation([backdropAnimation, wrapperAnimation]); .addAnimation(wrapperAnimation);
if (presentingEl) { if (presentingEl) {
const transformOffset = (!CSS.supports('width', 'max(0px, 1px)')) ? '30px' : 'max(30px, var(--ion-safe-area-top))'; const isMobile = window.innerWidth < 768;
const modalTransform = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined) ? '-10px' : transformOffset; const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);
const bodyEl = document.body;
const currentPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const presentingAnimation = createAnimation() const presentingAnimation = createAnimation()
.addElement(presentingEl)
.beforeClearStyles(['transform']) .beforeClearStyles(['transform'])
.afterClearStyles(['transform']) .afterClearStyles(['transform'])
.onFinish(currentStep => { .onFinish(currentStep => {
@ -45,13 +42,58 @@ export const iosLeaveAnimation = (
if (numModals <= 1) { if (numModals <= 1) {
bodyEl.style.setProperty('background-color', ''); bodyEl.style.setProperty('background-color', '');
} }
}) });
const bodyEl = document.body;
if (isMobile) {
const transformOffset = (!CSS.supports('width', 'max(0px, 1px)')) ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const modalTransform = hasCardModal ? '-10px' : transformOffset;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const finalTransform = `translateY(${modalTransform}) scale(${toPresentingScale})`;
presentingAnimation
.addElement(presentingEl)
.keyframes([ .keyframes([
{ offset: 0, filter: 'contrast(0.85)', transform: `translateY(${modalTransform}) scale(${currentPresentingScale})`, borderRadius: '10px 10px 0 0' }, { offset: 0, filter: 'contrast(0.85)', transform: finalTransform, borderRadius: '10px 10px 0 0' },
{ offset: 1, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' } { offset: 1, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' }
]); ]);
baseAnimation.addAnimation(presentingAnimation); baseAnimation.addAnimation(presentingAnimation);
} else {
baseAnimation.addAnimation(backdropAnimation);
if (!hasCardModal) {
wrapperAnimation.fromTo('opacity', '1', '0');
} else {
const toPresentingScale = (hasCardModal) ? SwipeToCloseDefaults.MIN_PRESENTING_SCALE : 1;
const finalTransform = `translateY(-10px) scale(${toPresentingScale})`;
presentingAnimation
.addElement(presentingEl.querySelector('.modal-wrapper')!)
.afterStyles({
'transform': 'translate3d(0, 0, 0)'
})
.keyframes([
{ offset: 0, filter: 'contrast(0.85)', transform: finalTransform },
{ offset: 1, filter: 'contrast(1)', transform: 'translateY(0) scale(1)' }
]);
const shadowAnimation = createAnimation()
.addElement(presentingEl.querySelector('.modal-shadow')!)
.afterStyles({
'transform': 'translateY(0) scale(1)'
})
.keyframes([
{ offset: 0, opacity: '0', transform: finalTransform },
{ offset: 1, opacity: '1', transform: 'translateY(0) scale(1)' }
]);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
}
} else {
baseAnimation.addAnimation(backdropAnimation);
} }
return baseAnimation; return baseAnimation;

View File

@ -19,24 +19,58 @@
@include transform(translate3d(0, 100%, 0)); @include transform(translate3d(0, 100%, 0));
} }
:host(.modal-card) { @media screen and (max-width: 767px) {
@supports (width: max(0px, 1px)) {
:host(.modal-card) .modal-wrapper {
height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
}
}
@supports not (width: max(0px, 1px)) {
:host(.modal-card) .modal-wrapper {
height: calc(100% - 40px);
}
}
:host(.modal-card) .modal-wrapper {
@include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0);
}
:host(.modal-card) {
--backdrop-opacity: 0; --backdrop-opacity: 0;
--width: 100%; --width: 100%;
align-items: flex-end; align-items: flex-end;
} }
:host(.modal-card) ion-backdrop { :host(.modal-card) .modal-shadow {
display: none;
}
:host(.modal-card) ion-backdrop {
pointer-events: none; pointer-events: none;
} }
}
:host(.modal-card) .modal-wrapper {
@include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0); @media screen and (min-width: 768px) {
height: calc(100% - 40px); :host {
} --width: calc(100% - 120px);
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));
@supports (width: max(0px, 1px)) { --max-width: 720px;
:host(.modal-card) .modal-wrapper { --max-height: 1000px;
height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px); }
:host(.modal-card) {
--backdrop-opacity: 0;
transition: all 0.5s ease-in-out;
&:first-of-type {
--backdrop-opacity: 0.18;
}
}
:host(.modal-card) .modal-shadow {
box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.1);
} }
} }

View File

@ -52,7 +52,8 @@
display: none; display: none;
} }
.modal-wrapper { .modal-wrapper,
.modal-shadow {
@include border-radius(var(--border-radius)); @include border-radius(var(--border-radius));
width: var(--width); width: var(--width);
@ -74,6 +75,12 @@
z-index: 10; z-index: 10;
} }
.modal-shadow {
position: absolute;
background: transparent;
}
@media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-small) { @media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-small) {
:host { :host {
--width: #{$modal-inset-width}; --width: #{$modal-inset-width};

View File

@ -272,6 +272,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
onIonModalDidDismiss={this.onLifecycle} onIonModalDidDismiss={this.onLifecycle}
> >
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/> <ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
{mode === 'ios' && <div class="modal-shadow"></div>}
<div <div
role="dialog" role="dialog"
class="modal-wrapper" class="modal-wrapper"

View File

@ -78,7 +78,7 @@
.fromTo('opacity', '0.01', '0.4'); .fromTo('opacity', '0.01', '0.4');
const wrapperAnimation = createAnimation() const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')) .addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow'))
.keyframes([ .keyframes([
{ offset: 0, opacity: '0', transform: 'scale(0)' }, { offset: 0, opacity: '0', transform: 'scale(0)' },
{ offset: 1, opacity: '0.99', transform: 'scale(1)' } { offset: 1, opacity: '0.99', transform: 'scale(1)' }
@ -97,7 +97,7 @@
.fromTo('opacity', '0.4', '0.01'); .fromTo('opacity', '0.4', '0.01');
const wrapperAnimation = createAnimation() const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')) .addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow'))
.keyframes([ .keyframes([
{ offset: 0, opacity: '0.99', transform: 'scale(1)' }, { offset: 0, opacity: '0.99', transform: 'scale(1)' },
{ offset: 1, opacity: '0', transform: 'scale(0)' } { offset: 1, opacity: '0', transform: 'scale(0)' }