Skip to content

Commit

Permalink
fix(modal): respect card-style modal spec for iPadOS (#20750)
Browse files Browse the repository at this point in the history
fixes #20700
  • Loading branch information
liamdebeasi committed Mar 25, 2020
1 parent 794c3d4 commit ae7fe54
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 57 deletions.
88 changes: 64 additions & 24 deletions core/src/components/modal/animations/ios.enter.ts
Expand Up @@ -9,7 +9,6 @@ export const iosEnterAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
): Animation => {
// The top translate Y for the presenting element
const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
Expand All @@ -19,45 +18,86 @@ export const iosEnterAnimation = (
.afterClearStyles(['pointer-events']);

const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!)
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');

const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]);
.addAnimation(wrapperAnimation);

if (presentingEl) {
/**
* Fallback for browsers that does not support `max()` (ex: Firefox)
* 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 isMobile = window.innerWidth < 768;
const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);

const presentingAnimation = createAnimation()
.beforeStyles({
'transform': 'translateY(0)',
'transform-origin': 'top center',
'overflow': 'hidden'
})
.afterStyles({
'transform': finalTransform
})
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
.addElement(presentingEl)
.keyframes([
{ offset: 0, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' },
{ offset: 1, filter: 'contrast(0.85)', transform: finalTransform, borderRadius: '10px 10px 0 0' }
]);
});

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({
'transform': finalTransform
})
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
.addElement(presentingEl)
.keyframes([
{ offset: 0, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' },
{ offset: 1, filter: 'contrast(0.85)', transform: finalTransform, borderRadius: '10px 10px 0 0' }
]);

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);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
}
} else {
baseAnimation.addAnimation(backdropAnimation);
}

return baseAnimation;
Expand Down
70 changes: 56 additions & 14 deletions core/src/components/modal/animations/ios.leave.ts
Expand Up @@ -10,29 +10,26 @@ export const iosLeaveAnimation = (
presentingEl?: HTMLElement,
duration = 500
): Animation => {

const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);

const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!)
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');

const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration)
.addAnimation([backdropAnimation, wrapperAnimation]);
.addAnimation(wrapperAnimation);

if (presentingEl) {
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 currentPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const isMobile = window.innerWidth < 768;
const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);

const presentingAnimation = createAnimation()
.addElement(presentingEl)
.beforeClearStyles(['transform'])
.afterClearStyles(['transform'])
.onFinish(currentStep => {
Expand All @@ -45,13 +42,58 @@ export const iosLeaveAnimation = (
if (numModals <= 1) {
bodyEl.style.setProperty('background-color', '');
}
})
.keyframes([
{ offset: 0, filter: 'contrast(0.85)', transform: `translateY(${modalTransform}) scale(${currentPresentingScale})`, borderRadius: '10px 10px 0 0' },
{ offset: 1, filter: 'contrast(1)', transform: 'translateY(0px) scale(1)', borderRadius: '0px' }
]);
});

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([
{ 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' }
]);

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);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
}
} else {
baseAnimation.addAnimation(backdropAnimation);
}

return baseAnimation;
Expand Down
66 changes: 50 additions & 16 deletions core/src/components/modal/modal.ios.scss
Expand Up @@ -19,24 +19,58 @@
@include transform(translate3d(0, 100%, 0));
}

:host(.modal-card) {
--backdrop-opacity: 0;
--width: 100%;

align-items: flex-end;
}
@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);
}
}

:host(.modal-card) ion-backdrop {
pointer-events: none;
}
@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;
--width: 100%;

align-items: flex-end;
}

:host(.modal-card) .modal-shadow {
display: none;
}

:host(.modal-card) .modal-wrapper {
@include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0);
height: calc(100% - 40px);
:host(.modal-card) ion-backdrop {
pointer-events: none;
}
}

@supports (width: max(0px, 1px)) {
:host(.modal-card) .modal-wrapper {
height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
@media screen and (min-width: 768px) {
:host {
--width: calc(100% - 120px);
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));
--max-width: 720px;
--max-height: 1000px;
}
}

: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);
}
}
9 changes: 8 additions & 1 deletion core/src/components/modal/modal.scss
Expand Up @@ -52,7 +52,8 @@
display: none;
}

.modal-wrapper {
.modal-wrapper,
.modal-shadow {
@include border-radius(var(--border-radius));

width: var(--width);
Expand All @@ -74,6 +75,12 @@
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) {
:host {
--width: #{$modal-inset-width};
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/modal/modal.tsx
Expand Up @@ -272,6 +272,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
onIonModalDidDismiss={this.onLifecycle}
>
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>

{mode === 'ios' && <div class="modal-shadow"></div>}
<div
role="dialog"
class="modal-wrapper"
Expand Down
4 changes: 2 additions & 2 deletions core/src/components/modal/test/spec/index.html
Expand Up @@ -78,7 +78,7 @@
.fromTo('opacity', '0.01', '0.4');

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

const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper'))
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow'))
.keyframes([
{ offset: 0, opacity: '0.99', transform: 'scale(1)' },
{ offset: 1, opacity: '0', transform: 'scale(0)' }
Expand Down

0 comments on commit ae7fe54

Please sign in to comment.