@@ -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,84 @@ 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
+ // 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
+ } ) ;
191
218
}
192
219
} ;
193
220
@@ -400,6 +427,16 @@ export const createSheetGesture = (
400
427
* is not scrolled to the top.
401
428
*/
402
429
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
+ }
403
440
return ;
404
441
}
405
442
0 commit comments