@@ -84,7 +84,7 @@ export const createSheetGesture = (
84
84
let offset = 0 ;
85
85
let canDismissBlocksGesture = false ;
86
86
let cachedScrollEl : HTMLElement | null = null ;
87
- let cachedFooterEl : HTMLIonFooterElement | null = null ;
87
+ let cachedFooterEls : HTMLIonFooterElement [ ] | null = null ;
88
88
let cachedFooterYPosition : number | null = null ;
89
89
let currentFooterState : 'moving' | 'stationary' | null = null ;
90
90
const canDismissMaxStep = 0.95 ;
@@ -126,9 +126,9 @@ export const createSheetGesture = (
126
126
* @param newPosition Whether the footer is in a moving or stationary position.
127
127
*/
128
128
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 ) {
132
132
return ;
133
133
}
134
134
}
@@ -137,57 +137,80 @@ export const createSheetGesture = (
137
137
138
138
currentFooterState = newPosition ;
139
139
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
+ } ) ;
151
153
} 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
+ } ) ;
191
214
}
192
215
} ;
193
216
@@ -400,6 +423,14 @@ export const createSheetGesture = (
400
423
* is not scrolled to the top.
401
424
*/
402
425
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' ) ;
403
434
return ;
404
435
}
405
436
0 commit comments