Skip to content

Commit 4f418e6

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

File tree

1 file changed

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

1 file changed

+91
-54
lines changed

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

Lines changed: 91 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,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

Comments
 (0)