-
Notifications
You must be signed in to change notification settings - Fork 13.3k
fix(rtl): respect dir of ancestor elements throughout components
#31459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7af565a
30ad163
b0dd608
666b48e
1b50a47
287adf8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { newSpecPage } from '@stencil/core/testing'; | ||
|
|
||
| import { Col } from '../col'; | ||
|
|
||
| describe('ion-col: rtl', () => { | ||
| const newCol = async (html: string) => { | ||
| const page = await newSpecPage({ components: [Col], html }); | ||
| return page.body.querySelector('ion-col')!; | ||
| }; | ||
|
|
||
| it('should offset, push and pull from the start when no dir is declared', async () => { | ||
| const col = await newCol(`<ion-col offset="3" push="2" pull="1"></ion-col>`); | ||
| expect(col.style.marginLeft).not.toBe(''); | ||
| expect(col.style.marginRight).toBe(''); | ||
| expect(col.style.left).not.toBe(''); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the attributes are needed because These two can't fail either way. Push and pull are both always written, they just swap between Asserting the exact |
||
| expect(col.style.right).not.toBe(''); | ||
| }); | ||
|
|
||
| it('should mirror offset, push and pull when an ancestor declares rtl', async () => { | ||
| const col = await newCol(`<div dir="rtl"><div><ion-col offset="3" push="2" pull="1"></ion-col></div></div>`); | ||
| expect(col.style.marginRight).not.toBe(''); | ||
| expect(col.style.marginLeft).toBe(''); | ||
| }); | ||
|
|
||
| it('should not mirror when an ancestor declares ltr', async () => { | ||
| const col = await newCol(`<div dir="ltr"><ion-col offset="3"></ion-col></div>`); | ||
| expect(col.style.marginLeft).not.toBe(''); | ||
| expect(col.style.marginRight).toBe(''); | ||
| }); | ||
|
|
||
| it('should use the nearest ancestor that declares a dir', async () => { | ||
| const col = await newCol(`<div dir="rtl"><div dir="ltr"><ion-col offset="3"></ion-col></div></div>`); | ||
| expect(col.style.marginLeft).not.toBe(''); | ||
| expect(col.style.marginRight).toBe(''); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { newSpecPage } from '@stencil/core/testing'; | ||
|
|
||
| import { ItemOptions } from '../item-options'; | ||
|
|
||
| describe('ion-item-options: rtl', () => { | ||
| const newItemOptions = async (html: string) => { | ||
| const page = await newSpecPage({ components: [ItemOptions], html }); | ||
| return page.body.querySelector('ion-item-options')!; | ||
| }; | ||
|
|
||
| it('should change sides when an ancestor declares rtl', async () => { | ||
| const itemOptions = await newItemOptions( | ||
| `<div dir="rtl"><div><ion-item-options side="start"></ion-item-options></div></div>` | ||
| ); | ||
| expect(itemOptions).toHaveClass('item-options-end'); | ||
| expect(itemOptions).not.toHaveClass('item-options-start'); | ||
| }); | ||
|
|
||
| it('should not change sides when an ancestor declares ltr', async () => { | ||
| const itemOptions = await newItemOptions( | ||
| `<div dir="ltr"><div><ion-item-options side="start"></ion-item-options></div></div>` | ||
| ); | ||
| expect(itemOptions).toHaveClass('item-options-start'); | ||
| expect(itemOptions).not.toHaveClass('item-options-end'); | ||
| }); | ||
|
|
||
| it('should use the nearest ancestor that declares a dir', async () => { | ||
| const itemOptions = await newItemOptions( | ||
| `<div dir="rtl"><div dir="ltr"><ion-item-options side="start"></ion-item-options></div></div>` | ||
| ); | ||
| expect(itemOptions).toHaveClass('item-options-start'); | ||
| }); | ||
| }); |
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generated; it seems too dependent on implementation details. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { newSpecPage } from '@stencil/core/testing'; | ||
|
|
||
| import { ItemOptions } from '../../item-options/item-options'; | ||
| import { ItemSliding } from '../item-sliding'; | ||
|
|
||
| describe('ion-item-sliding: rtl', () => { | ||
| const newItemSliding = async (optionsAttrs: string) => { | ||
| const page = await newSpecPage({ | ||
| components: [ItemSliding, ItemOptions], | ||
| html: `<ion-item-sliding> | ||
| <ion-item>Item</ion-item> | ||
| <ion-item-options ${optionsAttrs}></ion-item-options> | ||
| </ion-item-sliding>`, | ||
| }); | ||
|
|
||
| await page.waitForChanges(); | ||
|
|
||
| return page; | ||
| }; | ||
|
|
||
| /** | ||
| * Opening only moves the item when the requested side matches the side the | ||
| * options were filed under, so it is what reveals the direction that was | ||
| * resolved for them. | ||
| */ | ||
| const opensFrom = async (optionsAttrs: string, side: 'start' | 'end') => { | ||
| const page = await newItemSliding(optionsAttrs); | ||
| const itemSliding = page.body.querySelector('ion-item-sliding')!; | ||
|
|
||
| await itemSliding.open(side); | ||
| await page.waitForChanges(); | ||
|
|
||
| return itemSliding.classList.contains('item-sliding-active-slide'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're half right, though the bigger problem is a coverage gap. This PR changed two lines here, the side resolution in There isn't a non-class observable to swap in, since The class you want is |
||
| }; | ||
|
|
||
| it('should file a start-side option under the start when no dir is declared', async () => { | ||
| expect(await opensFrom(`side="start"`, 'start')).toBe(true); | ||
| expect(await opensFrom(`side="start"`, 'end')).toBe(false); | ||
| }); | ||
|
|
||
| it('should file a start-side option under the end when the options element declares rtl', async () => { | ||
| expect(await opensFrom(`side="start" dir="rtl"`, 'end')).toBe(true); | ||
| expect(await opensFrom(`side="start" dir="rtl"`, 'start')).toBe(false); | ||
| }); | ||
|
|
||
| it('should file a start-side option under the end when an ancestor declares rtl', async () => { | ||
| const page = await newSpecPage({ | ||
| components: [ItemSliding, ItemOptions], | ||
| html: `<div dir="rtl"> | ||
| <ion-item-sliding> | ||
| <ion-item>Item</ion-item> | ||
| <ion-item-options side="start"></ion-item-options> | ||
| </ion-item-sliding> | ||
| </div>`, | ||
| }); | ||
| await page.waitForChanges(); | ||
|
|
||
| const itemSliding = page.body.querySelector('ion-item-sliding')!; | ||
| await itemSliding.open('end'); | ||
| await page.waitForChanges(); | ||
|
|
||
| expect(itemSliding).toHaveClass('item-sliding-active-slide'); | ||
| }); | ||
|
|
||
| /** | ||
| * ion-item-options resolves its own side, so a dir declared on it has to | ||
| * resolve the same way here or the options would render on one side while | ||
| * opening from the other. | ||
| */ | ||
| it('should resolve the same side that the options element renders', async () => { | ||
| const page = await newItemSliding(`side="start" dir="rtl"`); | ||
| const itemSliding = page.body.querySelector('ion-item-sliding')!; | ||
| const itemOptions = page.body.querySelector('ion-item-options')!; | ||
|
|
||
| expect(itemOptions).toHaveClass('item-options-end'); | ||
|
|
||
| await itemSliding.open('end'); | ||
| await page.waitForChanges(); | ||
|
|
||
| expect(itemSliding).toHaveClass('item-sliding-active-slide'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { newSpecPage } from '@stencil/core/testing'; | ||
|
|
||
| import { Label } from '../label'; | ||
|
|
||
| describe('ion-label: rtl', () => { | ||
| const newLabel = async (html: string) => { | ||
| const page = await newSpecPage({ components: [Label], html }); | ||
| return page.body.querySelector('ion-label')!; | ||
| }; | ||
|
|
||
| it('should set label-rtl when an ancestor declares rtl', async () => { | ||
| const label = await newLabel(`<div dir="rtl"><div><ion-label>Label</ion-label></div></div>`); | ||
| expect(label).toHaveClass('label-rtl'); | ||
| }); | ||
|
|
||
| it('should not set label-rtl when an ancestor declares ltr', async () => { | ||
| const label = await newLabel(`<div dir="ltr"><ion-label>Label</ion-label></div>`); | ||
| expect(label).not.toHaveClass('label-rtl'); | ||
| }); | ||
|
|
||
| it('should use the nearest ancestor that declares a dir', async () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every For the precedence check to mean anything the document has to declare rtl, and setting const newInRtlDocument = async (html: string) => {
const page = await newSpecPage({ components: [Label], html: `<div></div>` });
page.doc.documentElement.setAttribute('dir', 'rtl');
await page.setContent(html);
return page.body.querySelector('ion-label')!;
}; |
||
| const label = await newLabel(`<div dir="rtl"><div dir="ltr"><ion-label>Label</ion-label></div></div>`); | ||
| expect(label).not.toHaveClass('label-rtl'); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generated this one with Claude. I don't get why the parameters to
ion-colare needed, but it doesn't work without them.