diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 4769f609914..4bf4564c40f 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -150,7 +150,7 @@ export class Menu implements ComponentInterface, MenuI { @Watch('side') protected sideChanged() { - this.isEndSide = isEnd(this.side); + this.isEndSide = isEnd(this.side, this.el); /** * Menu direction animation is calculated based on the document direction. * If the document direction changes, we need to create a new animation. @@ -499,7 +499,7 @@ export class Menu implements ComponentInterface, MenuI { * Menu direction animation is calculated based on the document direction. * If the document direction changes, we need to create a new animation. */ - const isEndSide = isEnd(this.side); + const isEndSide = isEnd(this.side, this.el); if (width === this.width && this.animation !== undefined && isEndSide === this.isEndSide) { return; } diff --git a/core/src/components/menu/test/basic/menu.e2e.ts b/core/src/components/menu/test/basic/menu.e2e.ts index e715469f282..5dbe59a7d6f 100644 --- a/core/src/components/menu/test/basic/menu.e2e.ts +++ b/core/src/components/menu/test/basic/menu.e2e.ts @@ -137,6 +137,30 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co }); await ionDidClose.next(); }); + + test('should render on the correct side when ion-app direction is rtl', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30226', + }); + + const ionDidOpen = await page.spyOnEvent('ionDidOpen'); + const ionDidClose = await page.spyOnEvent('ionDidClose'); + + await page.evaluate(() => { + document.dir = 'ltr'; + document.querySelector('ion-app')?.setAttribute('dir', 'rtl'); + }); + await page.click('#open-start'); + await ionDidOpen.next(); + + await expect(page).toHaveScreenshot(screenshot(`menu-basic-ion-app-dir-rtl`)); + + await page.locator('[menu-id="start-menu"]').evaluate(async (el: HTMLIonMenuElement) => { + await el.close(); + }); + await ionDidClose.next(); + }); }); }); diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..34d34f9de85 Binary files /dev/null and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5de3cf5bdd3 Binary files /dev/null and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e288b014a41 Binary files /dev/null and b/core/src/components/menu/test/basic/menu.e2e.ts-snapshots/menu-basic-ion-app-dir-rtl-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/utils/helpers.spec.ts b/core/src/utils/helpers.spec.ts index 44dd9d8c3ce..5ca9c571b39 100644 --- a/core/src/utils/helpers.spec.ts +++ b/core/src/utils/helpers.spec.ts @@ -1,4 +1,44 @@ -import { inheritAriaAttributes } from './helpers'; +import { inheritAriaAttributes, isEndSide } from './helpers'; + +describe('isEndSide', () => { + afterEach(() => { + document.dir = 'ltr'; + }); + + it('should use document direction when no host element is provided', () => { + document.dir = 'rtl'; + expect(isEndSide('start')).toBe(true); + expect(isEndSide('end')).toBe(false); + }); + + it('should use the nearest ancestor dir attribute', () => { + document.dir = 'ltr'; + + const app = document.createElement('ion-app'); + app.setAttribute('dir', 'rtl'); + + const menu = document.createElement('ion-menu'); + app.appendChild(menu); + document.body.appendChild(app); + + expect(isEndSide('start', menu)).toBe(true); + expect(isEndSide('end', menu)).toBe(false); + + document.body.removeChild(app); + }); + + it('should fall back to document direction when no ancestor has dir', () => { + document.dir = 'rtl'; + + const menu = document.createElement('ion-menu'); + document.body.appendChild(menu); + + expect(isEndSide('start', menu)).toBe(true); + expect(isEndSide('end', menu)).toBe(false); + + document.body.removeChild(menu); + }); +}); describe('inheritAriaAttributes', () => { it('should inherit aria attributes', () => { diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index e32956c35eb..9d8c0a7b839 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -1,5 +1,6 @@ import type { EventEmitter } from '@stencil/core'; import { printIonError } from '@utils/logging'; +import { isRTL } from '@utils/rtl'; import type { Side } from '../components/menu/menu-interface'; @@ -319,15 +320,17 @@ export const pointerCoord = (ev: any): { x: number; y: number } => { * Given a side, return if it should be on the end * based on the value of dir * @param side the side - * @param isRTL whether the application dir is rtl + * @param hostElement the host element used to resolve the nearest ancestor `dir` */ -export const isEndSide = (side: Side): boolean => { - const isRTL = document.dir === 'rtl'; +export const isEndSide = (side: Side, hostElement?: Element): boolean => { + const dirHost = hostElement?.closest('[dir]') as HTMLElement | undefined; + const rtl = isRTL(dirHost); + switch (side) { case 'start': - return isRTL; + return rtl; case 'end': - return !isRTL; + return !rtl; default: throw new Error(`"${side}" is not a valid value for [side]. Use "start" or "end" instead.`); }