Skip to content

Commit fa41131

Browse files
fix(menu): emit ionMenuChange when re-mounted (#28049)
Issue number: resolves #28030 --------- <!-- 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. --> `ion-menu` registers itself with the menu controller when `connectedCallback` fires and then unregisters itself when `disconnectedCallback` fires. When the menu was removed from the DOM, `disconnectedCallback` was not always being fired due to stenciljs/core#4070. `ion-menu-button` checks to see if it should be visible by grabbing the current menu in https://github.com/ionic-team/ionic-framework/blob/314055cf7a66a58e4e88c22e6e755fadd494ed70/core/src/components/menu-button/menu-button.tsx#L74. Since `disconnectedCallback` was not being fired, `ion-menu-button` would still find the menu even when it was no longer in the DOM. In this case, the menu was not being unregistered due to `disconnectedCallback` not firing. When the linked Stencil bug was resolved in Stencil 4.0.3, the menu button started to disappear. This is happening because `disconnectedCallback` is now correctly called when the menu is removed from the DOM. Since `disconnectedCallback` is called, the menu is un-registered from the menu controller, and the menu button can no longer find it. However, this revealed a long-standing bug where re-adding the menu would not fire `ionMenuChange` again. As a result, the menu button remained hidden. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - The menu now fires `ionMenuChange` on `connectedCallback` as long as `componentDidLoad` has already been run. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## 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: `7.3.2-dev.11692803611.15c1bc87` --------- Co-authored-by: Sean Perkins <[email protected]>
1 parent f450f0a commit fa41131

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Menu - Async</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
<script type="module">
16+
import { menuController } from '../../../../dist/ionic/index.esm.js';
17+
window.menuController = menuController;
18+
</script>
19+
</head>
20+
21+
<body>
22+
<ion-app>
23+
<div id="menu-container">
24+
<ion-menu content-id="main">
25+
<ion-content class="ion-padding"> Menu Content </ion-content>
26+
</ion-menu>
27+
</div>
28+
29+
<div class="ion-page" id="main">
30+
<ion-header>
31+
<ion-toolbar>
32+
<ion-title>Menu - Async</ion-title>
33+
<ion-buttons slot="start" id="buttons-container"></ion-buttons>
34+
</ion-toolbar>
35+
</ion-header>
36+
<ion-content class="ion-padding">
37+
Main Content
38+
<button onclick="trigger()" id="trigger">Add Menu To DOM</button>
39+
</ion-content>
40+
</div>
41+
</ion-app>
42+
43+
<script>
44+
const buttons = document.querySelector('ion-buttons');
45+
const menuButton = document.createElement('ion-menu-button');
46+
const menu = document.querySelector('ion-menu');
47+
const menuContainer = document.querySelector('#menu-container');
48+
49+
let firstLoad = true;
50+
51+
// When the menu loads, immediately remove it from the DOM
52+
document.body.addEventListener('ionMenuChange', () => {
53+
if (firstLoad) {
54+
menuContainer.removeChild(menu);
55+
buttons.appendChild(menuButton);
56+
firstLoad = false;
57+
}
58+
});
59+
const trigger = () => {
60+
menuContainer.append(menu);
61+
};
62+
</script>
63+
</body>
64+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* This behavior does not vary across modes/directions
6+
*/
7+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
8+
test.describe(title('menu button: async'), () => {
9+
test('menu button should be visible if menu is moved', async ({ page }) => {
10+
await page.goto(`/src/components/menu-button/test/async`, config);
11+
12+
const menu = page.locator('ion-menu');
13+
const menuButton = page.locator('ion-menu-button');
14+
const triggerButton = page.locator('#trigger');
15+
16+
await expect(menu).not.toBeAttached();
17+
await expect(menuButton).toBeHidden();
18+
19+
await triggerButton.click();
20+
21+
await expect(menu).toBeAttached();
22+
await expect(menuButton).toBeVisible();
23+
});
24+
});
25+
});

core/src/components/menu/menu.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class Menu implements ComponentInterface, MenuI {
3838
private lastOnEnd = 0;
3939
private gesture?: Gesture;
4040
private blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true });
41+
private didLoad = false;
4142

4243
isAnimating = false;
4344
width!: number;
@@ -216,6 +217,7 @@ export class Menu implements ComponentInterface, MenuI {
216217

217218
// register this menu with the app's menu controller
218219
menuController._register(this);
220+
this.menuChanged();
219221

220222
this.gesture = (await import('../../utils/gesture')).createGesture({
221223
el: document,
@@ -237,10 +239,22 @@ export class Menu implements ComponentInterface, MenuI {
237239
}
238240

239241
async componentDidLoad() {
240-
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
242+
this.didLoad = true;
243+
this.menuChanged();
241244
this.updateState();
242245
}
243246

247+
private menuChanged() {
248+
/**
249+
* Inform dependent components such as ion-menu-button
250+
* that the menu is ready. Note that we only want to do this
251+
* once the menu has been rendered which is why we check for didLoad.
252+
*/
253+
if (this.didLoad) {
254+
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
255+
}
256+
}
257+
244258
async disconnectedCallback() {
245259
/**
246260
* The menu should be closed when it is

0 commit comments

Comments
 (0)