Compare commits

..

4 Commits

50 changed files with 234 additions and 755 deletions

View File

@@ -10,7 +10,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v6
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -62,7 +62,7 @@ runs:
working-directory: ./core
- name: 📦 Archive Updated Screenshots
if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip

View File

@@ -10,7 +10,7 @@ runs:
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 24.x
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v6
with:
path: ./artifacts
- name: 🔎 Extract Archives

View File

@@ -13,7 +13,7 @@ runs:
- name: 🗄️ Create Archive
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
shell: bash
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v5
with:
name: ${{ inputs.name }}
path: ${{ inputs.output }}

View File

@@ -3,25 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package ionic-framework
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/core
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/core
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)

45
core/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -52,7 +52,7 @@
"stylelint-order": "^4.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"custom-rules": {
@@ -94,6 +94,7 @@
"version": "7.16.12",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.16.8",
@@ -633,6 +634,7 @@
"integrity": "sha512-250HTVd/W/KdMygoqaedisvNbHbpbQTN2Hy/8ZYGm1nAqE0Fx7sGss4l0nDg33STxEdDhtVRoL2fIaaiukKseA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -870,6 +872,7 @@
"version": "4.33.0",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "4.33.0",
"@typescript-eslint/types": "4.33.0",
@@ -1807,6 +1810,7 @@
"node_modules/@stencil/core": {
"version": "4.38.0",
"license": "MIT",
"peer": true,
"bin": {
"stencil": "bin/stencil"
},
@@ -2231,6 +2235,7 @@
"version": "6.7.2",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.7.2",
"@typescript-eslint/types": "6.7.2",
@@ -2456,7 +2461,6 @@
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.25",
@@ -2471,7 +2475,6 @@
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.12"
},
@@ -2484,8 +2487,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.25",
@@ -2493,7 +2495,6 @@
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.5.25",
"@vue/shared": "3.5.25"
@@ -2505,7 +2506,6 @@
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.25",
@@ -2523,8 +2523,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
"version": "8.5.6",
@@ -2546,7 +2545,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -2562,7 +2560,6 @@
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/shared": "3.5.25"
@@ -2574,7 +2571,6 @@
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.25"
}
@@ -2585,7 +2581,6 @@
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/shared": "3.5.25"
@@ -2597,7 +2592,6 @@
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/runtime-core": "3.5.25",
@@ -2611,7 +2605,6 @@
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.25",
"@vue/shared": "3.5.25"
@@ -2625,8 +2618,7 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@zeit/schemas": {
"version": "2.21.0",
@@ -2649,6 +2641,7 @@
"version": "7.4.0",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3809,8 +3802,7 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
@@ -4104,6 +4096,7 @@
"version": "7.32.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.3",
@@ -7299,7 +7292,6 @@
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
@@ -7621,7 +7613,6 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -7976,6 +7967,7 @@
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"playwright-core": "cli.js"
},
@@ -7987,6 +7979,7 @@
"version": "7.0.35",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
@@ -8092,6 +8085,7 @@
"version": "0.36.2",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"postcss": ">=5.0.0"
}
@@ -8140,6 +8134,7 @@
"version": "2.6.1",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -8497,6 +8492,7 @@
"version": "2.35.1",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -8718,7 +8714,6 @@
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9839,4 +9834,4 @@
}
}
}
}
}

View File

@@ -1,9 +1,9 @@
{
"name": "@ionic/core",
"version": "8.7.14",
"version": "8.7.12",
"description": "Base components for Ionic",
"engines": {
"node": ">= 16"
"node": "24.x"
},
"keywords": [
"ionic",

View File

@@ -21,6 +21,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
);
const datetimeButton = page.locator('ion-datetime-button');
await page.locator('.datetime-ready').waitFor();
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale`));
});
@@ -40,6 +41,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
);
const datetimeButton = page.locator('ion-datetime-button');
await page.locator('.datetime-ready').waitFor();
await expect(datetimeButton).toHaveScreenshot(screenshot(`datetime-button-scale-wrap`));
});

View File

@@ -24,6 +24,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await dateButton.click();
await ionModalDidPresent.next();
// Wait for datetime to be ready before taking screenshot
await page.locator('ion-datetime.datetime-ready').waitFor();
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-modal`));
});
@@ -44,6 +47,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await dateButton.click();
await ionPopoverDidPresent.next();
// Wait for datetime to be ready before taking screenshot
await page.locator('ion-datetime.datetime-ready').waitFor();
await expect(page).toHaveScreenshot(screenshot(`datetime-overlay-popover`));
});
});

View File

@@ -108,8 +108,8 @@ export class Datetime implements ComponentInterface {
private inputId = `ion-dt-${datetimeIds++}`;
private calendarBodyRef?: HTMLElement;
private popoverRef?: HTMLIonPopoverElement;
private intersectionTrackerRef?: HTMLElement;
private clearFocusVisible?: () => void;
private resizeObserver?: ResizeObserver;
private parsedMinuteValues?: number[];
private parsedHourValues?: number[];
private parsedMonthValues?: number[];
@@ -118,6 +118,7 @@ export class Datetime implements ComponentInterface {
private destroyCalendarListener?: () => void;
private destroyKeyboardMO?: () => void;
private destroyOverlayListeners?: () => void;
// TODO(FW-2832): types (DatetimeParts causes some errors that need untangling)
private minParts?: any;
@@ -1077,6 +1078,14 @@ export class Datetime implements ComponentInterface {
this.clearFocusVisible();
this.clearFocusVisible = undefined;
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
if (this.destroyOverlayListeners) {
this.destroyOverlayListeners();
this.destroyOverlayListeners = undefined;
}
}
/**
@@ -1102,113 +1111,100 @@ export class Datetime implements ComponentInterface {
}
/**
* TODO(FW-6931): Remove this fallback upon solving the root cause
* Fallback to ensure the datetime becomes ready even if
* IntersectionObserver never reports it as intersecting.
* Sets up visibility detection for the datetime component.
*
* This is primarily used in environments where the observer
* might not fire as expected, such as when running under
* synthetic tests that stub IntersectionObserver.
* Uses multiple strategies to reliably detect when the datetime becomes
* visible, which is necessary for proper initialization of scrollable areas:
* 1. ResizeObserver - detects dimension changes
* 2. Overlay event listeners - for datetime inside modals/popovers
* 3. Polling fallback - for browsers where observers are unreliable (WebKit)
*/
private ensureReadyIfVisible = () => {
if (this.el.classList.contains('datetime-ready')) {
return;
private initializeVisibilityObserver() {
const { el } = this;
const markReady = () => {
if (el.classList.contains('datetime-ready')) {
return;
}
this.initializeListeners();
writeTask(() => {
el.classList.add('datetime-ready');
});
};
const markHidden = () => {
this.destroyInteractionListeners();
this.showMonthAndYear = false;
writeTask(() => {
el.classList.remove('datetime-ready');
});
startVisibilityPolling();
};
/**
* FW-6931: Poll for visibility as a fallback for browsers where
* ResizeObserver doesn't fire reliably (e.g., WebKit).
*/
const startVisibilityPolling = () => {
let pollCount = 0;
const poll = () => {
if (el.classList.contains('datetime-ready') || pollCount++ >= 60) {
return;
}
const { width, height } = el.getBoundingClientRect();
if (width > 0 && height > 0) {
markReady();
} else {
raf(poll);
}
};
raf(poll);
};
/**
* FW-6931: Listen for overlay present/dismiss events when datetime
* is inside a modal or popover.
*/
const parentOverlay = el.closest('ion-modal, ion-popover') as HTMLIonModalElement | HTMLIonPopoverElement | null;
if (parentOverlay) {
const handlePresent = () => markReady();
const handleDismiss = () => markHidden();
parentOverlay.addEventListener('didPresent', handlePresent);
parentOverlay.addEventListener('didDismiss', handleDismiss);
this.destroyOverlayListeners = () => {
parentOverlay.removeEventListener('didPresent', handlePresent);
parentOverlay.removeEventListener('didDismiss', handleDismiss);
};
}
const rect = this.el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return;
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
const isVisible = width > 0 && height > 0;
const isReady = el.classList.contains('datetime-ready');
if (isVisible && !isReady) {
markReady();
} else if (!isVisible && isReady) {
markHidden();
}
});
// Use raf to avoid race condition with modal/popover animations
raf(() => this.resizeObserver?.observe(el));
startVisibilityPolling();
} else {
// Test environment fallback - mark ready immediately
writeTask(() => {
el.classList.add('datetime-ready');
});
}
this.initializeListeners();
writeTask(() => {
this.el.classList.add('datetime-ready');
});
};
}
componentDidLoad() {
const { el, intersectionTrackerRef } = this;
/**
* If a scrollable element is hidden using `display: none`,
* it will not have a scroll height meaning we cannot scroll elements
* into view. As a result, we will need to wait for the datetime to become
* visible if used inside of a modal or a popover otherwise the scrollable
* areas will not have the correct values snapped into place.
*/
const visibleCallback = (entries: IntersectionObserverEntry[]) => {
const ev = entries[0];
if (!ev.isIntersecting) {
return;
}
this.initializeListeners();
/**
* TODO FW-2793: Datetime needs a frame to ensure that it
* can properly scroll contents into view. As a result
* we hide the scrollable content until after that frame
* so users do not see the content quickly shifting. The downside
* is that the content will pop into view a frame after. Maybe there
* is a better way to handle this?
*/
writeTask(() => {
this.el.classList.add('datetime-ready');
});
};
const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el });
/**
* Use raf to avoid a race condition between the component loading and
* its display animation starting (such as when shown in a modal). This
* could cause the datetime to start at a visibility of 0, erroneously
* triggering the `hiddenIO` observer below.
*/
raf(() => visibleIO?.observe(intersectionTrackerRef!));
/**
* TODO(FW-6931): Remove this fallback upon solving the root cause
* Fallback: If IntersectionObserver never reports that the
* datetime is visible but the host clearly has layout, ensure
* we still initialize listeners and mark the component as ready.
*
* We schedule this after everything has had a chance to run.
*/
setTimeout(() => {
this.ensureReadyIfVisible();
}, 100);
/**
* We need to clean up listeners when the datetime is hidden
* in a popover/modal so that we can properly scroll containers
* back into view if they are re-presented. When the datetime is hidden
* the scroll areas have scroll widths/heights of 0px, so any snapping
* we did originally has been lost.
*/
const hiddenCallback = (entries: IntersectionObserverEntry[]) => {
const ev = entries[0];
if (ev.isIntersecting) {
return;
}
this.destroyInteractionListeners();
/**
* When datetime is hidden, we need to make sure that
* the month/year picker is closed. Otherwise,
* it will be open when the datetime re-appears
* and the scroll area of the calendar grid will be 0.
* As a result, the wrong month will be shown.
*/
this.showMonthAndYear = false;
writeTask(() => {
this.el.classList.remove('datetime-ready');
});
};
const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0, root: el });
raf(() => hiddenIO?.observe(intersectionTrackerRef!));
this.initializeVisibilityObserver();
/**
* Datetime uses Ionic components that emit
@@ -2693,20 +2689,6 @@ export class Datetime implements ComponentInterface {
}),
}}
>
{/*
WebKit has a quirk where IntersectionObserver callbacks are delayed until after
an accelerated animation finishes if the "root" specified in the config is the
browser viewport (the default behavior if "root" is not specified). This means
that when presenting a datetime in a modal on iOS the calendar body appears
blank until the modal animation finishes.
We can work around this by observing .intersection-tracker and using the host
(ion-datetime) as the "root". This allows the IO callback to fire the moment
the datetime is visible. The .intersection-tracker element should not have
dimensions or additional styles, and it should not be positioned absolutely
otherwise the IO callback may fire at unexpected times.
*/}
<div class="intersection-tracker" ref={(el) => (this.intersectionTrackerRef = el)}></div>
{this.renderDatetime(mode)}
</Host>
);

View File

@@ -395,40 +395,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
});
/**
* Synthetic IntersectionObserver fallback behavior.
*
* This test stubs IntersectionObserver so that the callback
* never reports an intersecting entry. The datetime should
* still become ready via its internal fallback logic.
* Verify that datetime becomes ready via ResizeObserver.
* This tests that the datetime properly initializes when it has
* dimensions, using ResizeObserver to detect visibility.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: IO fallback'), () => {
test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => {
test.describe(title('datetime: visibility detection'), () => {
test('should become ready when rendered with dimensions', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
});
await page.addInitScript(() => {
const OriginalIO = window.IntersectionObserver;
(window as any).IntersectionObserver = function (callback: any, options: any) {
const instance = new OriginalIO(() => {}, options);
const originalObserve = instance.observe.bind(instance);
instance.observe = (target: Element) => {
originalObserve(target);
callback([
{
isIntersecting: false,
target,
} as IntersectionObserverEntry,
]);
};
return instance;
} as any;
});
await page.setContent(
`
<ion-datetime value="2022-05-03"></ion-datetime>
@@ -438,8 +416,8 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
const datetime = page.locator('ion-datetime');
// Give the fallback a short amount of time to run
await page.waitForTimeout(100);
// Wait for the datetime to become ready via ResizeObserver
await page.locator('.datetime-ready').waitFor();
await expect(datetime).toHaveClass(/datetime-ready/);

View File

@@ -52,6 +52,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('datetime: custom focus'), () => {
test('should focus the selected day and then the day after', async ({ page }) => {
await page.goto(`/src/components/datetime/test/custom`, config);
await page.locator('.datetime-ready').last().waitFor();
const datetime = page.locator('#custom-calendar-days');

View File

@@ -7,6 +7,7 @@ configs().forEach(({ title, screenshot, config }) => {
await page.goto('/src/components/datetime/test/first-day-of-week', config);
const datetime = page.locator('ion-datetime');
await page.locator('.datetime-ready').waitFor();
await expect(datetime).toHaveScreenshot(screenshot(`datetime-day-of-week`));
});
});

View File

@@ -10,6 +10,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
`,
config
);
await page.locator('.datetime-ready').waitFor();
});
test('should render highlights correctly when using an array', async ({ page }) => {

View File

@@ -3,5 +3,5 @@
"core",
"packages/*"
],
"version": "8.7.14"
"version": "8.7.12"
}

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.14"
"@ionic/core": "^8.7.12"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^16.0.0",
@@ -1031,9 +1031,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -1041,7 +1041,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/eslint-config": {
@@ -7309,9 +7309,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",
@@ -11289,4 +11289,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "8.7.14",
"version": "8.7.12",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^8.7.14"
"@ionic/core": "^8.7.12"
}
}

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/angular
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/angular
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -1398,9 +1398,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -1408,7 +1408,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/eslint-config": {
@@ -9095,4 +9095,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "8.7.14",
"version": "8.7.12",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -48,7 +48,7 @@
}
},
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/docs
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/docs
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
**Note:** Version bump only for package @ionic/docs

View File

@@ -1,13 +1,13 @@
{
"name": "@ionic/docs",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "8.7.14",
"version": "8.7.12",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/react-router
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/react-router
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
**Note:** Version bump only for package @ionic/react-router

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react-router",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/react-router",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/react": "^8.7.14",
"@ionic/react": "^8.7.12",
"tslib": "*"
},
"devDependencies": {
@@ -238,9 +238,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -248,7 +248,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/eslint-config": {
@@ -418,12 +418,12 @@
}
},
"node_modules/@ionic/react": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.13.tgz",
"integrity": "sha512-PCuIpaurVYxYZ/CoUN3gP56Fwdx+bx78Qy7V5Ac61nGGW7XpVlV4vM9328Kv7OPs5fkmIvKI6LoY78BnjF0PkA==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.12.tgz",
"integrity": "sha512-gNm5L++aiwkwJrUFKhcHUUgjqnj9n03gK7UcoL7Oz+271arzmwF/FNd47G85b6PovwiYQXY2CUBvNJ7Nh4qE/A==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.12",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -4178,9 +4178,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",
@@ -4284,11 +4284,11 @@
"requires": {}
},
"@ionic/react": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.13.tgz",
"integrity": "sha512-PCuIpaurVYxYZ/CoUN3gP56Fwdx+bx78Qy7V5Ac61nGGW7XpVlV4vM9328Kv7OPs5fkmIvKI6LoY78BnjF0PkA==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.12.tgz",
"integrity": "sha512-gNm5L++aiwkwJrUFKhcHUUgjqnj9n03gK7UcoL7Oz+271arzmwF/FNd47G85b6PovwiYQXY2CUBvNJ7Nh4qE/A==",
"requires": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.12",
"ionicons": "^8.0.13",
"tslib": "*"
}
@@ -6847,4 +6847,4 @@
"dev": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react-router",
"version": "8.7.14",
"version": "8.7.12",
"description": "React Router wrapper for @ionic/react",
"keywords": [
"ionic",
@@ -36,7 +36,7 @@
"dist/"
],
"dependencies": {
"@ionic/react": "^8.7.14",
"@ionic/react": "^8.7.12",
"tslib": "*"
},
"peerDependencies": {

View File

@@ -3,25 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/react
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/react",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/react",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -736,9 +736,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -746,7 +746,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/eslint-config": {
@@ -11916,4 +11916,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react",
"version": "8.7.14",
"version": "8.7.12",
"description": "React specific wrapper for @ionic/core",
"keywords": [
"ionic",
@@ -40,7 +40,7 @@
"css/"
],
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"ionicons": "^8.0.13",
"tslib": "*"
},

View File

@@ -40,20 +40,6 @@ interface IonTabBarState {
// TODO(FW-2959): types
/**
* Checks if pathname matches the tab's href using path segment matching.
* Avoids false matches like /home2 matching /home by requiring exact match
* or a path segment boundary (/).
*/
const matchesTab = (pathname: string, href: string | undefined): boolean => {
if (href === undefined) {
return false;
}
const normalizedHref = href.endsWith('/') && href !== '/' ? href.slice(0, -1) : href;
return pathname === normalizedHref || pathname.startsWith(normalizedHref + '/');
};
class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarState> {
context!: React.ContextType<typeof NavContext>;
@@ -93,7 +79,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
const tabKeys = Object.keys(tabs);
const activeTab = tabKeys.find((key) => {
const href = tabs[key].originalHref;
return matchesTab(this.props.routeInfo!.pathname, href);
return this.props.routeInfo!.pathname.startsWith(href);
});
if (activeTab) {
@@ -135,7 +121,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
const tabKeys = Object.keys(state.tabs);
const activeTab = tabKeys.find((key) => {
const href = state.tabs[key].originalHref;
return matchesTab(props.routeInfo!.pathname, href);
return props.routeInfo!.pathname.startsWith(href);
});
// Check to see if the tab button href has changed, and if so, update it in the tabs state

View File

@@ -28,7 +28,6 @@ import Tabs from './pages/Tabs';
import TabsBasic from './pages/TabsBasic';
import NavComponent from './pages/navigation/NavComponent';
import TabsDirectNavigation from './pages/TabsDirectNavigation';
import TabsSimilarPrefixes from './pages/TabsSimilarPrefixes';
import IonModalConditional from './pages/overlay-components/IonModalConditional';
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
@@ -68,7 +67,6 @@ const App: React.FC = () => (
<Route path="/tabs" component={Tabs} />
<Route path="/tabs-basic" component={TabsBasic} />
<Route path="/tabs-direct-navigation" component={TabsDirectNavigation} />
<Route path="/tabs-similar-prefixes" component={TabsSimilarPrefixes} />
<Route path="/icons" component={Icons} />
<Route path="/inputs" component={Inputs} />
<Route path="/reorder-group" component={ReorderGroup} />

View File

@@ -46,9 +46,6 @@ const Main: React.FC<MainProps> = () => {
<IonItem routerLink="/tabs-direct-navigation">
<IonLabel>Tabs with Direct Navigation</IonLabel>
</IonItem>
<IonItem routerLink="/tabs-similar-prefixes">
<IonLabel>Tabs with Similar Route Prefixes</IonLabel>
</IonItem>
<IonItem routerLink="/icons">
<IonLabel>Icons</IonLabel>
</IonItem>

View File

@@ -1,87 +0,0 @@
import {
IonContent,
IonHeader,
IonIcon,
IonLabel,
IonPage,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { homeOutline, radioOutline, libraryOutline } from 'ionicons/icons';
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const HomePage: React.FC = () => (
<IonPage data-testid="home-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home-content">Home Content</div>
</IonContent>
</IonPage>
);
const Home2Page: React.FC = () => (
<IonPage data-testid="home2-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home 2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home2-content">Home 2 Content</div>
</IonContent>
</IonPage>
);
const Home3Page: React.FC = () => (
<IonPage data-testid="home3-page">
<IonHeader>
<IonToolbar>
<IonTitle>Home 3</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div data-testid="home3-content">Home 3 Content</div>
</IonContent>
</IonPage>
);
const TabsSimilarPrefixes: React.FC = () => {
return (
<IonTabs data-testid="tabs-similar-prefixes">
<IonRouterOutlet>
<Redirect exact path="/tabs-similar-prefixes" to="/tabs-similar-prefixes/home" />
<Route path="/tabs-similar-prefixes/home" render={() => <HomePage />} exact={true} />
<Route path="/tabs-similar-prefixes/home2" render={() => <Home2Page />} exact={true} />
<Route path="/tabs-similar-prefixes/home3" render={() => <Home3Page />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom" data-testid="tab-bar">
<IonTabButton tab="home" href="/tabs-similar-prefixes/home" data-testid="home-tab">
<IonIcon icon={homeOutline}></IonIcon>
<IonLabel>Home</IonLabel>
</IonTabButton>
<IonTabButton tab="home2" href="/tabs-similar-prefixes/home2" data-testid="home2-tab">
<IonIcon icon={radioOutline}></IonIcon>
<IonLabel>Home 2</IonLabel>
</IonTabButton>
<IonTabButton tab="home3" href="/tabs-similar-prefixes/home3" data-testid="home3-tab">
<IonIcon icon={libraryOutline}></IonIcon>
<IonLabel>Home 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default TabsSimilarPrefixes;

View File

@@ -1,45 +1,4 @@
describe('IonTabs', () => {
/**
* Verifies that tabs with similar route prefixes (e.g., /home, /home2, /home3)
* correctly select the matching tab instead of the first prefix match.
*
* @see https://github.com/ionic-team/ionic-framework/issues/30448
*/
describe('Similar Route Prefixes', () => {
it('should select the correct tab when routes have similar prefixes', () => {
cy.visit('/tabs-similar-prefixes/home2');
cy.get('[data-testid="home2-content"]').should('be.visible');
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when navigating via tab buttons', () => {
cy.visit('/tabs-similar-prefixes/home');
cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').click();
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home3-tab"]').click();
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when directly navigating to home3', () => {
cy.visit('/tabs-similar-prefixes/home3');
cy.get('[data-testid="home3-content"]').should('be.visible');
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
});
describe('With IonRouterOutlet', () => {
beforeEach(() => {
cy.visit('/tabs/tab1');

View File

@@ -3,22 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/vue-router
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
**Note:** Version bump only for package @ionic/vue-router

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue-router",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue-router",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/vue": "^8.7.14"
"@ionic/vue": "^8.7.12"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",
@@ -673,9 +673,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -683,7 +683,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/eslint-config": {
@@ -868,12 +868,12 @@
}
},
"node_modules/@ionic/vue": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.13.tgz",
"integrity": "sha512-hflvGaNPABYP0Qt68YgrauVaXyjKeHODOkYzJhk36kcr+VexwTWm1FGJG1/nKKgdh6fwDIsubJvlhoZaRhtVtg==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.12.tgz",
"integrity": "sha512-fH/acQ7dYYd1XY1HFqKf0Th6klbfNNzhlnYUf9kNQpkHqUpMamSzA8TWDOV5f4PNKYq6X4oKAPPeIQA4DOayUQ==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.12",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}
@@ -8044,9 +8044,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"requires": {
"@stencil/core": "4.38.0",
"ionicons": "^8.0.13",
@@ -8159,11 +8159,11 @@
"requires": {}
},
"@ionic/vue": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.13.tgz",
"integrity": "sha512-hflvGaNPABYP0Qt68YgrauVaXyjKeHODOkYzJhk36kcr+VexwTWm1FGJG1/nKKgdh6fwDIsubJvlhoZaRhtVtg==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.12.tgz",
"integrity": "sha512-fH/acQ7dYYd1XY1HFqKf0Th6klbfNNzhlnYUf9kNQpkHqUpMamSzA8TWDOV5f4PNKYq6X4oKAPPeIQA4DOayUQ==",
"requires": {
"@ionic/core": "8.7.13",
"@ionic/core": "8.7.12",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}
@@ -12994,4 +12994,4 @@
"dev": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue-router",
"version": "8.7.14",
"version": "8.7.12",
"description": "Vue Router integration for @ionic/vue",
"scripts": {
"test.spec": "jest",
@@ -44,7 +44,7 @@
},
"homepage": "https://github.com/ionic-team/ionic-framework#readme",
"dependencies": {
"@ionic/vue": "^8.7.14"
"@ionic/vue": "^8.7.12"
},
"devDependencies": {
"@ionic/eslint-config": "^0.3.0",

View File

@@ -3,25 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.14](https://github.com/ionic-team/ionic-framework/compare/v8.7.13...v8.7.14) (2025-12-17)
### Bug Fixes
* **tabs:** select correct tab when routes have similar prefixes ([#30863](https://github.com/ionic-team/ionic-framework/issues/30863)) ([03fb422](https://github.com/ionic-team/ionic-framework/commit/03fb422bfa775e3e9dd695ea1857fa88d4245ecd)), closes [#30448](https://github.com/ionic-team/ionic-framework/issues/30448)
## [8.7.13](https://github.com/ionic-team/ionic-framework/compare/v8.7.12...v8.7.13) (2025-12-13)
**Note:** Version bump only for package @ionic/vue
## [8.7.12](https://github.com/ionic-team/ionic-framework/compare/v8.7.11...v8.7.12) (2025-12-10)
**Note:** Version bump only for package @ionic/vue

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue",
"version": "8.7.14",
"version": "8.7.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue",
"version": "8.7.14",
"version": "8.7.12",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
},
@@ -222,9 +222,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.13",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.13.tgz",
"integrity": "sha512-72sbep6UOiGn+KYKtVSPZhKuq0o68X6mWi5sCyXYE/V1nzUknew9RGohcxbtt5iMVgjuny/m4liIUwVgvvQ5mw==",
"version": "8.7.12",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.12.tgz",
"integrity": "sha512-+QnytOHsMMDEz45hi/t9AN8ATaWMNZ7jNdx621BGSHi0JkEl1c4NylL3cfYIPJ/78y40ZG5NzprwNiR9sXdswg==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.38.0",
@@ -232,7 +232,7 @@
"tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
"node": "24.x"
}
},
"node_modules/@ionic/core/node_modules/tslib": {
@@ -4022,4 +4022,4 @@
"dev": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue",
"version": "8.7.14",
"version": "8.7.12",
"description": "Vue specific wrapper for @ionic/core",
"scripts": {
"eslint": "eslint src",
@@ -68,7 +68,7 @@
"vue-router": "^4.0.16"
},
"dependencies": {
"@ionic/core": "^8.7.14",
"@ionic/core": "^8.7.12",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
},

View File

@@ -24,23 +24,6 @@ interface TabBarData {
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
/**
* Checks if pathname matches the tab's href using path segment matching.
* Avoids false matches like /home2 matching /home by requiring exact match
* or a path segment boundary (/).
*/
const matchesTab = (pathname: string, href: string | undefined): boolean => {
if (href === undefined) {
return false;
}
const normalizedHref =
href.endsWith("/") && href !== "/" ? href.slice(0, -1) : href;
return (
pathname === normalizedHref || pathname.startsWith(normalizedHref + "/")
);
};
const getTabs = (nodes: VNode[]) => {
let tabs: VNode[] = [];
nodes.forEach((node: VNode) => {
@@ -152,9 +135,7 @@ export const IonTabBar = defineComponent({
const tabKeys = Object.keys(tabs);
let activeTab = tabKeys.find((key) => {
const href = tabs[key].originalHref;
return (
currentRoute?.pathname && matchesTab(currentRoute.pathname, href)
);
return currentRoute?.pathname.startsWith(href);
});
/**

View File

@@ -165,28 +165,6 @@ const routes: Array<RouteRecordRaw> = [
path: '/tabs-basic',
component: () => import('@/views/TabsBasic.vue')
},
{
path: '/tabs-similar-prefixes/',
component: () => import('@/views/tabs-similar-prefixes/TabsSimilarPrefixes.vue'),
children: [
{
path: '',
redirect: '/tabs-similar-prefixes/home'
},
{
path: 'home',
component: () => import('@/views/tabs-similar-prefixes/Home.vue'),
},
{
path: 'home2',
component: () => import('@/views/tabs-similar-prefixes/Home2.vue'),
},
{
path: 'home3',
component: () => import('@/views/tabs-similar-prefixes/Home3.vue'),
}
]
},
]
const router = createRouter({

View File

@@ -50,9 +50,6 @@
<ion-item router-link="/tabs-basic" id="tab-basic">
<ion-label>Tabs with Basic Navigation</ion-label>
</ion-item>
<ion-item router-link="/tabs-similar-prefixes" id="tabs-similar-prefixes">
<ion-label>Tabs with Similar Route Prefixes</ion-label>
</ion-item>
<ion-item router-link="/lifecycle" id="lifecycle">
<ion-label>Lifecycle</ion-label>
</ion-item>

View File

@@ -1,16 +0,0 @@
<template>
<ion-page data-pageid="home">
<ion-header>
<ion-toolbar>
<ion-title>Home</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home-content">Home Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -1,16 +0,0 @@
<template>
<ion-page data-pageid="home2">
<ion-header>
<ion-toolbar>
<ion-title>Home 2</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home2-content">Home 2 Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -1,16 +0,0 @@
<template>
<ion-page data-pageid="home3">
<ion-header>
<ion-toolbar>
<ion-title>Home 3</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div data-testid="home3-content">Home 3 Content</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

View File

@@ -1,54 +0,0 @@
<template>
<ion-page data-pageid="tabs-similar-prefixes">
<ion-content>
<ion-tabs id="tabs-similar-prefixes">
<ion-router-outlet></ion-router-outlet>
<ion-tab-bar slot="bottom" data-testid="tab-bar">
<ion-tab-button
tab="home"
href="/tabs-similar-prefixes/home"
data-testid="home-tab"
id="tab-button-home"
>
<ion-icon :icon="homeOutline" />
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button
tab="home2"
href="/tabs-similar-prefixes/home2"
data-testid="home2-tab"
id="tab-button-home2"
>
<ion-icon :icon="radioOutline" />
<ion-label>Home 2</ion-label>
</ion-tab-button>
<ion-tab-button
tab="home3"
href="/tabs-similar-prefixes/home3"
data-testid="home3-tab"
id="tab-button-home3"
>
<ion-icon :icon="libraryOutline" />
<ion-label>Home 3</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import {
IonContent,
IonIcon,
IonLabel,
IonPage,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
} from '@ionic/vue';
import { homeOutline, radioOutline, libraryOutline } from 'ionicons/icons';
</script>

View File

@@ -1,45 +1,4 @@
describe('Tabs', () => {
/**
* Verifies that tabs with similar route prefixes (e.g., /home, /home2, /home3)
* correctly select the matching tab instead of the first prefix match.
*
* @see https://github.com/ionic-team/ionic-framework/issues/30448
*/
describe('Similar Route Prefixes', () => {
it('should select the correct tab when routes have similar prefixes', () => {
cy.visit('/tabs-similar-prefixes/home2');
cy.get('[data-testid="home2-content"]').should('be.visible');
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when navigating via tab buttons', () => {
cy.visit('/tabs-similar-prefixes/home');
cy.get('[data-testid="home-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').click();
cy.get('[data-testid="home2-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home3-tab"]').click();
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
it('should select the correct tab when directly navigating to home3', () => {
cy.visit('/tabs-similar-prefixes/home3');
cy.get('[data-testid="home3-content"]').should('be.visible');
cy.get('[data-testid="home3-tab"]').should('have.class', 'tab-selected');
cy.get('[data-testid="home-tab"]').should('not.have.class', 'tab-selected');
cy.get('[data-testid="home2-tab"]').should('not.have.class', 'tab-selected');
});
});
describe('With IonRouterOutlet', () => {
it('should go back from child pages', () => {
cy.visit('/tabs');