Skip to content

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

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

OS-jacobbell wants to merge 16 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 13, 2026 6:33pm 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.

* hierarchy. Copy the current direction onto this element so that child
* elements can match.
*/
el.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.

This dir never gets removed on dismiss, and isRTL checks the host's own dir first, so every present after the first one reads back whatever was written the first time. If an app switches the document from ltr to rtl between presents, the popover gets positioned for rtl but its content still renders ltr, where on main the content follows the document. It also permanently replaces a dir="auto" someone set on the ion-popover.

This seems like a big issue since runtime language switching works on main today. Setting it only when the host doesn't already have a dir, and removing it on dismiss, fixed every case I tried without breaking the position tests:

if (!el.hasAttribute('dir')) {
  el.setAttribute('dir', isRTL(el) ? 'rtl' : 'ltr');
  this.dirSetOnPresent = true;
}

with a private dirSetOnPresent = false; field, and this in dismiss() right after detachComponent:

if (this.dirSetOnPresent) {
  this.el.removeAttribute('dir');
  this.dirSetOnPresent = false;
}

Comment on lines +626 to +627
await expect(nextPrevIcons.first()).toHaveClass(/flip-rtl/);
await expect(nextPrevIcons.last()).toHaveClass(/flip-rtl/);

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.

Suggested change
await expect(nextPrevIcons.first()).toHaveClass(/flip-rtl/);
await expect(nextPrevIcons.last()).toHaveClass(/flip-rtl/);
await expect(nextPrevIcons.first()).toHaveClass(/icon-rtl/);
await expect(nextPrevIcons.last()).toHaveClass(/icon-rtl/);

This one passes on main too, because ion-icon adds flip-rtl whenever flipRtl is set, whatever the direction is. The class that follows direction is icon-rtl, which is missing on main here and present with your change. The test above has the same gap, but it was already there, so up to you on that one. The describe title and its comment also still say RTL is set on the component.

};

const CALC_STYLE = `calc(calc(3 / var(--ion-grid-columns, 12)) * 100%)`;
const COL_PARAMS = `<ion-col offset="3" push="2" pull="1"></ion-col>`;

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.

Following up on push/pull from last round, the margins are covered now, but nothing asserts left or right, so hardcoding pull to right and push to left still leaves all three green. Something like this catches it and fails on main:

const cols = (n: number) => `calc(calc(${n} / var(--ion-grid-columns, 12)) * 100%)`;

it('should mirror push and pull when an ancestor declares rtl', async () => {
  const col = await newCol(`<div dir="rtl"><div>${COL_PARAMS}</div></div>`);
  expect(col.style.right).toBe(cols(2));
  expect(col.style.left).toBe(cols(1));
});

plus the reverse for the ltr-in-an-rtl-document case.

Also, this constant already includes the <ion-col> tags, and the tests wrap it in another <ion-col ...>, so <ion-col gets parsed as an attribute name and a stray > is left in the body. It only works by accident. I'd make this just the attributes.

</ion-popover>
</div>

<div class="grid-item" , dir="rtl">

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.

Suggested change
<div class="grid-item" , dir="rtl">
<div class="grid-item" dir="rtl">

Stray comma here.

This branch was successfully deployed

1 active deployment
Preview — de737eee Deployed Oct 13, 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.

3 participants