Skip to content

Commit 37c5832

Browse files
committed
feat(modal): added footer animation.
1 parent 4d0d56f commit 37c5832

File tree

6 files changed

+75
-12
lines changed

6 files changed

+75
-12
lines changed

core/src/components/modal/animations/ios.enter.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const createEnterAnimation = () => {
1717

1818
const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
1919

20-
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
20+
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined, footerAnimation: undefined };
2121
};
2222

2323
/**
@@ -26,25 +26,31 @@ const createEnterAnimation = () => {
2626
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
2727
const { presentingEl, currentBreakpoint, animateContentHeight } = opts;
2828
const root = getElementRoot(baseEl);
29-
const { wrapperAnimation, backdropAnimation, contentAnimation } =
30-
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
29+
const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } =
30+
currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation();
3131

3232
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
3333

3434
wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 });
3535

3636
contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
3737

38+
footerAnimation?.addElement(root.querySelector('ion-footer')!);
39+
3840
const baseAnimation = createAnimation('entering-base')
3941
.addElement(baseEl)
4042
.easing('cubic-bezier(0.32,0.72,0,1)')
4143
.duration(500)
4244
.addAnimation([wrapperAnimation]);
4345

44-
if (contentAnimation && animateContentHeight) {
46+
if (animateContentHeight && contentAnimation) {
4547
baseAnimation.addAnimation(contentAnimation);
4648
}
4749

50+
if (animateContentHeight && footerAnimation) {
51+
baseAnimation.addAnimation(footerAnimation);
52+
}
53+
4854
if (presentingEl) {
4955
const isMobile = window.innerWidth < 768;
5056
const hasCardModal =

core/src/components/modal/animations/md.enter.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const createEnterAnimation = () => {
1919
{ offset: 1, opacity: 1, transform: `translateY(0px)` },
2020
]);
2121

22-
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
22+
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined, footerAnimation: undefined };
2323
};
2424

2525
/**
@@ -28,24 +28,30 @@ const createEnterAnimation = () => {
2828
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
2929
const { currentBreakpoint, animateContentHeight } = opts;
3030
const root = getElementRoot(baseEl);
31-
const { wrapperAnimation, backdropAnimation, contentAnimation } =
32-
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
31+
const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } =
32+
currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation();
3333

3434
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
3535

3636
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
3737

3838
contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
3939

40+
footerAnimation?.addElement(root.querySelector('ion-footer')!);
41+
4042
const baseAnimation = createAnimation()
4143
.addElement(baseEl)
4244
.easing('cubic-bezier(0.36,0.66,0.04,1)')
4345
.duration(280)
4446
.addAnimation([backdropAnimation, wrapperAnimation]);
4547

46-
if (contentAnimation && animateContentHeight) {
48+
if (animateContentHeight && contentAnimation) {
4749
baseAnimation.addAnimation(contentAnimation);
4850
}
4951

52+
if (animateContentHeight && footerAnimation) {
53+
baseAnimation.addAnimation(footerAnimation);
54+
}
55+
5056
return baseAnimation;
5157
};

core/src/components/modal/animations/sheet.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createAnimation } from '@utils/animation/animation';
33
import type { ModalAnimationOptions } from '../modal-interface';
44
import { getBackdropValueForSheet } from '../utils';
55

6-
export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
6+
export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions) => {
77
const { currentBreakpoint, backdropBreakpoint } = opts;
88

99
/**
@@ -34,7 +34,13 @@ export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
3434
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
3535
]);
3636

37-
return { wrapperAnimation, backdropAnimation, contentAnimation };
37+
const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight;
38+
const footerAnimation = createAnimation('footerAnimation').keyframes([
39+
{ offset: 0, opacity: 1, transform: `translateY(-${wrapperHeight}px)` },
40+
{ offset: 1, opacity: 1, transform: `translateY(-${wrapperHeight! * (1 - currentBreakpoint!)}px)` },
41+
]);
42+
43+
return { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation };
3844
};
3945

4046
export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {

core/src/components/modal/gestures/sheet.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export const createSheetGesture = (
7676
{ offset: 0, maxHeight: '100%' },
7777
{ offset: 1, maxHeight: '0%' },
7878
],
79+
FOOTER_KEYFRAMES: [
80+
{ offset: 0, transform: `translateY(0)` },
81+
{ offset: 1, transform: `translateY(-${wrapperEl.clientHeight}px)` },
82+
],
7983
};
8084

8185
const contentEl = baseEl.querySelector('ion-content');
@@ -89,6 +93,7 @@ export const createSheetGesture = (
8993
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
9094
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
9195
const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation');
96+
const footerAnimation = animation.childAnimations.find((ani) => ani.id === 'footerAnimation');
9297

9398
const enableBackdrop = () => {
9499
baseEl.style.setProperty('pointer-events', 'auto');
@@ -128,6 +133,8 @@ export const createSheetGesture = (
128133
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
129134
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
130135
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
136+
footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]);
137+
131138
animation.progressStart(true, 1 - currentBreakpoint);
132139

133140
/**
@@ -338,7 +345,7 @@ export const createSheetGesture = (
338345
},
339346
]);
340347

341-
if (contentAnimation) {
348+
if (contentAnimation && footerAnimation) {
342349
/**
343350
* The modal content should scroll at any breakpoint when scrollAtEdge
344351
* is disabled. In order to do this, the content needs to be completely
@@ -350,6 +357,11 @@ export const createSheetGesture = (
350357
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
351358
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
352359
]);
360+
361+
footerAnimation.keyframes([
362+
{ offset: 0, transform: `translateY(-${height * (breakpointOffset)}px)` },
363+
{ offset: 1, transform: `translateY(-${height * (1 - snapToBreakpoint)}px)` },
364+
])
353365
}
354366

355367
animation.progressStep(0);
@@ -396,6 +408,7 @@ export const createSheetGesture = (
396408
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
397409
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
398410
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
411+
footerAnimation?.keyframes([...SheetDefaults.FOOTER_KEYFRAMES]);
399412
animation.progressStart(true, 1 - snapToBreakpoint);
400413
currentBreakpoint = snapToBreakpoint;
401414
onBreakpointChange(currentBreakpoint);

core/src/components/modal/modal.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,22 @@ export class Modal implements ComponentInterface, OverlayInterface {
532532

533533
this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
534534

535+
/**
536+
* If scrollAtEdge is false, footer must be fixed to
537+
* the bottom of the modal and be always visible.
538+
*/
539+
if (this.scrollAtEdge === false){
540+
const footer = this.usersElement.querySelector('ion-footer');
541+
if (!footer) return;
542+
footer.style.position = 'fixed';
543+
footer.style.bottom = '0';
544+
const fixedFooter = footer.cloneNode(true) as HTMLIonFooterElement;
545+
footer.remove();
546+
this.usersElement.style.setProperty('padding-bottom', `${fixedFooter.clientHeight}px`);
547+
el.shadowRoot!.querySelector('.modal-wrapper')!.appendChild(footer);
548+
}
549+
550+
535551
/**
536552
* When using the lazy loaded build of Stencil, we need to wait
537553
* for every Stencil component instance to be ready before presenting

core/src/components/modal/test/sheet/index.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
</button>
103103
<button
104104
id="custom-breakpoint-modal"
105-
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0,0.25, 0.5, 0.75], scrollAtEdge: false })"
105+
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0.25, 0.5, 0.75, 1], scrollAtEdge: false })"
106106
>
107107
Present Sheet Modal (scrollAtEdge)
108108
</button>
@@ -193,7 +193,9 @@
193193
<ion-footer>
194194
<ion-toolbar>
195195
<ion-title>Footer</ion-title>
196+
<ion-button id="add-content">Add More Content</ion-button>
196197
</ion-toolbar>
198+
<div id="dynamic-content"></div>
197199
</ion-footer>
198200
`;
199201

@@ -220,6 +222,20 @@
220222
button.addEventListener('click', () => {
221223
modalElement.dismiss();
222224
});
225+
226+
const buttonAdd = element.querySelector('#add-content');
227+
buttonAdd.addEventListener('click', () => {
228+
const dynamicContent = modalElement.shadowRoot.querySelector('#dynamic-content');
229+
console.log(modalElement.shadowRoot)
230+
const newToolbar = document.createElement('ion-toolbar');
231+
newToolbar.innerHTML = `
232+
<ion-title>Additional Content</ion-title>
233+
`;
234+
dynamicContent.appendChild(newToolbar);
235+
dynamicContent
236+
});
237+
238+
223239
document.body.appendChild(modalElement);
224240
return modalElement;
225241
}

0 commit comments

Comments
 (0)