diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index a3f548879f8..219769e4879 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -84,7 +84,7 @@ export const createSheetGesture = ( let offset = 0; let canDismissBlocksGesture = false; let cachedScrollEl: HTMLElement | null = null; - let cachedFooterEl: HTMLIonFooterElement | null = null; + let cachedFooterEls: HTMLIonFooterElement[] | null = null; let cachedFooterYPosition: number | null = null; let currentFooterState: 'moving' | 'stationary' | null = null; const canDismissMaxStep = 0.95; @@ -126,9 +126,9 @@ export const createSheetGesture = ( * @param newPosition Whether the footer is in a moving or stationary position. */ const swapFooterPosition = (newPosition: 'moving' | 'stationary') => { - if (!cachedFooterEl) { - cachedFooterEl = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null; - if (!cachedFooterEl) { + if (!cachedFooterEls) { + cachedFooterEls = Array.from(baseEl.querySelectorAll('ion-footer')); + if (!cachedFooterEls.length) { return; } } @@ -137,57 +137,80 @@ export const createSheetGesture = ( currentFooterState = newPosition; if (newPosition === 'stationary') { - // Reset positioning styles to allow normal document flow - cachedFooterEl.classList.remove('modal-footer-moving'); - cachedFooterEl.style.removeProperty('position'); - cachedFooterEl.style.removeProperty('width'); - cachedFooterEl.style.removeProperty('height'); - cachedFooterEl.style.removeProperty('top'); - cachedFooterEl.style.removeProperty('left'); - page?.style.removeProperty('padding-bottom'); - - // Move to page - page?.appendChild(cachedFooterEl); + cachedFooterEls.forEach((cachedFooterEl) => { + // Reset positioning styles to allow normal document flow + cachedFooterEl.classList.remove('modal-footer-moving'); + cachedFooterEl.style.removeProperty('position'); + cachedFooterEl.style.removeProperty('width'); + cachedFooterEl.style.removeProperty('height'); + cachedFooterEl.style.removeProperty('top'); + cachedFooterEl.style.removeProperty('left'); + page?.style.removeProperty('padding-bottom'); + + // Move to page + page?.appendChild(cachedFooterEl); + }); } else { - // Get both the footer and document body positions - const cachedFooterElRect = cachedFooterEl.getBoundingClientRect(); - const bodyRect = document.body.getBoundingClientRect(); - - // Add padding to the parent element to prevent content from being hidden - // when the footer is positioned absolutely. This has to be done before we - // make the footer absolutely positioned or we may accidentally cause the - // sheet to scroll. - const footerHeight = cachedFooterEl.clientHeight; - page?.style.setProperty('padding-bottom', `${footerHeight}px`); - - // Apply positioning styles to keep footer at bottom - cachedFooterEl.classList.add('modal-footer-moving'); - - // Calculate absolute position relative to body - // We need to subtract the body's offsetTop to get true position within document.body - const absoluteTop = cachedFooterElRect.top - bodyRect.top; - const absoluteLeft = cachedFooterElRect.left - bodyRect.left; - - // Capture the footer's current dimensions and hard code them during the drag - cachedFooterEl.style.setProperty('position', 'absolute'); - cachedFooterEl.style.setProperty('width', `${cachedFooterEl.clientWidth}px`); - cachedFooterEl.style.setProperty('height', `${cachedFooterEl.clientHeight}px`); - cachedFooterEl.style.setProperty('top', `${absoluteTop}px`); - cachedFooterEl.style.setProperty('left', `${absoluteLeft}px`); - - // Also cache the footer Y position, which we use to determine if the - // sheet has been moved below the footer. When that happens, we need to swap - // the position back so it will collapse correctly. - cachedFooterYPosition = absoluteTop; - // If there's a toolbar, we need to combine the toolbar height with the footer position - // because the toolbar moves with the drag handle, so when it starts overlapping the footer, - // we need to account for that. - const toolbar = baseEl.querySelector('ion-toolbar') as HTMLIonToolbarElement | null; - if (toolbar) { - cachedFooterYPosition -= toolbar.clientHeight; - } - - document.body.appendChild(cachedFooterEl); + let footerHeights = 0; + cachedFooterEls.forEach((cachedFooterEl, index) => { + // Get both the footer and document body positions + const cachedFooterElRect = cachedFooterEl.getBoundingClientRect(); + const bodyRect = document.body.getBoundingClientRect(); + + // Calculate the total height of all footers + // so we can add padding to the page element + footerHeights += cachedFooterEl.clientHeight; + + // Calculate absolute position relative to body + // We need to subtract the body's offsetTop to get true position within document.body + const absoluteTop = cachedFooterElRect.top - bodyRect.top; + const absoluteLeft = cachedFooterElRect.left - bodyRect.left; + + // Capture the footer's current dimensions and store them in CSS variables for + // later use when applying absolute positioning. + cachedFooterEl.style.setProperty('--pinned-width', `${cachedFooterEl.clientWidth}px`); + cachedFooterEl.style.setProperty('--pinned-height', `${cachedFooterEl.clientHeight}px`); + cachedFooterEl.style.setProperty('--pinned-top', `${absoluteTop}px`); + cachedFooterEl.style.setProperty('--pinned-left', `${absoluteLeft}px`); + + // Only cache the first footer's Y position + // This is used to determine if the sheet has been moved below the footer + // and needs to be swapped back to stationary so it collapses correctly. + if (index === 0) { + cachedFooterYPosition = absoluteTop; + // If there's a header, we need to combine the header height with the footer position + // because the header moves with the drag handle, so when it starts overlapping the footer, + // we need to account for that. + const header = baseEl.querySelector('ion-header') as HTMLIonHeaderElement | null; + if (header) { + cachedFooterYPosition -= header.clientHeight; + } + } + }); + + // Apply the pinning of styles after we've calculated everything + // so that we don't cause layouts to shift while calculating the footer positions. + // Otherwise, with multiple footers we'll end up capturing the wrong positions. + cachedFooterEls.forEach((cachedFooterEl) => { + // Add padding to the parent element to prevent content from being hidden + // when the footer is positioned absolutely. This has to be done before we + // make the footer absolutely positioned or we may accidentally cause the + // sheet to scroll. + page?.style.setProperty('padding-bottom', `${footerHeights}px`); + + // Apply positioning styles to keep footer at bottom + cachedFooterEl.classList.add('modal-footer-moving'); + + // Apply our preserved styles to pin the footer + cachedFooterEl.style.setProperty('position', 'absolute'); + cachedFooterEl.style.setProperty('width', 'var(--pinned-width)'); + cachedFooterEl.style.setProperty('height', 'var(--pinned-height)'); + cachedFooterEl.style.setProperty('top', 'var(--pinned-top)'); + cachedFooterEl.style.setProperty('left', 'var(--pinned-left)'); + + // Move the element to the body when everything else is done + document.body.appendChild(cachedFooterEl); + }); } }; @@ -400,6 +423,14 @@ export const createSheetGesture = ( * is not scrolled to the top. */ if (!expandToScroll && detail.deltaY <= 0 && cachedScrollEl && cachedScrollEl.scrollTop > 0) { + /** + * If expand to scroll is disabled, we need to make sure we swap the footer position + * back to stationary so that it will collapse correctly if the modal is dismissed without + * dragging (e.g. through a dismiss button). + * This can cause issues if the user has a modal with content that can be dragged, as we'll + * swap to moving on drag and if we don't swap back here then the footer will get stuck. + */ + swapFooterPosition('stationary'); return; }