Skip to content
Merged
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
25 changes: 14 additions & 11 deletions core/src/components/col/col.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Listen, Prop, forceUpdate, h } from '@stencil/core';
import { Component, Element, Host, Listen, Prop, forceUpdate, h } from '@stencil/core';
import { matchBreakpoint } from '@utils/media';
import { isRTL } from '@utils/rtl';

import { getIonMode } from '../../global/ionic-global';

Expand All @@ -15,6 +16,8 @@ const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl'];
shadow: true,
})
export class Col implements ComponentInterface {
@Element() el!: HTMLElement;

/**
* The amount to offset the column, in terms of how many columns it should shift to the end
* of the total available.
Expand Down Expand Up @@ -235,30 +238,30 @@ export class Col implements ComponentInterface {
};
}

private calculateOffset(isRTL: boolean) {
return this.calculatePosition('offset', isRTL ? 'margin-right' : 'margin-left');
private calculateOffset(rtl: boolean) {
return this.calculatePosition('offset', rtl ? 'margin-right' : 'margin-left');
}

private calculatePull(isRTL: boolean) {
return this.calculatePosition('pull', isRTL ? 'left' : 'right');
private calculatePull(rtl: boolean) {
return this.calculatePosition('pull', rtl ? 'left' : 'right');
}

private calculatePush(isRTL: boolean) {
return this.calculatePosition('push', isRTL ? 'right' : 'left');
private calculatePush(rtl: boolean) {
return this.calculatePosition('push', rtl ? 'right' : 'left');
}

render() {
const isRTL = document.dir === 'rtl';
const rtl = isRTL(this.el);
const mode = getIonMode(this);
return (
<Host
class={{
[mode]: true,
}}
style={{
...this.calculateOffset(isRTL),
...this.calculatePull(isRTL),
...this.calculatePush(isRTL),
...this.calculateOffset(rtl),
...this.calculatePull(rtl),
...this.calculatePush(rtl),
...this.calculateSize(),
}}
>
Expand Down
36 changes: 36 additions & 0 deletions core/src/components/col/test/col.spec.ts
Comment thread
ShaneK marked this conversation as resolved.
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, documentDir?: 'rtl') => {
const page = await newSpecPage({ components: [Col], html: `<div></div>` });
if (documentDir) {
page.doc.documentElement.setAttribute('dir', documentDir);
}
await page.setContent(html);
return page.body.querySelector('ion-col')!;
};

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

const COL_PARAMS = `offset="3" push="2" pull="1"`;

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

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

it('should not mirror push and pull when an ancestor declares ltr in an rtl document', async () => {
const col = await newCol(`<div dir="ltr"><div><ion-col ${COL_PARAMS}></ion-col></div></div>`, 'rtl');
expect(col.style.right).toBe(cols(1));
expect(col.style.left).toBe(cols(2));
});
});
4 changes: 2 additions & 2 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2259,8 +2259,7 @@ export class Datetime implements ComponentInterface {
const prevMonthDisabled = disabled || isPrevMonthDisabled(this.workingParts, this.minParts, this.maxParts);
const nextMonthDisabled = disabled || isNextMonthDisabled(this.workingParts, this.maxParts);

// don't use the inheritAttributes util because it removes dir from the host, and we still need that
const hostDir = this.el.getAttribute('dir') || undefined;
const hostDir = isRTL(this.el) ? 'rtl' : 'ltr';

return (
<div class="calendar-header" part="calendar-header">
Expand All @@ -2280,6 +2279,7 @@ export class Datetime implements ComponentInterface {
<span id="toggle-wrapper">
{getMonthAndYear(this.locale, this.workingParts)}
<ion-icon
dir={hostDir}
aria-hidden="true"
icon={this.showMonthAndYear ? expandedIcon : collapsedIcon}
lazy={false}
Expand Down
27 changes: 22 additions & 5 deletions core/src/components/datetime/test/basic/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,13 +590,13 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
});

/**
* We are setting RTL on the component
* instead, so we don't need to test
* RTL is set in the markup, on the component
* or an ancestor, so we don't need to test
* both directions. Also, this behavior
* does not vary across modes.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: RTL set on component'), () => {
test.describe(title('datetime: RTL'), () => {
test('should flip icons when RTL is set on component directly', async ({ page }) => {
await page.setContent(
`
Expand All @@ -606,8 +606,25 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
);

const nextPrevIcons = page.locator('ion-datetime .calendar-next-prev ion-icon');
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/);
});

test('should flip icons when RTL is set on an ancestor', async ({ page }) => {
await page.setContent(
`
<div dir="rtl">
<div>
<ion-datetime></ion-datetime>
</div>
</div>
`,
config
);

const nextPrevIcons = page.locator('ion-datetime .calendar-next-prev ion-icon');
await expect(nextPrevIcons.first()).toHaveClass(/icon-rtl/);
await expect(nextPrevIcons.last()).toHaveClass(/icon-rtl/);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/item-options/item-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ItemOptions implements ComponentInterface {

render() {
const mode = getIonMode(this);
const isEnd = isEndSide(this.side);
const isEnd = isEndSide(this.side, this.el);
return (
<Host
class={{
Expand Down
37 changes: 37 additions & 0 deletions core/src/components/item-options/test/item-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { newSpecPage } from '@stencil/core/testing';

import { ItemOptions } from '../item-options';

describe('ion-item-options: rtl', () => {
const newItemOptions = async (html: string, documentDir?: 'rtl') => {
const page = await newSpecPage({ components: [ItemOptions], html: `<div></div>` });
if (documentDir) {
page.doc.documentElement.setAttribute('dir', documentDir);
}
await page.setContent(html);
return page.body.querySelector('ion-item-options')!;
};

it('should change sides when it declares rtl', async () => {
const itemOptions = await newItemOptions(`<ion-item-options side="start" dir="rtl"></ion-item-options>`);
expect(itemOptions).toHaveClass('item-options-end');
expect(itemOptions).not.toHaveClass('item-options-start');
});

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 in an rtl document', async () => {
const itemOptions = await newItemOptions(
`<div dir="ltr"><div><ion-item-options side="start"></ion-item-options></div></div>`,
'rtl'
);
expect(itemOptions).toHaveClass('item-options-start');
expect(itemOptions).not.toHaveClass('item-options-end');
});
});
7 changes: 4 additions & 3 deletions core/src/components/item-sliding/item-sliding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
import { componentOnReady, isEndSide } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { isRTL } from '@utils/rtl';
import { watchForOptions } from '@utils/watch-options';

import { getIonMode } from '../../global/ionic-global';
Expand Down Expand Up @@ -176,7 +177,7 @@ export class ItemSliding implements ComponentInterface {
}

// In RTL we want to switch the sides
side = isEndSide(side) ? 'end' : 'start';
side = isEndSide(side, this.el) ? 'end' : 'start';

const isStartOpen = this.openAmount < 0;
const isEndOpen = this.openAmount > 0;
Expand Down Expand Up @@ -260,7 +261,7 @@ export class ItemSliding implements ComponentInterface {
this.leftOptions = this.rightOptions = undefined;

for (const option of options) {
const side = isEndSide(option.side ?? option.getAttribute('side')) ? 'end' : 'start';
const side = isEndSide(option.side ?? option.getAttribute('side'), option) ? 'end' : 'start';

if (side === 'start') {
this.leftOptions = option;
Expand All @@ -280,7 +281,7 @@ export class ItemSliding implements ComponentInterface {
* do not open left side so swipe to go
* back will still work.
*/
const rtl = document.dir === 'rtl';
const rtl = isRTL(this.el);
const atEdge = rtl ? window.innerWidth - gesture.startX < 15 : gesture.startX < 15;
if (atEdge) {
return false;
Expand Down
28 changes: 28 additions & 0 deletions core/src/components/item-sliding/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,34 @@ <h2>RIGHT/LEFT side - many buttons</h2>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>

<div dir="ltr">
<ion-item-sliding id="item-ltr-start">
<ion-item detail>
<ion-label class="ion-text-wrap">
<h2>LTR on start side</h2>
<p>Label and button should always be on the left</p>
</ion-label>
</ion-item>
<ion-item-options side="start">
<ion-item-option>Button</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</div>

<div dir="rtl">
<ion-item-sliding id="item-rtl-start">
<ion-item detail>
<ion-label class="ion-text-wrap">
<h2>RTL on start side</h2>
<p>Label and button should always be on the right</p>
</ion-label>
</ion-item>
<ion-item-options side="start">
<ion-item-option>Button</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</div>
</div>
</ion-list>

Expand Down
27 changes: 27 additions & 0 deletions core/src/components/item-sliding/test/basic/item-sliding.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,30 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
});
});
});

configs({ directions: ['ltr', 'rtl'] }).forEach(({ title, config }) => {
test.describe(title('item-sliding: basic'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/item-sliding/test/basic`, config);
});
test.describe('direction', () => {
test('should be ltr when ancestor defines dir="ltr"', async ({ page }) => {
const item = page.locator('#item-ltr-start');
await item.scrollIntoViewIfNeeded();
await dragElementBy(item, page, 150);
await page.waitForChanges();

await expect(item).toHaveClass(/item-sliding-active-options-start/);
});

test('should be rtl when ancestor defines dir="rtl"', async ({ page }) => {
Comment thread
brandyscarney marked this conversation as resolved.
const item = page.locator('#item-rtl-start');
await item.scrollIntoViewIfNeeded();
await dragElementBy(item, page, -150);
await page.waitForChanges();

await expect(item).toHaveClass(/item-sliding-active-options-end/);
});
});
});
});
4 changes: 3 additions & 1 deletion core/src/components/item/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AttributeController } from '@utils/attribute-controller';
import { createAttributeController } from '@utils/attribute-controller';
import type { AnchorInterface, ButtonInterface } from '@utils/element-interface';
import { raf } from '@utils/helpers';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext, openURL } from '@utils/theme';
import { chevronForward } from 'ionicons/icons';

Expand Down Expand Up @@ -367,6 +368,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
routerAnimation,
routerDirection,
multipleInputs,
el,
} = this;
const inheritedAriaAttributes = this.ariaController?.attributes ?? {};
const childStyles = {} as StyleEventDetail;
Expand Down Expand Up @@ -473,7 +475,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
'item-focus-indicator-room': slottedIndicatorNeedsRoom,
'ion-activatable': canActivate,
'ion-focusable': this.focusable,
'item-rtl': document.dir === 'rtl',
'item-rtl': isRTL(el),
}),
}}
role={inList ? 'listitem' : null}
Expand Down
26 changes: 26 additions & 0 deletions core/src/components/item/test/item.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,30 @@ describe('item', () => {
expect(item).not.toHaveClass('item-focus-indicator-room');
});
});

describe('rtl', () => {
const newItemPage = async (html: string, documentDir?: 'rtl') => {
const page = await newSpecPage({ components: [Item], html: `<div></div>` });
if (documentDir) {
page.doc.documentElement.setAttribute('dir', documentDir);
}
await page.setContent(html);
return page.body.querySelector('ion-item')!;
};

it('should set item-rtl when it declares rtl', async () => {
const item = await newItemPage(`<ion-item dir="rtl">Item</ion-item>`);
expect(item).toHaveClass('item-rtl');
});

it('should set item-rtl when an ancestor declares rtl', async () => {
const item = await newItemPage(`<div dir="rtl"><div><ion-item>Item</ion-item></div></div>`);
expect(item).toHaveClass('item-rtl');
});

it('should not set item-rtl when an ancestor declares ltr in an rtl document', async () => {
const item = await newItemPage(`<div dir="ltr"><div><ion-item>Item</ion-item></div></div>`, 'rtl');
expect(item).not.toHaveClass('item-rtl');
});
});
});
3 changes: 2 additions & 1 deletion core/src/components/label/label.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme';

import { getIonMode } from '../../global/ionic-global';
Expand Down Expand Up @@ -112,7 +113,7 @@ export class Label implements ComponentInterface {
'in-item-color': hostContext('ion-item.ion-color', this.el),
[`label-${position}`]: position !== undefined,
[`label-no-animate`]: this.noAnimate,
'label-rtl': document.dir === 'rtl',
'label-rtl': isRTL(this.el),
})}
>
<slot></slot>
Expand Down
Loading
Loading