Skip to content

Commit 7d64307

Browse files
authored
merge release-8.7.15 (#30887)
v8.7.15
2 parents 8573bf8 + 4360e39 commit 7d64307

35 files changed

+418
-89
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/button/button.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
170170
*/
171171
@Watch('aria-checked')
172172
@Watch('aria-label')
173+
@Watch('aria-pressed')
173174
onAriaChanged(newValue: string, _oldValue: string, propName: string) {
174175
this.inheritedAttributes = {
175176
...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
@@ -126,9 +126,8 @@ export class InputPasswordToggle implements ComponentInterface {
126126
color={color}
127127
fill="clear"
128128
shape="round"
129-
aria-checked={isPasswordVisible ? 'true' : 'false'}
130129
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
131-
role="switch"
130+
aria-pressed={isPasswordVisible ? 'true' : 'false'}
132131
type="button"
133132
onPointerDown={(ev) => {
134133
/**

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
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Modal - Dismiss Behavior</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
</head>
16+
17+
<body>
18+
<ion-app>
19+
<div class="ion-page">
20+
<ion-header>
21+
<ion-toolbar>
22+
<ion-title>Modal - Dismiss Behavior</ion-title>
23+
</ion-toolbar>
24+
</ion-header>
25+
26+
<ion-content class="ion-padding">
27+
<button id="present-first-modal" onclick="presentFirstModal()">Present Modal</button>
28+
</ion-content>
29+
</div>
30+
</ion-app>
31+
32+
<script type="module">
33+
import { modalController } from '../../../../../dist/ionic/index.esm.js';
34+
35+
window.modalController = modalController;
36+
37+
const sharedId = 'shared-modal-id';
38+
const maxModals = 5;
39+
let modalCount = 0;
40+
41+
function createModalComponent(modalNumber) {
42+
const element = document.createElement('div');
43+
const canPresentNext = modalNumber < maxModals;
44+
const presentNextButton = canPresentNext
45+
? `<ion-button id="present-next-modal" onclick="presentNextModal(${modalNumber + 1})">Present Modal ${
46+
modalNumber + 1
47+
}</ion-button>`
48+
: '';
49+
element.innerHTML = `
50+
<ion-header>
51+
<ion-toolbar>
52+
<ion-title>Modal ${modalNumber}</ion-title>
53+
</ion-toolbar>
54+
</ion-header>
55+
<ion-content class="ion-padding">
56+
<p>This is modal number ${modalNumber}</p>
57+
${presentNextButton}
58+
<ion-button class="dismiss-by-id">Dismiss By ID</ion-button>
59+
<ion-button class="dismiss-default">Dismiss Default</ion-button>
60+
</ion-content>
61+
`;
62+
return element;
63+
}
64+
65+
window.presentFirstModal = async () => {
66+
modalCount = 0;
67+
await presentNextModal(1);
68+
};
69+
70+
window.presentNextModal = async (modalNumber) => {
71+
if (modalNumber > maxModals) {
72+
return;
73+
}
74+
modalCount = Math.max(modalCount, modalNumber);
75+
const element = createModalComponent(modalNumber);
76+
const modal = await modalController.create({
77+
component: element,
78+
htmlAttributes: {
79+
id: sharedId,
80+
'data-testid': `modal-${modalNumber}`,
81+
},
82+
});
83+
await modal.present();
84+
85+
const dismissByIdButton = element.querySelector('ion-button.dismiss-by-id');
86+
dismissByIdButton.addEventListener('click', () => {
87+
modalController.dismiss(undefined, undefined, sharedId);
88+
});
89+
90+
const dismissDefaultButton = element.querySelector('ion-button.dismiss-default');
91+
dismissDefaultButton.addEventListener('click', () => {
92+
modalController.dismiss();
93+
});
94+
};
95+
</script>
96+
</body>
97+
</html>

0 commit comments

Comments
 (0)