@@ -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,84 @@ 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+ // When we are moving the pinning the footers, we need to reverse the order
155+ // so that the last footer is the first one pinned. This is because, since they
156+ // are currently positioned relatively, pinning them from the top will cause the
157+ // bottom ones to move up, making them all overlap on top.
158+ let footerHeights = 0 ;
159+ cachedFooterEls . forEach ( ( cachedFooterEl , index ) => {
160+ // Get both the footer and document body positions
161+ const cachedFooterElRect = cachedFooterEl . getBoundingClientRect ( ) ;
162+ const bodyRect = document . body . getBoundingClientRect ( ) ;
163+
164+ // Calculate the total height of all footers
165+ // so we can add padding to the page element
166+ footerHeights += cachedFooterEl . clientHeight ;
167+
168+ // Calculate absolute position relative to body
169+ // We need to subtract the body's offsetTop to get true position within document.body
170+ const absoluteTop = cachedFooterElRect . top - bodyRect . top ;
171+ const absoluteLeft = cachedFooterElRect . left - bodyRect . left ;
172+
173+ // Capture the footer's current dimensions and store them in CSS variables for
174+ // later use when applying absolute positioning.
175+ cachedFooterEl . style . setProperty ( '--pinned-width' , `${ cachedFooterEl . clientWidth } px` ) ;
176+ cachedFooterEl . style . setProperty ( '--pinned-height' , `${ cachedFooterEl . clientHeight } px` ) ;
177+ cachedFooterEl . style . setProperty ( '--pinned-top' , `${ absoluteTop } px` ) ;
178+ cachedFooterEl . style . setProperty ( '--pinned-left' , `${ absoluteLeft } px` ) ;
179+
180+ // Only cache the first footer's Y position
181+ // This is used to determine if the sheet has been moved below the footer
182+ // and needs to be swapped back to stationary so it collapses correctly.
183+ if ( index === 0 ) {
184+ cachedFooterYPosition = absoluteTop ;
185+ // If there's a toolbar, we need to combine the toolbar height with the footer position
186+ // because the toolbar moves with the drag handle, so when it starts overlapping the footer,
187+ // we need to account for that.
188+ const toolbar = baseEl . querySelector ( 'ion-toolbar' ) as HTMLIonToolbarElement | null ;
189+ if ( toolbar ) {
190+ cachedFooterYPosition -= toolbar . clientHeight ;
191+ }
192+ }
193+ } ) ;
194+
195+ // Apply the pinning of styles after we've calculated everything
196+ // so that we don't cause layouts to shift while calculating the footer positions.
197+ // Otherwise, with multiple footers we'll end up capturing the wrong positions.
198+ cachedFooterEls . forEach ( ( cachedFooterEl ) => {
199+ // Add padding to the parent element to prevent content from being hidden
200+ // when the footer is positioned absolutely. This has to be done before we
201+ // make the footer absolutely positioned or we may accidentally cause the
202+ // sheet to scroll.
203+ page ?. style . setProperty ( 'padding-bottom' , `${ footerHeights } px` ) ;
204+
205+ // Apply positioning styles to keep footer at bottom
206+ cachedFooterEl . classList . add ( 'modal-footer-moving' ) ;
207+
208+ // Apply our preserved styles to pin the footer
209+ cachedFooterEl . style . setProperty ( 'position' , 'absolute' ) ;
210+ cachedFooterEl . style . setProperty ( 'width' , 'var(--pinned-width)' ) ;
211+ cachedFooterEl . style . setProperty ( 'height' , 'var(--pinned-height)' ) ;
212+ cachedFooterEl . style . setProperty ( 'top' , 'var(--pinned-top)' ) ;
213+ cachedFooterEl . style . setProperty ( 'left' , 'var(--pinned-left)' ) ;
214+
215+ // Move the element to the body when everything else is done
216+ document . body . appendChild ( cachedFooterEl ) ;
217+ } ) ;
191218 }
192219 } ;
193220
@@ -400,6 +427,16 @@ export const createSheetGesture = (
400427 * is not scrolled to the top.
401428 */
402429 if ( ! expandToScroll && detail . deltaY <= 0 && cachedScrollEl && cachedScrollEl . scrollTop > 0 ) {
430+ /**
431+ * If expand to scroll is disabled, we need to make sure we swap the footer position
432+ * back to stationary so that it will collapse correctly if the modal is dismissed without
433+ * dragging (e.g. through a dismiss button).
434+ * This can cause issues if the user has a modal with content that can be dragged, as we'll
435+ * swap to moving on drag and if we don't swap back here then the footer will get stuck.
436+ */
437+ if ( ! expandToScroll ) {
438+ swapFooterPosition ( 'stationary' ) ;
439+ }
403440 return ;
404441 }
405442
0 commit comments