Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions core/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { printIonError } from '@utils/logging';
import { menuController } from '@utils/menu-controller';
import { BACKDROP, GESTURE, getPresentedOverlay } from '@utils/overlays';
import { isPlatform } from '@utils/platform';
import { hostContext } from '@utils/theme';

import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
Expand Down Expand Up @@ -269,7 +268,7 @@ export class Menu implements ComponentInterface, MenuI {
* of the split pane. If the split pane is
* hidden then the menu should be too.
*/
const splitPane = this.el.closest('ion-split-pane');
const splitPane = this.getSplitPane();
if (splitPane !== null) {
this.isPaneVisible = await splitPane.isVisible();
}
Expand Down Expand Up @@ -315,9 +314,9 @@ export class Menu implements ComponentInterface, MenuI {

@Listen('ionSplitPaneVisible', { target: 'body' })
onSplitPaneChanged(ev: CustomEvent<{ visible: boolean }>) {
const closestSplitPane = this.el.closest<HTMLIonSplitPaneElement>('ion-split-pane');
const splitPane = this.getSplitPane();

if (closestSplitPane !== null && closestSplitPane === ev.target) {
if (splitPane !== null && splitPane === ev.target) {
this.isPaneVisible = ev.detail.visible;

this.updateState();
Expand Down Expand Up @@ -550,6 +549,27 @@ export class Menu implements ComponentInterface, MenuI {
return !this.disabled && !this.isPaneVisible;
}

/**
* Returns the split pane this menu is a side pane of.
* A menu inside the split pane's main content, such as one
* in a page, is a regular menu. The menu doesn't need to be
* a direct child, since it can be wrapped in a component.
*/
private getSplitPane(): HTMLIonSplitPaneElement | null {
const splitPane = this.el.closest('ion-split-pane');
const main = this.el.closest('.split-pane-main');

/**
* A split pane can be the main content of an outer split
* pane, so only this split pane's main content counts.
*/
if (splitPane === null || (main !== null && main !== splitPane && splitPane.contains(main))) {
return null;
}

return splitPane;
}

private canSwipe(): boolean {
return this.swipeGesture && !this.isAnimating && this._isActive();
}
Expand Down Expand Up @@ -830,7 +850,7 @@ export class Menu implements ComponentInterface, MenuI {
}

render() {
const { type, disabled, el, isPaneVisible, inheritedAttributes, side } = this;
const { type, disabled, isPaneVisible, inheritedAttributes, side } = this;
const mode = getIonMode(this);

/**
Expand All @@ -849,7 +869,7 @@ export class Menu implements ComponentInterface, MenuI {
'menu-enabled': !disabled,
[`menu-side-${side}`]: true,
'menu-pane-visible': isPaneVisible,
'split-pane-side': hostContext('ion-split-pane', el),
'split-pane-side': this.getSplitPane() !== null,
}}
>
<div class="menu-inner" part="container" ref={(el) => (this.menuInnerEl = el)}>
Expand Down
111 changes: 111 additions & 0 deletions core/src/components/split-pane/test/nested-menu/split-pane.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright';
import { configs, test, Viewports } from '@utils/test/playwright';

/**
* This behavior does not vary across modes/directions.
*/
configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) => {
test.describe(title('split-pane: nested menu'), () => {
const setup = async (page: E2EPage) => {
await page.setContent(
`
<ion-app>
<ion-split-pane content-id="main">
<ion-menu side="start" content-id="main" id="side-menu">
<ion-content>Side Menu</ion-content>
</ion-menu>
<div class="ion-page" id="main">
<ion-menu side="end" content-id="page-content" id="page-menu">
<ion-content>Page Menu</ion-content>
</ion-menu>
<div class="ion-page" id="page-content">
<ion-content>Main Content</ion-content>
</div>
</div>
</ion-split-pane>
</ion-app>
`,
config
);
};

test('should open a menu inside the main content when the split pane is visible', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30591',
});

await page.setViewportSize(Viewports.large);
await setup(page);

const sideMenu = page.locator('#side-menu');
const pageMenu = page.locator('#page-menu');

await expect(sideMenu).toBeVisible();
await expect(sideMenu).toHaveClass(/split-pane-side/);
await expect(pageMenu).not.toHaveClass(/split-pane-side/);

const didOpen = await pageMenu.evaluate((el: HTMLIonMenuElement) => el.open());

expect(didOpen).toBe(true);
await expect(pageMenu).toHaveClass(/show-menu/);
});

test('should open a menu inside the main content after the split pane becomes visible', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30591',
});

await page.setViewportSize(Viewports.tablet.portrait);
await setup(page);

const sideMenu = page.locator('#side-menu');
const pageMenu = page.locator('#page-menu');

await expect(sideMenu).toBeHidden();

await page.setViewportSize(Viewports.large);
await expect(sideMenu).toBeVisible();

const didOpen = await pageMenu.evaluate((el: HTMLIonMenuElement) => el.open());

expect(didOpen).toBe(true);
await expect(pageMenu).toHaveClass(/show-menu/);
});

test('should treat a menu as a side pane of a split pane that is the main content of another split pane', async ({
page,
}) => {
await page.setViewportSize(Viewports.large);
await page.setContent(
`
<ion-app>
<ion-split-pane content-id="outer-main">
<ion-menu side="start" content-id="outer-main" id="outer-menu">
<ion-content>Outer Menu</ion-content>
</ion-menu>
<ion-split-pane id="outer-main" content-id="inner-main">
<ion-menu side="start" content-id="inner-main" id="inner-menu">
<ion-content>Inner Menu</ion-content>
</ion-menu>
<div class="ion-page" id="inner-main">
<ion-content>Main Content</ion-content>
</div>
</ion-split-pane>
</ion-split-pane>
</ion-app>
`,
config
);

const innerMenu = page.locator('#inner-menu');

await expect(innerMenu).toHaveClass(/split-pane-side/);
await expect(innerMenu).toHaveClass(/menu-pane-visible/);
});
});
});
Loading