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
102 changes: 102 additions & 0 deletions demo/e2e/rtl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expect, test } from '@playwright/test';

for (const direction of ['ltr', 'rtl'] as const) {
test(`${direction} mirrors range geometry`, async ({ page }) => {
await page.goto('/main/index', { waitUntil: 'networkidle' });
await page.evaluate((dir) => {
const fixture = document.createElement('div');
fixture.id = 'rtl-range-probe';
fixture.dir = dir;

const range = document.createElement('ion-range');
range.id = 'single-range';
range.mode = 'md';
range.min = 0;
range.max = 100;
range.value = 50;

const dual = document.createElement('ion-range');
dual.id = 'dual-range';
dual.mode = 'md';
dual.dualKnobs = true;
dual.min = 0;
dual.max = 100;
dual.value = { lower: 0, upper: 100 };

fixture.append(range, dual);
document.body.append(fixture);
}, direction);

await expect(page.locator('#single-range')).toHaveClass(/hydrated/);
await expect(page.locator('#dual-range')).toHaveClass(/hydrated/);

const normalOffset = await page.locator('#single-range').evaluate((range) => {
const knob = range.shadowRoot!.querySelector<HTMLElement>('[part~="knob"]')!;
return new DOMMatrixReadOnly(getComputedStyle(knob).transform).e;
});
expect(normalOffset).toBeCloseTo(direction === 'ltr' ? 20 : -20, 1);

await page.locator('#single-range').evaluate((range) => range.classList.add('range-pressed'));
await expect
.poll(() =>
page.locator('#single-range').evaluate((range) => {
const knob = range.shadowRoot!.querySelector<HTMLElement>('[part~="knob"]')!;
return new DOMMatrixReadOnly(getComputedStyle(knob).transform).e;
}),
)
.toBeCloseTo(direction === 'ltr' ? 21 : -21, 1);

const dualMargins = await page.locator('#dual-range').evaluate((range) => {
const bar = range.shadowRoot!.querySelector<HTMLElement>('[part~="bar"]')!;
const style = getComputedStyle(bar);
return { start: style.marginInlineStart, end: style.marginInlineEnd };
});
expect(dualMargins).toEqual({ start: '6px', end: '6px' });
});

test(`${direction} mirrors Material forward and back transitions`, async ({ page }) => {
await page.goto('/main/index', { waitUntil: 'networkidle' });
await page.evaluate((dir) => {
document.documentElement.dir = dir;
const originalAnimate = Element.prototype.animate;
(window as any).__RTL_ANIMATIONS__ = [];
Element.prototype.animate = function (keyframes, options) {
const frames = Array.isArray(keyframes) ? keyframes : [];
(window as any).__RTL_ANIMATIONS__.push({
tag: this.localName,
transforms: frames.map((frame) => frame.transform).filter(Boolean),
});
return originalAnimate.call(this, keyframes, options);
};
}, direction);
await page.getByRole('button', { name: 'button', exact: true }).click();

const expected = direction === 'ltr' ? 'translateX(40px)' : 'translateX(-40px)';
const stacked = direction === 'ltr' ? 'translateX(-40px)' : 'translateX(40px)';
await expect
.poll(() =>
page.evaluate(
({ expected, stacked }) => {
const calls = (window as any).__RTL_ANIMATIONS__ as { tag: string; transforms: string[] }[];
return {
entering: calls.some((call) => call.tag === 'app-button' && call.transforms.includes(expected)),
stacked: calls.some((call) => call.transforms.includes(stacked)),
};
},
{ expected, stacked },
),
)
.toEqual({ entering: true, stacked: true });

await page.evaluate(() => ((window as any).__RTL_ANIMATIONS__ = []));
await page.locator('app-button > ion-header ion-back-button').click();
await expect
.poll(() =>
page.evaluate((expectedTransform) => {
const calls = (window as any).__RTL_ANIMATIONS__ as { tag: string; transforms: string[] }[];
return calls.some((call) => call.tag === 'app-button' && call.transforms.includes(expectedTransform));
}, expected),
)
.toBe(true);
});
}
2 changes: 1 addition & 1 deletion demo/src/app/album/album-page.component.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ion-header {
position: absolute;
top: 0;
left: 0;
inset-inline-start: 0;
}

ion-content {
Expand Down
2 changes: 1 addition & 1 deletion demo/src/app/index/pages/button/button.page.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ion-item *[slot='end'] {
margin-right: 6px;
margin-inline-end: 6px;
}
7 changes: 5 additions & 2 deletions demo/src/app/index/pages/tabs/tabs.page.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
:host {
ion-tab-bar {
margin-left: calc(16px + var(--ion-safe-area-left, 0px));
margin-right: calc(16px + var(--ion-safe-area-right, 0px));
margin-inline: calc(16px + var(--ion-safe-area-left, 0px)) calc(16px + var(--ion-safe-area-right, 0px));

&:dir(rtl) {
margin-inline: calc(16px + var(--ion-safe-area-right, 0px)) calc(16px + var(--ion-safe-area-left, 0px));
}
}
}
4 changes: 2 additions & 2 deletions demo/src/app/settings/settings-page.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ion-item > ion-icon[slot='start'] {
ion-item > ion-icon[slot='start'] {
padding: 4px;
font-size: 1.3rem;
margin-right: 14px;
margin-inline-end: 14px;
border-radius: 8px;
}
}
Expand All @@ -24,7 +24,7 @@ ion-item > ion-icon[slot='start'] {
padding: 8px;
width: calc(40px - 16px);
height: calc(40px - 16px);
margin-right: 16px;
margin-inline-end: 16px;
border-radius: 50%;
background: var(--ion-color-light-shade);
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"homepage": "https://docs.rdlabo.dev/projects/ionic-theme-md3",
"dependencies": {
"@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#main"
"@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#66eb2fccca10646ebfccdfb3b6c2f3b4c9b00bf6"
},
"devDependencies": {
"@ionic/angular": "^9.0.0",
Expand Down
6 changes: 5 additions & 1 deletion src/styles/components/ion-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,9 @@ ion-list.md:not(.md3-disabled).list-inset {
}

ion-list-header.md:not(.md3-disabled) {
padding-left: calc(var(--ion-safe-area-left, 0px) + 4px);
padding-inline-start: calc(var(--ion-safe-area-left, 0px) + 4px);

&:dir(rtl) {
padding-inline-start: calc(var(--ion-safe-area-right, 0px) + 4px);
}
}
43 changes: 28 additions & 15 deletions src/styles/components/ion-range.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ ion-range.md:not(.md3-disabled) {
}
}

&:dir(rtl)::part(knob) {
transform: translateX(calc((var(--knob-size) / 2 - 2px) * -1));
}

&.ion-color {
--knob-background: var(--ion-color-base, var(--ion-color-primary));
}
Expand All @@ -42,6 +46,10 @@ ion-range.md:not(.md3-disabled) {
transform: translateX(calc(var(--knob-size) / 2 - 1px));
}

&.range-pressed:dir(rtl)::part(knob) {
transform: translateX(calc((var(--knob-size) / 2 - 1px) * -1));
}

&:not(.range-value-min):not(.range-value-max)::part(knob) {
outline: 6px solid var(--ion-background-color, #ffffff);
}
Expand All @@ -50,27 +58,27 @@ ion-range.md:not(.md3-disabled) {
}

&:not(.range-value-max)::part(bar-active) {
border-top-right-radius: calc(var(--knob-border-radius) / 2);
border-bottom-right-radius: calc(var(--knob-border-radius) / 2);
margin-right: 6px;
border-start-end-radius: calc(var(--knob-border-radius) / 2);
border-end-end-radius: calc(var(--knob-border-radius) / 2);
margin-inline-end: 6px;
}

&.range-value-min {
&::part(bar) {
border-top-left-radius: calc(var(--knob-border-radius) / 2);
border-bottom-left-radius: calc(var(--knob-border-radius) / 2);
border-start-start-radius: calc(var(--knob-border-radius) / 2);
border-end-start-radius: calc(var(--knob-border-radius) / 2);
width: calc(100% - 6px);
right: 0;
inset-inline-end: 0;
}
}

&.range-value-max {
&::part(bar),
&::part(bar-active) {
border-top-right-radius: calc(var(--knob-border-radius) / 2);
border-bottom-right-radius: calc(var(--knob-border-radius) / 2);
border-start-end-radius: calc(var(--knob-border-radius) / 2);
border-end-end-radius: calc(var(--knob-border-radius) / 2);
width: calc(100% - 6px);
left: 0;
inset-inline-start: 0;
}
}
}
Expand All @@ -82,12 +90,17 @@ ion-range.md:not(.md3-disabled) {
transform: translateX(calc(var(--knob-size) / 2 - 1px));
}

&.range-pressed-a:dir(rtl)::part(knob-a),
&.range-pressed-b:dir(rtl)::part(knob-b) {
transform: translateX(calc((var(--knob-size) / 2 - 1px) * -1));
}

&.range-value-min {
&::part(bar),
&::part(bar-active) {
border-top-left-radius: calc(var(--knob-border-radius) / 2);
border-bottom-left-radius: calc(var(--knob-border-radius) / 2);
margin-left: 6px;
border-start-start-radius: calc(var(--knob-border-radius) / 2);
border-end-start-radius: calc(var(--knob-border-radius) / 2);
margin-inline-start: 6px;
}
&::part(bar) {
width: calc(100% - 6px);
Expand All @@ -96,9 +109,9 @@ ion-range.md:not(.md3-disabled) {
&.range-value-max {
&::part(bar),
&::part(bar-active) {
border-top-right-radius: calc(var(--knob-border-radius) / 2);
border-bottom-right-radius: calc(var(--knob-border-radius) / 2);
margin-right: 6px;
border-start-end-radius: calc(var(--knob-border-radius) / 2);
border-end-end-radius: calc(var(--knob-border-radius) / 2);
margin-inline-end: 6px;
}
&::part(bar) {
width: calc(100% - 6px);
Expand Down
7 changes: 3 additions & 4 deletions src/styles/components/ion-toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ ion-footer.md:not(.md3-disabled) {
ion-toolbar.md:not(.md3-disabled) {
ion-buttons[slot='start'],
ion-back-button[slot='start'] {
margin-left: 16px;
margin-inline-start: 16px;
}
ion-buttons[slot='end'],
ion-back-button[slot='end'] {
margin-right: 16px;
margin-inline-end: 16px;
}
/* fallback ion-buttons[slot=end], ion-back-button[slot=end] */
ion-buttons > ion-back-button {
margin-left: 0;
margin-right: 0;
margin-inline: 0;
}

&:has(> [slot='start']) ion-title {
Expand Down
11 changes: 6 additions & 5 deletions src/transition/md.transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { TransitionOptions } from './index';
import { getIonPageElement } from './index';

export const mdTransitionAnimation = (_: HTMLElement, opts: TransitionOptions): Animation => {
const OFF_RIGHT = 40;
const isRTL = opts.enteringEl.ownerDocument.dir === 'rtl';
const OFF_END = isRTL ? -40 : 40;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 要素単位の RTL と遷移方向の不一致

dir="rtl" が ion-app などに設定されると、:dir(rtl) は反映されても遷移は LTR のままです。文書ルート限定の RTL 契約か確認してください。

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const CENTER = 0;

const backDirection = opts.direction === 'back';
Expand All @@ -22,12 +23,12 @@ export const mdTransitionAnimation = (_: HTMLElement, opts: TransitionOptions):
rootTransition
.duration((opts.duration ?? 0) || 200)
.easing('cubic-bezier(0.47,0,0.745,0.715)')
.fromTo('transform', `translateX(${OFF_RIGHT * -1}px)`, `translateX(${CENTER}px)`);
.fromTo('transform', `translateX(${OFF_END * -1}px)`, `translateX(${CENTER}px)`);
} else {
rootTransition
.duration((opts.duration ?? 0) || 280)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.fromTo('transform', `translateX(${OFF_RIGHT}px)`, `translateX(${CENTER}px)`)
.fromTo('transform', `translateX(${OFF_END}px)`, `translateX(${CENTER}px)`)
.fromTo('opacity', 0.01, 1);
}

Expand All @@ -46,7 +47,7 @@ export const mdTransitionAnimation = (_: HTMLElement, opts: TransitionOptions):
stackedTransition
.duration((opts.duration ?? 0) || 280)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.fromTo('transform', `translateX(${CENTER}px)`, `translateX(${OFF_RIGHT * -1}px)`);
.fromTo('transform', `translateX(${CENTER}px)`, `translateX(${OFF_END * -1}px)`);
rootTransition.addAnimation(stackedTransition);
}

Expand All @@ -63,7 +64,7 @@ export const mdTransitionAnimation = (_: HTMLElement, opts: TransitionOptions):
leavingPage.elements[0].style.setProperty('display', 'none');
}
})
.fromTo('transform', `translateX(${CENTER}px)`, `translateX(${OFF_RIGHT}px)`)
.fromTo('transform', `translateX(${CENTER}px)`, `translateX(${OFF_END}px)`)
.fromTo('opacity', 1, 0);

rootTransition.addAnimation(leavingPage);
Expand Down
Loading