Skip to content

Commit 5b9a0bb

Browse files
committed
Merge branch 'main' into fix/merge-next-12-23
2 parents e227fd9 + 72826ed commit 5b9a0bb

37 files changed

+480
-103
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
7+
8+
9+
### Bug Fixes
10+
11+
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
12+
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
13+
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
14+
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
15+
16+
17+
18+
19+
620
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
721

822

core/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [8.7.15](https://github.com/ionic-team/ionic-framework/compare/v8.7.14...v8.7.15) (2025-12-23)
7+
8+
9+
### Bug Fixes
10+
11+
* **core:** use Capacitor safe-area CSS variables on older WebViews ([#30865](https://github.com/ionic-team/ionic-framework/issues/30865)) ([8573bf8](https://github.com/ionic-team/ionic-framework/commit/8573bf8083f75eda13c954a56731a6aac8ca5724))
12+
* **header:** show iOS condense header when app is in MD mode ([#30690](https://github.com/ionic-team/ionic-framework/issues/30690)) ([f83b000](https://github.com/ionic-team/ionic-framework/commit/f83b0005309400d674e43c497bdffbcb9d2c4d94)), closes [#29929](https://github.com/ionic-team/ionic-framework/issues/29929)
13+
* **input-password-toggle:** improve screen reader announcements ([#30885](https://github.com/ionic-team/ionic-framework/issues/30885)) ([12ede4b](https://github.com/ionic-team/ionic-framework/commit/12ede4b79c8d5cffc2b014c7c8a0d2ef1d3bf90d))
14+
* **modal:** dismiss top-most overlay when multiple IDs match ([#30883](https://github.com/ionic-team/ionic-framework/issues/30883)) ([3b60a1d](https://github.com/ionic-team/ionic-framework/commit/3b60a1d68a1df1606ffee0bde7db7a206bac404a)), closes [#30030](https://github.com/ionic-team/ionic-framework/issues/30030)
15+
16+
17+
18+
19+
620
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
721

822
**Note:** Version bump only for package @ionic/core

core/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ionic/core",
3-
"version": "8.7.14",
3+
"version": "8.7.15",
44
"description": "Base components for Ionic",
55
"engines": {
66
"node": ">= 16"

core/src/components/app/test/safe-area/app.e2e.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,66 @@ configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ co
1818

1919
await expect(page).toHaveScreenshot(screenshot(`app-${screenshotModifier}-diff`));
2020
};
21+
2122
test.beforeEach(async ({ page }) => {
2223
await page.goto(`/src/components/app/test/safe-area`, config);
2324
});
24-
test('should not have visual regressions with action sheet', async ({ page }) => {
25-
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
26-
});
27-
test('should not have visual regressions with menu', async ({ page }) => {
28-
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
29-
});
30-
test('should not have visual regressions with picker', async ({ page }) => {
31-
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
25+
26+
test.describe(title('Ionic safe area variables'), () => {
27+
test.beforeEach(async ({ page }) => {
28+
const htmlTag = page.locator('html');
29+
const hasSafeAreaClass = await htmlTag.evaluate((el) => el.classList.contains('safe-area'));
30+
31+
expect(hasSafeAreaClass).toBe(true);
32+
});
33+
34+
test('should not have visual regressions with action sheet', async ({ page }) => {
35+
await testOverlay(page, '#show-action-sheet', 'ionActionSheetDidPresent', 'action-sheet');
36+
});
37+
test('should not have visual regressions with menu', async ({ page }) => {
38+
await testOverlay(page, '#show-menu', 'ionDidOpen', 'menu');
39+
});
40+
test('should not have visual regressions with picker', async ({ page }) => {
41+
await testOverlay(page, '#show-picker', 'ionPickerDidPresent', 'picker');
42+
});
43+
test('should not have visual regressions with toast', async ({ page }) => {
44+
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
45+
});
3246
});
33-
test('should not have visual regressions with toast', async ({ page }) => {
34-
await testOverlay(page, '#show-toast', 'ionToastDidPresent', 'toast');
47+
48+
test.describe(title('Capacitor safe area variables'), () => {
49+
test('should use safe-area-inset vars when safe-area class is not defined', async ({ page }) => {
50+
await page.evaluate(() => {
51+
const html = document.documentElement;
52+
53+
// Remove the safe area class
54+
html.classList.remove('safe-area');
55+
56+
// Set the safe area inset variables
57+
html.style.setProperty('--safe-area-inset-top', '10px');
58+
html.style.setProperty('--safe-area-inset-bottom', '20px');
59+
html.style.setProperty('--safe-area-inset-left', '30px');
60+
html.style.setProperty('--safe-area-inset-right', '40px');
61+
});
62+
63+
const top = await page.evaluate(() =>
64+
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-top').trim()
65+
);
66+
const bottom = await page.evaluate(() =>
67+
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-bottom').trim()
68+
);
69+
const left = await page.evaluate(() =>
70+
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-left').trim()
71+
);
72+
const right = await page.evaluate(() =>
73+
getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-right').trim()
74+
);
75+
76+
expect(top).toBe('10px');
77+
expect(bottom).toBe('20px');
78+
expect(left).toBe('30px');
79+
expect(right).toBe('40px');
80+
});
3581
});
3682
});
3783
});

core/src/components/button/button.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
174174
*/
175175
@Watch('aria-checked')
176176
@Watch('aria-label')
177+
@Watch('aria-pressed')
177178
onAriaChanged(newValue: string, _oldValue: string, propName: string) {
178179
this.inheritedAttributes = {
179180
...this.inheritedAttributes,

core/src/components/header/header.md.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
box-shadow: $header-md-box-shadow;
99
}
1010

11-
.header-collapse-condense {
11+
.header-md.header-collapse-condense {
1212
display: none;
1313
}
1414

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* This test verifies that collapsible headers with mode="ios" work correctly
6+
* when both iOS and MD stylesheets are loaded. The bug occurred because
7+
* `.header-collapse-condense { display: none }` in the MD stylesheet was not
8+
* scoped to `.header-md`, causing it to hide iOS condense headers when both
9+
* stylesheets were present.
10+
*/
11+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
12+
test.describe(title('header: condense with iOS mode override'), () => {
13+
test('should show iOS condense header when both MD and iOS styles are loaded', async ({ page }) => {
14+
test.info().annotations.push({
15+
type: 'issue',
16+
description: 'https://github.com/ionic-team/ionic-framework/issues/29929',
17+
});
18+
19+
// Include both an MD header and an iOS modal to force both stylesheets to load
20+
await page.setContent(
21+
`
22+
<!-- MD header to force MD stylesheet to load -->
23+
<ion-header mode="md" id="mdHeader">
24+
<ion-toolbar>
25+
<ion-title>MD Header</ion-title>
26+
</ion-toolbar>
27+
</ion-header>
28+
29+
<!-- Modal with iOS condense header -->
30+
<ion-modal>
31+
<ion-header mode="ios" id="smallTitleHeader">
32+
<ion-toolbar>
33+
<ion-title>Header</ion-title>
34+
</ion-toolbar>
35+
</ion-header>
36+
<ion-content fullscreen="true">
37+
<ion-header collapse="condense" mode="ios" id="largeTitleHeader">
38+
<ion-toolbar>
39+
<ion-title size="large">Large Header</ion-title>
40+
</ion-toolbar>
41+
</ion-header>
42+
<p>Content</p>
43+
</ion-content>
44+
</ion-modal>
45+
`,
46+
config
47+
);
48+
49+
const modal = page.locator('ion-modal');
50+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
51+
52+
await modal.evaluate((el: HTMLIonModalElement) => el.present());
53+
await ionModalDidPresent.next();
54+
55+
const largeTitleHeader = modal.locator('#largeTitleHeader');
56+
57+
// The large title header should be visible, not hidden by MD styles
58+
await expect(largeTitleHeader).toBeVisible();
59+
60+
// Verify it has the iOS mode class
61+
await expect(largeTitleHeader).toHaveClass(/header-ios/);
62+
63+
// Verify it does NOT have display: none applied
64+
// This would fail if the MD stylesheet's unscoped .header-collapse-condense rule applies
65+
const display = await largeTitleHeader.evaluate((el) => {
66+
return window.getComputedStyle(el).display;
67+
});
68+
expect(display).not.toBe('none');
69+
});
70+
});
71+
});

core/src/components/input-password-toggle/input-password-toggle.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,8 @@ export class InputPasswordToggle implements ComponentInterface {
169169
color={color}
170170
fill="clear"
171171
shape="round"
172-
aria-checked={isPasswordVisible ? 'true' : 'false'}
173172
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
174-
role="switch"
173+
aria-pressed={isPasswordVisible ? 'true' : 'false'}
175174
type="button"
176175
onPointerDown={(ev) => {
177176
/**

core/src/components/input-password-toggle/test/a11y/input-password-toggle.e2e.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
2222
});
2323

2424
test.describe(title('input password toggle: aria attributes'), () => {
25-
test('should inherit aria attributes to inner button on load', async ({ page }) => {
25+
test('should have correct aria attributes on load', async ({ page }) => {
2626
await page.setContent(
2727
`
2828
<ion-input label="input" type="password">
@@ -35,9 +35,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
3535
const nativeButton = page.locator('ion-input-password-toggle button');
3636

3737
await expect(nativeButton).toHaveAttribute('aria-label', 'Show password');
38-
await expect(nativeButton).toHaveAttribute('aria-checked', 'false');
38+
await expect(nativeButton).toHaveAttribute('aria-pressed', 'false');
3939
});
40-
test('should inherit aria attributes to inner button after toggle', async ({ page }) => {
40+
test('should update aria attributes after toggle', async ({ page }) => {
4141
await page.setContent(
4242
`
4343
<ion-input label="input" type="password">
@@ -51,7 +51,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
5151
await nativeButton.click();
5252

5353
await expect(nativeButton).toHaveAttribute('aria-label', 'Hide password');
54-
await expect(nativeButton).toHaveAttribute('aria-checked', 'true');
54+
await expect(nativeButton).toHaveAttribute('aria-pressed', 'true');
5555
});
5656
});
5757
});

0 commit comments

Comments
 (0)