From ae7fe543fe71c57a2b6ce7082a9279f60344af35 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 25 Mar 2020 13:36:54 -0400 Subject: [PATCH] fix(modal): respect card-style modal spec for iPadOS (#20750) fixes #20700 --- .../components/modal/animations/ios.enter.ts | 88 ++++++++++++++----- .../components/modal/animations/ios.leave.ts | 70 ++++++++++++--- core/src/components/modal/modal.ios.scss | 74 +++++++++++----- core/src/components/modal/modal.scss | 9 +- core/src/components/modal/modal.tsx | 2 + .../src/components/modal/test/spec/index.html | 4 +- 6 files changed, 186 insertions(+), 61 deletions(-) diff --git a/core/src/components/modal/animations/ios.enter.ts b/core/src/components/modal/animations/ios.enter.ts index cc3295cfd1..4902da4dfc 100644 --- a/core/src/components/modal/animations/ios.enter.ts +++ b/core/src/components/modal/animations/ios.enter.ts @@ -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)') @@ -19,7 +18,7 @@ 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)'); @@ -27,37 +26,78 @@ export const iosEnterAnimation = ( .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' } - ]); + }); - baseAnimation.addAnimation(presentingAnimation); + 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, shadowAnimation]); + } + } + } else { + baseAnimation.addAnimation(backdropAnimation); } return baseAnimation; diff --git a/core/src/components/modal/animations/ios.leave.ts b/core/src/components/modal/animations/ios.leave.ts index 1500f224bf..5d05c0f33c 100644 --- a/core/src/components/modal/animations/ios.leave.ts +++ b/core/src/components/modal/animations/ios.leave.ts @@ -10,13 +10,12 @@ 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)'); @@ -24,15 +23,13 @@ export const iosLeaveAnimation = ( .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 => { @@ -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' } - ]); + }); - baseAnimation.addAnimation(presentingAnimation); + 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, shadowAnimation]); + } + } + } else { + baseAnimation.addAnimation(backdropAnimation); } return baseAnimation; diff --git a/core/src/components/modal/modal.ios.scss b/core/src/components/modal/modal.ios.scss index 32a45d89cc..4fa1a57c64 100644 --- a/core/src/components/modal/modal.ios.scss +++ b/core/src/components/modal/modal.ios.scss @@ -19,24 +19,58 @@ @include transform(translate3d(0, 100%, 0)); } -:host(.modal-card) { - --backdrop-opacity: 0; - --width: 100%; - - align-items: flex-end; -} - -:host(.modal-card) ion-backdrop { - pointer-events: none; -} - -:host(.modal-card) .modal-wrapper { - @include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0); - height: calc(100% - 40px); -} - -@supports (width: max(0px, 1px)) { - :host(.modal-card) .modal-wrapper { - height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px); +@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); + } } -} \ No newline at end of file + + @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) ion-backdrop { + pointer-events: none; + } +} + +@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); + } +} diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.scss index 747944e119..581de3e0a4 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.scss @@ -52,7 +52,8 @@ display: none; } -.modal-wrapper { +.modal-wrapper, +.modal-shadow { @include border-radius(var(--border-radius)); width: var(--width); @@ -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}; diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 04557f91de..cd5f7e8c1f 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -272,6 +272,8 @@ export class Modal implements ComponentInterface, OverlayInterface { onIonModalDidDismiss={this.onLifecycle} > + + {mode === 'ios' && }