Skip to content

Commit 52b544d

Browse files
committed
fix(modal): fixing footer pinnning after dragging content, fixing multiple footer support
1 parent 5ca8fc8 commit 52b544d

File tree

1 file changed

+87
-54
lines changed
  • core/src/components/modal/gestures

1 file changed

+87
-54
lines changed

core/src/components/modal/gestures/sheet.ts

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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 toolbar, we need to combine the toolbar height with the footer position
182+
// because the toolbar moves with the drag handle, so when it starts overlapping the footer,
183+
// we need to account for that.
184+
const toolbar = baseEl.querySelector('ion-toolbar') as HTMLIonToolbarElement | null;
185+
if (toolbar) {
186+
cachedFooterYPosition -= toolbar.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,16 @@ 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+
if (!expandToScroll) {
434+
swapFooterPosition('stationary');
435+
}
403436
return;
404437
}
405438

0 commit comments

Comments
 (0)