@@ -84,7 +84,7 @@ export const createSheetGesture = (
8484 let offset = 0 ;
8585 let canDismissBlocksGesture = false ;
8686 let cachedScrollEl : HTMLElement | null = null ;
87- let cachedFooterEl : HTMLIonFooterElement | null = null ;
87+ let cachedFooterEls : HTMLIonFooterElement [ ] | null = null ;
8888 let cachedFooterYPosition : number | null = null ;
8989 let currentFooterState : 'moving' | 'stationary' | null = null ;
9090 const canDismissMaxStep = 0.95 ;
@@ -126,9 +126,9 @@ export const createSheetGesture = (
126126 * @param newPosition Whether the footer is in a moving or stationary position.
127127 */
128128 const swapFooterPosition = ( newPosition : 'moving' | 'stationary' ) => {
129- if ( ! cachedFooterEl ) {
130- cachedFooterEl = baseEl . querySelector ( 'ion-footer' ) as HTMLIonFooterElement | null ;
131- if ( ! cachedFooterEl ) {
129+ if ( ! cachedFooterEls ) {
130+ cachedFooterEls = Array . from ( baseEl . querySelectorAll ( 'ion-footer' ) ) ;
131+ if ( ! cachedFooterEls . length ) {
132132 return ;
133133 }
134134 }
@@ -137,57 +137,80 @@ export const createSheetGesture = (
137137
138138 currentFooterState = newPosition ;
139139 if ( newPosition === 'stationary' ) {
140- // Reset positioning styles to allow normal document flow
141- cachedFooterEl . classList . remove ( 'modal-footer-moving' ) ;
142- cachedFooterEl . style . removeProperty ( 'position' ) ;
143- cachedFooterEl . style . removeProperty ( 'width' ) ;
144- cachedFooterEl . style . removeProperty ( 'height' ) ;
145- cachedFooterEl . style . removeProperty ( 'top' ) ;
146- cachedFooterEl . style . removeProperty ( 'left' ) ;
147- page ?. style . removeProperty ( 'padding-bottom' ) ;
148-
149- // Move to page
150- page ?. appendChild ( cachedFooterEl ) ;
140+ cachedFooterEls . forEach ( ( cachedFooterEl ) => {
141+ // Reset positioning styles to allow normal document flow
142+ cachedFooterEl . classList . remove ( 'modal-footer-moving' ) ;
143+ cachedFooterEl . style . removeProperty ( 'position' ) ;
144+ cachedFooterEl . style . removeProperty ( 'width' ) ;
145+ cachedFooterEl . style . removeProperty ( 'height' ) ;
146+ cachedFooterEl . style . removeProperty ( 'top' ) ;
147+ cachedFooterEl . style . removeProperty ( 'left' ) ;
148+ page ?. style . removeProperty ( 'padding-bottom' ) ;
149+
150+ // Move to page
151+ page ?. appendChild ( cachedFooterEl ) ;
152+ } ) ;
151153 } else {
152- // Get both the footer and document body positions
153- const cachedFooterElRect = cachedFooterEl . getBoundingClientRect ( ) ;
154- const bodyRect = document . body . getBoundingClientRect ( ) ;
155-
156- // Add padding to the parent element to prevent content from being hidden
157- // when the footer is positioned absolutely. This has to be done before we
158- // make the footer absolutely positioned or we may accidentally cause the
159- // sheet to scroll.
160- const footerHeight = cachedFooterEl . clientHeight ;
161- page ?. style . setProperty ( 'padding-bottom' , `${ footerHeight } px` ) ;
162-
163- // Apply positioning styles to keep footer at bottom
164- cachedFooterEl . classList . add ( 'modal-footer-moving' ) ;
165-
166- // Calculate absolute position relative to body
167- // We need to subtract the body's offsetTop to get true position within document.body
168- const absoluteTop = cachedFooterElRect . top - bodyRect . top ;
169- const absoluteLeft = cachedFooterElRect . left - bodyRect . left ;
170-
171- // Capture the footer's current dimensions and hard code them during the drag
172- cachedFooterEl . style . setProperty ( 'position' , 'absolute' ) ;
173- cachedFooterEl . style . setProperty ( 'width' , `${ cachedFooterEl . clientWidth } px` ) ;
174- cachedFooterEl . style . setProperty ( 'height' , `${ cachedFooterEl . clientHeight } px` ) ;
175- cachedFooterEl . style . setProperty ( 'top' , `${ absoluteTop } px` ) ;
176- cachedFooterEl . style . setProperty ( 'left' , `${ absoluteLeft } px` ) ;
177-
178- // Also cache the footer Y position, which we use to determine if the
179- // sheet has been moved below the footer. When that happens, we need to swap
180- // the position back so it will collapse correctly.
181- cachedFooterYPosition = absoluteTop ;
182- // If there's a toolbar, we need to combine the toolbar height with the footer position
183- // because the toolbar moves with the drag handle, so when it starts overlapping the footer,
184- // we need to account for that.
185- const toolbar = baseEl . querySelector ( 'ion-toolbar' ) as HTMLIonToolbarElement | null ;
186- if ( toolbar ) {
187- cachedFooterYPosition -= toolbar . clientHeight ;
188- }
189-
190- document . body . appendChild ( cachedFooterEl ) ;
154+ let footerHeights = 0 ;
155+ cachedFooterEls . forEach ( ( cachedFooterEl , index ) => {
156+ // Get both the footer and document body positions
157+ const cachedFooterElRect = cachedFooterEl . getBoundingClientRect ( ) ;
158+ const bodyRect = document . body . getBoundingClientRect ( ) ;
159+
160+ // Calculate the total height of all footers
161+ // so we can add padding to the page element
162+ footerHeights += cachedFooterEl . clientHeight ;
163+
164+ // Calculate absolute position relative to body
165+ // We need to subtract the body's offsetTop to get true position within document.body
166+ const absoluteTop = cachedFooterElRect . top - bodyRect . top ;
167+ const absoluteLeft = cachedFooterElRect . left - bodyRect . left ;
168+
169+ // Capture the footer's current dimensions and store them in CSS variables for
170+ // later use when applying absolute positioning.
171+ cachedFooterEl . style . setProperty ( '--pinned-width' , `${ cachedFooterEl . clientWidth } px` ) ;
172+ cachedFooterEl . style . setProperty ( '--pinned-height' , `${ cachedFooterEl . clientHeight } px` ) ;
173+ cachedFooterEl . style . setProperty ( '--pinned-top' , `${ absoluteTop } px` ) ;
174+ cachedFooterEl . style . setProperty ( '--pinned-left' , `${ absoluteLeft } px` ) ;
175+
176+ // Only cache the first footer's Y position
177+ // This is used to determine if the sheet has been moved below the footer
178+ // and needs to be swapped back to stationary so it collapses correctly.
179+ if ( index === 0 ) {
180+ cachedFooterYPosition = absoluteTop ;
181+ // If there's a header, we need to combine the header height with the footer position
182+ // because the header moves with the drag handle, so when it starts overlapping the footer,
183+ // we need to account for that.
184+ const header = baseEl . querySelector ( 'ion-header' ) as HTMLIonHeaderElement | null ;
185+ if ( header ) {
186+ cachedFooterYPosition -= header . clientHeight ;
187+ }
188+ }
189+ } ) ;
190+
191+ // Apply the pinning of styles after we've calculated everything
192+ // so that we don't cause layouts to shift while calculating the footer positions.
193+ // Otherwise, with multiple footers we'll end up capturing the wrong positions.
194+ cachedFooterEls . forEach ( ( cachedFooterEl ) => {
195+ // Add padding to the parent element to prevent content from being hidden
196+ // when the footer is positioned absolutely. This has to be done before we
197+ // make the footer absolutely positioned or we may accidentally cause the
198+ // sheet to scroll.
199+ page ?. style . setProperty ( 'padding-bottom' , `${ footerHeights } px` ) ;
200+
201+ // Apply positioning styles to keep footer at bottom
202+ cachedFooterEl . classList . add ( 'modal-footer-moving' ) ;
203+
204+ // Apply our preserved styles to pin the footer
205+ cachedFooterEl . style . setProperty ( 'position' , 'absolute' ) ;
206+ cachedFooterEl . style . setProperty ( 'width' , 'var(--pinned-width)' ) ;
207+ cachedFooterEl . style . setProperty ( 'height' , 'var(--pinned-height)' ) ;
208+ cachedFooterEl . style . setProperty ( 'top' , 'var(--pinned-top)' ) ;
209+ cachedFooterEl . style . setProperty ( 'left' , 'var(--pinned-left)' ) ;
210+
211+ // Move the element to the body when everything else is done
212+ document . body . appendChild ( cachedFooterEl ) ;
213+ } ) ;
191214 }
192215 } ;
193216
@@ -400,6 +423,14 @@ export const createSheetGesture = (
400423 * is not scrolled to the top.
401424 */
402425 if ( ! expandToScroll && detail . deltaY <= 0 && cachedScrollEl && cachedScrollEl . scrollTop > 0 ) {
426+ /**
427+ * If expand to scroll is disabled, we need to make sure we swap the footer position
428+ * back to stationary so that it will collapse correctly if the modal is dismissed without
429+ * dragging (e.g. through a dismiss button).
430+ * This can cause issues if the user has a modal with content that can be dragged, as we'll
431+ * swap to moving on drag and if we don't swap back here then the footer will get stuck.
432+ */
433+ swapFooterPosition ( 'stationary' ) ;
403434 return ;
404435 }
405436
0 commit comments