mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 23:58:13 +08:00
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`
This commit is contained in:
@ -84,7 +84,7 @@ export const createSheetGesture = (
|
|||||||
let offset = 0;
|
let offset = 0;
|
||||||
let canDismissBlocksGesture = false;
|
let canDismissBlocksGesture = false;
|
||||||
let cachedScrollEl: HTMLElement | null = null;
|
let cachedScrollEl: HTMLElement | null = null;
|
||||||
let cachedFooterEl: HTMLIonFooterElement | null = null;
|
let cachedFooterEls: HTMLIonFooterElement[] | null = null;
|
||||||
let cachedFooterYPosition: number | null = null;
|
let cachedFooterYPosition: number | null = null;
|
||||||
let currentFooterState: 'moving' | 'stationary' | null = null;
|
let currentFooterState: 'moving' | 'stationary' | null = null;
|
||||||
const canDismissMaxStep = 0.95;
|
const canDismissMaxStep = 0.95;
|
||||||
@ -126,9 +126,9 @@ export const createSheetGesture = (
|
|||||||
* @param newPosition Whether the footer is in a moving or stationary position.
|
* @param newPosition Whether the footer is in a moving or stationary position.
|
||||||
*/
|
*/
|
||||||
const swapFooterPosition = (newPosition: 'moving' | 'stationary') => {
|
const swapFooterPosition = (newPosition: 'moving' | 'stationary') => {
|
||||||
if (!cachedFooterEl) {
|
if (!cachedFooterEls) {
|
||||||
cachedFooterEl = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null;
|
cachedFooterEls = Array.from(baseEl.querySelectorAll('ion-footer'));
|
||||||
if (!cachedFooterEl) {
|
if (!cachedFooterEls.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,6 +137,7 @@ export const createSheetGesture = (
|
|||||||
|
|
||||||
currentFooterState = newPosition;
|
currentFooterState = newPosition;
|
||||||
if (newPosition === 'stationary') {
|
if (newPosition === 'stationary') {
|
||||||
|
cachedFooterEls.forEach((cachedFooterEl) => {
|
||||||
// Reset positioning styles to allow normal document flow
|
// Reset positioning styles to allow normal document flow
|
||||||
cachedFooterEl.classList.remove('modal-footer-moving');
|
cachedFooterEl.classList.remove('modal-footer-moving');
|
||||||
cachedFooterEl.style.removeProperty('position');
|
cachedFooterEl.style.removeProperty('position');
|
||||||
@ -148,46 +149,68 @@ export const createSheetGesture = (
|
|||||||
|
|
||||||
// Move to page
|
// Move to page
|
||||||
page?.appendChild(cachedFooterEl);
|
page?.appendChild(cachedFooterEl);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
let footerHeights = 0;
|
||||||
|
cachedFooterEls.forEach((cachedFooterEl, index) => {
|
||||||
// Get both the footer and document body positions
|
// Get both the footer and document body positions
|
||||||
const cachedFooterElRect = cachedFooterEl.getBoundingClientRect();
|
const cachedFooterElRect = cachedFooterEl.getBoundingClientRect();
|
||||||
const bodyRect = document.body.getBoundingClientRect();
|
const bodyRect = document.body.getBoundingClientRect();
|
||||||
|
|
||||||
// Add padding to the parent element to prevent content from being hidden
|
// Calculate the total height of all footers
|
||||||
// when the footer is positioned absolutely. This has to be done before we
|
// so we can add padding to the page element
|
||||||
// make the footer absolutely positioned or we may accidentally cause the
|
footerHeights += cachedFooterEl.clientHeight;
|
||||||
// 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
|
// Calculate absolute position relative to body
|
||||||
// We need to subtract the body's offsetTop to get true position within document.body
|
// We need to subtract the body's offsetTop to get true position within document.body
|
||||||
const absoluteTop = cachedFooterElRect.top - bodyRect.top;
|
const absoluteTop = cachedFooterElRect.top - bodyRect.top;
|
||||||
const absoluteLeft = cachedFooterElRect.left - bodyRect.left;
|
const absoluteLeft = cachedFooterElRect.left - bodyRect.left;
|
||||||
|
|
||||||
// Capture the footer's current dimensions and hard code them during the drag
|
// Capture the footer's current dimensions and store them in CSS variables for
|
||||||
cachedFooterEl.style.setProperty('position', 'absolute');
|
// later use when applying absolute positioning.
|
||||||
cachedFooterEl.style.setProperty('width', `${cachedFooterEl.clientWidth}px`);
|
cachedFooterEl.style.setProperty('--pinned-width', `${cachedFooterEl.clientWidth}px`);
|
||||||
cachedFooterEl.style.setProperty('height', `${cachedFooterEl.clientHeight}px`);
|
cachedFooterEl.style.setProperty('--pinned-height', `${cachedFooterEl.clientHeight}px`);
|
||||||
cachedFooterEl.style.setProperty('top', `${absoluteTop}px`);
|
cachedFooterEl.style.setProperty('--pinned-top', `${absoluteTop}px`);
|
||||||
cachedFooterEl.style.setProperty('left', `${absoluteLeft}px`);
|
cachedFooterEl.style.setProperty('--pinned-left', `${absoluteLeft}px`);
|
||||||
|
|
||||||
// Also cache the footer Y position, which we use to determine if the
|
// Only cache the first footer's Y position
|
||||||
// sheet has been moved below the footer. When that happens, we need to swap
|
// This is used to determine if the sheet has been moved below the footer
|
||||||
// the position back so it will collapse correctly.
|
// and needs to be swapped back to stationary so it collapses correctly.
|
||||||
|
if (index === 0) {
|
||||||
cachedFooterYPosition = absoluteTop;
|
cachedFooterYPosition = absoluteTop;
|
||||||
// If there's a toolbar, we need to combine the toolbar height with the footer position
|
// If there's a header, we need to combine the header height with the footer position
|
||||||
// because the toolbar moves with the drag handle, so when it starts overlapping the footer,
|
// because the header moves with the drag handle, so when it starts overlapping the footer,
|
||||||
// we need to account for that.
|
// we need to account for that.
|
||||||
const toolbar = baseEl.querySelector('ion-toolbar') as HTMLIonToolbarElement | null;
|
const header = baseEl.querySelector('ion-header') as HTMLIonHeaderElement | null;
|
||||||
if (toolbar) {
|
if (header) {
|
||||||
cachedFooterYPosition -= toolbar.clientHeight;
|
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);
|
document.body.appendChild(cachedFooterEl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -400,6 +423,14 @@ export const createSheetGesture = (
|
|||||||
* is not scrolled to the top.
|
* is not scrolled to the top.
|
||||||
*/
|
*/
|
||||||
if (!expandToScroll && detail.deltaY <= 0 && cachedScrollEl && cachedScrollEl.scrollTop > 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user