diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 789922816..b4e3993fe 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -14,7 +14,7 @@ import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core'; import { IonContent } from '@ionic/angular'; -import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core'; +import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes, getMode } from '@ionic/core'; import { Md5 } from 'ts-md5'; import { CoreApp } from '@services/app'; @@ -37,6 +37,7 @@ import { CoreBSTooltipComponent } from '@components/bs-tooltip/bs-tooltip'; import { CoreViewerImageComponent } from '@features/viewer/components/image/image'; import { CoreFormFields, CoreForms } from '../../singletons/form'; import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition'; +import { CoreZoomLevel } from '@features/settings/services/settings-helper'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1734,9 +1735,19 @@ export class CoreDomUtilsProvider { ): Promise { const popover = await PopoverController.create(popoverOptions); + const zoomLevel = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL); await popover.present(); + // Fix popover position if zoom is applied. + if (zoomLevel !== CoreZoomLevel.NORMAL) { + switch (getMode()) { + case 'ios': + fixIOSPopoverPosition(popover, popoverOptions.event); + break; + } + } + // If onDidDismiss is nedded we can add a new param to the function to wait one function or the other. const result = await popover.onWillDismiss(); if (result?.data) { @@ -1849,6 +1860,96 @@ export class CoreDomUtilsProvider { } +/** + * Fix the position of a popover that was created with a zoom level applied in iOS. + * + * This is necessary because Ionic's implementation gets the body dimensions from `element.ownerDocument.defaultView.innerXXX`, + * which doesn't return the correct dimensions when the `zoom` CSS property is being used. This is only necessary + * in iOS because Android already respects system font sizes, and setting the zoom level is unnecessary. Eventually, we + * should find an alternative implementation for iOS that doesn't require this workaround (also because the `zoom` CSS + * property is not standard and its usage is discouraged for production). + * + * This function has been copied in its entirety from Ionic's source code, only changing the aforementioned calculation + * of the body dimensions with `document.body.clientXXX`. + * + * @see https://github.com/ionic-team/ionic-framework/blob/v5.6.6/core/src/components/popover/animations/ios.enter.ts + */ +function fixIOSPopoverPosition(baseEl: HTMLElement, ev?: Event): void { + let originY = 'top'; + let originX = 'left'; + + const POPOVER_IOS_BODY_PADDING = 5; + const contentEl = baseEl.querySelector('.popover-content') as HTMLElement; + const contentDimentions = contentEl.getBoundingClientRect(); + const contentWidth = contentDimentions.width; + const contentHeight = contentDimentions.height; + const bodyWidth = document.body.clientWidth; + const bodyHeight = document.body.clientHeight; + const targetDim = ev && ev.target && (ev.target as HTMLElement).getBoundingClientRect(); + const targetTop = targetDim != null && 'top' in targetDim ? targetDim.top : bodyHeight / 2 - contentHeight / 2; + const targetLeft = targetDim != null && 'left' in targetDim ? targetDim.left : bodyWidth / 2; + const targetWidth = (targetDim && targetDim.width) || 0; + const targetHeight = (targetDim && targetDim.height) || 0; + const arrowEl = baseEl.querySelector('.popover-arrow') as HTMLElement; + const arrowDim = arrowEl.getBoundingClientRect(); + const arrowWidth = arrowDim.width; + const arrowHeight = arrowDim.height; + + if (targetDim == null) { + arrowEl.style.display = 'none'; + } + + const arrowCSS = { + top: targetTop + targetHeight, + left: targetLeft + targetWidth / 2 - arrowWidth / 2, + }; + + const popoverCSS: { top: number; left: number } = { + top: targetTop + targetHeight + (arrowHeight - 1), + left: targetLeft + targetWidth / 2 - contentWidth / 2, + }; + + let checkSafeAreaLeft = false; + let checkSafeAreaRight = false; + + if (popoverCSS.left < POPOVER_IOS_BODY_PADDING + 25) { + checkSafeAreaLeft = true; + popoverCSS.left = POPOVER_IOS_BODY_PADDING; + } else if ( + contentWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left + 25 > bodyWidth + ) { + checkSafeAreaRight = true; + popoverCSS.left = bodyWidth - contentWidth - POPOVER_IOS_BODY_PADDING; + originX = 'right'; + } + + if (targetTop + targetHeight + contentHeight > bodyHeight && targetTop - contentHeight > 0) { + arrowCSS.top = targetTop - (arrowHeight + 1); + popoverCSS.top = targetTop - contentHeight - (arrowHeight - 1); + + baseEl.className = baseEl.className + ' popover-bottom'; + originY = 'bottom'; + } else if (targetTop + targetHeight + contentHeight > bodyHeight) { + contentEl.style.bottom = POPOVER_IOS_BODY_PADDING + '%'; + } + + arrowEl.style.top = arrowCSS.top + 'px'; + arrowEl.style.left = arrowCSS.left + 'px'; + + contentEl.style.top = popoverCSS.top + 'px'; + contentEl.style.left = popoverCSS.left + 'px'; + + if (checkSafeAreaLeft) { + contentEl.style.left = `calc(${popoverCSS.left}px + var(--ion-safe-area-left, 0px))`; + } + + if (checkSafeAreaRight) { + contentEl.style.left = `calc(${popoverCSS.left}px - var(--ion-safe-area-right, 0px))`; + } + + contentEl.style.transformOrigin = originY + ' ' + originX; +} + export const CoreDomUtils = makeSingleton(CoreDomUtilsProvider); type AnchorOrMediaElement =