Trying to improve accuracy

This commit is contained in:
ShaneK
2026-02-03 12:18:38 -08:00
parent edc202db34
commit 9097a1d146
3 changed files with 91 additions and 67 deletions

View File

@@ -93,8 +93,37 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
arrowHeight
);
/**
* Safe area CSS variable adjustments.
* When the popover is positioned near an edge, we add the corresponding
* safe-area inset to ensure the popover doesn't overlap with system UI
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
*/
const safeAreaTop = ' + var(--ion-safe-area-top, 0px)';
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0px)';
const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
let topValue = `${top}px`;
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
let leftValue = `${left}px`;
if (checkSafeAreaTop) {
topValue = `${top}px${safeAreaTop}`;
}
if (checkSafeAreaBottom && bottomValue !== undefined) {
bottomValue = `${bottom}px${safeAreaBottom}`;
}
if (checkSafeAreaLeft) {
leftValue = `${left}px${safeAreaLeft}`;
}
if (checkSafeAreaRight) {
leftValue = `${left}px${safeAreaRight}`;
}
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const arrowAnimation = createAnimation();
const contentAnimation = createAnimation();
backdropAnimation
@@ -109,11 +138,42 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
// The Chromium team stated that this behavior is expected and not a bug. The element animating opacity creates a backdrop root for the backdrop-filter.
// To get around this, instead of animating the wrapper, animate both the arrow and content.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1148826
contentAnimation
.addElement(root.querySelector('.popover-arrow')!)
.addElement(root.querySelector('.popover-content')!)
.fromTo('opacity', 0.01, 1);
// TODO(FW-4376) Ensure that arrow also blurs when translucent
if (arrowEl !== null) {
arrowAnimation.addElement(arrowEl).fromTo('opacity', 0.01, 1);
}
contentAnimation
.addElement(contentEl)
.beforeAddWrite(() => {
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0px))`);
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0px))`);
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
if (bottomValue !== undefined) {
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
/**
* When both top and bottom are explicitly constrained (isFullyConstrained),
* we need to explicitly calculate the height to ensure the popover
* fits within the safe area boundaries.
*
* Using CSS calc with 100vh minus top and bottom values ensures the
* popover height respects both safe areas. We also override max-height
* to prevent it from interfering with the calculated height.
*/
if (isFullyConstrained) {
/**
* Wrap topValue and bottomValue in parentheses to ensure correct
* order of operations in the CSS calc. Without parentheses, the
* safe-area additions would have wrong signs.
*/
const heightCalc = `calc(100vh - (${topValue}) - (${bottomValue}) - var(--offset-y, 0px))`;
contentEl.style.setProperty('height', heightCalc);
contentEl.style.setProperty('max-height', heightCalc);
}
}
})
.fromTo('opacity', 0.01, 1);
return baseAnimation
.easing('ease')
@@ -127,54 +187,6 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
baseEl.classList.add('popover-bottom');
}
/**
* Safe area CSS variable adjustments.
* When the popover is positioned near an edge, we add the corresponding
* safe-area inset to ensure the popover doesn't overlap with system UI
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
*/
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
let topValue = `${top}px`;
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
let leftValue = `${left}px`;
if (checkSafeAreaTop) {
topValue = `${top}px${safeAreaTop}`;
}
if (checkSafeAreaBottom && bottomValue !== undefined) {
bottomValue = `${bottom}px${safeAreaBottom}`;
}
if (checkSafeAreaLeft) {
leftValue = `${left}px${safeAreaLeft}`;
}
if (checkSafeAreaRight) {
leftValue = `${left}px${safeAreaRight}`;
}
if (bottomValue !== undefined) {
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
/**
* When both top and bottom are explicitly constrained (isFullyConstrained),
* we need to override the height: var(--height) style to allow the
* top/bottom constraint to determine the height.
*
* We only do this when fully constrained because setting height: unset
* when only bottom is set (without explicit top) would result in an
* incorrectly sized popover.
*/
if (isFullyConstrained) {
contentEl.style.setProperty('height', 'unset');
}
}
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
if (arrowEl !== null) {
const didAdjustBounds = results.top !== top || results.left !== left;
/**
@@ -184,12 +196,12 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
const showArrow = shouldShowArrow(side, didAdjustBounds, ev, trigger) && !isFullyConstrained;
if (showArrow) {
arrowEl.style.setProperty('top', `calc(${arrowTop}px + var(--offset-y, 0))`);
arrowEl.style.setProperty('left', `calc(${arrowLeft}px + var(--offset-x, 0))`);
arrowEl.style.setProperty('top', `calc(${arrowTop}px + var(--offset-y, 0px))`);
arrowEl.style.setProperty('left', `calc(${arrowLeft}px + var(--offset-x, 0px))`);
} else {
arrowEl.style.setProperty('display', 'none');
}
}
})
.addAnimation([backdropAnimation, contentAnimation]);
.addAnimation([backdropAnimation, arrowAnimation, contentAnimation]);
};

View File

@@ -137,15 +137,22 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
/**
* When both top and bottom are explicitly constrained (isFullyConstrained),
* we need to override the height: var(--height) style to allow the
* top/bottom constraint to determine the height.
* we need to explicitly calculate the height to ensure the popover
* fits within the safe area boundaries.
*
* We only do this when fully constrained because setting height: unset
* when only bottom is set (without explicit top) would result in an
* incorrectly sized popover.
* Using CSS calc with 100vh minus top and bottom values ensures the
* popover height respects both safe areas. We also override max-height
* to prevent it from interfering with the calculated height.
*/
if (isFullyConstrained) {
contentEl.style.setProperty('height', 'unset');
/**
* Wrap topValue and bottomValue in parentheses to ensure correct
* order of operations in the CSS calc. Without parentheses, the
* safe-area additions would have wrong signs.
*/
const heightCalc = `calc(100vh - (${topValue}) - (${bottomValue}) - var(--offset-y, 0px))`;
contentEl.style.setProperty('height', heightCalc);
contentEl.style.setProperty('max-height', heightCalc);
}
}
})

View File

@@ -927,13 +927,13 @@ export const calculateWindowAdjustment = (
*
* When checkSafeAreaTop is true, the CSS will add safe-area-top to the
* top position, pushing the popover down. Since we don't know the exact
* CSS safe-area value, we use a threshold that accounts for likely
* safe-area sizes. This only triggers when:
* 1. We're already applying safe-area-top (checkSafeAreaTop), and
* 2. The popover is close enough to overflowing that any safe-area
* would push it past the viewport
* CSS safe-area value at runtime, we use a conservative threshold that
* accounts for typical safe-area sizes (usually 40-50px). By checking
* against (safeAreaMargin * 2), we ensure that:
* 1. Any popover close to the viewport boundary gets constrained
* 2. The safe-area CSS variables have room to be applied without overflow
*/
if (checkSafeAreaTop && top + contentHeight > bodyHeight - safeAreaMargin - bodyPadding) {
if (checkSafeAreaTop && top + contentHeight > bodyHeight - safeAreaMargin * 2 - bodyPadding) {
bottom = bodyPadding;
checkSafeAreaBottom = true;
isFullyConstrained = true;
@@ -965,8 +965,13 @@ export const calculateWindowAdjustment = (
/**
* Set a bottom constraint to push the popover up out of the safe-area zone.
* The animation will add the safe-area CSS variable to this value.
*
* We also set isFullyConstrained so that height: unset is applied,
* allowing the bottom constraint to actually take effect (otherwise
* the explicit height would override the bottom constraint).
*/
bottom = bodyPadding;
isFullyConstrained = true;
}
if (top < safeAreaMargin) {
checkSafeAreaTop = true;