feat(modal): add expandToScroll property to allow scrolling at all breakpoints (#30097)

Issue number: resolves #24631

Co-authored-by: Maria Hutt <13530427+thetaPC@users.noreply.github.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
Israel de la Barrera
2025-02-03 19:45:15 +01:00
committed by GitHub
parent 621333d927
commit 166e43554e
23 changed files with 365 additions and 32 deletions

View File

@ -17,27 +17,78 @@ const createEnterAnimation = () => {
const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
return { backdropAnimation, wrapperAnimation };
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
};
/**
* iOS Modal Enter Animation for the Card presentation style
*/
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
const { presentingEl, currentBreakpoint } = opts;
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
const { wrapperAnimation, backdropAnimation, contentAnimation } =
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 });
// The content animation is only added if scrolling is enabled for
// all the breakpoints.
!expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
const baseAnimation = createAnimation('entering-base')
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
.addAnimation(wrapperAnimation);
.addAnimation([wrapperAnimation])
.beforeAddWrite(() => {
if (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}
/**
* There are some browsers that causes flickering when
* dragging the content when scroll is enabled at every
* breakpoint. This is due to the wrapper element being
* transformed off the screen and having a snap animation.
*
* A workaround is to clone the footer element and append
* it outside of the wrapper element. This way, the footer
* is still visible and the drag can be done without
* flickering. The original footer is hidden until the modal
* is dismissed. This maintains the animation of the footer
* when the modal is dismissed.
*
* The workaround needs to be done before the animation starts
* so there are no flickering issues.
*/
const ionFooter = baseEl.querySelector('ion-footer');
/**
* This check is needed to prevent more than one footer
* from being appended to the shadow root.
* Otherwise, iOS and MD enter animations would append
* the footer twice.
*/
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
if (ionFooter && !ionFooterAlreadyAppended) {
const footerHeight = ionFooter.clientHeight;
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
baseEl.shadowRoot!.appendChild(clonedFooter);
ionFooter.style.setProperty('display', 'none');
ionFooter.setAttribute('aria-hidden', 'true');
// Padding is added to prevent some content from being hidden.
const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.setProperty('padding-bottom', `${footerHeight}px`);
}
});
if (contentAnimation) {
baseAnimation.addAnimation(contentAnimation);
}
if (presentingEl) {
const isMobile = window.innerWidth < 768;

View File

@ -19,7 +19,7 @@ const createLeaveAnimation = () => {
* iOS Modal Leave Animation
*/
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => {
const { presentingEl, currentBreakpoint } = opts;
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
@ -32,7 +32,33 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration)
.addAnimation(wrapperAnimation);
.addAnimation(wrapperAnimation)
.beforeAddWrite(() => {
if (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}
/**
* If expandToScroll is disabled, we need to swap
* the visibility to the original, so the footer
* dismisses with the modal and doesn't stay
* until the modal is removed from the DOM.
*/
const ionFooter = baseEl.querySelector('ion-footer');
if (ionFooter) {
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;
ionFooter.style.removeProperty('display');
ionFooter.removeAttribute('aria-hidden');
clonedFooter.style.setProperty('display', 'none');
clonedFooter.setAttribute('aria-hidden', 'true');
const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.removeProperty('padding-bottom');
}
});
if (presentingEl) {
const isMobile = window.innerWidth < 768;

View File

@ -19,25 +19,78 @@ const createEnterAnimation = () => {
{ offset: 1, opacity: 1, transform: `translateY(0px)` },
]);
return { backdropAnimation, wrapperAnimation };
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
};
/**
* Md Modal Enter Animation
*/
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
const { currentBreakpoint } = opts;
const { currentBreakpoint, expandToScroll } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
const { wrapperAnimation, backdropAnimation, contentAnimation } =
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
return createAnimation()
// The content animation is only added if scrolling is enabled for
// all the breakpoints.
expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(280)
.addAnimation([backdropAnimation, wrapperAnimation]);
.addAnimation([backdropAnimation, wrapperAnimation])
.beforeAddWrite(() => {
if (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}
/**
* There are some browsers that causes flickering when
* dragging the content when scroll is enabled at every
* breakpoint. This is due to the wrapper element being
* transformed off the screen and having a snap animation.
*
* A workaround is to clone the footer element and append
* it outside of the wrapper element. This way, the footer
* is still visible and the drag can be done without
* flickering. The original footer is hidden until the modal
* is dismissed. This maintains the animation of the footer
* when the modal is dismissed.
*
* The workaround needs to be done before the animation starts
* so there are no flickering issues.
*/
const ionFooter = baseEl.querySelector('ion-footer');
/**
* This check is needed to prevent more than one footer
* from being appended to the shadow root.
* Otherwise, iOS and MD enter animations would append
* the footer twice.
*/
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
if (ionFooter && !ionFooterAlreadyAppended) {
const footerHeight = ionFooter.clientHeight;
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
baseEl.shadowRoot!.appendChild(clonedFooter);
ionFooter.style.setProperty('display', 'none');
ionFooter.setAttribute('aria-hidden', 'true');
// Padding is added to prevent some content from being hidden.
const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.setProperty('padding-bottom', `${footerHeight}px`);
}
});
if (contentAnimation) {
baseAnimation.addAnimation(contentAnimation);
}
return baseAnimation;
};

View File

@ -21,7 +21,7 @@ const createLeaveAnimation = () => {
* Md Modal Leave Animation
*/
export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
const { currentBreakpoint } = opts;
const { currentBreakpoint, expandToScroll } = opts;
const root = getElementRoot(baseEl);
const { wrapperAnimation, backdropAnimation } =
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
@ -29,8 +29,36 @@ export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
return createAnimation()
const baseAnimation = createAnimation()
.easing('cubic-bezier(0.47,0,0.745,0.715)')
.duration(200)
.addAnimation([backdropAnimation, wrapperAnimation]);
.addAnimation([backdropAnimation, wrapperAnimation])
.beforeAddWrite(() => {
if (expandToScroll) {
// Scroll can only be done when the modal is fully expanded.
return;
}
/**
* If expandToScroll is disabled, we need to swap
* the visibility to the original, so the footer
* dismisses with the modal and doesn't stay
* until the modal is removed from the DOM.
*/
const ionFooter = baseEl.querySelector('ion-footer');
if (ionFooter) {
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;
ionFooter.style.removeProperty('display');
ionFooter.removeAttribute('aria-hidden');
clonedFooter.style.setProperty('display', 'none');
clonedFooter.setAttribute('aria-hidden', 'true');
const page = baseEl.querySelector('.ion-page') as HTMLElement;
page.style.removeProperty('padding-bottom');
}
});
return baseAnimation;
};

View File

@ -4,7 +4,7 @@ import type { ModalAnimationOptions } from '../modal-interface';
import { getBackdropValueForSheet } from '../utils';
export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
const { currentBreakpoint, backdropBreakpoint } = opts;
const { currentBreakpoint, backdropBreakpoint, expandToScroll } = opts;
/**
* If the backdropBreakpoint is undefined, then the backdrop
@ -29,7 +29,17 @@ export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
{ offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` },
]);
return { wrapperAnimation, backdropAnimation };
/**
* This allows the content to be scrollable at any breakpoint.
*/
const contentAnimation = !expandToScroll
? createAnimation('contentAnimation').keyframes([
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
])
: undefined;
return { wrapperAnimation, backdropAnimation, contentAnimation };
};
export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {

View File

@ -49,6 +49,7 @@ export const createSheetGesture = (
backdropBreakpoint: number,
animation: Animation,
breakpoints: number[] = [],
expandToScroll: boolean,
getCurrentBreakpoint: () => number,
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
@ -71,6 +72,10 @@ export const createSheetGesture = (
{ offset: 1, transform: 'translateY(100%)' },
],
BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
CONTENT_KEYFRAMES: [
{ offset: 0, maxHeight: '100%' },
{ offset: 1, maxHeight: '0%' },
],
};
const contentEl = baseEl.querySelector('ion-content');
@ -79,10 +84,11 @@ export const createSheetGesture = (
let offset = 0;
let canDismissBlocksGesture = false;
const canDismissMaxStep = 0.95;
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
const maxBreakpoint = breakpoints[breakpoints.length - 1];
const minBreakpoint = breakpoints[0];
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation');
const enableBackdrop = () => {
baseEl.style.setProperty('pointer-events', 'auto');
@ -110,6 +116,36 @@ export const createSheetGesture = (
baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
};
/**
* Toggles the visible modal footer when `expandToScroll` is disabled.
* @param footer The footer to show.
*/
const swapFooterVisibility = (footer: 'original' | 'cloned') => {
const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null;
if (!originalFooter) {
return;
}
const clonedFooter = wrapperEl.nextElementSibling as HTMLIonFooterElement;
const footerToHide = footer === 'original' ? clonedFooter : originalFooter;
const footerToShow = footer === 'original' ? originalFooter : clonedFooter;
footerToShow.style.removeProperty('display');
footerToShow.removeAttribute('aria-hidden');
const page = baseEl.querySelector('.ion-page') as HTMLElement;
if (footer === 'original') {
page.style.removeProperty('padding-bottom');
} else {
const pagePadding = footerToShow.clientHeight;
page.style.setProperty('padding-bottom', `${pagePadding}px`);
}
footerToHide.style.setProperty('display', 'none');
footerToHide.setAttribute('aria-hidden', 'true');
};
/**
* After the entering animation completes,
* we need to set the animation to go from
@ -121,6 +157,7 @@ export const createSheetGesture = (
if (wrapperAnimation && backdropAnimation) {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
animation.progressStart(true, 1 - currentBreakpoint);
/**
@ -138,7 +175,7 @@ export const createSheetGesture = (
}
}
if (contentEl && currentBreakpoint !== maxBreakpoint) {
if (contentEl && currentBreakpoint !== maxBreakpoint && expandToScroll) {
contentEl.scrollY = false;
}
@ -154,6 +191,14 @@ export const createSheetGesture = (
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
currentBreakpoint = getCurrentBreakpoint();
/**
* If we have expandToScroll disabled, we should not allow the swipe gesture to start
* if the content is being swiped.
*/
if (!expandToScroll && contentEl) {
return false;
}
if (currentBreakpoint === 1 && contentEl) {
/**
* The modal should never swipe to close on the content with a refresher.
@ -187,6 +232,16 @@ export const createSheetGesture = (
*/
canDismissBlocksGesture = baseEl.canDismiss !== undefined && baseEl.canDismiss !== true && minBreakpoint === 0;
/**
* If expandToScroll is disabled, we need to swap
* the footer visibility to the original, so if the modal
* is dismissed, the footer dismisses with the modal
* and doesn't stay on the screen after the modal is gone.
*/
if (!expandToScroll) {
swapFooterVisibility('original');
}
/**
* If we are pulling down, then it is possible we are pulling on the content.
* We do not want scrolling to happen at the same time as the gesture.
@ -323,6 +378,20 @@ export const createSheetGesture = (
},
]);
if (contentAnimation) {
/**
* The modal content should scroll at any breakpoint when expandToScroll
* is disabled. In order to do this, the content needs to be completely
* viewable so scrolling can access everything. Otherwise, the default
* behavior would show the content off the screen and only allow
* scrolling when the sheet is fully expanded.
*/
contentAnimation.keyframes([
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
]);
}
animation.progressStep(0);
}
@ -332,6 +401,15 @@ export const createSheetGesture = (
*/
gesture.enable(false);
/**
* If expandToScroll is disabled, we need to swap
* the footer visibility to the cloned one so the footer
* doesn't flicker when the sheet's height is animated.
*/
if (!expandToScroll && shouldRemainOpen) {
swapFooterVisibility('cloned');
}
if (shouldPreventDismiss) {
handleCanDismiss(baseEl, animation);
} else if (!shouldRemainOpen) {
@ -339,13 +417,13 @@ export const createSheetGesture = (
}
/**
* If the sheet is going to be fully expanded then we should enable
* scrolling immediately. The sheet modal animation takes ~500ms to finish
* so if we wait until then there is a visible delay for when scrolling is
* re-enabled. Native iOS allows for scrolling on the sheet modal as soon
* as the gesture is released, so we align with that.
* Enables scrolling immediately if the sheet is about to fully expand
* or if it allows scrolling at any breakpoint. Without this, there would
* be a ~500ms delay while the modal animation completes, causing a
* noticeable lag. Native iOS allows scrolling as soon as the gesture is
* released, so we align with that behavior.
*/
if (contentEl && snapToBreakpoint === breakpoints[breakpoints.length - 1]) {
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !expandToScroll)) {
contentEl.scrollY = true;
}
@ -365,6 +443,7 @@ export const createSheetGesture = (
raf(() => {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
animation.progressStart(true, 1 - snapToBreakpoint);
currentBreakpoint = snapToBreakpoint;
onBreakpointChange(currentBreakpoint);

View File

@ -31,6 +31,7 @@ export interface ModalAnimationOptions {
presentingEl?: HTMLElement;
currentBreakpoint?: number;
backdropBreakpoint?: number;
expandToScroll: boolean;
}
export interface ModalBreakpointChangeEventDetail {

View File

@ -87,3 +87,16 @@
:host(.modal-sheet) .modal-wrapper {
@include border-radius(var(--border-radius), var(--border-radius), 0, 0);
}
// iOS Sheet Modal - Scroll at all breakpoints
// --------------------------------------------------
/**
* Sheet modals require an additional padding as mentioned in the
* `core.scss` file. However, there's a workaround that requires
* a cloned footer to be added to the modal. This is only necessary
* because the core styles are not being applied to the cloned footer.
*/
:host(.modal-sheet.modal-no-expand-scroll) ion-footer ion-toolbar:first-of-type {
padding-top: $modal-sheet-padding-top;
}

View File

@ -166,3 +166,13 @@ ion-backdrop {
position: absolute;
bottom: 0;
}
// Sheet Modal - Scroll at all breakpoints
// --------------------------------------------------
:host(.modal-sheet.modal-no-expand-scroll) ion-footer {
position: absolute;
bottom: 0;
width: var(--width);
}

View File

@ -130,6 +130,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() breakpoints?: number[];
/**
* Controls whether scrolling or dragging within the sheet modal expands
* it to a larger breakpoint. This only takes effect when `breakpoints`
* and `initialBreakpoint` are set.
*
* If `true`, scrolling or dragging anywhere in the modal will first expand
* it to the next breakpoint. Once fully expanded, scrolling will affect the content.
* If `false`, scrolling will always affect the content, and the modal will only expand
* when dragging the header or handle.
*/
@Prop() expandToScroll = true;
/**
* A decimal value between 0 and 1 that indicates the
* initial point the modal will open at when creating a
@ -562,6 +574,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
presentingEl: presentingElement,
currentBreakpoint: this.initialBreakpoint,
backdropBreakpoint: this.backdropBreakpoint,
expandToScroll: this.expandToScroll,
});
/* tslint:disable-next-line */
@ -616,7 +629,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
// should be in the DOM and referenced by now, except
// for the presenting el
const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
const ani = (this.animation = animationBuilder(el, { presentingEl: this.presentingElement }));
const ani = (this.animation = animationBuilder(el, {
presentingEl: this.presentingElement,
expandToScroll: this.expandToScroll,
}));
const contentEl = findIonContent(el);
if (!contentEl) {
@ -668,6 +684,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
presentingEl: this.presentingElement,
currentBreakpoint: initialBreakpoint,
backdropBreakpoint,
expandToScroll: this.expandToScroll,
}));
ani.progressStart(true, 1);
@ -680,6 +697,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
backdropBreakpoint,
ani,
this.sortedBreakpoints,
this.expandToScroll,
() => this.currentBreakpoint ?? 0,
() => this.sheetOnDismiss(),
(breakpoint: number) => {
@ -778,6 +796,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
presentingEl: presentingElement,
currentBreakpoint: this.currentBreakpoint ?? this.initialBreakpoint,
backdropBreakpoint: this.backdropBreakpoint,
expandToScroll: this.expandToScroll,
}
);
@ -927,9 +946,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
};
render() {
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } =
this;
const {
handle,
isSheetModal,
presentingElement,
htmlAttributes,
handleBehavior,
inheritedAttributes,
focusTrap,
expandToScroll,
} = this;
const showHandle = handle !== false && isSheetModal;
const mode = getIonMode(this);
const isCardModal = presentingElement !== undefined && mode === 'ios';
@ -948,6 +974,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
['modal-default']: !isCardModal && !isSheetModal,
[`modal-card`]: isCardModal,
[`modal-sheet`]: isSheetModal,
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
'overlay-hidden': true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
...getClassMap(this.cssClass),
@ -1019,6 +1046,12 @@ interface ModalOverlayOptions {
* to fade in when using a sheet modal.
*/
backdropBreakpoint: number;
/**
* Whether or not the modal should scroll/drag
* the content only when fully expanded.
*/
expandToScroll?: boolean;
}
type ModalPresentOptions = ModalOverlayOptions;

View File

@ -23,3 +23,9 @@ $modal-inset-height-large: 600px;
/// @prop - Text color of the modal content
$modal-text-color: $text-color;
/// @prop - Padding top of the sheet modal
$modal-sheet-padding-top: 6px;
/// @prop - Padding bottom of the sheet modal
$modal-sheet-padding-bottom: 6px;

View File

@ -100,6 +100,12 @@
>
Present Sheet Modal (Max breakpoint is not 1)
</button>
<button
id="scroll-at-edge-modal"
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.25, 0.5, 0.75, 1], expandToScroll: false })"
>
Present Sheet Modal (Scroll at any breakpoint)
</button>
<button
id="custom-backdrop-modal"
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"
@ -184,6 +190,11 @@
${items}
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
`;
let extraOptions = {
@ -209,6 +220,7 @@
button.addEventListener('click', () => {
modalElement.dismiss();
});
document.body.appendChild(modalElement);
return modalElement;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB