Skip to content

Commit 59dd5b0

Browse files
authored
Merge pull request #1 from thetaPC/scroll
refactor(modal): use a clone footer to prevent flickering
2 parents ac11277 + 804d043 commit 59dd5b0

File tree

10 files changed

+152
-161
lines changed

10 files changed

+152
-161
lines changed

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

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,55 +17,79 @@ const createEnterAnimation = () => {
1717

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

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

2323
/**
2424
* iOS Modal Enter Animation for the Card presentation style
2525
*/
2626
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
27-
const { presentingEl, currentBreakpoint, animateContentHeight } = opts;
27+
const { presentingEl, currentBreakpoint, scrollAtEdge } = opts;
2828
const root = getElementRoot(baseEl);
29-
const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } =
30-
currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation();
29+
const { wrapperAnimation, backdropAnimation, contentAnimation } =
30+
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
3131

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

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

36-
contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
36+
// The content animation is only added if scrolling is enabled for
37+
// all the breakpoints.
38+
!scrollAtEdge && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
3739

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
.beforeAddWrite(() => {
44-
if (!animateContentHeight) return;
46+
if (scrollAtEdge) {
47+
// Scroll can only be done when the modal is fully expanded.
48+
return;
49+
}
4550

51+
/**
52+
* There are some browsers that causes flickering when
53+
* dragging the content when scroll is enabled at every
54+
* breakpoint. This is due to the wrapper element being
55+
* transformed off the screen and having a snap animation.
56+
*
57+
* A workaround is to clone the footer element and append
58+
* it outside of the wrapper element. This way, the footer
59+
* is still visible and the drag can be done without
60+
* flickering. The original footer is hidden until the modal
61+
* is dismissed. This maintains the animation of the footer
62+
* when the modal is dismissed.
63+
*
64+
* The workaround needs to be done before the animation starts
65+
* so there are no flickering issues.
66+
*/
4667
const ionFooter = baseEl.querySelector('ion-footer');
47-
if (ionFooter && footerAnimation) {
68+
/**
69+
* This check is needed to prevent more than one footer
70+
* from being appended to the shadow root.
71+
* Otherwise, iOS and MD enter animations would append
72+
* the footer twice.
73+
*/
74+
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
75+
if (ionFooter && !ionFooterAlreadyAppended) {
4876
const footerHeight = ionFooter.clientHeight;
49-
const clonedFooter = ionFooter.cloneNode(true) as HTMLElement;
77+
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
78+
5079
baseEl.shadowRoot!.appendChild(clonedFooter);
51-
ionFooter.remove();
80+
ionFooter.style.setProperty('display', 'none');
81+
ionFooter.setAttribute('aria-hidden', 'true');
5282

53-
// add padding bottom to the .ion-page element to be
54-
// the same as the cloned footer height
83+
// Padding is added to prevent some content from being hidden.
5584
const page = baseEl.querySelector('.ion-page') as HTMLElement;
5685
page.style.setProperty('padding-bottom', `${footerHeight}px`);
57-
footerAnimation.addElement(root.querySelector('ion-footer')!);
58-
}
59-
});
86+
}
87+
});
6088

61-
if (animateContentHeight && contentAnimation) {
89+
if (contentAnimation) {
6290
baseAnimation.addAnimation(contentAnimation);
6391
}
6492

65-
if (animateContentHeight && footerAnimation) {
66-
baseAnimation.addAnimation(footerAnimation);
67-
}
68-
6993
if (presentingEl) {
7094
const isMobile = window.innerWidth < 768;
7195
const hasCardModal =

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const createLeaveAnimation = () => {
1212

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

15-
return { backdropAnimation, wrapperAnimation, footerAnimation: undefined };
15+
return { backdropAnimation, wrapperAnimation };
1616
};
1717

1818
/**
@@ -21,25 +21,19 @@ const createLeaveAnimation = () => {
2121
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => {
2222
const { presentingEl, currentBreakpoint } = opts;
2323
const root = getElementRoot(baseEl);
24-
const { wrapperAnimation, backdropAnimation, footerAnimation } =
25-
currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation();
24+
const { wrapperAnimation, backdropAnimation } =
25+
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
2626

2727
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
2828

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

31-
footerAnimation?.addElement(root.querySelector('ion-footer')!);
32-
3331
const baseAnimation = createAnimation('leaving-base')
3432
.addElement(baseEl)
3533
.easing('cubic-bezier(0.32,0.72,0,1)')
3634
.duration(duration)
3735
.addAnimation(wrapperAnimation);
3836

39-
if (footerAnimation) {
40-
baseAnimation.addAnimation(footerAnimation);
41-
}
42-
4337
if (presentingEl) {
4438
const isMobile = window.innerWidth < 768;
4539
const hasCardModal =

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

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

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

2525
/**
2626
* Md Modal Enter Animation
2727
*/
2828
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
29-
const { currentBreakpoint, animateContentHeight } = opts;
29+
const { currentBreakpoint, scrollAtEdge } = opts;
3030
const root = getElementRoot(baseEl);
31-
const { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation } =
32-
currentBreakpoint !== undefined ? createSheetEnterAnimation(baseEl, opts) : createEnterAnimation();
31+
const { wrapperAnimation, backdropAnimation, contentAnimation } =
32+
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
3333

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

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

38-
contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
38+
// The content animation is only added if scrolling is enabled for
39+
// all the breakpoints.
40+
scrollAtEdge && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
3941

4042
const baseAnimation = createAnimation()
41-
.addElement(baseEl)
42-
.easing('cubic-bezier(0.36,0.66,0.04,1)')
43-
.duration(280)
44-
.addAnimation([backdropAnimation, wrapperAnimation])
45-
.beforeAddWrite(() => {
46-
if (!animateContentHeight) return;
43+
.addElement(baseEl)
44+
.easing('cubic-bezier(0.36,0.66,0.04,1)')
45+
.duration(280)
46+
.addAnimation([backdropAnimation, wrapperAnimation])
47+
.beforeAddWrite(() => {
48+
if (scrollAtEdge) {
49+
// Scroll can only be done when the modal is fully expanded.
50+
return;
51+
}
52+
53+
/**
54+
* There are some browsers that causes flickering when
55+
* dragging the content when scroll is enabled at every
56+
* breakpoint. This is due to the wrapper element being
57+
* transformed off the screen and having a snap animation.
58+
*
59+
* A workaround is to clone the footer element and append
60+
* it outside of the wrapper element. This way, the footer
61+
* is still visible and the drag can be done without
62+
* flickering. The original footer is hidden until the modal
63+
* is dismissed. This maintains the animation of the footer
64+
* when the modal is dismissed.
65+
*
66+
* The workaround needs to be done before the animation starts
67+
* so there are no flickering issues.
68+
*/
69+
const ionFooter = baseEl.querySelector('ion-footer');
70+
/**
71+
* This check is needed to prevent more than one footer
72+
* from being appended to the shadow root.
73+
* Otherwise, iOS and MD enter animations would append
74+
* the footer twice.
75+
*/
76+
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
77+
if (ionFooter && !ionFooterAlreadyAppended) {
78+
const footerHeight = ionFooter.clientHeight;
79+
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
4780

48-
const ionFooter = baseEl.querySelector('ion-footer');
49-
if (ionFooter && footerAnimation) {
50-
const footerHeight = ionFooter.clientHeight;
51-
const clonedFooter = ionFooter.cloneNode(true) as HTMLElement;
52-
baseEl.shadowRoot!.appendChild(clonedFooter);
53-
ionFooter.remove();
81+
baseEl.shadowRoot!.appendChild(clonedFooter);
82+
ionFooter.style.setProperty('display', 'none');
83+
ionFooter.setAttribute('aria-hidden', 'true');
5484

55-
// add padding bottom to the .ion-page element to be
56-
// the same as the cloned footer height
57-
const page = baseEl.querySelector('.ion-page') as HTMLElement;
58-
page.style.setProperty('padding-bottom', `${footerHeight}px`);
59-
footerAnimation.addElement(root.querySelector('ion-footer')!);
85+
// Padding is added to prevent some content from being hidden.
86+
const page = baseEl.querySelector('.ion-page') as HTMLElement;
87+
page.style.setProperty('padding-bottom', `${footerHeight}px`);
6088
}
61-
});
89+
});
6290

63-
if (animateContentHeight && contentAnimation) {
91+
if (contentAnimation) {
6492
baseAnimation.addAnimation(contentAnimation);
6593
}
6694

67-
if (animateContentHeight && footerAnimation) {
68-
baseAnimation.addAnimation(footerAnimation);
69-
}
70-
7195
return baseAnimation;
7296
};

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const createLeaveAnimation = () => {
1414
{ offset: 1, opacity: 0, transform: 'translateY(40px)' },
1515
]);
1616

17-
return { backdropAnimation, wrapperAnimation, footerAnimation: undefined };
17+
return { backdropAnimation, wrapperAnimation };
1818
};
1919

2020
/**
@@ -23,21 +23,16 @@ const createLeaveAnimation = () => {
2323
export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
2424
const { currentBreakpoint } = opts;
2525
const root = getElementRoot(baseEl);
26-
const { wrapperAnimation, backdropAnimation, footerAnimation } =
27-
currentBreakpoint !== undefined ? createSheetLeaveAnimation(baseEl, opts) : createLeaveAnimation();
26+
const { wrapperAnimation, backdropAnimation } =
27+
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
2828

2929
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
3030
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
31-
footerAnimation?.addElement(root.querySelector('ion-footer')!);
3231

3332
const baseAnimation = createAnimation()
3433
.easing('cubic-bezier(0.47,0,0.745,0.715)')
3534
.duration(200)
3635
.addAnimation([backdropAnimation, wrapperAnimation]);
3736

38-
if (footerAnimation) {
39-
baseAnimation.addAnimation(footerAnimation);
40-
}
41-
4237
return baseAnimation;
4338
};

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

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

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

99
/**
1010
* If the backdropBreakpoint is undefined, then the backdrop
@@ -29,32 +29,20 @@ export const createSheetEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimat
2929
{ offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` },
3030
]);
3131

32-
const contentAnimation = createAnimation('contentAnimation').keyframes([
33-
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
34-
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
35-
]);
36-
37-
const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0;
38-
39-
const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight
40-
?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0;
41-
42-
const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100;
43-
44-
const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2));
45-
46-
const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2));
47-
48-
const footerAnimation = createAnimation('footerAnimation').keyframes([
49-
{ offset: 0, opacity: 1, transform: `translateY(${footerHeight}px)` },
50-
{ offset: headerOffset, opacity: 1, transform: `translateY(${footerHeight}px)` },
51-
{ offset: ((footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' },
52-
]);
53-
54-
return { wrapperAnimation, backdropAnimation, contentAnimation, footerAnimation };
32+
/**
33+
* This allows the content to be scrollable at any breakpoint.
34+
*/
35+
const contentAnimation = !scrollAtEdge
36+
? createAnimation('contentAnimation').keyframes([
37+
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
38+
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
39+
])
40+
: undefined;
41+
42+
return { wrapperAnimation, backdropAnimation, contentAnimation };
5543
};
5644

57-
export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions) => {
45+
export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {
5846
const { currentBreakpoint, backdropBreakpoint } = opts;
5947

6048
/**
@@ -86,22 +74,5 @@ export const createSheetLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimat
8674
{ offset: 1, opacity: 1, transform: `translateY(100%)` },
8775
]);
8876

89-
const headerHeight = baseEl.querySelector('ion-header')?.clientHeight ?? 0;
90-
91-
const footerHeight = baseEl.querySelector('ion-footer')?.clientHeight
92-
?? baseEl.shadowRoot?.querySelector('ion-footer')?.clientHeight ?? 0;
93-
94-
const wrapperHeight = baseEl.shadowRoot?.querySelector('.modal-wrapper, .modal-shadow')?.clientHeight ?? 100;
95-
96-
const footerOffset = parseFloat(((footerHeight ? (footerHeight / wrapperHeight) : 0)).toFixed(2));
97-
98-
const headerOffset = parseFloat(((headerHeight ? (headerHeight / wrapperHeight) : 0)).toFixed(2));
99-
100-
const footerAnimation = createAnimation('footerAnimation').keyframes([
101-
{ offset: 0, opacity: 1, transform: 'translateY(0)' },
102-
{ offset: (1 - (footerOffset + headerOffset) * 2), opacity: 1, transform: 'translateY(0)' },
103-
{ offset: (1), opacity: 1, transform: `translateY(${footerHeight}px)` },
104-
]);
105-
106-
return { wrapperAnimation, backdropAnimation, footerAnimation };
77+
return { wrapperAnimation, backdropAnimation };
10778
};

0 commit comments

Comments
 (0)