Skip to content

fix(rtl): respect dir of ancestor elements throughout components - #31459

Open
OS-jacobbell wants to merge 21 commits into
mainfrom
FW-7698
Open

OS-jacobbell wants to merge 21 commits into
mainfrom
FW-7698

Conversation

@OS-jacobbell

Copy link
Copy Markdown
Contributor

Issue number: internal


What is the current behavior?

Some components check document.dir === 'rtl' to determine whether to be left-to-right or right-to-left.

What is the new behavior?

  • Components consistently use isRTL(this.el). This way, any element in the parent chain can define RTL, not just the root document element.
  • The host element is a required parameter to isRTL.

Does this introduce a breaking change?

  • Yes
  • No

@OS-jacobbell
OS-jacobbell requested a review from a team as a code owner September 18, 2026 18:47
@vercel

vercel Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ionic-framework Ready Ready Preview Oct 23, 2026 1:56pm UTC

Request Review

@github-actions github-actions Bot added the package: core @ionic/core package label Sep 18, 2026
@OS-jacobbell OS-jacobbell changed the title fix(i18n): respect parent rtl setting throughout components fix(rtl): respect dir setting of ancestor elements throughout components Sep 21, 2026
@OS-jacobbell OS-jacobbell changed the title fix(rtl): respect dir setting of ancestor elements throughout components fix(rtl): respect dir of ancestor elements throughout components Sep 21, 2026

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice sweep of this! Just a few comments inline, though I'd want the test coverage added before it goes in.

Comment thread core/src/utils/rtl/dir.spec.ts
Comment thread docs/component-guide.md Outdated
Comment thread core/src/components/progress-bar/progress-bar.tsx Outdated
Co-authored-by: Shane <shane.king@outsystems.com>
Comment thread core/src/components/col/test/col.spec.ts
Comment thread core/src/components/item-sliding/test/item-sliding.spec.ts Outdated
Comment thread core/src/components/popover/test/rtl/popover.e2e.ts Outdated

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the tests and taking the suggestions! Most of them pass on main too though, so they aren't covering the change yet, and CI is red on the new popover one. I've left comments inline.

Comment thread core/src/components/label/test/label.spec.ts Outdated
Comment thread core/src/components/col/test/col.spec.ts Outdated
Comment thread core/src/components/item-sliding/test/item-sliding.spec.ts Outdated
Comment thread core/src/components/popover/test/rtl/popover.e2e.ts Outdated
Comment thread core/src/utils/input-shims/hacks/common.ts
Comment thread core/src/utils/rtl/dir.ts
Comment thread core/src/utils/rtl/dir.spec.ts

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really close! Just a few more comments inline.

Comment thread core/src/components/popover/popover.tsx Outdated
Comment thread core/src/components/datetime/test/basic/datetime.e2e.ts Outdated
Comment thread core/src/components/col/test/col.spec.ts Outdated
Comment thread core/src/components/popover/test/position/index.html Outdated

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks for sticking with this one! Just need the datetime test fixed so lint passes. 🚀

Comment thread core/src/components/datetime/test/basic/datetime.e2e.ts Outdated
Comment thread core/src/components/datetime/test/basic/datetime.e2e.ts Outdated
OS-jacobbell and others added 2 commits October 20, 2026 15:02
Co-authored-by: Shane <shane.king@outsystems.com>

@brandyscarney brandyscarney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Requested changes for the popover direction regression on popover.tsx. The other comments are non-blocking.

Comment thread core/src/components/item-sliding/test/basic/item-sliding.e2e.ts
Comment thread core/src/components/popover/test/position/index.html
* elements can match.
*/
if (!el.hasAttribute('dir')) {
el.setAttribute('dir', isRTL(el) ? 'rtl' : 'ltr');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this sets the host's dir from isRTL(el), which is wherever the popover currently sits, but the enter animations (ios.enter.ts:35, md.enter.ts:23) take their direction from the trigger or event target. Controller popovers (including ion-select's) and React inline popovers are already under ion-app before present() runs. So for a popover opened from inside <div dir="rtl"> in an LTR app, it gets placed for RTL but its content renders LTR. On main both followed the document, so they always agreed. This is a regression introduced by this PR.

Could present() pick the source element once (trigger, then event target, then el), set the host dir from that, and have both enter animations just call isRTL(baseEl)? That keeps placement and content in sync, respects a dir set directly on ion-popover, and means the direction lookup isn't duplicated across both enter animations.

To reproduce, add this to an index.html file in core (expand):
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <title>Popover - RTL Repro</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
    />
    <link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
    <link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
    <script src="../../../../../scripts/testing/scripts.js"></script>
    <script nomodule src="../../../../../dist/ionic/ionic.js"></script>
    <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
  </head>

  <body>
    <ion-app>
      <ion-content class="ion-padding">
        <div dir="rtl">
          <!-- This is added to move the button away from the right edge -->
          <div style="display: inline-block; width: 200px; height: 20px"></div>

          <button id="open">Open</button>
        </div>
      </ion-content>
    </ion-app>

    <script type="module">
      import { popoverController } from '../../../../../dist/ionic/index.esm.js';

      document.querySelector('#open').addEventListener('click', async (ev) => {
        const content = document.createElement('div');
        content.className = 'ion-padding';
        content.textContent = 'Hello world!';

        const popover = await popoverController.create({
          event: ev,
          component: content,
          side: 'start',
          alignment: 'start',
        });
        await popover.present();
      });
    </script>
  </body>
</html>

The popover opens to the right of the button with transform-origin: left top (RTL), but ion-popover gets dir="ltr" and its content renders LTR.


Not blocking and could go in FW-7850 instead: once present() resolves from the trigger or event target, ion-select with the default label placement still won't pick up an ancestor dir. It builds its popover event as { ...ev, detail: { ionShadowTarget } } (select.tsx:784), and spreading drops target since it's a prototype getter, so its options keep rendering LTR inside an RTL ancestor. Resolving direction from the same element getPopoverPosition positions against (utils.ts:637, using its host since isRTL stops at shadow roots) would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, Shane and I decided on using the trigger element back when popover wasn't saving its own dir. Now that it is, the animations can use just baseEl (the popover itself).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe PopoverOptions should have a dir property?

This branch was successfully deployed

1 active deployment
Preview — 6a7a0413 Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants