Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c099224
feat(tokens): introduce Material Design 3 token foundation
rdlabo Sep 16, 2026
4291e80
feat(components): align Button and Toolbar with MD3 tokens
rdlabo Sep 16, 2026
fe0e8dd
feat(components): tokenize Card, Chip and FAB
rdlabo Sep 16, 2026
b1783cc
feat(components): tokenize Searchbar, Segment, Tabs and Toggle
rdlabo Sep 16, 2026
69f55d2
feat(components): tokenize List and Range
rdlabo Sep 16, 2026
595acb3
feat(components): tokenize overlays (Alert, Action Sheet, Loading, Mo…
rdlabo Sep 16, 2026
c32dc34
feat(components): set Content and DateTime surface roles
rdlabo Sep 16, 2026
5a83ef7
fix: respect Ionic `color` prop and restore legacy elevation tokens
rdlabo Sep 16, 2026
91d8809
Merge main
rdlabo Sep 16, 2026
960073f
fix(button): keep iOS shape and use MD3 color/typography
rdlabo Sep 16, 2026
1784885
fix(list): restore iOS list group divider color
rdlabo Sep 16, 2026
8d56597
fix(content): use default Ionic background instead of surface
rdlabo Sep 16, 2026
edfcdd0
fix(components): align MD3 token migration with Ionic shape/defaults
rdlabo Sep 16, 2026
2e1d177
chore: update E2E screenshots
github-actions[bot] Sep 16, 2026
bfc7cd2
fix(list): inset list divider matches item background
rdlabo Sep 16, 2026
1851e7a
fix(range): restore Ionic slider geometry while keeping MD3 colors
rdlabo Sep 16, 2026
d111463
test(rtl): update range geometry assertions for default Ionic shape
rdlabo Sep 16, 2026
21e992a
test(rtl): expect used left/right pixel values for range handle
rdlabo Sep 16, 2026
34bcc3e
chore: update E2E screenshots
github-actions[bot] Sep 16, 2026
4d5c081
feat(action-sheet, alert): add role='preferred' styling and demo exam…
rdlabo Sep 16, 2026
d704faf
chore: update E2E screenshots
github-actions[bot] Sep 16, 2026
6b80372
fix(alert): remove focus outline from alert wrapper
rdlabo Sep 16, 2026
664f2ff
chore: update E2E screenshots
github-actions[bot] Sep 16, 2026
5a8426f
fix(action-sheet): set --color on action-sheet buttons for icon tint
rdlabo Sep 16, 2026
0efd136
chore: update E2E screenshots
github-actions[bot] Sep 16, 2026
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
28 changes: 12 additions & 16 deletions demo/e2e/rtl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ for (const direction of ['ltr', 'rtl'] as const) {
const fixture = document.createElement('div');
fixture.id = 'rtl-range-probe';
fixture.dir = dir;
fixture.style.width = '200px';
fixture.style.position = 'relative';

const range = document.createElement('ion-range');
range.id = 'single-range';
Expand All @@ -30,28 +32,22 @@ for (const direction of ['ltr', 'rtl'] as const) {
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);
// The knob handle is positioned from the inline-start edge by the value
// ratio, so LTR uses left: 50% and RTL uses right: 50%.
const handleSide = direction === 'ltr' ? 'left' : 'right';
const handlePosition = await page.locator('#single-range').evaluate((range, side) => {
const handle = range.shadowRoot!.querySelector<HTMLElement>('[part~="knob-handle"]')!;
return getComputedStyle(handle)[side];
}, handleSide);
expect(handlePosition).toBe('100px');

// With the default Ionic range geometry the bar has no extra inline margins.
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' });
expect(dualMargins).toEqual({ start: '0px', end: '0px' });
});

test(`${direction} mirrors Material forward and back transitions`, async ({ page }) => {
Expand Down
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/accordion-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/accordion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/action-sheet-all-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/action-sheet-all.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/action-sheet-no-cancel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-all-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-all.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-button-only-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-button-only.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-no-cancel-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-no-cancel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-remove-app-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/alert-remove-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/breadcrumbs-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/breadcrumbs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/button-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/card-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/checkbox-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/checkbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/chip-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/chip.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/date-and-time-pickers.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/floating-action-button.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/inputs.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/item-list-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/item-list.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/modal-card-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/modal-card.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/modal-normal-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/modal-normal.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/popover-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/popover.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/radio-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/radio.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/range-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/range.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/reorder-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/reorder.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/searchbar-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/searchbar.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/segment-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/segment.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-alert-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-alert.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-modal-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-modal.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-popover-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/select-popover.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/tabs-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/tabs.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-anchor-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-anchor.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-middle-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-middle.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-top-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toast-top.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toggle-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toggle.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toolbar-dark.png
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/toolbar.png
8 changes: 8 additions & 0 deletions demo/src/app/index/pages/action-sheet/action-sheet.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export const actionSheetUtil = () => ({
action: 'delete',
},
},
{
text: 'Confirm',
role: 'preferred',
icon: 'checkmark-outline',
data: {
action: 'confirm',
},
},
{
text: 'Share',
icon: 'share-outline',
Expand Down
19 changes: 16 additions & 3 deletions demo/src/app/index/pages/alert/alert.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,25 @@ export class AlertPage implements OnInit {
subHeader: undefined,
message: 'Removing from Home Screen will keep the app in your App Library',
buttons: [
...alertUtil().buttons,
{
text: 'Cancel',
role: 'cancel',
handler: () => {
console.log('Alert canceled');
},
},
{
text: 'Keep',
role: 'preferred',
handler: () => {
console.log('Alert kept');
},
},
{
text: 'Remove from Home Screen',
role: 'confirm',
role: 'destructive',
handler: () => {
console.log('Alert confirmed');
console.log('Alert removed');
},
},
],
Expand Down
11 changes: 9 additions & 2 deletions demo/src/app/index/pages/alert/alert.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ export const alertUtil = () => ({
},
},
{
text: 'OK',
role: 'destructive',
text: 'Confirm',
role: 'preferred',
handler: () => {
console.log('Alert confirmed');
},
},
{
text: 'Delete',
role: 'destructive',
handler: () => {
console.log('Alert deleted');
},
},
],
});
19 changes: 19 additions & 0 deletions src/styles/components/ion-action-sheet.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
ion-action-sheet.md:not(.md3-disabled) {
--max-width: 640px;
--background: var(--md-sys-color-surface-container-low);
--color: var(--md-sys-color-on-surface);

.action-sheet-container {
overflow: hidden;
Expand All @@ -12,6 +14,23 @@ ion-action-sheet.md:not(.md3-disabled) {

.action-sheet-button {
padding: 12px 24px;

&.action-sheet-destructive {
--button-color: var(--md-sys-color-error);
--color: var(--md-sys-color-error);
}

&.action-sheet-preferred {
--button-color: var(--md-sys-color-on-primary);
--color: var(--md-sys-color-on-primary);
--button-background: var(--md-sys-color-primary);
--button-background-activated: var(--md-sys-color-on-primary);
--button-background-activated-opacity: 0.12;
--button-background-hover: var(--md-sys-color-on-primary);
--button-background-hover-opacity: 0.08;
--button-background-focused: var(--md-sys-color-on-primary);
--button-background-focused-opacity: 0.12;
}
}
}
}
39 changes: 38 additions & 1 deletion src/styles/components/ion-alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ ion-alert.md:not(.md3-disabled) {
--backdrop-opacity: 0.2;
--min-width: 280px;
--max-width: min(560px, calc(100% - 48px * 2));
transition: transform 200ms ease;

transition: transform var(--md-sys-motion-duration-short-3) var(--md-sys-motion-easing-emphasized-decelerate);

.alert-wrapper {
border-radius: 24px;
box-shadow: var(--token-elevation-3);
background: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
outline: none;

.alert-head {
padding-top: 24px;
Expand All @@ -15,9 +19,42 @@ ion-alert.md:not(.md3-disabled) {

.alert-message {
padding-bottom: 24px;

&:empty {
padding-bottom: 0;
}
}

.alert-button {
color: var(--md-sys-color-primary);

&.alert-button-role-destructive {
color: var(--md-sys-color-error);
}

&.alert-button-role-preferred {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 999px;
text-transform: none;

&:hover {
background: color-mix(
in srgb,
var(--md-sys-color-on-primary) var(--md-sys-state-hover-state-layer-opacity),
var(--md-sys-color-primary)
);
}

&.ion-activated,
&.ion-focused {
background: color-mix(
in srgb,
var(--md-sys-color-on-primary) var(--md-sys-state-pressed-state-layer-opacity),
var(--md-sys-color-primary)
);
}
}
}
}
}
92 changes: 82 additions & 10 deletions src/styles/components/ion-button.scss
Original file line number Diff line number Diff line change
@@ -1,104 +1,176 @@
@mixin theme-button() {
// Default is be round button
// Default is a round/pill button. Match the iOS/Ionic shape.
--box-shadow: none;
--padding-start: 16px;
--padding-end: 16px;
--border-radius: 999px;

// Keep both selectors so Ionic's :not(.button-round) rules don't override.
&,
&:not(.button-round) {
/**
* for remove default style:
* .sc-ion-buttons-md-s ion-button:not(.button-round) {
* --border-radius: 2px;
* }
*/
--border-radius: 999px;
}

// Typography uses M3 label-large / sentence case.
font-size: var(--md-sys-typescale-label-large-size);
font-weight: var(--md-sys-typescale-label-large-weight);
letter-spacing: var(--md-sys-typescale-label-large-tracking);
text-transform: none;

// Default fill is "solid" in Ionic, which maps to M3 "filled".
--background: var(--md-sys-color-primary);
--color: var(--md-sys-color-on-primary);
Comment on lines +21 to +22

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.

🟡 Ionic の色指定が MD3 の固定色に置き換わる

color 属性付きボタンにも primary 背景と文字色が常に適用されます。同じ固定指定を持つ toolbar、card、chip、content、FAB、toggle でも指定色が表示されません。

Learn more

Ionic は color 属性から .ion-color--ion-color-base / --ion-color-contrast を設定します。今回、文書側のコンポーネント規則が --background--color を直接指定するため、その公開 API より MD3 の固定ロールが優先されます。デモにも color="success" の button と toggle、color="primary" の toolbar と card など、影響を受ける既存利用があります。

Example: <ion-button color="success" fill="solid">Save</ion-button> は success 色になるべきですが、常に --md-sys-color-primary--md-sys-color-on-primary で描画されます。

Recommended fix: 各コンポーネントで .ion-color の場合は --ion-color-base / --ion-color-contrast を使う上書きを追加してください。button の outline/clear、toggle の checked 状態、toolbar、card、chip、content、FAB など、固定ロールを設定した全コンポーネントを同じ方針で監査してください。

Devin Review

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

--background-hover: var(--md-sys-color-on-primary);
--background-hover-opacity: var(--md-sys-state-hover-state-layer-opacity);
--background-focused: var(--md-sys-color-on-primary);
--background-focused-opacity: var(--md-sys-state-focus-state-layer-opacity);
--background-activated: var(--md-sys-color-on-primary);
--background-activated-opacity: var(--md-sys-state-pressed-state-layer-opacity);

&::part(native) {
height: 40px;
}

ion-icon {
font-size: 24px;

&[slot='start'] {
margin-inline-start: 0;
margin-inline-end: 8px;
}

&[slot='end'] {
margin-inline-start: 8px;
margin-inline-end: 0;
}
}

// Small size (32dp)
&.button-small {
--padding-start: 12px;
--padding-end: 12px;

font-size: var(--md-sys-typescale-label-medium-size);
font-weight: var(--md-sys-typescale-label-medium-weight);
letter-spacing: var(--md-sys-typescale-label-medium-tracking);

&::part(native) {
height: 32px;
}

&.button-has-icon-only {
width: 32px;
height: 32px;
}

&.button-has-icon-only::part(native) {
width: 32px;
height: 32px;
}

ion-icon {
font-size: 20px;

&[slot='start'] {
margin-inline-start: 0;
margin-inline-end: 4px;
}

&[slot='end'] {
margin-inline-start: 4px;
margin-inline-end: 0;
}
}
}

// Large size (56dp)
&.button-large {
--padding-start: 24px;
--padding-end: 24px;

&::part(native) {
height: 56px;
}

&.button-has-icon-only {
width: 56px;
height: 56px;
}

&.button-has-icon-only::part(native) {
width: 56px;
height: 56px;
}

ion-icon {
font-size: 24px;

&[slot='start'] {
margin-inline-start: 0;
margin-inline-end: 8px;
}

&[slot='end'] {
margin-inline-start: 8px;
margin-inline-end: 0;
}
}
}

// Ionic "outline" maps to M3 "outlined".
&.button-outline {
--background: transparent;
--color: var(--md-sys-color-primary);
--border-width: 1px;
--border-style: solid;
--border-color: var(--md-sys-color-outline);
--background-hover: var(--md-sys-color-primary);
--background-focused: var(--md-sys-color-primary);
--background-activated: var(--md-sys-color-primary);
}

// Ionic "clear" maps to M3 "text".
&.button-clear {
--background: transparent;
--color: var(--md-sys-color-primary);
--border-width: 0;
--background-hover: var(--md-sys-color-primary);
--background-focused: var(--md-sys-color-primary);
--background-activated: var(--md-sys-color-primary);
}

// Icon-only buttons. Keep the iOS circular 40x40 with a 24dp icon.
&.button-has-icon-only {
&::part(native) {
width: 40px;
height: 40px;
}

--padding-start: 0;
--padding-end: 0;
}

// Square shape option. Match the iOS squared-button radius.
&.button-square {
--border-radius: 8px;
&.button-square {
--border-radius: 12px;
--border-radius: 12px;
}

// Respect the Ionic `color` prop on any M3 variant.
&.ion-color {
--background: var(--ion-color-base);
--color: var(--ion-color-contrast);
--border-color: var(--ion-color-base);
--background-hover: var(--ion-color-contrast);
--background-focused: var(--ion-color-contrast);
--background-activated: var(--ion-color-contrast);

// Outlined / text buttons have a transparent background, so the state
// layer should be the user color itself, not its contrast.
&.button-outline,
&.button-clear {
--background-hover: var(--ion-color-base);
--background-focused: var(--ion-color-base);
--background-activated: var(--ion-color-base);
}
}
}
Expand Down
Loading