Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modal): update iOS design, add swipe gesture #19428

Merged
merged 50 commits into from Dec 10, 2019
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
90280a8
update animation
manucorporat Sep 12, 2019
32dc3ae
merge 1
manucorporat Sep 12, 2019
9007a46
commit
manucorporat Sep 12, 2019
1b41551
wip
manucorporat Sep 12, 2019
efb479a
wip
manucorporat Sep 12, 2019
ffda359
revert menu
manucorporat Sep 12, 2019
5712020
wip
manucorporat Sep 17, 2019
a6fa6e5
Merge branch 'master' into fix-animation-manu-2
manucorporat Sep 20, 2019
6a48397
wip
manucorporat Sep 23, 2019
ed98612
Merge branch 'master' into fix-animation-manu-2
manucorporat Sep 23, 2019
a1ccbd8
wip
manucorporat Sep 23, 2019
6f41212
wip
manucorporat Sep 23, 2019
0c95251
feat(modal): new modal gestures
manucorporat Sep 23, 2019
e021370
sync with master
liamdebeasi Nov 20, 2019
a78504c
one more thing
liamdebeasi Nov 20, 2019
e4e1509
begin fixing swipe modal
liamdebeasi Nov 20, 2019
853948c
rounding errors
liamdebeasi Nov 20, 2019
483ee17
fix test
liamdebeasi Nov 21, 2019
dddd619
sync with master:
liamdebeasi Nov 21, 2019
c21df96
revert hide caret changes
liamdebeasi Nov 21, 2019
ff1578d
remove non-modal css changes
liamdebeasi Nov 21, 2019
da3d9d8
one more fix
liamdebeasi Nov 21, 2019
c5e27c8
more clean up
liamdebeasi Nov 21, 2019
df80e53
fix a few small issues
liamdebeasi Nov 21, 2019
78d2533
remove old code
liamdebeasi Nov 21, 2019
0c2282a
remove running animations
liamdebeasi Nov 21, 2019
78c3830
add initial animation clean up fix
liamdebeasi Nov 21, 2019
88f8376
clamp out of bounds values
liamdebeasi Nov 22, 2019
e8e00e5
fix safari bug, fix dismiss via modalcontroller
liamdebeasi Nov 22, 2019
7d33588
clear styles after in case user dismisses quickly
liamdebeasi Nov 22, 2019
8d39031
update transition effect to avoid additional paints on modal
liamdebeasi Nov 22, 2019
894100a
remove unused velocity
liamdebeasi Nov 22, 2019
edfdec3
add role
liamdebeasi Nov 22, 2019
f6d2750
revert old style changes
liamdebeasi Nov 22, 2019
74e5139
remove old styles
liamdebeasi Nov 22, 2019
703c2a5
update tests
liamdebeasi Nov 22, 2019
ae44cf7
fix dir
liamdebeasi Nov 25, 2019
46d5c1e
update api
liamdebeasi Nov 25, 2019
e7025bf
make native el public
liamdebeasi Nov 25, 2019
11eab87
update API, fix bug with multiple modals
liamdebeasi Nov 25, 2019
d51e8b2
fix interface
liamdebeasi Nov 25, 2019
5cb91d9
usage docs
liamdebeasi Nov 25, 2019
e5212ec
chore(docs): updating react swipable modal doc
Nov 26, 2019
e60f230
clean up code, remove online imgs, run build
liamdebeasi Dec 2, 2019
eec775e
only modify transform on card style modals
liamdebeasi Dec 2, 2019
6723751
update comment
liamdebeasi Dec 2, 2019
67654ba
cleanup
liamdebeasi Dec 10, 2019
3dab6ab
add border radius animation
liamdebeasi Dec 10, 2019
4e3bb95
Merge remote-tracking branch 'origin/master' into new-modal-swipe
liamdebeasi Dec 10, 2019
c35c97d
fix background color removal
liamdebeasi Dec 10, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/api.txt
Expand Up @@ -683,7 +683,9 @@ ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefin
ion-modal,prop,keyboardClose,boolean,true,false,false
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,mode,"ios" | "md",undefined,false,false
ion-modal,prop,presentingElement,HTMLElement | undefined,undefined,false,false
ion-modal,prop,showBackdrop,boolean,true,false,false
ion-modal,prop,swipeGesture,boolean,false,false,false
ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise<boolean>
ion-modal,method,onDidDismiss,onDidDismiss() => Promise<OverlayEventDetail<any>>
ion-modal,method,onWillDismiss,onWillDismiss() => Promise<OverlayEventDetail<any>>
Expand Down
16 changes: 16 additions & 0 deletions core/src/components.d.ts
Expand Up @@ -1500,9 +1500,17 @@ export namespace Components {
*/
'present': () => Promise<void>;
/**
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other.
*/
'presentingElement'?: HTMLElement;
/**
* If `true`, a backdrop will be displayed behind the modal.
*/
'showBackdrop': boolean;
/**
* If `true`, the modal will support a swipe and pan drag gesture to close. Only supported on iOS as Android does not use this type of interaction.
*/
'swipeGesture': boolean;
}
interface IonModalController {
/**
Expand Down Expand Up @@ -4853,9 +4861,17 @@ declare namespace LocalJSX {
*/
'onIonModalWillPresent'?: (event: CustomEvent<void>) => void;
/**
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other.
*/
'presentingElement'?: HTMLElement;
/**
* If `true`, a backdrop will be displayed behind the modal.
*/
'showBackdrop'?: boolean;
/**
* If `true`, the modal will support a swipe and pan drag gesture to close. Only supported on iOS as Android does not use this type of interaction.
*/
'swipeGesture'?: boolean;
}
interface IonModalController {}
interface IonNav {
Expand Down
3 changes: 1 addition & 2 deletions core/src/components/alert/alert.tsx
@@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core';

import { getIonMode } from '../../global/ionic-global';
import { AlertButton, AlertInput, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { AlertButton, AlertInput, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
Expand Down Expand Up @@ -30,7 +30,6 @@ export class Alert implements ComponentInterface, OverlayInterface {
private processedButtons: AlertButton[] = [];

presented = false;
animation?: Animation;
mode = getIonMode(this);

@Element() el!: HTMLIonAlertElement;
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/content/content.scss
Expand Up @@ -120,6 +120,8 @@
}

.transition-effect {

display: none;
position: absolute;

/* stylelint-disable property-blacklist */
Expand Down
3 changes: 1 addition & 2 deletions core/src/components/loading/loading.tsx
Expand Up @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth

import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, OverlayEventDetail, OverlayInterface, SpinnerTypes } from '../../interface';
import { AnimationBuilder, OverlayEventDetail, OverlayInterface, SpinnerTypes } from '../../interface';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
Expand All @@ -27,7 +27,6 @@ export class Loading implements ComponentInterface, OverlayInterface {
private durationTimeout: any;

presented = false;
animation?: Animation;
mode = getIonMode(this);

@Element() el!: HTMLIonLoadingElement;
Expand Down
120 changes: 46 additions & 74 deletions core/src/components/modal/animations/ios.enter.ts
@@ -1,90 +1,62 @@
import { Animation } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';

/**
* iOS Modal Enter Animation
* iOS Modal Enter Animation for the Card presentation style
*/
export const iosEnterAnimation = (baseEl: HTMLElement): Animation => {
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();

backdropAnimation
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)');

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

return baseAnimation
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(400)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
.beforeAddClass('show-modal')
.addAnimation([backdropAnimation, wrapperAnimation]);
};

/**
* Animations for modals
*/
// export function modalSlideIn(rootEl: HTMLElement) {

// }

// export class ModalSlideOut {
// constructor(el: HTMLElement) {
// let backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// let wrapperEle = <HTMLElement>el.querySelector('.modal-wrapper');
// let wrapperEleRect = wrapperEle.getBoundingClientRect();
// let wrapper = new Animation(this.plt, wrapperEle);

// // height of the screen - top of the container tells us how much to scoot it down
// // so it's off-screen
// wrapper.fromTo('translateY', '0px', `${this.plt.height() - wrapperEleRect.top}px`);
// backdrop.fromTo('opacity', 0.4, 0.0);

// this
// .element(this.leavingView.pageRef())
// .easing('ease-out')
// .duration(250)
// .add(backdrop)
// .add(wrapper);
// }
// }

// export class ModalMDSlideIn {
// constructor(el: HTMLElement) {
// const backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// const wrapper = new Animation(this.plt, el.querySelector('.modal-wrapper'));

// backdrop.fromTo('opacity', 0.01, 0.4);
// wrapper.fromTo('translateY', '40px', '0px');
// wrapper.fromTo('opacity', 0.01, 1);

// const DURATION = 280;
// const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
// this.element(this.enteringView.pageRef()).easing(EASING).duration(DURATION)
// .add(backdrop)
// .add(wrapper);
// }
// }

// export class ModalMDSlideOut {
// constructor(el: HTMLElement) {
// const backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// const wrapper = new Animation(this.plt, el.querySelector('.modal-wrapper'));

// backdrop.fromTo('opacity', 0.4, 0.0);
// wrapper.fromTo('translateY', '0px', '40px');
// wrapper.fromTo('opacity', 0.99, 0);

// this
// .element(this.leavingView.pageRef())
// .duration(200)
// .easing('cubic-bezier(0.47,0,0.745,0.715)')
// .add(wrapper)
// .add(backdrop);
// }
// }
// BEFORE 0->1 AFTER
// BEFORE 1->0 AFTER

// onstart(0.5) 0.5 -> 1 onFinish(1)
// onstart(0.5) 0.5 -> 0 onFinish(0)
// onstart(false) START 0->1 END onFInish(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// onstart(false) START 0->1 END onFInish(true)
// onstart(false) START 0->1 END onFinish(true)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manucorporat do you still want these comments in here, or can I remove then?

// onstart(true) END 1->0 START onFinish(false)

// onstart(false) START 0->1 END onFInish(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// onstart(false) START 0->1 END onFInish(true)
// onstart(false) START 0->1 END onFinish(true)

// onstart(true) END 1->0 START onFinish(false)

if (presentingEl) {
const bodyEl = document.body;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const presentingToY = SwipeToCloseDefaults.MIN_PRESENTING_Y;
const finalTransform = `translateY(${presentingToY}%) scale(${toPresentingScale})`;
const presentingAnimation = createAnimation()
.beforeStyles({
'transform': 'translateY(0)',
'border-radius': '10px 10px 0 0'
})
.afterStyles({
'transform': finalTransform
})
.addElement(presentingEl)
.fromTo('transform', 'translateY(0px) scale(1)', finalTransform);

// Wrap around animation code
bodyEl.style.backgroundColor = 'black';
baseAnimation.addAnimation(presentingAnimation);
}

return baseAnimation;
};
57 changes: 44 additions & 13 deletions core/src/components/modal/animations/ios.leave.ts
@@ -1,28 +1,59 @@
import { Animation } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { raf } from '../../../utils/helpers';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';

/**
* iOS Modal Leave Animation
*/
export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const wrapperEl = baseEl.querySelector('.modal-wrapper');
const wrapperElRect = wrapperEl!.getBoundingClientRect();
export const iosLeaveAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
duration = 500
): Animation => {

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

wrapperAnimation
.addElement(wrapperEl!)
const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(0%)', `translateY(${(baseEl.ownerDocument as any).defaultView.innerHeight - wrapperElRect.top}px)`);
.fromTo('transform', `translateY(0%)`, 'translateY(100%)');

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

if (presentingEl) {
const currentPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const presentingFromY = SwipeToCloseDefaults.MIN_PRESENTING_Y;
const presentingAnimation = createAnimation()
.addElement(presentingEl)
.beforeClearStyles(['transform'])
.afterClearStyles(['transform'])
.onFinish(currentStep => {
if (currentStep === 1) {
/**
* This is a hack to work around an issue in Safari
* where the border-radius change is not rendered
* unless a layout is forced
*/
raf(() => {
presentingEl.style.removeProperty('border-radius');
presentingEl.style.setProperty('overflow', 'unset');
raf(() => {
presentingEl.style.setProperty('overflow', '');
});
});
}
})
.fromTo('transform', `translateY(${presentingFromY}px) scale(${currentPresentingScale})`, 'translateY(0px) scale(1)');

baseAnimation.addAnimation(presentingAnimation);
}

return baseAnimation;
};
97 changes: 97 additions & 0 deletions core/src/components/modal/gestures/swipe-to-close.ts
@@ -0,0 +1,97 @@
import { Animation } from '../../../interface';
import { getTimeGivenProgression } from '../../../utils/animation/cubic-bezier';
import { GestureDetail, createGesture } from '../../../utils/gesture';
import { clamp } from '../../../utils/helpers';

// Defaults for the card swipe animation
export const SwipeToCloseDefaults = {
MIN_BACKDROP_OPACITY: 0.4,
MIN_PRESENTING_SCALE: 0.95,
MIN_Y_CARD: 44,
MIN_Y_FULLSCREEN: 0,
MIN_PRESENTING_Y: 0
};

export const createSwipeToCloseGesture = (
el: HTMLIonModalElement,
animation: Animation,
onDismiss: () => void
) => {
const height = el.offsetHeight;
let isOpen = false;

const canStart = (detail: GestureDetail) => {
const target = detail.event.target as HTMLElement | null;

if (target === null ||
!(target as any).closest) {
return true;
}

const content = target.closest('ion-content');
if (content === null) {
return true;
}
// Target is in the content so we don't start the gesture.
// We could be more nuanced here and allow it for content that
// does not need to scroll.
return false;
};

const onStart = () => {
animation.progressStart(true, (isOpen) ? 1 : 0);
};

const onMove = (detail: GestureDetail) => {
const step = detail.deltaY / height;
if (step < 0) { return; }

animation.progressStep(step);
};

const onEnd = (detail: GestureDetail) => {
const velocity = detail.velocityY;
const step = detail.deltaY / height;
if (step < 0) { return; }

const threshold = (detail.deltaY + velocity * 1000) / height;

const shouldComplete = threshold >= 0.5;
let newStepValue = (shouldComplete) ? -0.001 : 0.001;

if (!shouldComplete) {
animation.easing('cubic-bezier(1, 0, 0.68, 0.28)');
newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], step)[0];
} else {
animation.easing('cubic-bezier(0.32, 0.72, 0, 1)');
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], step)[0];
}

const duration = (shouldComplete) ? computeDuration(step * height, velocity) : computeDuration((1 - step) * height, velocity);
isOpen = shouldComplete;

animation
.onFinish(() => {
if (shouldComplete) {
onDismiss();
}
})
.progressEnd((shouldComplete) ? 1 : 0, newStepValue, duration);
};

return createGesture({
el,
gestureName: 'modalSwipeToClose',
gesturePriority: 40,
direction: 'y',
threshold: 10,
canStart,
onStart,
onMove,
onEnd
});
};

const computeDuration = (remaining: number, velocity: number) => {
return clamp(100, remaining / Math.abs(velocity * 1.1), 400);
};