Skip to content

Commit 071b414

Browse files
authored
fix(modal): reset footer positioning after content drag and multi-footer support (#30470)
Issue number: resolves #30468 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Currently, if you use pointer events to drag the content of a sheet modal with `expandToScroll` disabled and have you have a footer and a dismiss button, then you use the dismiss button to close the modal, the footer will be stuck in its pinned position at the bottom of the screen. Additionally, if you have multiple footers, only one of them properly gets pinned and unpinned. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - We now move footers back to their stationary position when we finish our drag event on modal content - We support pinning and unpinning multiple footers at the same time now ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `8.6.1-dev.11749575087.1b86eb67`
1 parent 5ca8fc8 commit 071b414

File tree

1 file changed

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

1 file changed

+85
-54
lines changed

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

Lines changed: 85 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 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+
});
191214
}
192215
};
193216

@@ -400,6 +423,14 @@ 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+
swapFooterPosition('stationary');
403434
return;
404435
}
405436

0 commit comments

Comments
 (0)