@@ -93,8 +93,37 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
9393 arrowHeight
9494 ) ;
9595
96+ /**
97+ * Safe area CSS variable adjustments.
98+ * When the popover is positioned near an edge, we add the corresponding
99+ * safe-area inset to ensure the popover doesn't overlap with system UI
100+ * (status bars, home indicators, navigation bars on Android API 36+, etc.)
101+ */
102+ const safeAreaTop = ' + var(--ion-safe-area-top, 0px)' ;
103+ const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0px)' ;
104+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)' ;
105+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)' ;
106+
107+ let topValue = `${ top } px` ;
108+ let bottomValue = bottom !== undefined ? `${ bottom } px` : undefined ;
109+ let leftValue = `${ left } px` ;
110+
111+ if ( checkSafeAreaTop ) {
112+ topValue = `${ top } px${ safeAreaTop } ` ;
113+ }
114+ if ( checkSafeAreaBottom && bottomValue !== undefined ) {
115+ bottomValue = `${ bottom } px${ safeAreaBottom } ` ;
116+ }
117+ if ( checkSafeAreaLeft ) {
118+ leftValue = `${ left } px${ safeAreaLeft } ` ;
119+ }
120+ if ( checkSafeAreaRight ) {
121+ leftValue = `${ left } px${ safeAreaRight } ` ;
122+ }
123+
96124 const baseAnimation = createAnimation ( ) ;
97125 const backdropAnimation = createAnimation ( ) ;
126+ const arrowAnimation = createAnimation ( ) ;
98127 const contentAnimation = createAnimation ( ) ;
99128
100129 backdropAnimation
@@ -109,71 +138,54 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
109138 // 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.
110139 // To get around this, instead of animating the wrapper, animate both the arrow and content.
111140 // https://bugs.chromium.org/p/chromium/issues/detail?id=1148826
112- contentAnimation
113- . addElement ( root . querySelector ( '.popover-arrow' ) ! )
114- . addElement ( root . querySelector ( '.popover-content' ) ! )
115- . fromTo ( 'opacity' , 0.01 , 1 ) ;
116141 // TODO(FW-4376) Ensure that arrow also blurs when translucent
142+ if ( arrowEl !== null ) {
143+ arrowAnimation . addElement ( arrowEl ) . fromTo ( 'opacity' , 0.01 , 1 ) ;
144+ }
117145
118- return baseAnimation
119- . easing ( 'ease' )
120- . duration ( 100 )
146+ contentAnimation
147+ . addElement ( contentEl )
121148 . beforeAddWrite ( ( ) => {
122- if ( size === 'cover' ) {
123- baseEl . style . setProperty ( '--width' , `${ contentWidth } px` ) ;
124- }
125-
126- if ( addPopoverBottomClass ) {
127- baseEl . classList . add ( 'popover-bottom' ) ;
128- }
129-
130- /**
131- * Safe area CSS variable adjustments.
132- * When the popover is positioned near an edge, we add the corresponding
133- * safe-area inset to ensure the popover doesn't overlap with system UI
134- * (status bars, home indicators, navigation bars on Android API 36+, etc.)
135- */
136- const safeAreaTop = ' + var(--ion-safe-area-top, 0)' ;
137- const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)' ;
138- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)' ;
139- const safeAreaRight = ' - var(--ion-safe-area-right, 0)' ;
140-
141- let topValue = `${ top } px` ;
142- let bottomValue = bottom !== undefined ? `${ bottom } px` : undefined ;
143- let leftValue = `${ left } px` ;
144-
145- if ( checkSafeAreaTop ) {
146- topValue = `${ top } px${ safeAreaTop } ` ;
147- }
148- if ( checkSafeAreaBottom && bottomValue !== undefined ) {
149- bottomValue = `${ bottom } px${ safeAreaBottom } ` ;
150- }
151- if ( checkSafeAreaLeft ) {
152- leftValue = `${ left } px${ safeAreaLeft } ` ;
153- }
154- if ( checkSafeAreaRight ) {
155- leftValue = `${ left } px${ safeAreaRight } ` ;
156- }
149+ contentEl . style . setProperty ( 'top' , `calc(${ topValue } + var(--offset-y, 0px))` ) ;
150+ contentEl . style . setProperty ( 'left' , `calc(${ leftValue } + var(--offset-x, 0px))` ) ;
151+ contentEl . style . setProperty ( 'transform-origin' , `${ originY } ${ originX } ` ) ;
157152
158153 if ( bottomValue !== undefined ) {
159154 contentEl . style . setProperty ( 'bottom' , `calc(${ bottomValue } )` ) ;
160155 /**
161156 * When both top and bottom are explicitly constrained (isFullyConstrained),
162- * we need to override the height: var(--height) style to allow the
163- * top/bottom constraint to determine the height .
157+ * we need to explicitly calculate the height to ensure the popover
158+ * fits within the safe area boundaries .
164159 *
165- * We only do this when fully constrained because setting height: unset
166- * when only bottom is set (without explicit top) would result in an
167- * incorrectly sized popover .
160+ * Using CSS calc with 100vh minus top and bottom values ensures the
161+ * popover height respects both safe areas. We also override max-height
162+ * to prevent it from interfering with the calculated height .
168163 */
169164 if ( isFullyConstrained ) {
170- contentEl . style . setProperty ( 'height' , 'unset' ) ;
165+ /**
166+ * Wrap topValue and bottomValue in parentheses to ensure correct
167+ * order of operations in the CSS calc. Without parentheses, the
168+ * safe-area additions would have wrong signs.
169+ */
170+ const heightCalc = `calc(100vh - (${ topValue } ) - (${ bottomValue } ) - var(--offset-y, 0px))` ;
171+ contentEl . style . setProperty ( 'height' , heightCalc ) ;
172+ contentEl . style . setProperty ( 'max-height' , heightCalc ) ;
171173 }
172174 }
175+ } )
176+ . fromTo ( 'opacity' , 0.01 , 1 ) ;
173177
174- contentEl . style . setProperty ( 'top' , `calc(${ topValue } + var(--offset-y, 0))` ) ;
175- contentEl . style . setProperty ( 'left' , `calc(${ leftValue } + var(--offset-x, 0))` ) ;
176- contentEl . style . setProperty ( 'transform-origin' , `${ originY } ${ originX } ` ) ;
178+ return baseAnimation
179+ . easing ( 'ease' )
180+ . duration ( 100 )
181+ . beforeAddWrite ( ( ) => {
182+ if ( size === 'cover' ) {
183+ baseEl . style . setProperty ( '--width' , `${ contentWidth } px` ) ;
184+ }
185+
186+ if ( addPopoverBottomClass ) {
187+ baseEl . classList . add ( 'popover-bottom' ) ;
188+ }
177189
178190 if ( arrowEl !== null ) {
179191 const didAdjustBounds = results . top !== top || results . left !== left ;
@@ -184,12 +196,12 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
184196 const showArrow = shouldShowArrow ( side , didAdjustBounds , ev , trigger ) && ! isFullyConstrained ;
185197
186198 if ( showArrow ) {
187- arrowEl . style . setProperty ( 'top' , `calc(${ arrowTop } px + var(--offset-y, 0 ))` ) ;
188- arrowEl . style . setProperty ( 'left' , `calc(${ arrowLeft } px + var(--offset-x, 0 ))` ) ;
199+ arrowEl . style . setProperty ( 'top' , `calc(${ arrowTop } px + var(--offset-y, 0px ))` ) ;
200+ arrowEl . style . setProperty ( 'left' , `calc(${ arrowLeft } px + var(--offset-x, 0px ))` ) ;
189201 } else {
190202 arrowEl . style . setProperty ( 'display' , 'none' ) ;
191203 }
192204 }
193205 } )
194- . addAnimation ( [ backdropAnimation , contentAnimation ] ) ;
206+ . addAnimation ( [ backdropAnimation , arrowAnimation , contentAnimation ] ) ;
195207} ;
0 commit comments