mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 15:51:16 +08:00
fix(modal): dismiss child modals when parent is dismissed (#30540)
Issue number: resolves #30389 --------- <!-- 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, when a child modal is present and a parent modal is somehow dismissed, the child modal stays open. This can cause issues in some frameworks like React and Angular, where this cuts the connection to the child modal and it can no longer be dismissed programmatically. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> This change enables modals to identify their children and close the children when they're closed. This prevents orphaned modals that can cause a poor UX. Note: This fix is only for inline modals, which is the biggest cause of the above issue. ## 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. --> [Relevant test page](https://ionic-framework-git-fw-6597-ionic1.vercel.app/src/components/modal/test/inline) **Current dev build**: ``` 8.6.5-dev.11752242329.17d249a3 ```
This commit is contained in:
@ -784,6 +784,13 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
*/
|
*/
|
||||||
const unlock = await this.lockController.lock();
|
const unlock = await this.lockController.lock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss all child modals. This is especially important in
|
||||||
|
* Angular and React because it's possible to lose control of a child
|
||||||
|
* modal when the parent modal is dismissed.
|
||||||
|
*/
|
||||||
|
await this.dismissNestedModals();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a canDismiss handler is responsible
|
* If a canDismiss handler is responsible
|
||||||
* for calling the dismiss method, we should
|
* for calling the dismiss method, we should
|
||||||
@ -1115,6 +1122,34 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the slot changes, we need to find all the modals in the slot
|
||||||
|
* and set the data-parent-ion-modal attribute on them so we can find them
|
||||||
|
* and dismiss them when we get dismissed.
|
||||||
|
* We need to do it this way because when a modal is opened, it's moved to
|
||||||
|
* the end of the body and is no longer an actual child of the modal.
|
||||||
|
*/
|
||||||
|
private onSlotChange = ({ target }: Event) => {
|
||||||
|
const slot = target as HTMLSlotElement;
|
||||||
|
slot.assignedElements().forEach((el) => {
|
||||||
|
el.querySelectorAll('ion-modal').forEach((childModal) => {
|
||||||
|
// We don't need to write to the DOM if the modal is already tagged
|
||||||
|
// If this is a deeply nested modal, this effect should cascade so we don't
|
||||||
|
// need to worry about another modal claiming the same child.
|
||||||
|
if (childModal.getAttribute('data-parent-ion-modal') === null) {
|
||||||
|
childModal.setAttribute('data-parent-ion-modal', this.el.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private async dismissNestedModals(): Promise<void> {
|
||||||
|
const nestedModals = document.querySelectorAll(`ion-modal[data-parent-ion-modal="${this.el.id}"]`);
|
||||||
|
nestedModals?.forEach(async (modal) => {
|
||||||
|
await (modal as HTMLIonModalElement).dismiss(undefined, 'parent-dismissed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
handle,
|
handle,
|
||||||
@ -1192,7 +1227,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
ref={(el) => (this.dragHandleEl = el)}
|
ref={(el) => (this.dragHandleEl = el)}
|
||||||
></button>
|
></button>
|
||||||
)}
|
)}
|
||||||
<slot></slot>
|
<slot onSlotchange={this.onSlotChange}></slot>
|
||||||
</div>
|
</div>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,29 +24,76 @@
|
|||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<button id="open-inline-modal" onclick="openModal(event)">Open Modal</button>
|
<button id="open-inline-modal" onclick="openModal(event)">Open Modal</button>
|
||||||
|
|
||||||
<ion-modal swipe-to-close="true">
|
<div id="modal-container">
|
||||||
<ion-header>
|
<ion-modal swipe-to-close="true">
|
||||||
<ion-toolbar>
|
<ion-header>
|
||||||
<ion-title> Modal </ion-title>
|
<ion-toolbar>
|
||||||
</ion-toolbar>
|
<ion-title> Modal </ion-title>
|
||||||
</ion-header>
|
</ion-toolbar>
|
||||||
<ion-content class="ion-padding"> This is my inline modal content! </ion-content>
|
</ion-header>
|
||||||
</ion-modal>
|
<ion-content class="ion-padding">
|
||||||
|
<p>This is my inline modal content!</p>
|
||||||
|
<button id="open-child-modal" onclick="openChildModal(event)">Open Child Modal</button>
|
||||||
|
|
||||||
|
<ion-modal id="child-modal" swipe-to-close="true">
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Child Modal</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<p>This is the child modal content!</p>
|
||||||
|
<p>When the parent modal is dismissed, this child modal should also be dismissed automatically.</p>
|
||||||
|
<button id="dismiss-parent" onclick="dismissParent(event)">Dismiss Parent Modal</button>
|
||||||
|
<button id="dismiss-child" onclick="dismissChild(event)">Dismiss Child Modal</button>
|
||||||
|
</ion-content>
|
||||||
|
</ion-modal>
|
||||||
|
</ion-content>
|
||||||
|
</ion-modal>
|
||||||
|
</div>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</div>
|
</div>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const modal = document.querySelector('ion-modal');
|
const modal = document.querySelector('ion-modal');
|
||||||
|
const childModal = document.querySelector('#child-modal');
|
||||||
|
|
||||||
modal.presentingElement = document.querySelector('.ion-page');
|
modal.presentingElement = document.querySelector('.ion-page');
|
||||||
|
childModal.presentingElement = modal;
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
modal.isOpen = true;
|
modal.isOpen = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openChildModal = () => {
|
||||||
|
childModal.isOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissParent = () => {
|
||||||
|
modal.isOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissChild = () => {
|
||||||
|
childModal.isOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
modal.addEventListener('didDismiss', () => {
|
modal.addEventListener('didDismiss', () => {
|
||||||
modal.isOpen = false;
|
modal.isOpen = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
childModal.addEventListener('didDismiss', () => {
|
||||||
|
childModal.isOpen = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners to demonstrate the new functionality
|
||||||
|
modal.addEventListener('ionModalDidDismiss', (event) => {
|
||||||
|
console.log('Parent modal dismissed with role:', event.detail.role);
|
||||||
|
});
|
||||||
|
|
||||||
|
childModal.addEventListener('ionModalDidDismiss', (event) => {
|
||||||
|
console.log('Child modal dismissed with role:', event.detail.role);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
await page.goto('/src/components/modal/test/inline', config);
|
await page.goto('/src/components/modal/test/inline', config);
|
||||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||||
const modal = page.locator('ion-modal');
|
const modal = page.locator('ion-modal').first();
|
||||||
|
|
||||||
await page.click('#open-inline-modal');
|
await page.click('#open-inline-modal');
|
||||||
|
|
||||||
@ -22,6 +22,67 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
await expect(modal).toBeHidden();
|
await expect(modal).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should dismiss child modals when parent modal is dismissed', async ({ page }) => {
|
||||||
|
await page.goto('/src/components/modal/test/inline', config);
|
||||||
|
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||||
|
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||||
|
|
||||||
|
const parentModal = page.locator('ion-modal').first();
|
||||||
|
const childModal = page.locator('#child-modal');
|
||||||
|
|
||||||
|
// Open the parent modal
|
||||||
|
await page.click('#open-inline-modal');
|
||||||
|
await ionModalDidPresent.next();
|
||||||
|
await expect(parentModal).toBeVisible();
|
||||||
|
|
||||||
|
// Open the child modal
|
||||||
|
await page.click('#open-child-modal');
|
||||||
|
await ionModalDidPresent.next();
|
||||||
|
await expect(childModal).toBeVisible();
|
||||||
|
|
||||||
|
// Both modals should be visible
|
||||||
|
await expect(parentModal).toBeVisible();
|
||||||
|
await expect(childModal).toBeVisible();
|
||||||
|
|
||||||
|
// Dismiss the parent modal
|
||||||
|
await page.click('#dismiss-parent');
|
||||||
|
|
||||||
|
// Wait for both modals to be dismissed
|
||||||
|
await ionModalDidDismiss.next(); // child modal dismissed
|
||||||
|
await ionModalDidDismiss.next(); // parent modal dismissed
|
||||||
|
|
||||||
|
// Both modals should be hidden
|
||||||
|
await expect(parentModal).toBeHidden();
|
||||||
|
await expect(childModal).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should only dismiss child modal when child dismiss button is clicked', async ({ page }) => {
|
||||||
|
await page.goto('/src/components/modal/test/inline', config);
|
||||||
|
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||||
|
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||||
|
|
||||||
|
const parentModal = page.locator('ion-modal').first();
|
||||||
|
const childModal = page.locator('#child-modal');
|
||||||
|
|
||||||
|
// Open the parent modal
|
||||||
|
await page.click('#open-inline-modal');
|
||||||
|
await ionModalDidPresent.next();
|
||||||
|
await expect(parentModal).toBeVisible();
|
||||||
|
|
||||||
|
// Open the child modal
|
||||||
|
await page.click('#open-child-modal');
|
||||||
|
await ionModalDidPresent.next();
|
||||||
|
await expect(childModal).toBeVisible();
|
||||||
|
|
||||||
|
// Dismiss only the child modal
|
||||||
|
await page.click('#dismiss-child');
|
||||||
|
await ionModalDidDismiss.next();
|
||||||
|
|
||||||
|
// Parent modal should still be visible, child modal should be hidden
|
||||||
|
await expect(parentModal).toBeVisible();
|
||||||
|
await expect(childModal).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
test('presenting should create a single root element with the ion-page class', async ({ page }, testInfo) => {
|
test('presenting should create a single root element with the ion-page class', async ({ page }, testInfo) => {
|
||||||
testInfo.annotations.push({
|
testInfo.annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
|
|||||||
Reference in New Issue
Block a user