From f01e9a754d1267e2cdd60aae5d292ebbf2ef3deb Mon Sep 17 00:00:00 2001 From: Shane Date: Wed, 30 Sep 2026 16:40:42 +0000 Subject: [PATCH 01/53] fix(react-router): dispatch view lifecycle events on non-animated transitions (#31497) Issue number: resolves #31479 --------- ## What is the current behavior? Currently, switching tabs or following a link with `routerDirection="none"` fires none of the four view lifecycle events, so a page that loads its data in `useIonViewWillEnter` renders empty. The non-animated branch of `StackManager.transitionPage()` skips `routerOutlet.commit()` and swaps the page classes itself to avoid intermediate paints, and core dispatches all four events from inside `commit()`, so they never fire. A capturing listener on the `ion-router-outlet` gets nothing either, so it isn't a problem with the hooks. ## What is the new behavior? That branch now dispatches the four events itself, in the same order core's `transition()` uses. The class swap moved to after all four, so the leaving page is still on screen for its leave events and the entering page is revealed only once they've fired. That's what core does on the animated path, where `beforeTransition` un-hides the leaving page and `ion-page-hidden` only goes back on after `commit()` resolves, and it matters because `ion-page-hidden` is `display: none`, so a `useIonViewDidLeave` handler reading `scrollTop` off the outgoing page was getting 0. The two dispatch sites that already existed in this file for out-of-scope and root navigation now share the same helper. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The events used to come from core here. This path called `commit(enteringEl, undefined, { duration: 0 })` until [12f0d5e](https://github.com/ionic-team/ionic-framework/commit/12f0d5ee049d47f2f4f0f1c84d3b6a177dbca448) dropped it to fix a white flash during tab switches, and with no leaving element core took its `noAnimation` branch and fired the enter pair, so the enter half of this is a regression from that commit. Putting the call back would get the events plus focus and z-index handling from core for free, but it risks the flash it was removed for, so this dispatches them directly instead. The patch on the issue fires the enter events unconditionally. This gates them on the generation check so a superseded transition doesn't announce an entry, and it skips the leave events when the leaving page already has `ion-page-hidden`, because two transitions sharing a leaving element would otherwise run its `useIonViewDidLeave` teardown twice. The check is the class rather than `isViewVisible` since a nested outlet marks its leaving page `visibility: hidden` before we get here and still needs `ionViewDidLeave` to unmount. Preview: - [Tab lifecycle](https://ionic-framework-git-fix-rr6-lifecycle-non-animated-ionic1.vercel.app/react-router/tab-lifecycle/home) - [routerDirection="none"](https://ionic-framework-git-fix-rr6-lifecycle-non-animated-ionic1.vercel.app/react-router/direction-none-back/a) ## Current Dev Build ``` 9.0.6-dev.11790615145.1d8c56a2 ``` --- packages/react-router/package-lock.json | 1 + packages/react-router/package.json | 1 + .../src/ReactRouter/StackManager.tsx | 65 ++++++++++++++---- .../direction-none-back/DirectionNoneBack.tsx | 15 ++++ .../src/pages/tab-lifecycle/TabLifecycle.tsx | 22 +++--- .../react-router/test/base/src/utils/index.ts | 1 + .../test/base/src/utils/lifecycleEvents.ts | 5 ++ .../direction-none-lifecycle.spec.ts | 44 ++++++++++++ .../e2e/playwright/tab-lifecycle.spec.ts | 68 +++++++++++++++++-- .../tests/e2e/playwright/utils/test-utils.ts | 35 ++++++++++ 10 files changed, 223 insertions(+), 34 deletions(-) create mode 100644 packages/react-router/test/base/src/utils/lifecycleEvents.ts create mode 100644 packages/react-router/test/base/tests/e2e/playwright/direction-none-lifecycle.spec.ts diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index a3d57cb94be..c947c699c42 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -9,6 +9,7 @@ "version": "9.0.5", "license": "MIT", "dependencies": { + "@ionic/core": "^9.0.5", "@ionic/react": "^9.0.5", "tslib": "*" }, diff --git a/packages/react-router/package.json b/packages/react-router/package.json index b7809e30215..4e223a8da8f 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -37,6 +37,7 @@ "dist/" ], "dependencies": { + "@ionic/core": "^9.0.5", "@ionic/react": "^9.0.5", "tslib": "*" }, diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index 7fa9173eb29..6848c4941bb 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -4,6 +4,12 @@ * particularly with animations and swipe gestures. */ +import { + LIFECYCLE_DID_ENTER, + LIFECYCLE_DID_LEAVE, + LIFECYCLE_WILL_ENTER, + LIFECYCLE_WILL_LEAVE, +} from '@ionic/core/components'; import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react'; import { IonRoute, RouteManagerContext, StackContext, createDebugLogger, generateId, getConfig } from '@ionic/react'; import React from 'react'; @@ -89,6 +95,19 @@ const revealIonPageForSwipeBack = (element: HTMLElement | undefined): void => { } }; +type ViewLifecycleEvent = + | typeof LIFECYCLE_WILL_ENTER + | typeof LIFECYCLE_DID_ENTER + | typeof LIFECYCLE_WILL_LEAVE + | typeof LIFECYCLE_DID_LEAVE; + +/** Dispatches a view lifecycle event the way core's `lifecycle()` does. */ +const dispatchLifecycleEvent = (element: HTMLElement | undefined, eventName: ViewLifecycleEvent): void => { + if (element) { + element.dispatchEvent(new CustomEvent(eventName, { bubbles: false, cancelable: false })); + } +}; + /** * A leaf view is "preservable" on browser-back (pop) when its React state * should survive a forward-pop round-trip. Non-parameterized leaf paths @@ -367,12 +386,8 @@ export class StackManager extends React.PureComponent { const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id); allViewsInOutlet.forEach((viewItem) => { if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) { - viewItem.ionPageElement.dispatchEvent( - new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }) - ); - viewItem.ionPageElement.dispatchEvent( - new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }) - ); + dispatchLifecycleEvent(viewItem.ionPageElement, LIFECYCLE_WILL_LEAVE); + dispatchLifecycleEvent(viewItem.ionPageElement, LIFECYCLE_DID_LEAVE); } }); @@ -409,12 +424,8 @@ export class StackManager extends React.PureComponent { return; } if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) { - viewItem.ionPageElement.dispatchEvent( - new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }) - ); - viewItem.ionPageElement.dispatchEvent( - new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }) - ); + dispatchLifecycleEvent(viewItem.ionPageElement, LIFECYCLE_WILL_LEAVE); + dispatchLifecycleEvent(viewItem.ionPageElement, LIFECYCLE_DID_LEAVE); } this.context.unMountViewItem(viewItem); }); @@ -1782,10 +1793,36 @@ export class StackManager extends React.PureComponent { // Bail out if the component unmounted during waitForComponentsReady if (!this._isMounted) return; + const isCurrent = myGeneration === this.transitionGeneration; + // A page the newest transition is entering is not leaving after all. + const isLeaving = isCurrent || leavingEl !== this.transitionEnteringElement; + // Already hidden means the leave events have fired. This checks the class rather + // than `isViewVisible` because a nested outlet marks its leaving page + // `visibility: hidden` before we get here and still needs `ionViewDidLeave`. + const announceLeaving = isLeaving && !leavingEl.classList.contains('ion-page-hidden'); + + /** + * Dispatch the lifecycle events, since we skipped `commit()`. Only the + * newest transition fires the entering events, and the class swap follows + * all four so the ordering matches core's `transition()`. + * + * These run after `waitForComponentsReady` because on a first mount the + * page has not attached its listeners yet. + */ + if (announceLeaving) { + dispatchLifecycleEvent(leavingEl, LIFECYCLE_WILL_LEAVE); + } + if (isCurrent) { + dispatchLifecycleEvent(enteringEl, LIFECYCLE_WILL_ENTER); + dispatchLifecycleEvent(enteringEl, LIFECYCLE_DID_ENTER); + } + if (announceLeaving) { + dispatchLifecycleEvent(leavingEl, LIFECYCLE_DID_LEAVE); + } + // Swap visibility synchronously - show entering, hide leaving - // Skip hiding if a newer transition already made leavingEl the entering view enteringEl.classList.remove('ion-page-invisible'); - if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) { + if (isLeaving) { leavingEl.classList.add('ion-page-hidden'); leavingEl.setAttribute('aria-hidden', 'true'); } diff --git a/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx b/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx index 078ef707d19..4d29266cc27 100644 --- a/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx +++ b/packages/react-router/test/base/src/pages/direction-none-back/DirectionNoneBack.tsx @@ -8,11 +8,16 @@ import { IonRouterOutlet, IonBackButton, IonButtons, + useIonViewDidEnter, + useIonViewDidLeave, + useIonViewWillEnter, + useIonViewWillLeave, } from '@ionic/react'; import React from 'react'; import { Route, Navigate } from 'react-router-dom'; import TestDescription from '../../components/TestDescription'; +import { pushLifecycleEvent } from '../../utils'; /** * Tests that IonBackButton works correctly after navigating with @@ -20,6 +25,11 @@ import TestDescription from '../../components/TestDescription'; * determine the previous page, not fall back to defaultHref. */ const PageA: React.FC = () => { + useIonViewWillEnter(() => pushLifecycleEvent('a:ionViewWillEnter')); + useIonViewDidEnter(() => pushLifecycleEvent('a:ionViewDidEnter')); + useIonViewWillLeave(() => pushLifecycleEvent('a:ionViewWillLeave')); + useIonViewDidLeave(() => pushLifecycleEvent('a:ionViewDidLeave')); + return ( @@ -41,6 +51,11 @@ const PageA: React.FC = () => { }; const PageB: React.FC = () => { + useIonViewWillEnter(() => pushLifecycleEvent('b:ionViewWillEnter')); + useIonViewDidEnter(() => pushLifecycleEvent('b:ionViewDidEnter')); + useIonViewWillLeave(() => pushLifecycleEvent('b:ionViewWillLeave')); + useIonViewDidLeave(() => pushLifecycleEvent('b:ionViewDidLeave')); + return ( diff --git a/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx index 00f176838d5..e64543783b4 100644 --- a/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx +++ b/packages/react-router/test/base/src/pages/tab-lifecycle/TabLifecycle.tsx @@ -21,11 +21,7 @@ import React from 'react'; import { Route, Navigate } from 'react-router'; import TestDescription from '../../components/TestDescription'; - -const pushEvent = (event: string) => { - (window as any).lifecycleEvents = (window as any).lifecycleEvents || []; - (window as any).lifecycleEvents.push(event); -}; +import { pushLifecycleEvent } from '../../utils'; const TabLifecycle: React.FC = () => { return ( @@ -50,10 +46,10 @@ const TabLifecycle: React.FC = () => { }; const HomeTab: React.FC = () => { - useIonViewWillEnter(() => pushEvent('home:ionViewWillEnter')); - useIonViewDidEnter(() => pushEvent('home:ionViewDidEnter')); - useIonViewWillLeave(() => pushEvent('home:ionViewWillLeave')); - useIonViewDidLeave(() => pushEvent('home:ionViewDidLeave')); + useIonViewWillEnter(() => pushLifecycleEvent('home:ionViewWillEnter')); + useIonViewDidEnter(() => pushLifecycleEvent('home:ionViewDidEnter')); + useIonViewWillLeave(() => pushLifecycleEvent('home:ionViewWillLeave')); + useIonViewDidLeave(() => pushLifecycleEvent('home:ionViewDidLeave')); return ( @@ -73,10 +69,10 @@ const HomeTab: React.FC = () => { }; const SettingsTab: React.FC = () => { - useIonViewWillEnter(() => pushEvent('settings:ionViewWillEnter')); - useIonViewDidEnter(() => pushEvent('settings:ionViewDidEnter')); - useIonViewWillLeave(() => pushEvent('settings:ionViewWillLeave')); - useIonViewDidLeave(() => pushEvent('settings:ionViewDidLeave')); + useIonViewWillEnter(() => pushLifecycleEvent('settings:ionViewWillEnter')); + useIonViewDidEnter(() => pushLifecycleEvent('settings:ionViewDidEnter')); + useIonViewWillLeave(() => pushLifecycleEvent('settings:ionViewWillLeave')); + useIonViewDidLeave(() => pushLifecycleEvent('settings:ionViewDidLeave')); return ( diff --git a/packages/react-router/test/base/src/utils/index.ts b/packages/react-router/test/base/src/utils/index.ts index 5c9612f4969..45db8939265 100644 --- a/packages/react-router/test/base/src/utils/index.ts +++ b/packages/react-router/test/base/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './generateId'; +export * from './lifecycleEvents'; export * from './dev'; diff --git a/packages/react-router/test/base/src/utils/lifecycleEvents.ts b/packages/react-router/test/base/src/utils/lifecycleEvents.ts new file mode 100644 index 00000000000..416d3691989 --- /dev/null +++ b/packages/react-router/test/base/src/utils/lifecycleEvents.ts @@ -0,0 +1,5 @@ +/** Records a view lifecycle event on `window.lifecycleEvents` for a spec to assert on. */ +export const pushLifecycleEvent = (event: string) => { + (window as any).lifecycleEvents = (window as any).lifecycleEvents || []; + (window as any).lifecycleEvents.push(event); +}; diff --git a/packages/react-router/test/base/tests/e2e/playwright/direction-none-lifecycle.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/direction-none-lifecycle.spec.ts new file mode 100644 index 00000000000..7beacdcaf8b --- /dev/null +++ b/packages/react-router/test/base/tests/e2e/playwright/direction-none-lifecycle.spec.ts @@ -0,0 +1,44 @@ +import { test, expect, type Page } from '@playwright/test'; +import { ionPageVisible, resetLifecycleEvents, settledLifecycleEvents, withTestingMode } from './utils/test-utils'; + +/** + * A navigation with routerDirection="none" is not animated, but it must still + * fire the four view lifecycle events, in the same order an animated one does. + */ +test.describe('routerDirection="none" lifecycle events', () => { + const expectedEvents = ['a:ionViewWillLeave', 'b:ionViewWillEnter', 'b:ionViewDidEnter', 'a:ionViewDidLeave']; + + const goToPageA = async (page: Page) => { + await page.goto(withTestingMode('/direction-none-back/a')); + await ionPageVisible(page, 'direction-none-page-a'); + await resetLifecycleEvents(page); + }; + + test('should fire enter and leave events on a routerDirection="none" navigation', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31479', + }); + + await goToPageA(page); + + await page.locator('#go-none').click(); + await ionPageVisible(page, 'direction-none-page-b'); + + expect(await settledLifecycleEvents(page)).toEqual(expectedEvents); + }); + + /** + * The control. A forward navigation keeps its direction, so it takes the + * regular transition path, and its event order is the one the test above + * has to match. + */ + test('should fire the same events for a forward navigation', async ({ page }) => { + await goToPageA(page); + + await page.locator('#go-forward').click(); + await ionPageVisible(page, 'direction-none-page-b'); + + expect(await settledLifecycleEvents(page)).toEqual(expectedEvents); + }); +}); diff --git a/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts index 7287d4c75df..3e479c9cb05 100644 --- a/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts +++ b/packages/react-router/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '@playwright/test'; -import { ionPageVisible, ionTabClick, trackPeakMatchCount, withTestingMode } from './utils/test-utils'; +import { + ionPageVisible, + ionTabClick, + resetLifecycleEvents, + settledLifecycleEvents, + trackPeakMatchCount, + withTestingMode, +} from './utils/test-utils'; test.describe('Tab Lifecycle Events', () => { test.beforeEach(async ({ page }) => { @@ -17,12 +24,12 @@ test.describe('Tab Lifecycle Events', () => { await page.goto(withTestingMode('/tab-lifecycle/home')); await ionPageVisible(page, 'tab-lifecycle-home'); - await page.evaluate(() => { (window as any).lifecycleEvents = []; }); + await resetLifecycleEvents(page); await page.locator('#go-outside').click(); await ionPageVisible(page, 'tab-lifecycle-outside'); - const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]); + const events = await settledLifecycleEvents(page); expect(events).toContain('home:ionViewWillLeave'); expect(events).toContain('home:ionViewDidLeave'); }); @@ -39,12 +46,12 @@ test.describe('Tab Lifecycle Events', () => { await ionTabClick(page, 'Settings'); await ionPageVisible(page, 'tab-lifecycle-settings'); - await page.evaluate(() => { (window as any).lifecycleEvents = []; }); + await resetLifecycleEvents(page); await page.locator('#go-outside-settings').click(); await ionPageVisible(page, 'tab-lifecycle-outside'); - const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]); + const events = await settledLifecycleEvents(page); expect(events).toContain('settings:ionViewWillLeave'); expect(events).toContain('settings:ionViewDidLeave'); }); @@ -61,16 +68,63 @@ test.describe('Tab Lifecycle Events', () => { await page.locator('#go-outside').click(); await ionPageVisible(page, 'tab-lifecycle-outside'); - await page.evaluate(() => { (window as any).lifecycleEvents = []; }); + await resetLifecycleEvents(page); await page.locator('#go-back-to-tabs').click(); await ionPageVisible(page, 'tab-lifecycle-home'); - const events = await page.evaluate(() => (window as any).lifecycleEvents as string[]); + const events = await settledLifecycleEvents(page); expect(events).toContain('home:ionViewWillEnter'); expect(events).toContain('home:ionViewDidEnter'); }); + test('should fire enter and leave events when switching tabs', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31479', + }); + + await page.goto(withTestingMode('/tab-lifecycle/home')); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await resetLifecycleEvents(page); + + await ionTabClick(page, 'Settings'); + await ionPageVisible(page, 'tab-lifecycle-settings'); + + expect(await settledLifecycleEvents(page)).toEqual([ + 'home:ionViewWillLeave', + 'settings:ionViewWillEnter', + 'settings:ionViewDidEnter', + 'home:ionViewDidLeave', + ]); + }); + + test('should fire enter and leave events when switching back to a visited tab', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31479', + }); + + await page.goto(withTestingMode('/tab-lifecycle/home')); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await ionTabClick(page, 'Settings'); + await ionPageVisible(page, 'tab-lifecycle-settings'); + + await resetLifecycleEvents(page); + + await ionTabClick(page, 'Home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + expect(await settledLifecycleEvents(page)).toEqual([ + 'settings:ionViewWillLeave', + 'home:ionViewWillEnter', + 'home:ionViewDidEnter', + 'settings:ionViewDidLeave', + ]); + }); + // A duplicate tab page, even briefly, fails this spec's page assertions on a // strict mode violation. test('should not duplicate the tab page in the DOM while returning to the tabs', async ({ page }) => { diff --git a/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts index e651cab081c..7efd5018827 100644 --- a/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts +++ b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts @@ -17,6 +17,41 @@ export function withTestingMode(path: string): string { return `${path}${separator}ionic:_testing=true`; } +/** Clear the recorded events so an assertion only sees the navigation under test. */ +export async function resetLifecycleEvents(page: Page): Promise { + await page.evaluate(() => { + (window as any).lifecycleEvents = []; + }); +} + +/** + * Read `window.lifecycleEvents` once two consecutive reads match, so an event + * arriving just behind the expected ones is included rather than missed. + * + * An empty array never settles, so only use this where events are expected. + */ +export async function settledLifecycleEvents(page: Page): Promise { + let previous: string[] | undefined; + let current: string[] = []; + + try { + await expect + .poll(async () => { + current = await page.evaluate(() => ((window as any).lifecycleEvents ?? []) as string[]); + const settled = current.length > 0 && previous !== undefined && current.join('|') === previous.join('|'); + previous = current; + return settled; + }) + .toBe(true); + } catch (error) { + // The poll only yields a boolean, so report what was actually seen. A failing + // `page.evaluate` ends up here too, so keep the original as the cause. + throw new Error(`Lifecycle events never settled. Last read: ${JSON.stringify(current)}`, { cause: error }); + } + + return current; +} + let peakCounterId = 0; /** From a0610094444d26d425a0f2c3db1596ee7db21808 Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 30 Sep 2026 17:16:38 +0000 Subject: [PATCH 02/53] v9.0.6 --- CHANGELOG.md | 8 ++++++++ core/CHANGELOG.md | 8 ++++++++ core/package-lock.json | 6 +++--- core/package.json | 2 +- lerna.json | 2 +- packages/angular-server/CHANGELOG.md | 8 ++++++++ packages/angular-server/package-lock.json | 8 ++++---- packages/angular-server/package.json | 4 ++-- packages/angular/CHANGELOG.md | 8 ++++++++ packages/angular/package-lock.json | 8 ++++---- packages/angular/package.json | 4 ++-- packages/docs/CHANGELOG.md | 8 ++++++++ packages/docs/package-lock.json | 6 +++--- packages/docs/package.json | 2 +- packages/react-router/CHANGELOG.md | 8 ++++++++ packages/react-router/package-lock.json | 10 +++++----- packages/react-router/package.json | 6 +++--- packages/react/CHANGELOG.md | 8 ++++++++ packages/react/package-lock.json | 8 ++++---- packages/react/package.json | 4 ++-- packages/vue-router/CHANGELOG.md | 8 ++++++++ packages/vue-router/package-lock.json | 8 ++++---- packages/vue-router/package.json | 4 ++-- packages/vue/CHANGELOG.md | 8 ++++++++ packages/vue/package-lock.json | 8 ++++---- packages/vue/package.json | 4 ++-- 26 files changed, 119 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f27c8b343bd..f017d422729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +### Bug Fixes + +* **react-router:** dispatch view lifecycle events on non-animated transitions ([#31497](https://github.com/ionic-team/ionic-framework/issues/31497)) ([f01e9a7](https://github.com/ionic-team/ionic-framework/commit/f01e9a754d1267e2cdd60aae5d292ebbf2ef3deb)), closes [#31479](https://github.com/ionic-team/ionic-framework/issues/31479) +* **react-router:** keep splat route page mounted when a sibling route is pushed ([#31481](https://github.com/ionic-team/ionic-framework/issues/31481)) ([1130c2c](https://github.com/ionic-team/ionic-framework/commit/1130c2cc259c914a0a11fbe0976f4163de8d0e6e)), closes [#31477](https://github.com/ionic-team/ionic-framework/issues/31477) + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) ### Bug Fixes diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index d674ede539d..7532826ba38 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/core + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) ### Bug Fixes diff --git a/core/package-lock.json b/core/package-lock.json index 2bde401c74a..c07eb94a9e5 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -11661,4 +11661,4 @@ } } } -} +} \ No newline at end of file diff --git a/core/package.json b/core/package.json index 8444c47a09c..9e43d9b115f 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "9.0.5", + "version": "9.0.6", "description": "Base components for Ionic", "engines": { "node": ">= 16" diff --git a/lerna.json b/lerna.json index 42b3c003f70..3574ffe91d1 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "packages/vue", "packages/vue-router" ], - "version": "9.0.5" + "version": "9.0.6" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index 831afe9cf1d..bc1aabd84af 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 9a8cb16ca3a..b4e2f8ed9bb 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.5" + "@ionic/core": "^9.0.6" }, "devDependencies": { "@angular/animations": "^22.0.0", @@ -7175,4 +7175,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index a2a988b82f1..77424e5e919 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "9.0.5", + "version": "9.0.6", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -78,6 +78,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^9.0.5" + "@ionic/core": "^9.0.6" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index 3b416608446..b95833b97e7 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/angular + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/angular diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index 1d711964b88..ddf33cabd7b 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -9211,4 +9211,4 @@ "license": "MIT" } } -} +} \ No newline at end of file diff --git a/packages/angular/package.json b/packages/angular/package.json index c22700f8c9d..52fcb205f52 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "9.0.5", + "version": "9.0.6", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -168,7 +168,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index 67833761a51..c41faa00af0 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 659e6a2f786..017f1bcf4ff 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,13 +1,13 @@ { "name": "@ionic/docs", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT" } } -} +} \ No newline at end of file diff --git a/packages/docs/package.json b/packages/docs/package.json index a1990377554..073f240f010 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "9.0.5", + "version": "9.0.6", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 7eb5270a76b..a9b898792a1 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +### Bug Fixes + +* **react-router:** dispatch view lifecycle events on non-animated transitions ([#31497](https://github.com/ionic-team/ionic-framework/issues/31497)) ([f01e9a7](https://github.com/ionic-team/ionic-framework/commit/f01e9a754d1267e2cdd60aae5d292ebbf2ef3deb)), closes [#31479](https://github.com/ionic-team/ionic-framework/issues/31479) +* **react-router:** keep splat route page mounted when a sibling route is pushed ([#31481](https://github.com/ionic-team/ionic-framework/issues/31481)) ([1130c2c](https://github.com/ionic-team/ionic-framework/commit/1130c2cc259c914a0a11fbe0976f4163de8d0e6e)), closes [#31477](https://github.com/ionic-team/ionic-framework/issues/31477) + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/react-router diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index c947c699c42..369fd43b5e9 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,16 +1,16 @@ { "name": "@ionic/react-router", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.5", - "@ionic/react": "^9.0.5", + "@ionic/core": "^9.0.6", + "@ionic/react": "^9.0.6", "tslib": "*" }, "devDependencies": { @@ -4632,4 +4632,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 4e223a8da8f..d9751e3ec8d 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "9.0.5", + "version": "9.0.6", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -37,8 +37,8 @@ "dist/" ], "dependencies": { - "@ionic/core": "^9.0.5", - "@ionic/react": "^9.0.5", + "@ionic/core": "^9.0.6", + "@ionic/react": "^9.0.6", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 2bf934dde36..86049812550 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/react + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/react diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index c7baf497222..d950b7a4367 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" @@ -8837,4 +8837,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json index 3036654e2ba..6daf86bc3cd 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "9.0.5", + "version": "9.0.6", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -41,7 +41,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index 5d7f7e8a1f0..dfe731e8669 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/vue-router + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) **Note:** Version bump only for package @ionic/vue-router diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 435a14776fa..d81d1afe245 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/vue": "^9.0.5" + "@ionic/vue": "^9.0.6" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -5146,4 +5146,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 361b89bf60d..90a8eb8d520 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "9.0.5", + "version": "9.0.6", "description": "Vue Router integration for @ionic/vue", "scripts": { "eslint": "eslint src", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^9.0.5" + "@ionic/vue": "^9.0.6" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index b0307504663..4091f7d6fb2 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) + +**Note:** Version bump only for package @ionic/vue + + + + + ## [9.0.5](https://github.com/ionic-team/ionic-framework/compare/v9.0.4...v9.0.5) (2026-09-23) ### Bug Fixes diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index a2ed5c58182..21bca88312d 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "9.0.5", + "version": "9.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "9.0.5", + "version": "9.0.6", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" }, @@ -5234,4 +5234,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/vue/package.json b/packages/vue/package.json index ff6d6b26376..555d306681a 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "9.0.5", + "version": "9.0.6", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -78,7 +78,7 @@ } }, "dependencies": { - "@ionic/core": "^9.0.5", + "@ionic/core": "^9.0.6", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" }, From 5baeafe37a6f126a55a221db992bbe82a692e1a4 Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 30 Sep 2026 17:17:46 +0000 Subject: [PATCH 03/53] chore(): update package lock files --- core/package-lock.json | 2 +- packages/angular-server/package-lock.json | 8 ++++---- packages/angular/package-lock.json | 8 ++++---- packages/docs/package-lock.json | 2 +- packages/react-router/package-lock.json | 16 ++++++++-------- packages/react/package-lock.json | 8 ++++---- packages/vue-router/package-lock.json | 16 ++++++++-------- packages/vue/package-lock.json | 8 ++++---- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index c07eb94a9e5..7d84539b209 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -11661,4 +11661,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index b4e2f8ed9bb..3df144c5282 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1097,9 +1097,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -7175,4 +7175,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index ddf33cabd7b..88671477585 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1580,9 +1580,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -9211,4 +9211,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 017f1bcf4ff..0b4c32b1c40 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -10,4 +10,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index 369fd43b5e9..4010da905a1 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -257,9 +257,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -297,12 +297,12 @@ } }, "node_modules/@ionic/react": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-9.0.5.tgz", - "integrity": "sha512-a77jWFtVxWZ5nprZqu8DMOpPjwIoCX3UxUbgQcPQ2ARu4a+Lqr9s045ddFoo8NHDCfBTcpMFbIby0Sns6AOMrQ==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-9.0.6.tgz", + "integrity": "sha512-WAcbe7ZDIDqQaXvcAF7+AxpYW3Y3HFvZbDnpwQtoSUo8RTAw+AQQOTTSlp1eHOjwqpVqYceBFAdhfNbzBfw/1w==", "license": "MIT", "dependencies": { - "@ionic/core": "9.0.5", + "@ionic/core": "9.0.6", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" @@ -4632,4 +4632,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index d950b7a4367..c1f51ebfe4a 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -891,9 +891,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -8837,4 +8837,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index d81d1afe245..d5a50dbb7eb 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -342,9 +342,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -382,12 +382,12 @@ } }, "node_modules/@ionic/vue": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-9.0.5.tgz", - "integrity": "sha512-d5HDYRynpr6OlMO4gawIPR4sSEBnc8yCtYWTx6W616HSMi3tEloncj2HPMQC/f8D75wTOTaVGDHkGg+LETDgDQ==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-9.0.6.tgz", + "integrity": "sha512-Bg8RqfnsVejulSXF+YENjZBtx1cEwZPUZAA47oLhFceYlSodapLI2qWeHgn6hgKmapCe+GzyJQIuxyRIfXqlXQ==", "license": "MIT", "dependencies": { - "@ionic/core": "9.0.5", + "@ionic/core": "9.0.6", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" } @@ -5146,4 +5146,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 21bca88312d..20851337633 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -346,9 +346,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.5.tgz", - "integrity": "sha512-Fo3hC0f7YI3XP0eAUUlm49q00u0OG0z55+EZ+s2YZHCUyNL1xq20JKtiPf79pn2tJ8CAYw4u52x6QcWUkL4P3A==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", + "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -5234,4 +5234,4 @@ } } } -} \ No newline at end of file +} From 643584b3ac2ba1ca263cbf53960ccde3be2a96fc Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 1 Oct 2026 13:36:56 +0000 Subject: [PATCH 04/53] fix(vue-router): render the redirect target when a guard redirects (#31510) Issue number: internal --------- ## What is the current behavior? Currently, when a guard returns a location instead of `false` during a back navigation, the URL updates to the redirect target but Ionic renders a different page. A redirect doesn't fail, so the original navigation never reaches `afterEach` or `onError`. Its staged delta and params are never cleared and get applied to the redirect target instead. ## What is the new behavior? The ownership `beforeEach` now discards the state a navigation claimed when a guard redirects it. We match it through `redirectedFrom`, which vue-router points at the first location in the redirect chain. That covers leave guards, which run before our hook, and a guard redirect to the current page, which fails as a duplicate before reaching `beforeEach`. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information This was [raised while reviewing #31364](https://github.com/ionic-team/ionic-framework/pull/31364#discussion_r3808016936) and left out of scope there. --- packages/vue-router/src/router.ts | 73 ++++- .../vue/test/base/tests/unit/routing.spec.ts | 308 ++++++++++++++++++ 2 files changed, 370 insertions(+), 11 deletions(-) diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index 7045dad5947..f4308e1a7cf 100644 --- a/packages/vue-router/src/router.ts +++ b/packages/vue-router/src/router.ts @@ -117,10 +117,6 @@ export const createIonRouter = ( * instead, so there is no failure to inspect there and the staged state * would survive. The error is re-thrown to the app the same way it was * before, so navigation outcomes are unchanged. - * - * A guard that returns a location is still not covered, because that - * redirects rather than fails and afterEach is never called for the original - * navigation. */ addErrorHandler((error: unknown, to: RouteLocationNormalized) => { discardStagedStateFor(to); @@ -183,20 +179,71 @@ export const createIonRouter = ( incomingRouteParamsUnclaimed = false; }; + /** + * Whether `to` is a guard redirect of the navigation that claimed `owner`. + * vue-router points `redirectedFrom` at the first location in a redirect + * chain, so an owner that a `redirect:` record led to is matched on its own + * `redirectedFrom`. + */ + const isGuardRedirectOf = ( + owner: RouteLocationNormalized | undefined, + to: RouteLocationNormalized + ) => + owner !== undefined && + to.redirectedFrom !== undefined && + (owner.redirectedFrom ?? owner) === to.redirectedFrom; + + /** + * Whether `to` was redirected by a guard rather than a `redirect:` record. + * This can't tell a record redirect apart from one a leave guard redirected + * again afterwards. + */ + const isRedirectedByGuard = (to: RouteLocationNormalized) => { + const origin = to.redirectedFrom; + + return ( + origin !== undefined && + origin.matched[origin.matched.length - 1]?.redirect === undefined + ); + }; + /** * The navigation that starts first after params are staged is the one they * were staged for, so it takes ownership of them here. Registered before any * guard the app adds so that it still runs when one of those aborts. + * + * A guard that returns a location starts a new navigation and the original + * never reaches afterEach or onError, so we discard what it claimed here + * instead. Leave guards run before this hook, so a navigation they redirect + * hasn't claimed anything yet and its state is discarded unclaimed. */ router.beforeEach((to: RouteLocationNormalized) => { + if (isGuardRedirectOf(currentNavigationInfoOwner, to)) { + clearNavigationInfo(); + } + + if (isGuardRedirectOf(incomingRouteParamsOwner, to)) { + clearStagedParams(); + } + + const redirectedByGuard = isRedirectedByGuard(to); + if (incomingRouteParamsUnclaimed) { - incomingRouteParamsOwner = to; - incomingRouteParamsUnclaimed = false; + if (redirectedByGuard) { + clearStagedParams(); + } else { + incomingRouteParamsOwner = to; + incomingRouteParamsUnclaimed = false; + } } if (currentNavigationInfoUnclaimed) { - currentNavigationInfoOwner = to; - currentNavigationInfoUnclaimed = false; + if (redirectedByGuard) { + clearNavigationInfo(); + } else { + currentNavigationInfoOwner = to; + currentNavigationInfoUnclaimed = false; + } } }); @@ -219,18 +266,22 @@ export const createIonRouter = ( * Both are matched on the navigation that owns them rather than on where it * was heading, because two navigations can head for the same path and a path * cannot tell them apart. State still unclaimed belongs to this navigation, - * since nothing has started since it was staged. + * since nothing has started since it was staged. A guard redirect that fails + * before reaching beforeEach, like one to the current page, is matched + * through the navigation it redirected. */ const discardStagedStateFor = (to: RouteLocationNormalized) => { const deltaIsForThisNavigation = currentNavigationInfoOwner === undefined ? currentNavigationInfoUnclaimed - : currentNavigationInfoOwner === to; + : currentNavigationInfoOwner === to || + isGuardRedirectOf(currentNavigationInfoOwner, to); const paramsAreForThisNavigation = incomingRouteParamsOwner === undefined ? incomingRouteParamsUnclaimed - : incomingRouteParamsOwner === to; + : incomingRouteParamsOwner === to || + isGuardRedirectOf(incomingRouteParamsOwner, to); if (deltaIsForThisNavigation) { clearNavigationInfo(); diff --git a/packages/vue/test/base/tests/unit/routing.spec.ts b/packages/vue/test/base/tests/unit/routing.spec.ts index dcb7d39c6de..ffa5985f4ea 100644 --- a/packages/vue/test/base/tests/unit/routing.spec.ts +++ b/packages/vue/test/base/tests/unit/routing.spec.ts @@ -75,6 +75,16 @@ const waitUntil = async (predicate: () => boolean, label: string) => { throw new Error(`timed out waiting for ${label}`); }; +/* + * A redirect ends a navigation somewhere other than where it started, so wait + * for the path it should end on before letting the router settle. + */ +const navigateTo = async (router: any, path: string, navigate: () => void) => { + navigate(); + await waitUntil(() => router.currentRoute.value.path === path, `the navigation to ${path}`); + await waitForRouter(); +}; + describe('Routing', () => { it('should pass no props', async () => { const Page1 = { @@ -1609,6 +1619,304 @@ describe('Routing', () => { ]); }); + const mountBackRedirect = async () => { + let navManager: any; + + const Home = { + ...createPage('home'), + setup() { + navManager = inject('navManager'); + } + }; + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', redirect: '/home' }, + { path: '/home', component: Home }, + { path: '/login', component: createPage('login') }, + { path: '/profile', component: createPage('profile') } + ] + }); + + router.beforeEach((to, from) => { + if (from.path === '/profile' && to.path === '/home') { + return '/login'; + } + + return true; + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(IonRouterOutlet, { + global: { + plugins: [router, IonicVue] + } + }); + + router.push('/profile'); + await waitForRouter(); + + return { router, navManager, wrapper }; + }; + + it('should show the redirect target when a guard redirects a browser back', async () => { + const { router, navManager, wrapper } = await mountBackRedirect(); + + await navigateTo(router, '/login', () => router.back()); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'profile', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + it('should show the redirect target when a guard redirects a back button navigation', async () => { + const { router, navManager, wrapper } = await mountBackRedirect(); + + /* + * The back button stages the Home route it expects to go back to, which + * Login would inherit if it were carried over. + */ + await navigateTo(router, '/login', () => navManager.handleNavigateBack()); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'profile', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + it('should go back normally after a guard redirects a browser back', async () => { + const { router, navManager, wrapper } = await mountBackRedirect(); + + await navigateTo(router, '/login', () => router.back()); + + // The redirect replaced Profile's history entry, so back lands on Home. + await navigateTo(router, '/home', () => router.back()); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/home', + routerAction: 'pop', + routerDirection: 'back' + }); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: false }, + { id: 'profile', hidden: true } + ]); + }); + + /* + * Leave guards run before any global guard, so Ionic never sees the + * navigation they redirect start. + */ + const mountLeaveRedirect = async () => { + let navManager: any; + + const Home = { + ...createPage('home'), + setup() { + navManager = inject('navManager'); + } + }; + const Profile = { + ...createPage('profile'), + setup() { + onBeforeRouteLeave((to) => (to.path === '/home' ? '/login' : true)); + } + }; + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', redirect: '/home' }, + { path: '/home', component: Home }, + { path: '/login', component: createPage('login') }, + { path: '/profile', component: Profile } + ] + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(IonRouterOutlet, { + global: { + plugins: [router, IonicVue] + } + }); + + router.push('/profile'); + await waitForRouter(); + + return { router, navManager, wrapper }; + }; + + it('should show the redirect target when a leave guard redirects a browser back', async () => { + const { router, navManager, wrapper } = await mountLeaveRedirect(); + + await navigateTo(router, '/login', () => router.back()); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'profile', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + it('should show the redirect target when a leave guard redirects a back button navigation', async () => { + const { router, navManager, wrapper } = await mountLeaveRedirect(); + + await navigateTo(router, '/login', () => navManager.handleNavigateBack()); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'profile', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + /* + * Only a navigation that no guard redirected should keep the root direction, + * including one that a `redirect:` record redirected. + */ + const mountRootRedirect = async () => { + let navManager: any; + + const Home = { + ...createPage('home'), + setup() { + navManager = inject('navManager'); + } + }; + + const router = createRouter({ + history: createWebHistory(process.env.BASE_URL), + routes: [ + { path: '/', redirect: '/home' }, + { path: '/home', component: Home }, + { path: '/login', component: createPage('login') }, + { path: '/dashboard', component: createPage('dashboard') }, + { path: '/tabs', redirect: '/tabs/tab' }, + { path: '/tabs/tab', component: createPage('tab') }, + { path: '/admin', redirect: '/admin/users' }, + { path: '/admin/users', component: createPage('users') } + ] + }); + + router.beforeEach((to) => { + if (to.path === '/dashboard' || to.path === '/admin/users') { + return '/login'; + } + + return true; + }); + + router.push('/'); + await router.isReady(); + const wrapper = mount(IonRouterOutlet, { + global: { + plugins: [router, IonicVue] + } + }); + + return { router, navManager, wrapper }; + }; + + it('should not apply the params of a guard redirected navigation to the redirect target', async () => { + const { router, navManager, wrapper } = await mountRootRedirect(); + + // If it were carried over, the root direction would drop Home from the back stack. + await navigateTo(router, '/login', () => navManager.handleNavigate('/dashboard', 'push', 'root')); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(navManager.canGoBack()).toBe(true); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + // Guards against clearing the params of a navigation that only a route record redirected. + it('should keep the params of a navigation redirected by a route record', async () => { + const { router, navManager, wrapper } = await mountRootRedirect(); + + await navigateTo(router, '/tabs/tab', () => navManager.handleNavigate('/tabs', 'push', 'root')); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/tabs/tab', + routerAction: 'push', + routerDirection: 'root' + }); + expect(navManager.canGoBack()).toBe(false); + expect(viewStack(wrapper)).toEqual([{ id: 'tab', hidden: false }]); + }); + + it('should not apply the params when a guard redirects after a route record', async () => { + const { router, navManager, wrapper } = await mountRootRedirect(); + + await navigateTo(router, '/login', () => navManager.handleNavigate('/admin', 'push', 'root')); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/login', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(navManager.canGoBack()).toBe(true); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'login', hidden: false } + ]); + }); + + it('should not apply the params when a guard redirects to the current page', async () => { + const { router, navManager, wrapper } = await mountRootRedirect(); + + await navigateTo(router, '/login', () => router.push('/login')); + + // The guard sends it back to Login, where the router already is, so it fails as a duplicate. + navManager.handleNavigate('/dashboard', 'push', 'root'); + await waitForRouter(); + + await navigateTo(router, '/tabs/tab', () => router.push('/tabs')); + + expect(currentRoute(navManager)).toEqual({ + pathname: '/tabs/tab', + routerAction: 'push', + routerDirection: 'forward' + }); + expect(navManager.canGoBack()).toBe(true); + expect(viewStack(wrapper)).toEqual([ + { id: 'home', hidden: true }, + { id: 'login', hidden: true }, + { id: 'tab', hidden: false } + ]); + }); + /* * Registering an error handler stops vue-router logging an uncaught * navigation error itself, so these cover the log still reaching an app that From edbc304fbc4179a668d0e170241abd78311fdb62 Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 1 Oct 2026 15:24:37 +0000 Subject: [PATCH 05/53] fix(vue-router): reset tab with memory history (#31511) Issue number: resolves #29785 --------- ## What is the current behavior? Currently, with `createMemoryHistory`, tapping the active tab from a child page leaves the child on screen. Memory history doesn't store a position in `history.state`, so the offset `resetTab()` computes is `NaN` and `router.go(NaN)` goes nowhere. On 9.x it also throws `Cannot read properties of undefined (reading '0')`. ## What is the new behavior? When there's no position to traverse, `resetTab()` now replaces the current entry with the tab's `href`, the same fallback it already uses after a deep load. It uses the tab's `href` rather than the first route recorded for the tab, because after a deep load onto a child that first route is the child itself. Web and hash history always have a numeric position, so they're unaffected. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The new tests are in `memory.spec.ts`, since the Vue test app uses web history and can't reproduce this. They cover resetting from a child page, resetting after a deep load onto a child, and replacing the child entry rather than pushing over it. --- packages/vue-router/src/router.ts | 10 ++ .../vue/test/base/tests/unit/memory.spec.ts | 114 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/vue-router/src/router.ts b/packages/vue-router/src/router.ts index f4308e1a7cf..365b8454b40 100644 --- a/packages/vue-router/src/router.ts +++ b/packages/vue-router/src/router.ts @@ -827,6 +827,16 @@ export const createIonRouter = ( const routeInfo = locationHistory.getFirstRouteInfoForTab(tab); if (routeInfo) { const delta = routeInfo.position! - currentHistoryPosition; + /** + * Memory history doesn't store a position in `history.state`, so + * `delta` is NaN and there's nothing to traverse. Replace instead. + */ + if (Number.isNaN(delta)) { + if (originalHref) { + handleNavigate(originalHref, "pop", "back", undefined, tab); + } + return; + } if (delta !== 0) { router.go(delta); return; diff --git a/packages/vue/test/base/tests/unit/memory.spec.ts b/packages/vue/test/base/tests/unit/memory.spec.ts index 5a109446f8e..a1a482d482d 100644 --- a/packages/vue/test/base/tests/unit/memory.spec.ts +++ b/packages/vue/test/base/tests/unit/memory.spec.ts @@ -10,6 +10,9 @@ import { IonApp, IonRouterOutlet, IonPage, + IonTabs, + IonTabBar, + IonTabButton, } from '@ionic/vue'; import { waitForRouter } from './utils'; @@ -112,4 +115,115 @@ describe('createMemoryHistory', () => { await waitForRouter(); expect(router.currentRoute.value.path).toBe('/page3'); }); + + // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/29785 + describe('tapping the active tab', () => { + const Tabs = { + components: { IonPage, IonTabs, IonTabBar, IonTabButton, IonRouterOutlet }, + template: ` + + + + + Tab 1 + Tab 2 + + + + `, + }; + const Tab1 = { + components: { IonPage }, + template: 'Tab 1', + }; + const Tab1Child = { + components: { IonPage }, + template: 'Tab 1 Child', + }; + const Tab2 = { + components: { IonPage }, + template: 'Tab 2', + }; + + const createTabsRouter = () => + createRouter({ + history: createMemoryHistory(process.env.BASE_URL), + routes: [ + { path: '/', redirect: '/tabs/tab1' }, + { + path: '/tabs/', + component: Tabs, + children: [ + { path: 'tab1', component: Tab1 }, + { path: 'tab1/child', component: Tab1Child }, + { path: 'tab2', component: Tab2 }, + ], + }, + ], + }); + + const tapTab = async (wrapper: ReturnType, tab: string) => { + const button = wrapper + .findAllComponents(IonTabButton) + .find((b) => b.props('tab') === tab)!; + await button.trigger('click'); + await waitForRouter(); + }; + + it('should return to the tab root from a child page', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + router.push('/tabs/tab1/child'); + await waitForRouter(); + expect(router.currentRoute.value.path).toBe('/tabs/tab1/child'); + + await tapTab(wrapper, 'tab1'); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + + it('should return to the tab root when the app started on a child page', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1/child'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + await tapTab(wrapper, 'tab1'); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + + it('should replace the child page entry rather than push over it', async () => { + const router = createTabsRouter(); + + router.push('/tabs/tab1'); + await router.isReady(); + const wrapper = mount(App, { + global: { plugins: [router, IonicVue] }, + }); + await waitForRouter(); + + router.push('/tabs/tab1/child'); + await waitForRouter(); + + await tapTab(wrapper, 'tab1'); + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + + router.back(); + await waitForRouter(); + + expect(router.currentRoute.value.path).toBe('/tabs/tab1'); + }); + }); }) From 4c71c0be64a9e994e9829c1c466739cb75268102 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:57:11 +0000 Subject: [PATCH 06/53] chore(deps): update dependency vitest to v5.0.3 (#31512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [vitest](https://vitest.dev) ([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`5.0.2` → `5.0.3`](https://renovatebot.com/diffs/npm/vitest/5.0.2/5.0.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/5.0.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/5.0.2/5.0.3?slim=true) | --- ### Release Notes
vitest-dev/vitest (vitest) ### [`v5.0.3`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v5.0.3) [Compare Source](https://redirect.github.com/vitest-dev/vitest/compare/v5.0.2...v5.0.3) #####    🐞 Bug Fixes - Isolate `result.status` between `repeats` runs  -  by [@​hi-ogawa](https://redirect.github.com/hi-ogawa), **Hiroshi Ogawa** and **Codex (GPT-6)** in [#​11218](https://redirect.github.com/vitest-dev/vitest/issues/11218) [(5dbeb)](https://redirect.github.com/vitest-dev/vitest/commit/5dbebe9e3) - Don't print an interceptor warning in browser mode  -  by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11377](https://redirect.github.com/vitest-dev/vitest/issues/11377) [(15cc0)](https://redirect.github.com/vitest-dev/vitest/commit/15cc006aa) - Don't retry when `test.fails` expectedly failed  -  by [@​hi-ogawa](https://redirect.github.com/hi-ogawa), **Hiroshi Ogawa** and **Codex (GPT-6)** in [#​11219](https://redirect.github.com/vitest-dev/vitest/issues/11219) [(b2458)](https://redirect.github.com/vitest-dev/vitest/commit/b24585f08) - Scope cache key generators to projects  -  by [@​ecoyoung](https://redirect.github.com/ecoyoung) in [#​11281](https://redirect.github.com/vitest-dev/vitest/issues/11281) and [#​11301](https://redirect.github.com/vitest-dev/vitest/issues/11301) [(92ba7)](https://redirect.github.com/vitest-dev/vitest/commit/92ba7fc1d) - **browser**: - Delay server `listen` until tests start running  -  by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11366](https://redirect.github.com/vitest-dev/vitest/issues/11366) [(7d8ed)](https://redirect.github.com/vitest-dev/vitest/commit/7d8ed3e9b) - Check mock path boundaries  -  by [@​saryn17](https://redirect.github.com/saryn17), **Ryosei Sato** and [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11361](https://redirect.github.com/vitest-dev/vitest/issues/11361) and [#​11362](https://redirect.github.com/vitest-dev/vitest/issues/11362) [(1c388)](https://redirect.github.com/vitest-dev/vitest/commit/1c3888bce) - Keep config of browser-consumed environments  -  by [@​kasperpeulen](https://redirect.github.com/kasperpeulen) and **Claude** in [#​11378](https://redirect.github.com/vitest-dev/vitest/issues/11378) [(aafc0)](https://redirect.github.com/vitest-dev/vitest/commit/aafc0996f) - Ignore page crash while cancelling  -  by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11386](https://redirect.github.com/vitest-dev/vitest/issues/11386) [(7c367)](https://redirect.github.com/vitest-dev/vitest/commit/7c36748fa) - `toMatchScreenshot` uses wrong reference on retried tests  -  by [@​macarie](https://redirect.github.com/macarie) in [#​11393](https://redirect.github.com/vitest-dev/vitest/issues/11393) [(c22ab)](https://redirect.github.com/vitest-dev/vitest/commit/c22aba992) - **cache**: - Revalidate imports of cached modules  -  by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11381](https://redirect.github.com/vitest-dev/vitest/issues/11381) [(38f98)](https://redirect.github.com/vitest-dev/vitest/commit/38f98855f) - **deps**: - Pin `why-is-node-running` to `3.2.1` to avoid users running into `ERR_PNPM_TRUST_DOWNGRADE`  -  by [@​AriPerkkio](https://redirect.github.com/AriPerkkio) in [#​11403](https://redirect.github.com/vitest-dev/vitest/issues/11403) [(f6c9a)](https://redirect.github.com/vitest-dev/vitest/commit/f6c9a4977) - **expect**: - Pass current equality testers to `expect.extend` asymmetric matchers  -  by [@​hi-ogawa](https://redirect.github.com/hi-ogawa), **Hiroshi Ogawa** and **Claude** in [#​11401](https://redirect.github.com/vitest-dev/vitest/issues/11401) [(3e794)](https://redirect.github.com/vitest-dev/vitest/commit/3e794a96b) - **jsdom**: - Support Blob on jsdom 30.1  -  by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​11379](https://redirect.github.com/vitest-dev/vitest/issues/11379) [(6c49b)](https://redirect.github.com/vitest-dev/vitest/commit/6c49b7197) - **pool**: - Preserve unique pool ids when `groupOrder` is set  -  by [@​mtorp](https://redirect.github.com/mtorp) in [#​11392](https://redirect.github.com/vitest-dev/vitest/issues/11392) [(50312)](https://redirect.github.com/vitest-dev/vitest/commit/50312ebb4) - **ui**: - Split-pane handle overlapping iframe  -  by [@​macarie](https://redirect.github.com/macarie) in [#​11221](https://redirect.github.com/vitest-dev/vitest/issues/11221) [(f91db)](https://redirect.github.com/vitest-dev/vitest/commit/f91db0dfd) - **vitest**: - Remove root temp dir on close  -  by [@​abhinav-phi](https://redirect.github.com/abhinav-phi) in [#​11248](https://redirect.github.com/vitest-dev/vitest/issues/11248) [(7c711)](https://redirect.github.com/vitest-dev/vitest/commit/7c7119cf7) - **vm**: - Do not optimize deps from index.html  -  by [@​ezefernandezyf](https://redirect.github.com/ezefernandezyf), **Hiroshi Ogawa** and **Codex (GPT-6)** in [#​11329](https://redirect.github.com/vitest-dev/vitest/issues/11329) and [#​11360](https://redirect.github.com/vitest-dev/vitest/issues/11360) [(caf28)](https://redirect.github.com/vitest-dev/vitest/commit/caf2887de) - Don't reuse scripts across vite environments  -  by [@​MO2k4](https://redirect.github.com/MO2k4), **Martin Oehlert** and **Claude** in [#​11395](https://redirect.github.com/vitest-dev/vitest/issues/11395) [(346d3)](https://redirect.github.com/vitest-dev/vitest/commit/346d3896b) #####     [View changes on GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v5.0.2...v5.0.3)
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/migrate/package-lock.json | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/migrate/package-lock.json b/packages/migrate/package-lock.json index 4714971f7aa..e3d91f8fba2 100644 --- a/packages/migrate/package-lock.json +++ b/packages/migrate/package-lock.json @@ -980,14 +980,14 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", - "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.2", + "@vitest/spy": "5.0.3", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, @@ -1008,9 +1008,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", - "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", "dev": true, "license": "MIT", "funding": { @@ -1509,14 +1509,14 @@ } }, "node_modules/vitest": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", - "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.2", + "@vitest/mocker": "5.0.3", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -1527,7 +1527,7 @@ "tinybench": "^6.1.4", "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", - "why-is-node-running": "^3.2.1" + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" @@ -1542,12 +1542,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.2", - "@vitest/browser-preview": "5.0.2", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.2", - "@vitest/coverage-v8": "5.0.2", - "@vitest/ui": "5.0.2", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" @@ -1591,10 +1591,10 @@ } } }, - "node_modules/why-is-node-running": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", - "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", + "node_modules/vitest/node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, "license": "MIT", "bin": { From 35bd2fa9f1046b3f5fd3cd6b3778b0b5945be99c Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 1 Oct 2026 23:23:24 +0000 Subject: [PATCH 07/53] fix(css): stop emitting :host-context selectors in global stylesheets (#31516) Issue number: resolves #30024 --------- ## What is the current behavior? Currently, `float-elements.css` (and `ionic.bundle.css` / `utils.bundle.css`, which include it) contains `:host-context([dir=rtl]) .ion-float-*` selectors from the `rtl()` mixin. Lightning CSS, the default CSS minifier in Vite 8, warns about every one of them. They can never match anyway, because these stylesheets apply to the document, not a shadow root. ## What is the new behavior? The `rtl()` mixin now skips its `:host-context` rule when `$rtl-use-host-context` is false, and `float-elements.scss` sets it. The float utilities still flip in RTL through the existing `[dir=rtl]` and `:dir(rtl)` rules. Component styles are unchanged, since they still need `:host-context`. A new spec compiles every global stylesheet and asserts none of them contain `:host-context`. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The only output change is 10 removed `:host-context` rules in `float-elements.css`, which also come out of the two bundles that include it. ## Dev build ``` 9.0.7-dev.11790870774.174de601 ``` --- core/src/css/float-elements.scss | 3 ++ core/src/css/test/global-stylesheets.spec.ts | 38 ++++++++++++++++++++ core/src/themes/ionic.mixins.scss | 7 +++- 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 core/src/css/test/global-stylesheets.spec.ts diff --git a/core/src/css/float-elements.scss b/core/src/css/float-elements.scss index 0e543e3404e..78147a3798a 100644 --- a/core/src/css/float-elements.scss +++ b/core/src/css/float-elements.scss @@ -1,3 +1,6 @@ +// Global stylesheet: `:host-context()` never matches outside a shadow root +$rtl-use-host-context: false; + @import "../themes/ionic.globals"; @import "../themes/ionic.mixins"; diff --git a/core/src/css/test/global-stylesheets.spec.ts b/core/src/css/test/global-stylesheets.spec.ts new file mode 100644 index 00000000000..430e3702de1 --- /dev/null +++ b/core/src/css/test/global-stylesheets.spec.ts @@ -0,0 +1,38 @@ +import { execFileSync } from 'child_process'; +import { readdirSync } from 'fs'; +import { dirname, join } from 'path'; + +const cssDir = join(__dirname, '..'); +const sassCli = join(dirname(require.resolve('sass')), 'sass.js'); + +/** + * Compiles through the sass CLI, the same way `npm run css.sass` builds + * the published stylesheets. The sass JS API can't be imported here + * because it detects the spec window and never populates its exports. + */ +const compileStylesheet = (file: string) => + execFileSync(process.execPath, [sassCli, '--style=compressed', '--no-source-map', join(cssDir, file)], { + encoding: 'utf8', + }); + +const globalStylesheets = ['.', 'palettes'].flatMap((dir) => + readdirSync(join(cssDir, dir)) + .filter((file) => file.endsWith('.scss')) + .map((file) => join(dir, file)) +); + +describe('global stylesheets', () => { + // https://github.com/ionic-team/ionic-framework/issues/30024 + it.each(globalStylesheets)('%s should not contain :host-context', (file) => { + expect(compileStylesheet(file)).not.toContain(':host-context'); + }); + + it('float-elements.scss should flip start and end floats for an ancestor dir=rtl', () => { + const css = compileStylesheet('float-elements.scss'); + + expect(css).toContain('[dir=rtl] .ion-float-start{float:right !important}'); + expect(css).toContain('[dir=rtl] .ion-float-end{float:left !important}'); + expect(css).toContain('.ion-float-start:dir(rtl){float:right !important}'); + expect(css).toContain('.ion-float-end:dir(rtl){float:left !important}'); + }); +}); diff --git a/core/src/themes/ionic.mixins.scss b/core/src/themes/ionic.mixins.scss index da174046be7..aa25b62abe6 100644 --- a/core/src/themes/ionic.mixins.scss +++ b/core/src/themes/ionic.mixins.scss @@ -226,6 +226,11 @@ // } } +// Whether `rtl()` emits `:host-context()` selectors. Global stylesheets +// set this to false: they apply to the document, where `:host-context()` +// never matches, and Lightning CSS warns about every occurrence. +$rtl-use-host-context: true !default; + @mixin rtl() { $root: #{&}; @@ -254,7 +259,7 @@ } // Supported by Chrome. - @if length($hostContextSelectors) > 0 { + @if $rtl-use-host-context and length($hostContextSelectors) > 0 { @at-root #{$hostContextSelectors} { @content; } From 6ba81ad3f81a5ade291636c40445e9f4a4fb66c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:51:50 +0000 Subject: [PATCH 08/53] chore(deps): update dependency @types/node to v24.19.1 (#31522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.19.0` → `24.19.1`](https://renovatebot.com/diffs/npm/@types%2fnode/24.19.0/24.19.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.19.0/24.19.1?slim=true) | --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/migrate/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/migrate/package-lock.json b/packages/migrate/package-lock.json index e3d91f8fba2..6abc6ef26e5 100644 --- a/packages/migrate/package-lock.json +++ b/packages/migrate/package-lock.json @@ -970,9 +970,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", - "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", "dev": true, "license": "MIT", "dependencies": { From e425825815d3c0ab39800912558624b9de72dc6f Mon Sep 17 00:00:00 2001 From: Hemanth Darapureddy <41389351+hemanth004@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:44:18 +0000 Subject: [PATCH 09/53] fix(input-otp): sync value when length changes (#31485) Issue number: resolves #31484 --------- ## What is the current behavior? Changing the length prop at runtime updates the rendered input boxes but can leave the public value longer than the rendered inputs. ## What is the new behavior? The component now reinitializes its values and tab indexes when length changes, keeping the public value synchronized with the rendered inputs. - Added a runtime length-change end-to-end regression test. - ESLint passes for the changed files. - Targeted Playwright execution is pending because the local browser executable is unavailable. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Fixes #31484. No dependency updates or breaking API changes. --------- Co-authored-by: ShaneK --- core/src/components/input-otp/input-otp.tsx | 28 ++++++++- .../input-otp/test/a11y/input-otp.e2e.ts | 21 +++++++ .../input-otp/test/basic/input-otp.e2e.ts | 63 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index 7c46ca59d42..1c84b3c9a42 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -43,6 +43,12 @@ export class InputOTP implements ComponentInterface { */ private isKeyboardNavigation = false; + /** + * The `length` watcher runs before new inputs render, so + * `componentDidRender` updates their tab indexes instead. + */ + private updateTabIndexesAfterRender = false; + @Element() el!: HTMLIonInputOtpElement; @State() private inputValues: string[] = []; @@ -197,6 +203,12 @@ export class InputOTP implements ComponentInterface { this.updateTabIndexes(); } + @Watch('length') + lengthChanged() { + this.initializeValues(); + this.updateTabIndexesAfterRender = true; + } + /** * Processes the separators prop into an array of numbers. * @@ -272,6 +284,13 @@ export class InputOTP implements ComponentInterface { this.updateTabIndexes(); } + componentDidRender() { + if (this.updateTabIndexesAfterRender) { + this.updateTabIndexesAfterRender = false; + this.updateTabIndexes(); + } + } + /** * Get the regex pattern for allowed characters. * If a pattern is provided, use it to create a regex pattern @@ -851,7 +870,14 @@ export class InputOTP implements ComponentInterface { tabIndex={index === tabbableIndex ? 0 : -1} value={inputValues[index] || ''} autocomplete="one-time-code" - ref={(el) => (inputRefs[index] = el as HTMLInputElement)} + ref={(el) => { + if (el) { + inputRefs[index] = el as HTMLInputElement; + } else { + // The input was removed, so drop its ref and any after it. + inputRefs.splice(index); + } + }} onInput={this.onInput(index)} onBlur={this.onBlur} onFocus={this.onFocus(index)} diff --git a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts index 88b0be04800..7cc5374d8ef 100644 --- a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts +++ b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts @@ -46,6 +46,27 @@ configs().forEach(({ title, config }) => { await expect(inputBoxes.nth(3)).toHaveAttribute('aria-hidden', 'true'); }); + test('should update aria-hidden when length increases', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31484', + }); + + await page.setContent(``, config); + + const inputOtp = page.locator('ion-input-otp'); + await inputOtp.evaluate((el: HTMLIonInputOtpElement) => { + el.length = 4; + }); + + const inputBoxes = page.locator('ion-input-otp input'); + + await expect(inputBoxes.nth(0)).toHaveAttribute('aria-hidden', 'false'); + await expect(inputBoxes.nth(1)).toHaveAttribute('aria-hidden', 'true'); + await expect(inputBoxes.nth(2)).toHaveAttribute('aria-hidden', 'true'); + await expect(inputBoxes.nth(3)).toHaveAttribute('aria-hidden', 'true'); + }); + test('should update aria-hidden when typing a value', async ({ page }) => { await page.setContent(``, config); diff --git a/core/src/components/input-otp/test/basic/input-otp.e2e.ts b/core/src/components/input-otp/test/basic/input-otp.e2e.ts index 306e72f873d..a135f502f0a 100644 --- a/core/src/components/input-otp/test/basic/input-otp.e2e.ts +++ b/core/src/components/input-otp/test/basic/input-otp.e2e.ts @@ -71,6 +71,39 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { await verifyInputValues(inputOtp, ['1', '2', '3', '4', '5', '6', '7', '8']); }); + test('should synchronize the value when length changes', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31484', + }); + + await page.setContent(`Description`, config); + + const inputOtp = page.locator('ion-input-otp'); + await inputOtp.evaluate((el) => el.setAttribute('length', '2')); + + await verifyInputValues(inputOtp, ['1', '2']); + }); + + test('should add empty input boxes when length increases', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31484', + }); + + await page.setContent(`Description`, config); + + const inputOtp = page.locator('ion-input-otp'); + await inputOtp.evaluate((el: HTMLIonInputOtpElement) => { + el.length = 4; + }); + + const inputBoxes = page.locator('ion-input-otp input'); + await expect(inputBoxes).toHaveCount(4); + + await verifyInputValues(inputOtp, ['1', '2', '', '']); + }); + test('should accept numbers only by default', async ({ page }) => { await page.setContent(`Description`, config); @@ -1033,6 +1066,36 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await expect(ionChange).toHaveReceivedEventTimes(1); }); + test('should emit ionChange event when blurring with a new value after length decreases', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31484', + }); + + await page.setContent(`Description`, config); + + const ionChange = await page.spyOnEvent('ionChange'); + + const inputOtp = page.locator('ion-input-otp'); + await inputOtp.evaluate((el: HTMLIonInputOtpElement) => { + el.length = 2; + }); + await page.waitForChanges(); + + const firstInput = page.locator('ion-input-otp input').first(); + await firstInput.focus(); + + await page.keyboard.type('12'); + + // Click outside the input to trigger the blur event + await page.mouse.click(0, 0); + + await ionChange.next(); + await expect(ionChange).toHaveReceivedEventDetail({ value: '12', event: { isTrusted: true } }); + }); + test('should not emit ionChange event when blurring with the same value', async ({ page }) => { await page.setContent(`Description`, config); From d5bab2e89548311b2e3f62b5fe2be477685cb06d Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 5 Oct 2026 14:00:20 +0000 Subject: [PATCH 10/53] fix(react): prevent error when an outlet unmounts before it's ready (#31517) Issue number: resolves #31513 --------- ## What is the current behavior? Currently, an `IonRouterOutlet` with `ionPage` that unmounts before its `componentOnReady` callback fires throws `Cannot read properties of null (reading 'classList')`, because React has already cleared the ref the callback reads in `OutletPageManager`. ## What is the new behavior? We now capture the outlet element when the callback is scheduled and skip the callback if the ref no longer points to it, so an unmounted outlet doesn't touch its classes or register with the parent stack. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The new test page mounts an outlet and removes it in a layout effect, which reproduces the error on every tap on `main`. ## Current dev build ``` 9.0.7-dev.11790871695.14a4111c ``` --- packages/react-router/test/base/src/App.tsx | 2 + .../react-router/test/base/src/pages/Main.tsx | 3 ++ .../OutletUnmountBeforeReady.tsx | 52 +++++++++++++++++++ .../outlet-unmount-before-ready.spec.ts | 28 ++++++++++ .../react/src/routing/OutletPageManager.tsx | 9 +++- 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 packages/react-router/test/base/src/pages/outlet-unmount-before-ready/OutletUnmountBeforeReady.tsx create mode 100644 packages/react-router/test/base/tests/e2e/playwright/outlet-unmount-before-ready.spec.ts diff --git a/packages/react-router/test/base/src/App.tsx b/packages/react-router/test/base/src/App.tsx index 96cb242cedc..64dfdffb265 100644 --- a/packages/react-router/test/base/src/App.tsx +++ b/packages/react-router/test/base/src/App.tsx @@ -56,6 +56,7 @@ import { } from './pages/router-link-modifier-click/RouterLinkModifierClick'; import { NavigateRootPageA, NavigateRootPageB, NavigateRootPageC } from './pages/navigate-root/NavigateRoot'; import SuspenseOutlet from './pages/suspense-outlet/SuspenseOutlet'; +import OutletUnmountBeforeReady from './pages/outlet-unmount-before-ready/OutletUnmountBeforeReady'; import { PropsUpdateDirect, PropsUpdateRoutesWrapper } from './pages/props-update/PropsUpdate'; import DisabledButton from './pages/disabled-button/DisabledButton'; import SplatSibling from './pages/splat-sibling/SplatSibling'; @@ -126,6 +127,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/react-router/test/base/src/pages/Main.tsx b/packages/react-router/test/base/src/pages/Main.tsx index 3dc6709f531..f23ceb89a02 100644 --- a/packages/react-router/test/base/src/pages/Main.tsx +++ b/packages/react-router/test/base/src/pages/Main.tsx @@ -58,6 +58,9 @@ const Main: React.FC = () => { Suspense Outlet + + Outlet Unmount Before Ready + diff --git a/packages/react-router/test/base/src/pages/outlet-unmount-before-ready/OutletUnmountBeforeReady.tsx b/packages/react-router/test/base/src/pages/outlet-unmount-before-ready/OutletUnmountBeforeReady.tsx new file mode 100644 index 00000000000..f8bb19ff958 --- /dev/null +++ b/packages/react-router/test/base/src/pages/outlet-unmount-before-ready/OutletUnmountBeforeReady.tsx @@ -0,0 +1,52 @@ +import { IonButton, IonContent, IonHeader, IonPage, IonRouterOutlet, IonTitle, IonToolbar } from '@ionic/react'; +import React, { useLayoutEffect, useState } from 'react'; +import { Route } from 'react-router-dom'; + +import TestDescription from '../../components/TestDescription'; + +/** + * Unmounting from a layout effect removes the outlet before it's ready. + */ +const TransientOutlet: React.FC<{ onMounted: () => void }> = ({ onMounted }) => { + useLayoutEffect(onMounted, [onMounted]); + + return ( + + Transient page
} /> + + ); +}; + +const OutletUnmountBeforeReady: React.FC = () => { + const [mounted, setMounted] = useState(false); + const [attempts, setAttempts] = useState(0); + + return ( + + + + Outlet Unmount Before Ready + + + + + Tap the button a few times. Each tap mounts a nested outlet and removes it before it's ready, so the counter + should go up without any errors in the console. + + { + setAttempts((count) => count + 1); + setMounted(true); + }} + > + Mount and unmount outlet + +

{attempts}

+ {mounted && setMounted(false)} />} +
+
+ ); +}; + +export default OutletUnmountBeforeReady; diff --git a/packages/react-router/test/base/tests/e2e/playwright/outlet-unmount-before-ready.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/outlet-unmount-before-ready.spec.ts new file mode 100644 index 00000000000..b3c14a5dbbd --- /dev/null +++ b/packages/react-router/test/base/tests/e2e/playwright/outlet-unmount-before-ready.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; +import { ionPageVisible, withTestingMode } from './utils/test-utils'; + +test.describe('Outlet Unmount Before Ready', () => { + test('should not throw when an ionPage outlet unmounts before it is ready', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31513', + }); + + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(error.message)); + + await page.goto(withTestingMode('/outlet-unmount-before-ready')); + await ionPageVisible(page, 'outlet-unmount-before-ready'); + + for (let attempt = 1; attempt <= 3; attempt++) { + await page.locator('#mount-transient-outlet').click(); + await expect(page.locator('#attempts')).toHaveText(String(attempt)); + } + + // Give the async ready callback time to fire. + await page.waitForTimeout(500); + + expect(errors).toEqual([]); + await ionPageVisible(page, 'outlet-unmount-before-ready'); + }); +}); diff --git a/packages/react/src/routing/OutletPageManager.tsx b/packages/react/src/routing/OutletPageManager.tsx index abb9bda0481..056ab598534 100644 --- a/packages/react/src/routing/OutletPageManager.tsx +++ b/packages/react/src/routing/OutletPageManager.tsx @@ -46,7 +46,13 @@ export class OutletPageManager extends React.Component { * when React unmounts + remounts components. */ if (!this.outletIsReady) { - componentOnReady(this.ionRouterOutlet, () => { + const el = this.ionRouterOutlet; + componentOnReady(el, () => { + /** + * The outlet can unmount before this fires, which clears the ref. + */ + if (this.ionRouterOutlet !== el) return; + /** * Guard against duplicate callbacks from React strict mode double-mount. * Both componentDidMount calls pass the outer !outletIsReady check before @@ -65,7 +71,6 @@ export class OutletPageManager extends React.Component { * outlet's forward animation removes ion-page-invisible, preventing * a flash where the outlet is briefly visible at full opacity. */ - const el = this.ionRouterOutlet!; if (!el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden')) { el.classList.add('ion-page'); el.classList.add('ion-page-invisible'); From 6532bcbcf5b69f33724a66ab76720ec645adb00b Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 5 Oct 2026 18:02:52 +0000 Subject: [PATCH 11/53] fix(close-watcher): allow back navigation when nothing is open (#31520) Issue number: resolves #29648 --------- ## What is the current behavior? Currently, with `experimentalCloseWatcher` enabled, a `CloseWatcher` is always active, so back does nothing when no overlay or menu is open. In Capacitor and Cordova apps on Android the flag also breaks the hardware back button entirely, because Android WebView never sends it to a `CloseWatcher` and we stopped listening for the native `backbutton` event. ## What is the new behavior? The `CloseWatcher` now only exists while a menu or an overlay other than a toast is open, so back navigates normally otherwise. We also always listen for `backbutton` again, which browsers never fire, so the hardware back button works in hybrid apps. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information ## Current dev build ``` 9.0.7-dev.11790887756.13f9320e ``` --- core/src/components/menu/menu.tsx | 3 +- .../menu/test/close-watcher/menu.e2e.ts | 71 ++++++ core/src/utils/config.ts | 5 +- core/src/utils/hardware-back-button.ts | 59 ++++- core/src/utils/menu-controller/index.ts | 3 +- core/src/utils/overlays.ts | 40 +++- .../utils/test/hardware-back-button.spec.ts | 219 +++++++++++++++--- 7 files changed, 355 insertions(+), 45 deletions(-) create mode 100644 core/src/components/menu/test/close-watcher/menu.e2e.ts diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 4bf4564c40f..d9749f75b21 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -3,7 +3,7 @@ import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Wa import { getTimeGivenProgression } from '@utils/animation/cubic-bezier'; import { focusFirstDescendant, focusLastDescendant } from '@utils/focus-trap'; import { GESTURE_CONTROLLER } from '@utils/gesture'; -import { shouldUseCloseWatcher } from '@utils/hardware-back-button'; +import { shouldUseCloseWatcher, updateCloseWatcher } from '@utils/hardware-back-button'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers'; import { printIonError } from '@utils/logging'; @@ -726,6 +726,7 @@ export class Menu implements ComponentInterface, MenuI { // emit opened/closed events this._isOpen = isOpen; this.isAnimating = false; + updateCloseWatcher(); if (!this._isOpen) { this.blocker.unblock(); } diff --git a/core/src/components/menu/test/close-watcher/menu.e2e.ts b/core/src/components/menu/test/close-watcher/menu.e2e.ts new file mode 100644 index 00000000000..d77b89c5eeb --- /dev/null +++ b/core/src/components/menu/test/close-watcher/menu.e2e.ts @@ -0,0 +1,71 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('menu: close watcher'), () => { + test('should close the menu on a close request and release the back button', async ({ page, skip }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/29648', + }); + skip.browser((browserName: string) => browserName !== 'chromium', 'Only Chromium supports the CloseWatcher API'); + + await page.setContent( + ` + + + + Menu Content + +
+ Main Content +
+
+ `, + config + ); + + const menu = page.locator('ion-menu'); + const ionDidOpen = await page.spyOnEvent('ionDidOpen'); + const ionDidClose = await page.spyOnEvent('ionDidClose'); + + await menu.evaluate((el: HTMLIonMenuElement) => el.open()); + await ionDidOpen.next(); + expect(await page.evaluate(() => (window as any).liveCloseWatchers)).toBe(1); + + // Escape sends a close request to the active CloseWatcher + await page.keyboard.press('Escape'); + + await ionDidClose.next(); + await expect(menu).not.toHaveClass(/show-menu/); + expect(await page.evaluate(() => (window as any).liveCloseWatchers)).toBe(0); + }); + }); +}); diff --git a/core/src/utils/config.ts b/core/src/utils/config.ts index cca58752ee7..6a841a3fcf2 100644 --- a/core/src/utils/config.ts +++ b/core/src/utils/config.ts @@ -206,7 +206,10 @@ export interface IonicConfig { /** * @experimental * If `true`, the [CloseWatcher API](https://github.com/WICG/close-watcher) will be used to handle - * all Escape key and hardware back button presses to dismiss menus and overlays and to navigate. + * Escape key and Android back button presses that dismiss menus and overlays. It's only active + * while a menu or an overlay other than a toast is open, so back navigation works as usual + * otherwise. Hybrid apps still handle the hardware back button through the native `backbutton` + * event. * Note that the `hardwareBackButton` config option must also be `true`. */ experimentalCloseWatcher?: boolean; diff --git a/core/src/utils/hardware-back-button.ts b/core/src/utils/hardware-back-button.ts index 96da99979fb..46b4c19c5c0 100644 --- a/core/src/utils/hardware-back-button.ts +++ b/core/src/utils/hardware-back-button.ts @@ -48,6 +48,26 @@ export const blockHardwareBackButton = () => { document.addEventListener('backbutton', () => {}); }; +const closeWatcherConditions: (() => boolean)[] = []; +let syncCloseWatcher: (() => void) | undefined; + +/** + * Registers a check for whether the back button + * has something to close. The CloseWatcher is only + * active while one of these checks passes. + */ +export const addCloseWatcherCondition = (condition: () => boolean) => { + closeWatcherConditions.push(condition); +}; + +/** + * Call this whenever something the back button + * can close opens or closes. + */ +export const updateCloseWatcher = () => { + syncCloseWatcher?.(); +}; + export const startHardwareBackButton = () => { const doc = document; let busy = false; @@ -105,16 +125,32 @@ export const startHardwareBackButton = () => { }; /** - * If the CloseWatcher is defined then - * we don't want to also listen for the native - * backbutton event otherwise we may get duplicate - * events firing. + * Android WebView never sends the hardware back + * button to a CloseWatcher, so hybrid apps still + * need this. Browsers never fire backbutton. */ + doc.addEventListener('backbutton', backButtonCallback); + if (shouldUseCloseWatcher()) { let watcher: CloseWatcher | undefined; - const configureWatcher = () => { - watcher?.destroy(); + /** + * An active CloseWatcher stops the back button from + * navigating, so only keep one while there's something to close. + */ + syncCloseWatcher = () => { + const canClose = closeWatcherConditions.some((condition) => condition()); + + if (!canClose) { + watcher?.destroy(); + watcher = undefined; + return; + } + + if (watcher !== undefined) { + return; + } + watcher = new win!.CloseWatcher!(); /** @@ -122,17 +158,18 @@ export const startHardwareBackButton = () => { * the watcher gets destroyed. * As a result, we need to re-configure * the watcher so we can respond to other - * close requests. + * close requests, including after one that + * closed nothing, like a modal whose canDismiss + * returned false. */ watcher!.onclose = () => { + watcher = undefined; backButtonCallback(); - configureWatcher(); + syncCloseWatcher?.(); }; }; - configureWatcher(); - } else { - doc.addEventListener('backbutton', backButtonCallback); + syncCloseWatcher(); } }; diff --git a/core/src/utils/menu-controller/index.ts b/core/src/utils/menu-controller/index.ts index 5d170112340..6d90e958e87 100644 --- a/core/src/utils/menu-controller/index.ts +++ b/core/src/utils/menu-controller/index.ts @@ -1,6 +1,6 @@ import { doc } from '@utils/browser'; import type { BackButtonEvent } from '@utils/hardware-back-button'; -import { MENU_BACK_BUTTON_PRIORITY } from '@utils/hardware-back-button'; +import { MENU_BACK_BUTTON_PRIORITY, addCloseWatcherCondition } from '@utils/hardware-back-button'; import { printIonWarning } from '@utils/logging'; import type { MenuI, MenuControllerI } from '../../components/menu/menu-interface'; @@ -229,6 +229,7 @@ const createMenuController = (): MenuControllerI => { registerAnimation('push', menuPushAnimation); registerAnimation('overlay', menuOverlayAnimation); + addCloseWatcherCondition(() => _getOpenSync() !== undefined); doc?.addEventListener('ionBackButton', (ev: BackButtonEvent) => { const openMenu = _getOpenSync(); if (openMenu) { diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 40bbb7b88fc..7a1dd014bd3 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -1,7 +1,7 @@ import { doc } from '@utils/browser'; import { focusFirstDescendant, focusLastDescendant, focusableQueryString } from '@utils/focus-trap'; import type { BackButtonEvent } from '@utils/hardware-back-button'; -import { shouldUseCloseWatcher } from '@utils/hardware-back-button'; +import { addCloseWatcherCondition, shouldUseCloseWatcher, updateCloseWatcher } from '@utils/hardware-back-button'; import { printIonError, printIonWarning } from '@utils/logging'; import { config } from '../global/config'; @@ -35,6 +35,12 @@ import { let lastOverlayIndex = 0; let lastId = 0; +/** + * Every overlay except toast, which doesn't trap + * focus or close on back. + */ +const NON_TOAST_OVERLAYS = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-popover'; + export const activeAnimations = new WeakMap(); type OverlayWithFocusTrapProps = HTMLIonOverlayElement & { @@ -186,7 +192,7 @@ const focusElementInOverlay = (hostToFocus: HTMLElement | null | undefined, over * Should NOT include: Toast */ const trapKeyboardFocus = (ev: Event, doc: Document) => { - const lastOverlay = getPresentedOverlay(doc, 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-popover'); + const lastOverlay = getPresentedOverlay(doc, NON_TOAST_OVERLAYS); const target = ev.target as HTMLElement | null; /** @@ -383,6 +389,12 @@ const connectListeners = (doc: Document) => { true ); + /** + * Whether the overlay actually dismisses is checked at press time, + * since `backdropDismiss` can change while it's open. + */ + addCloseWatcherCondition(() => getPresentedOverlay(doc, NON_TOAST_OVERLAYS) !== undefined); + // handle back-button click doc.addEventListener('ionBackButton', (ev) => { const lastOverlay = getPresentedOverlay(doc); @@ -525,7 +537,8 @@ export const setRootAriaHidden = (hidden = false) => { * Cleans up root `aria-hidden` and `backdrop-no-scroll` when * an overlay is removed from the DOM without going through * the `dismiss()` flow (e.g., when a framework unmounts the - * overlay during a route change). + * overlay during a route change). Also releases the + * CloseWatcher if nothing is left to close. * * Should be called from an overlay's `disconnectedCallback` * when the overlay was still presented at the time of removal. @@ -535,6 +548,8 @@ export const cleanupRootFocusTrapAccessibility = () => { return; } + updateCloseWatcher(); + const remainingOverlays = getPresentedOverlays(document); const hasRemainingLocking = remainingOverlays.some((o) => locksAppRoot(o as OverlayWithFocusTrapProps)); @@ -559,7 +574,9 @@ const applyRootLock = (el: OverlayWithFocusTrapProps) => { /** * Re-applies the root lock that `cleanupRootFocusTrapAccessibility()` released. - * Call from `connectedCallback` when the overlay is still presented. + * Call from `connectedCallback` when the overlay is still presented. Also + * re-arms the CloseWatcher, which every overlay needs, so that happens + * before the `locksAppRoot` check. * * A synchronous move keeps the overlay connected, so the lock survives. A * detach with a re-insert in a later task releases it, which is the shape a @@ -570,6 +587,8 @@ export const restoreRootFocusTrapAccessibility = (overlayEl: HTMLIonOverlayEleme return; } + updateCloseWatcher(); + const el = overlayEl as OverlayWithFocusTrapProps; if (!locksAppRoot(el)) { return; @@ -828,6 +847,12 @@ export const dismiss = async ( overlay.el.remove(); + /** + * Release the back button if nothing + * else is left to close. + */ + updateCloseWatcher(); + return true; }; @@ -844,6 +869,13 @@ const overlayAnimation = async ( // Make overlay visible in case it's hidden baseEl.classList.remove('overlay-hidden'); + /** + * Removing `overlay-hidden` makes the overlay count + * as presented, so back can dismiss it while it + * animates in. + */ + updateCloseWatcher(); + const aniRoot = overlay.el; const animation = animationBuilder(aniRoot, opts); diff --git a/core/src/utils/test/hardware-back-button.spec.ts b/core/src/utils/test/hardware-back-button.spec.ts index 87f27d407cc..b195996c20d 100644 --- a/core/src/utils/test/hardware-back-button.spec.ts +++ b/core/src/utils/test/hardware-back-button.spec.ts @@ -1,7 +1,8 @@ +import { newSpecPage } from '@stencil/core/testing'; + import type { BackButtonEvent } from '../../../src/interface'; import { startHardwareBackButton } from '../hardware-back-button'; import { config } from '../../global/config'; -import { win } from '@utils/browser'; describe('Hardware Back Button', () => { beforeEach(() => startHardwareBackButton()); @@ -58,52 +59,216 @@ describe('Hardware Back Button', () => { describe('Experimental Close Watcher', () => { test('should not use the Close Watcher API when available', () => { - const mockAPI = mockCloseWatcher(); + const closeWatchers = mockCloseWatcher(); config.reset({ experimentalCloseWatcher: false }); startHardwareBackButton(); - expect(mockAPI.mock.calls).toHaveLength(0); + expect(closeWatchers.created).toBe(0); }); - test('should use the Close Watcher API when available', () => { - const mockAPI = mockCloseWatcher(); - config.reset({ experimentalCloseWatcher: true }); + test('should still handle the native back button event', async () => { + await newCloseWatcherPage(``); - startHardwareBackButton(); + const cbSpy = jest.fn(); + document.addEventListener('ionBackButton', (ev) => { + (ev as BackButtonEvent).detail.register(0, cbSpy); + }); + + dispatchBackButtonEvent(); - expect(mockAPI.mock.calls).toHaveLength(1); + expect(cbSpy).toHaveBeenCalled(); }); - test('Close Watcher should dispatch ionBackButton events', () => { - const mockAPI = mockCloseWatcher(); - config.reset({ experimentalCloseWatcher: true }); + // Fixes https://github.com/ionic-team/ionic-framework/issues/29648 + test('should not intercept the back button while nothing is open', async () => { + const { closeWatchers } = await newCloseWatcherPage(``); - startHardwareBackButton(); + expect(closeWatchers.active()).toBeUndefined(); + }); - const cbSpy = jest.fn(); - document.addEventListener('ionBackButton', cbSpy); + test('should not intercept the back button while only a toast is shown', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); - // Call onclose on Ionic's instance of CloseWatcher - mockAPI.getMockImplementation()!().onclose(); + await page.body.querySelector('ion-toast')!.present(); - expect(cbSpy).toHaveBeenCalled(); + expect(closeWatchers.active()).toBeUndefined(); + }); + + test('should dismiss a presented modal when the back button is pressed', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); + + const modal = page.body.querySelector('ion-modal')!; + await modal.present(); + + expect(closeWatchers.active()).toBeDefined(); + + const dismissed = modal.onDidDismiss(); + closeWatchers.requestClose(); + + await expect(dismissed).resolves.toEqual(expect.objectContaining({ role: 'backdrop' })); + }); + + // Fixes https://github.com/ionic-team/ionic-framework/issues/29648 + test('should stop intercepting the back button once the modal is dismissed', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); + + const modal = page.body.querySelector('ion-modal')!; + await modal.present(); + await modal.dismiss(); + + expect(closeWatchers.active()).toBeUndefined(); + }); + + test('should stop intercepting the back button when a presented modal is removed without dismissing', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); + + const modal = page.body.querySelector('ion-modal')!; + await modal.present(); + modal.remove(); + + expect(closeWatchers.active()).toBeUndefined(); + }); + + test('should keep intercepting the back button when a presented modal is moved', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(`
`); + + const modal = page.body.querySelector('ion-modal')!; + await modal.present(); + page.body.querySelector('#target')!.appendChild(modal); + + expect(closeWatchers.active()).toBeDefined(); + }); + + test('should keep intercepting the back button while a modal is still open underneath', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(` + + + `); + + const bottom = page.body.querySelector('#bottom')!; + const top = page.body.querySelector('#top')!; + await bottom.present(); + await top.present(); + + const topDismissed = top.onDidDismiss(); + closeWatchers.requestClose(); + await topDismissed; + + const bottomDismissed = bottom.onDidDismiss(); + closeWatchers.requestClose(); + + await expect(bottomDismissed).resolves.toEqual(expect.objectContaining({ role: 'backdrop' })); + expect(closeWatchers.active()).toBeUndefined(); + }); + + test('should keep intercepting the back button when the modal refuses to dismiss', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); + + const modal = page.body.querySelector('ion-modal')!; + let dismissAttempts = 0; + modal.canDismiss = () => { + dismissAttempts++; + return Promise.resolve(false); + }; + await modal.present(); + + closeWatchers.requestClose(); + await page.waitForChanges(); + closeWatchers.requestClose(); + await page.waitForChanges(); + + expect(dismissAttempts).toBe(2); + }); + + test('should dismiss a modal that allows backdrop dismissal only after it was presented', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(``); + + const modal = page.body.querySelector('ion-modal')!; + modal.backdropDismiss = false; + await modal.present(); + modal.backdropDismiss = true; + + const dismissed = modal.onDidDismiss(); + closeWatchers.requestClose(); + + await expect(dismissed).resolves.toEqual(expect.objectContaining({ role: 'backdrop' })); + }); + + test('should keep intercepting the back button while a toast is shown over a modal', async () => { + const { page, closeWatchers } = await newCloseWatcherPage(` + + + `); + + await page.body.querySelector('ion-modal')!.present(); + await page.body.querySelector('ion-toast')!.present(); + + expect(closeWatchers.active()).toBeDefined(); }); }); +/** + * Modules are reset first because overlays attach their back button + * listener once per module load, and every spec page gets a new document. + */ +const newCloseWatcherPage = async (html: string) => { + jest.resetModules(); + const { Modal } = await import('../../components/modal/modal'); + const { Toast } = await import('../../components/toast/toast'); + const { startHardwareBackButton } = await import('../hardware-back-button'); + const { config } = await import('../../global/config'); + + const page = await newSpecPage({ components: [Modal, Toast], html }); + + // Install the mock after newSpecPage, which resets the window + const closeWatchers = mockCloseWatcher(); + config.reset({ experimentalCloseWatcher: true }); + startHardwareBackButton(); + + return { page, closeWatchers }; +}; + +/** + * Like the browser, only the newest watcher that hasn't + * been destroyed gets close requests. + */ const mockCloseWatcher = () => { - const mockCloseWatcher = jest.fn(); - mockCloseWatcher.mockReturnValue({ - requestClose: () => null, - close: () => null, - destroy: () => null, - oncancel: () => null, - onclose: () => null, - }); - (win as any).CloseWatcher = mockCloseWatcher; + const watchers: { destroyed: boolean; onclose: (() => void) | null; destroy: () => void }[] = []; + + (window as any).CloseWatcher = class { + destroyed = false; + onclose: (() => void) | null = null; + + constructor() { + watchers.push(this); + } + + destroy() { + this.destroyed = true; + } + }; + + const active = () => watchers.filter((w) => !w.destroyed).slice(-1)[0]; - return mockCloseWatcher; + return { + get created() { + return watchers.length; + }, + active, + /** + * Presses back. Like the browser, this destroys + * the watcher before firing `close`. + */ + requestClose() { + const watcher = active(); + if (watcher) { + watcher.destroyed = true; + watcher.onclose?.(); + } + }, + }; }; const dispatchBackButtonEvent = () => { From 8191ef8235eb42a535c6ddd93200001ac3640314 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:23:33 +0000 Subject: [PATCH 12/53] chore(deps): update capacitor to v8.0.6 (#31530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@capacitor/keyboard](https://redirect.github.com/ionic-team/capacitor-keyboard) | [`8.0.5` → `8.0.6`](https://renovatebot.com/diffs/npm/@capacitor%2fkeyboard/8.0.5/8.0.6) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fkeyboard/8.0.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fkeyboard/8.0.5/8.0.6?slim=true) | | [@capacitor/status-bar](https://redirect.github.com/ionic-team/capacitor-plugins) | [`8.0.3` → `8.0.4`](https://renovatebot.com/diffs/npm/@capacitor%2fstatus-bar/8.0.3/8.0.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fstatus-bar/8.0.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fstatus-bar/8.0.3/8.0.4?slim=true) | --- ### Release Notes
ionic-team/capacitor-keyboard (@​capacitor/keyboard) ### [`v8.0.6`](https://redirect.github.com/ionic-team/capacitor-keyboard/blob/HEAD/CHANGELOG.md#806-2026-10-02) [Compare Source](https://redirect.github.com/ionic-team/capacitor-keyboard/compare/v8.0.5...v8.0.6) ##### Bug Fixes - **ios:** silence the protocol property synthesis warnings in Keyboard.m ([#​83](https://redirect.github.com/ionic-team/capacitor-keyboard/issues/83)) ([f28aef4](https://redirect.github.com/ionic-team/capacitor-keyboard/commit/f28aef4ed8736865232ed0595412e07d2372e024))
ionic-team/capacitor-plugins (@​capacitor/status-bar) ### [`v8.0.4`](https://redirect.github.com/ionic-team/capacitor-plugins/releases/tag/%40capacitor/status-bar%408.0.4) [Compare Source](https://redirect.github.com/ionic-team/capacitor-plugins/compare/@capacitor/status-bar@8.0.3...@capacitor/status-bar@8.0.4) ##### Bug Fixes - **status-bar:** avoid retain cycle between StatusBar and the bridge ([#​2569](https://redirect.github.com/ionic-team/capacitor-plugins/issues/2569)) ([bf42d23](https://redirect.github.com/ionic-team/capacitor-plugins/commit/bf42d237c1dca1b608dcbedf9c62559feccfb532))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 7d84539b209..60270fff6e7 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -653,9 +653,9 @@ } }, "node_modules/@capacitor/keyboard": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.5.tgz", - "integrity": "sha512-oFXygC4eKYA5l2MdpTR06L2M/4x6e2SLD5yS1T9+UBDKTkzyvhWKEhbYLUaTIBPpLKqlfGudJw1X73S1H9eUzQ==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.6.tgz", + "integrity": "sha512-Bbe+9V0qaT6dzEFJEK7BUWR01l2+vZE8FM5ITF/WgKtx+G6eGZ6d5+YiSMRJBAK0VGjdO59v8yiQ10Doa5KyvQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -663,9 +663,9 @@ } }, "node_modules/@capacitor/status-bar": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz", - "integrity": "sha512-csSpfNeN49Hx9JaQBSJEIiEbOLtXg3kcc2IpScq2fu5L520h3AWEvsxoH8Srk1jxfRKepaJ4S4sqSC3foI4AgA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.4.tgz", + "integrity": "sha512-ZMzBUyHBzwDUxcHadEhvOvmoPmMQGMZgaMRJu8ohELeI6IaxoVH9ZhdF/OHnY3/yykv3Glev8Tnbh4aZY/0B0w==", "dev": true, "license": "MIT", "peerDependencies": { From 64fbc78d23b3e7b66a94605233d752f2215ee02b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:23:45 +0000 Subject: [PATCH 13/53] chore(deps): update dependency @stencil/core to v4.45.2 (#31531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@stencil/core](https://stenciljs.com/) ([source](https://redirect.github.com/stenciljs/core)) | [`4.45.1` → `4.45.2`](https://renovatebot.com/diffs/npm/@stencil%2fcore/4.45.1/4.45.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@stencil%2fcore/4.45.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@stencil%2fcore/4.45.1/4.45.2?slim=true) | --- ### Release Notes
stenciljs/core (@​stencil/core) ### [`v4.45.2`](https://redirect.github.com/stenciljs/core/blob/HEAD/CHANGELOG.md#-4452-2026-10-05) [Compare Source](https://redirect.github.com/stenciljs/core/compare/v4.45.1...v4.45.2) ##### Bug Fixes - **mock-doc:** support force in classList.toggle ([#​6913](https://redirect.github.com/stenciljs/core/issues/6913)) ([992ab54](https://redirect.github.com/stenciljs/core/commit/992ab5466c0b7a0d6ff420f25eb095a42791f0f6)) - **runtime:** `document.hidden` page freezes ([#​6918](https://redirect.github.com/stenciljs/core/issues/6918)) ([5a92cc7](https://redirect.github.com/stenciljs/core/commit/5a92cc7e99de7395c8f8956203bfcb248af9d479)) - **runtime:** form-associated boolean attribute `"false"` parses as true ([#​6908](https://redirect.github.com/stenciljs/core/issues/6908)) ([f971133](https://redirect.github.com/stenciljs/core/commit/f971133d7d47dce9598c6417e12902942c2053d3)) - **runtime:** keep child host classes on first render after hydration ([#​6912](https://redirect.github.com/stenciljs/core/issues/6912)) ([b6842e9](https://redirect.github.com/stenciljs/core/commit/b6842e9817c16258d7b8b8f2cb6c8420aad76c0e)) - **runtime:** lazy getter/setter `@Prop` writes before first render ([#​6909](https://redirect.github.com/stenciljs/core/issues/6909)) ([e2e5dfc](https://redirect.github.com/stenciljs/core/commit/e2e5dfc059c4c8fedf5258f9ee83d4a091ef20ac)) - **scope-css:** scope rules inside [@​container](https://redirect.github.com/container) ([#​6914](https://redirect.github.com/stenciljs/core/issues/6914)) ([bc393d0](https://redirect.github.com/stenciljs/core/commit/bc393d028b86731cccce51d7af417e2186dcf7ac))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 60270fff6e7..c9255a77c9c 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1839,9 +1839,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.45.1.tgz", - "integrity": "sha512-/b8CItmSwOzZwafxXwomJnN9X/Ic/Fxum9ka/RDHmADVf4DA4152uY7zitzLV+vk8JdXxKA7FKyLT3GKNo9R3A==", + "version": "4.45.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.45.2.tgz", + "integrity": "sha512-CNkPVZsqyNMjFMX9bEkMJsvBbQxjLw480OaoamoqiLPdQCFjJQReeOjnWX1A5ivSn3UYEIJ6MoN5VXDkaHGt8Q==", "license": "MIT", "bin": { "stencil": "bin/stencil" From 881e432ac9958edfc6be2239fdb0bc646952daf8 Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 6 Oct 2026 18:14:36 +0000 Subject: [PATCH 14/53] chore(renovate): automerge minor and patch dev dependency updates (#31533) Issue number: internal --------- ## What is the current behavior? Every Renovate PR needs a manual approval and merge, including frequent low-risk bumps like `vitest`, `@types/node` and `github/codeql-action` that we always merge as-is. ## What is the new behavior? Minor and patch updates for `vitest`, the Playwright group, `@types/node`, `github/codeql-action` and `actions/*` now automerge once CI passes. Majors still open as normal PRs that need a review. A Playwright bump that changes rendering fails `verify-screenshots`, so it'll wait for a person the same as before. `platformAutomerge` is off, so Renovate merges the PR itself and waits for every check on the branch rather than relying on GitHub's native auto-merge. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information --- renovate.json5 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index d50c442adff..a80ede4ac17 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -60,6 +60,25 @@ */ minimumReleaseAge: "3 days" }, + { + /** + * Automerge minor and patch updates once CI passes. Majors still need + * a review. With platformAutomerge off, Renovate merges the PR itself + * and waits for every check, including ones that aren't required. + */ + matchPackageNames: [ + "vitest", + "@playwright/test", + "@axe-core/playwright", + "playwright-core", + "@types/node", + "github/codeql-action", + "actions/*" + ], + matchUpdateTypes: ["minor", "patch"], + automerge: true, + platformAutomerge: false + }, { /** * The built-in Docker manager tracks the mcr.microsoft.com/playwright tag From 3440a8171c19c15b4069c791a504443d9c01645f Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 7 Oct 2026 17:21:45 +0000 Subject: [PATCH 15/53] v9.0.7 --- CHANGELOG.md | 12 ++++++++++++ core/CHANGELOG.md | 9 +++++++++ core/package-lock.json | 6 +++--- core/package.json | 2 +- lerna.json | 2 +- packages/angular-server/CHANGELOG.md | 8 ++++++++ packages/angular-server/package-lock.json | 8 ++++---- packages/angular-server/package.json | 4 ++-- packages/angular/CHANGELOG.md | 8 ++++++++ packages/angular/package-lock.json | 8 ++++---- packages/angular/package.json | 4 ++-- packages/docs/CHANGELOG.md | 8 ++++++++ packages/docs/package-lock.json | 6 +++--- packages/docs/package.json | 2 +- packages/react-router/CHANGELOG.md | 7 +++++++ packages/react-router/package-lock.json | 10 +++++----- packages/react-router/package.json | 6 +++--- packages/react/CHANGELOG.md | 7 +++++++ packages/react/package-lock.json | 8 ++++---- packages/react/package.json | 4 ++-- packages/vue-router/CHANGELOG.md | 8 ++++++++ packages/vue-router/package-lock.json | 8 ++++---- packages/vue-router/package.json | 4 ++-- packages/vue/CHANGELOG.md | 8 ++++++++ packages/vue/package-lock.json | 8 ++++---- packages/vue/package.json | 4 ++-- 26 files changed, 122 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f017d422729..cf6be27ea7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **close-watcher:** allow back navigation when nothing is open ([#31520](https://github.com/ionic-team/ionic-framework/issues/31520)) ([6532bcb](https://github.com/ionic-team/ionic-framework/commit/6532bcbcf5b69f33724a66ab76720ec645adb00b)), closes [#29648](https://github.com/ionic-team/ionic-framework/issues/29648) +* **css:** stop emitting :host-context selectors in global stylesheets ([#31516](https://github.com/ionic-team/ionic-framework/issues/31516)) ([35bd2fa](https://github.com/ionic-team/ionic-framework/commit/35bd2fa9f1046b3f5fd3cd6b3778b0b5945be99c)), closes [#30024](https://github.com/ionic-team/ionic-framework/issues/30024) +* **input-otp:** sync value when length changes ([#31485](https://github.com/ionic-team/ionic-framework/issues/31485)) ([e425825](https://github.com/ionic-team/ionic-framework/commit/e425825815d3c0ab39800912558624b9de72dc6f)), closes [#31484](https://github.com/ionic-team/ionic-framework/issues/31484) [#31484](https://github.com/ionic-team/ionic-framework/issues/31484) +* **react:** prevent error when an outlet unmounts before it's ready ([#31517](https://github.com/ionic-team/ionic-framework/issues/31517)) ([d5bab2e](https://github.com/ionic-team/ionic-framework/commit/d5bab2e89548311b2e3f62b5fe2be477685cb06d)), closes [#31513](https://github.com/ionic-team/ionic-framework/issues/31513) +* **vue-router:** render the redirect target when a guard redirects ([#31510](https://github.com/ionic-team/ionic-framework/issues/31510)) ([643584b](https://github.com/ionic-team/ionic-framework/commit/643584b3ac2ba1ca263cbf53960ccde3be2a96fc)) +* **vue-router:** reset tab with memory history ([#31511](https://github.com/ionic-team/ionic-framework/issues/31511)) ([edbc304](https://github.com/ionic-team/ionic-framework/commit/edbc304fbc4179a668d0e170241abd78311fdb62)), closes [#29785](https://github.com/ionic-team/ionic-framework/issues/29785) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) ### Bug Fixes diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 7532826ba38..531652cf2e3 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **close-watcher:** allow back navigation when nothing is open ([#31520](https://github.com/ionic-team/ionic-framework/issues/31520)) ([6532bcb](https://github.com/ionic-team/ionic-framework/commit/6532bcbcf5b69f33724a66ab76720ec645adb00b)), closes [#29648](https://github.com/ionic-team/ionic-framework/issues/29648) +* **css:** stop emitting :host-context selectors in global stylesheets ([#31516](https://github.com/ionic-team/ionic-framework/issues/31516)) ([35bd2fa](https://github.com/ionic-team/ionic-framework/commit/35bd2fa9f1046b3f5fd3cd6b3778b0b5945be99c)), closes [#30024](https://github.com/ionic-team/ionic-framework/issues/30024) +* **input-otp:** sync value when length changes ([#31485](https://github.com/ionic-team/ionic-framework/issues/31485)) ([e425825](https://github.com/ionic-team/ionic-framework/commit/e425825815d3c0ab39800912558624b9de72dc6f)), closes [#31484](https://github.com/ionic-team/ionic-framework/issues/31484) [#31484](https://github.com/ionic-team/ionic-framework/issues/31484) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/core diff --git a/core/package-lock.json b/core/package-lock.json index c9255a77c9c..a8585f40530 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -11661,4 +11661,4 @@ } } } -} +} \ No newline at end of file diff --git a/core/package.json b/core/package.json index 9e43d9b115f..6c255150c0f 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "9.0.6", + "version": "9.0.7", "description": "Base components for Ionic", "engines": { "node": ">= 16" diff --git a/lerna.json b/lerna.json index 3574ffe91d1..e51c4efd2b8 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "packages/vue", "packages/vue-router" ], - "version": "9.0.6" + "version": "9.0.7" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index bc1aabd84af..e69e5c68e8a 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 3df144c5282..b582693e993 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.6" + "@ionic/core": "^9.0.7" }, "devDependencies": { "@angular/animations": "^22.0.0", @@ -7175,4 +7175,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index 77424e5e919..5ac7080e8e6 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "9.0.6", + "version": "9.0.7", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -78,6 +78,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^9.0.6" + "@ionic/core": "^9.0.7" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index b95833b97e7..21fcd6500c0 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +**Note:** Version bump only for package @ionic/angular + + + + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/angular diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index 88671477585..186a6be0fd4 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -9211,4 +9211,4 @@ "license": "MIT" } } -} +} \ No newline at end of file diff --git a/packages/angular/package.json b/packages/angular/package.json index 52fcb205f52..4ae8d969edc 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "9.0.6", + "version": "9.0.7", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -168,7 +168,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index c41faa00af0..3cfe8d8f92a 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 0b4c32b1c40..7321c80f8fa 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,13 +1,13 @@ { "name": "@ionic/docs", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT" } } -} +} \ No newline at end of file diff --git a/packages/docs/package.json b/packages/docs/package.json index 073f240f010..c135f08f56d 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "9.0.6", + "version": "9.0.7", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index a9b898792a1..2051b697ae5 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **react:** prevent error when an outlet unmounts before it's ready ([#31517](https://github.com/ionic-team/ionic-framework/issues/31517)) ([d5bab2e](https://github.com/ionic-team/ionic-framework/commit/d5bab2e89548311b2e3f62b5fe2be477685cb06d)), closes [#31513](https://github.com/ionic-team/ionic-framework/issues/31513) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) ### Bug Fixes diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index 4010da905a1..f422dad9a6e 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,16 +1,16 @@ { "name": "@ionic/react-router", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.6", - "@ionic/react": "^9.0.6", + "@ionic/core": "^9.0.7", + "@ionic/react": "^9.0.7", "tslib": "*" }, "devDependencies": { @@ -4632,4 +4632,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/react-router/package.json b/packages/react-router/package.json index d9751e3ec8d..710cdb4afcc 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "9.0.6", + "version": "9.0.7", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -37,8 +37,8 @@ "dist/" ], "dependencies": { - "@ionic/core": "^9.0.6", - "@ionic/react": "^9.0.6", + "@ionic/core": "^9.0.7", + "@ionic/react": "^9.0.7", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 86049812550..8dba032817f 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **react:** prevent error when an outlet unmounts before it's ready ([#31517](https://github.com/ionic-team/ionic-framework/issues/31517)) ([d5bab2e](https://github.com/ionic-team/ionic-framework/commit/d5bab2e89548311b2e3f62b5fe2be477685cb06d)), closes [#31513](https://github.com/ionic-team/ionic-framework/issues/31513) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/react diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index c1f51ebfe4a..e71b147950d 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" @@ -8837,4 +8837,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json index 6daf86bc3cd..4163da2e4f5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "9.0.6", + "version": "9.0.7", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -41,7 +41,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index dfe731e8669..975363c4fd2 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **vue-router:** render the redirect target when a guard redirects ([#31510](https://github.com/ionic-team/ionic-framework/issues/31510)) ([643584b](https://github.com/ionic-team/ionic-framework/commit/643584b3ac2ba1ca263cbf53960ccde3be2a96fc)) +* **vue-router:** reset tab with memory history ([#31511](https://github.com/ionic-team/ionic-framework/issues/31511)) ([edbc304](https://github.com/ionic-team/ionic-framework/commit/edbc304fbc4179a668d0e170241abd78311fdb62)), closes [#29785](https://github.com/ionic-team/ionic-framework/issues/29785) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/vue-router diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index d5a50dbb7eb..79bacd79595 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/vue": "^9.0.6" + "@ionic/vue": "^9.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -5146,4 +5146,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 90a8eb8d520..0b7b563c7f7 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "9.0.6", + "version": "9.0.7", "description": "Vue Router integration for @ionic/vue", "scripts": { "eslint": "eslint src", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^9.0.6" + "@ionic/vue": "^9.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 4091f7d6fb2..33c1e3c0950 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [9.0.7](https://github.com/ionic-team/ionic-framework/compare/v9.0.6...v9.0.7) (2026-10-07) + +### Bug Fixes + +* **vue-router:** render the redirect target when a guard redirects ([#31510](https://github.com/ionic-team/ionic-framework/issues/31510)) ([643584b](https://github.com/ionic-team/ionic-framework/commit/643584b3ac2ba1ca263cbf53960ccde3be2a96fc)) +* **vue-router:** reset tab with memory history ([#31511](https://github.com/ionic-team/ionic-framework/issues/31511)) ([edbc304](https://github.com/ionic-team/ionic-framework/commit/edbc304fbc4179a668d0e170241abd78311fdb62)), closes [#29785](https://github.com/ionic-team/ionic-framework/issues/29785) + + ## [9.0.6](https://github.com/ionic-team/ionic-framework/compare/v9.0.5...v9.0.6) (2026-09-30) **Note:** Version bump only for package @ionic/vue diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 20851337633..5598ae325bf 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "9.0.6", + "version": "9.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "9.0.6", + "version": "9.0.7", "license": "MIT", "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" }, @@ -5234,4 +5234,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/vue/package.json b/packages/vue/package.json index 555d306681a..7ab73cdf1ff 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "9.0.6", + "version": "9.0.7", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -78,7 +78,7 @@ } }, "dependencies": { - "@ionic/core": "^9.0.6", + "@ionic/core": "^9.0.7", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" }, From 515d89fb998a24154348b725d1cbdda3e3a0dcf3 Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 7 Oct 2026 17:35:11 +0000 Subject: [PATCH 16/53] chore(): update package lock files --- core/package-lock.json | 2 +- packages/angular-server/package-lock.json | 8 ++++---- packages/angular/package-lock.json | 8 ++++---- packages/docs/package-lock.json | 2 +- packages/react-router/package-lock.json | 16 ++++++++-------- packages/react/package-lock.json | 8 ++++---- packages/vue-router/package-lock.json | 16 ++++++++-------- packages/vue/package-lock.json | 8 ++++---- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index a8585f40530..97ed7e472fe 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -11661,4 +11661,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index b582693e993..4cf15f7c9f6 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1097,9 +1097,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -7175,4 +7175,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index 186a6be0fd4..72f99f57084 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1580,9 +1580,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -9211,4 +9211,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 7321c80f8fa..8b305da3546 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -10,4 +10,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index f422dad9a6e..bef9c768ae7 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -257,9 +257,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -297,12 +297,12 @@ } }, "node_modules/@ionic/react": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-9.0.6.tgz", - "integrity": "sha512-WAcbe7ZDIDqQaXvcAF7+AxpYW3Y3HFvZbDnpwQtoSUo8RTAw+AQQOTTSlp1eHOjwqpVqYceBFAdhfNbzBfw/1w==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-9.0.7.tgz", + "integrity": "sha512-KavLmQ2UBGHMbCX5qTjSRIGqgVcHJbZjEJFCjVxueVRflYy2SbYow63BL4xHSPALRB25jnu5bvaW9DqXExrg0w==", "license": "MIT", "dependencies": { - "@ionic/core": "9.0.6", + "@ionic/core": "9.0.7", "@stencil/react-output-target": "^1.6.2", "ionicons": "^8.0.13", "tslib": "*" @@ -4632,4 +4632,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index e71b147950d..d5747ea6984 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -891,9 +891,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -8837,4 +8837,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 79bacd79595..adb268e8289 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -342,9 +342,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -382,12 +382,12 @@ } }, "node_modules/@ionic/vue": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-9.0.6.tgz", - "integrity": "sha512-Bg8RqfnsVejulSXF+YENjZBtx1cEwZPUZAA47oLhFceYlSodapLI2qWeHgn6hgKmapCe+GzyJQIuxyRIfXqlXQ==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-9.0.7.tgz", + "integrity": "sha512-oFHXIM5eMukVEB27J7/iWYSyqNPF6ILxdCXiGyCqFEkJ32Tna6ayt2+F4p+e8hghCUZS1PC12KMypqTut9OH2Q==", "license": "MIT", "dependencies": { - "@ionic/core": "9.0.6", + "@ionic/core": "9.0.7", "@stencil/vue-output-target": "0.14.2", "ionicons": "^8.0.13" } @@ -5146,4 +5146,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 5598ae325bf..6f46778687b 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -346,9 +346,9 @@ } }, "node_modules/@ionic/core": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.6.tgz", - "integrity": "sha512-WWxjOjwKPU75BuEhc42t3n2Tn89YEDe5Pce9kA61w/TlWVM3tqiAYC+qfR9MXmER4pQcST01oM2gftheMzcV6g==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-9.0.7.tgz", + "integrity": "sha512-ypaMX9RujNJa9V12ex614dkvadggc1QGPaF+qq0ZO7WyLilseMfsEv7FiY1J+o2LnXyr++gOPwpvZnE2hvL0Lg==", "license": "MIT", "dependencies": { "@stencil/core": "^4.44.2", @@ -5234,4 +5234,4 @@ } } } -} \ No newline at end of file +} From f7b59882dc0fc248d07ddeee800dd19303a7fdf1 Mon Sep 17 00:00:00 2001 From: OS-jacobbell <228905018+OS-jacobbell@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:03:31 +0000 Subject: [PATCH 17/53] chore(ci): ignore react package-lock changes for nightly (#31542) ## What is the current behavior? Stencil Nightly Build installs the latest `@stencil/react-output-target`, which changes `packages/react/package-lock.json`. When it checks for a clean git diff, the altered package-lock file fails CI. ## What is the new behavior? - Clean up `packages/react/package-lock.json` along with `package.json`. ## Does this introduce a breaking change? - [ ] Yes - [X] No --- .github/workflows/actions/build-react/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index b7d3ac6b99a..a17e45c5094 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -43,7 +43,7 @@ runs: output: packages/react/ReactBuild.zip paths: packages/react/dist packages/react/css packages/react/package.json - name: Clean package.json updates - run: git checkout ./core/package.json ./packages/react/package.json + run: git checkout ./core/package.json ./packages/react/package.json ./packages/react/package-lock.json shell: bash - name: 🔍 Check Diff run: git diff --exit-code From 277097e1dfa13d8b34a6826aca462843810fc8f1 Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 8 Oct 2026 12:25:37 +0000 Subject: [PATCH 18/53] fix(angular, vue): fire lifecycle events on nested pages (#31532) Issue number: internal --------- ## What is the current behavior? Currently, leaving a page that hosts a nested `ion-router-outlet` (like an `ion-tabs` page) only fires leave events on that page, since core dispatches them with `bubbles: false`. The active tab page never gets `ionViewWillLeave` or `ionViewDidLeave`, and in Angular it also misses enter events on the way back. React was fixed as part of the RR6 upgrade in #30831. ## What is the new behavior? The nested outlet now fires the parent page's lifecycle events on its active page in step with the parent. Angular forwards all four events. Vue forwards leave only, because its tabs outlet already fires enter on return. Returning to a different tab no longer fires leave a second time on the page that already left. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information Angular skips its forward when the app already re-dispatched the event itself (the workaround from #16834), so those apps don't get it twice. Apps that call custom methods like `tabsWillLeave()` instead will now run both. A cancelled swipe back gives the tab page an unpaired `ionViewWillLeave`, same as core already does for the tabs page. ## Current dev build ``` 9.0.7-dev.11791305593.188c8151 ``` --- .../directives/navigation/router-outlet.ts | 130 ++++++++- .../e2e/src/standalone/tab-lifecycle.spec.ts | 254 ++++++++++++++++++ .../standalone/app-standalone/app.routes.ts | 17 ++ .../home-page/home-page.component.html | 5 + .../standalone/tab-lifecycle/lifecycle-log.ts | 5 + .../tab-lifecycle-home.component.ts | 49 ++++ .../tab-lifecycle-inner.component.ts | 49 ++++ .../tab-lifecycle-nested.component.ts | 10 + .../tab-lifecycle-outside.component.ts | 33 +++ .../tab-lifecycle-settings.component.ts | 49 ++++ .../tab-lifecycle/tab-lifecycle.component.ts | 80 ++++++ .../vue/src/components/IonRouterOutlet.ts | 116 +++++++- packages/vue/test/base/src/router/index.ts | 32 +++ packages/vue/test/base/src/views/Home.vue | 3 + .../src/views/tab-lifecycle/TabLifecycle.vue | 42 +++ .../views/tab-lifecycle/TabLifecycleHome.vue | 47 ++++ .../views/tab-lifecycle/TabLifecycleInner.vue | 47 ++++ .../tab-lifecycle/TabLifecycleNested.vue | 14 + .../tab-lifecycle/TabLifecycleOutside.vue | 34 +++ .../tab-lifecycle/TabLifecycleSettings.vue | 47 ++++ .../src/views/tab-lifecycle/lifecycle-log.ts | 5 + .../e2e/playwright/tab-lifecycle.spec.ts | 249 +++++++++++++++++ .../test/base/tests/unit/lifecycle.spec.ts | 9 +- 23 files changed, 1311 insertions(+), 15 deletions(-) create mode 100644 packages/angular/test/base/e2e/src/standalone/tab-lifecycle.spec.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/lifecycle-log.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-home.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-inner.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-nested.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-outside.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-settings.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle.component.ts create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycle.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleHome.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleInner.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleNested.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleOutside.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleSettings.vue create mode 100644 packages/vue/test/base/src/views/tab-lifecycle/lifecycle-log.ts create mode 100644 packages/vue/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts diff --git a/packages/angular/src/common/directives/navigation/router-outlet.ts b/packages/angular/src/common/directives/navigation/router-outlet.ts index 71b60f5fdf5..89fe5e83a7c 100644 --- a/packages/angular/src/common/directives/navigation/router-outlet.ts +++ b/packages/angular/src/common/directives/navigation/router-outlet.ts @@ -22,7 +22,13 @@ import { } from '@angular/core'; import type { Provider } from '@angular/core'; import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router'; -import { componentOnReady } from '@ionic/core/components'; +import { + LIFECYCLE_DID_ENTER, + LIFECYCLE_DID_LEAVE, + LIFECYCLE_WILL_ENTER, + LIFECYCLE_WILL_LEAVE, + componentOnReady, +} from '@ionic/core/components'; import type { AnimationBuilder } from '@ionic/core/components'; import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs'; import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; @@ -33,6 +39,26 @@ import { NavController } from '../../providers/nav-controller'; import { StackController } from './stack-controller'; import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils'; +/** + * A page that left along with its parent page is still the active view of its + * outlet. If the user comes back to a different page in that outlet, the + * outlet's own transition fires leave on it a second time, so swallow leave + * events on it until it enters again. Unblock on `ionViewDidEnter`, since a + * cancelled swipe back only fires `ionViewWillEnter`. + */ +const blockRepeatLeave = (pageEl: HTMLElement) => { + // Capture so this runs before the page's own lifecycle listeners. + const block = (ev: Event) => ev.stopImmediatePropagation(); + const unblock = () => { + pageEl.removeEventListener(LIFECYCLE_WILL_LEAVE, block, true); + pageEl.removeEventListener(LIFECYCLE_DID_LEAVE, block, true); + }; + + pageEl.addEventListener(LIFECYCLE_WILL_LEAVE, block, true); + pageEl.addEventListener(LIFECYCLE_DID_LEAVE, block, true); + pageEl.addEventListener(LIFECYCLE_DID_ENTER, unblock, { capture: true, once: true }); +}; + // TODO(FW-2827): types @Directive({ @@ -57,6 +83,7 @@ export abstract class IonRouterOutlet implements OnDestroy, OnInit { private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null); private activated: ComponentRef | null = null; + private unbindParentLifecycle?: () => void; /** @internal */ get activatedComponentRef(): ComponentRef | null { return this.activated; @@ -129,6 +156,7 @@ export abstract class IonRouterOutlet implements OnDestroy, OnInit { } ngOnDestroy(): void { + this.unbindParentLifecycle?.(); this.stackCtrl.destroy(); this.inputBinder?.unsubscribeFromRouteData(this); } @@ -139,6 +167,7 @@ export abstract class IonRouterOutlet implements OnDestroy, OnInit { ngOnInit(): void { this.initializeOutletWithName(); + this.bindParentLifecycle(); } // Note: Ionic deviates from the Angular Router implementation here @@ -159,6 +188,105 @@ export abstract class IonRouterOutlet implements OnDestroy, OnInit { }); } + /** + * Core doesn't bubble lifecycle events, so forward the ones on the page + * hosting this outlet (such as a page containing `ion-tabs`) to the page + * active in this outlet. Enter events only go to a page that left with the + * parent page. + */ + private bindParentLifecycle() { + const parentPageEl = this.getParentPageElement(); + if (!parentPageEl) { + return; + } + + let leftWithParent: RouteView | undefined; + + /** + * Apps using the workaround from + * https://github.com/ionic-team/ionic-framework/issues/16834 already + * re-dispatch the parent's lifecycle events onto the active tab, so track + * whether the active page got the event to avoid firing it twice. + */ + let parentEventType: string | undefined; + let activeViewReceived = false; + const onParentEventStart = (ev: Event) => { + if (ev.target === parentPageEl) { + parentEventType = ev.type; + activeViewReceived = false; + } + }; + const onOutletEvent = (ev: Event) => { + if (ev.type === parentEventType && ev.target === this.stackCtrl.getActiveView()?.element) { + activeViewReceived = true; + } + }; + + const forward = (ev: Event) => { + const alreadyReceived = activeViewReceived; + parentEventType = undefined; + activeViewReceived = false; + + const activeView = this.stackCtrl.getActiveView(); + if (!activeView) { + return; + } + + const isEnter = ev.type === LIFECYCLE_WILL_ENTER || ev.type === LIFECYCLE_DID_ENTER; + if (isEnter && activeView !== leftWithParent) { + return; + } + + if (!alreadyReceived) { + activeView.element.dispatchEvent(new CustomEvent(ev.type, { bubbles: false, cancelable: false })); + } + + if (ev.type === LIFECYCLE_DID_LEAVE) { + leftWithParent = activeView; + blockRepeatLeave(activeView.element); + } else if (ev.type === LIFECYCLE_DID_ENTER) { + leftWithParent = undefined; + } + }; + + /** + * Capture listeners see the event before the parent page's own hooks run, + * and see events dispatched on pages in this outlet even though they + * don't bubble. The bubble-phase `forward` listener runs after the parent + * page's own hooks because those are bound when the page is created, + * before this outlet inits. + */ + const events = [LIFECYCLE_WILL_ENTER, LIFECYCLE_DID_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE]; + events.forEach((eventName) => { + parentPageEl.addEventListener(eventName, onParentEventStart, true); + parentPageEl.addEventListener(eventName, forward); + this.nativeEl.addEventListener(eventName, onOutletEvent, true); + }); + this.unbindParentLifecycle = () => { + events.forEach((eventName) => { + parentPageEl.removeEventListener(eventName, onParentEventStart, true); + parentPageEl.removeEventListener(eventName, forward); + this.nativeEl.removeEventListener(eventName, onOutletEvent, true); + }); + }; + } + + /** + * The page in the parent outlet that contains this outlet, if any. + */ + private getParentPageElement(): HTMLElement | undefined { + const parentOutletEl = this.parentOutlet?.nativeEl; + if (!parentOutletEl) { + return undefined; + } + + let el: HTMLElement | null = this.nativeEl; + while (el && el.parentElement !== parentOutletEl) { + el = el.parentElement; + } + return el ?? undefined; + } + get isActivated(): boolean { return !!this.activated; } diff --git a/packages/angular/test/base/e2e/src/standalone/tab-lifecycle.spec.ts b/packages/angular/test/base/e2e/src/standalone/tab-lifecycle.spec.ts new file mode 100644 index 00000000000..ce0d5715094 --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/tab-lifecycle.spec.ts @@ -0,0 +1,254 @@ +import { test, expect, type Page } from '@playwright/test'; + +import { ionSwipeToGoBack } from '../../utils/drag-utils'; +import { ionPageHidden, ionPageVisible } from '../../utils/test-utils'; + +const getLifecycleEvents = async (page: Page, pageName?: string): Promise => { + const events: string[] = await page.evaluate(() => (window as any).lifecycleEvents ?? []); + return pageName ? events.filter((event) => event.startsWith(`${pageName}:`)) : events; +}; + +const clearLifecycleEvents = (page: Page) => page.evaluate(() => ((window as any).lifecycleEvents = [])); + +/** + * Drags far enough from the left edge to start the swipe back gesture, then + * returns to the edge before releasing so the gesture is cancelled. + * Calling `ionSwipeToGoBack(page, false)` won't work because it doesn't move at all. + */ +const cancelledSwipeBack = async (page: Page) => { + const box = (await page.locator('ion-router-outlet').first().boundingBox())!; + const y = box.y + box.height / 2; + + await page.mouse.move(box.x + 2, y); + await page.mouse.down(); + for (const x of [10, 20, 40, 60, 80, 100, 60, 30, 10, 2]) { + await page.mouse.move(box.x + x, y); + await page.waitForTimeout(25); + } + await page.mouse.up(); +}; + +/** + * Verifies that the active tab's page, and a page in an outlet nested inside + * it, get leave events when navigating out of the tabs, get enter events when + * coming back, and never get leave events twice. + */ +test.describe('Tabs: active tab lifecycle when leaving and returning to the tabs', () => { + test('should fire ionViewWillLeave and ionViewDidLeave on the active tab page', async ({ page }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/standalone/tab-lifecycle/home'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should fire leave events on the active tab page each time the tabs are left', async ({ page }) => { + await page.goto('/standalone/tab-lifecycle/home'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('app-tab-lifecycle-outside ion-back-button').click(); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should fire each event once on the active tab page when the tabs page also re-dispatches them', async ({ + page, + }) => { + await page.goto('/standalone/tab-lifecycle/home?propagate=true'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toContain('tabs:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('tabs:ionViewDidLeave'); + expect(await getLifecycleEvents(page, 'home')).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + await clearLifecycleEvents(page); + + await page.locator('app-tab-lifecycle-outside ion-back-button').click(); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toContain('tabs:ionViewDidEnter'); + expect(await getLifecycleEvents(page, 'home')).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + }); + + test('should fire ionViewWillLeave and ionViewDidLeave on the active tab page when a swipe back from the tabs completes', async ({ + page, + }) => { + await page.setViewportSize({ width: 320, height: 568 }); + await page.goto('/standalone?ionic:mode=ios'); + + await page.locator('ion-item[routerLink="/standalone/tab-lifecycle"]').click(); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await ionPageHidden(page, 'app-home-page'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await ionSwipeToGoBack(page, true); + await ionPageVisible(page, 'app-home-page'); + + await expect.poll(() => getLifecycleEvents(page, 'home')).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should fire ionViewWillLeave on the active tab page when a swipe back from the tabs is cancelled', async ({ + page, + }) => { + await page.setViewportSize({ width: 320, height: 568 }); + await page.goto('/standalone?ionic:mode=ios'); + + await page.locator('ion-item[routerLink="/standalone/tab-lifecycle"]').click(); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await ionPageHidden(page, 'app-home-page'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await cancelledSwipeBack(page); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + await page.waitForFunction(() => document.getAnimations().length === 0); + + expect(await getLifecycleEvents(page, 'home')).toEqual(['home:ionViewWillLeave']); + }); + + test('should not fire leave events on a previously visited tab that is hidden', async ({ page }) => { + await page.goto('/standalone/tab-lifecycle/home'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + + await page.locator('ion-tab-button[tab="settings"]').click(); + await ionPageVisible(page, 'app-tab-lifecycle-settings'); + await expect + .poll(() => getLifecycleEvents(page, 'settings')) + .toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-settings').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['settings:ionViewWillLeave', 'settings:ionViewDidLeave']); + }); + + test('should fire ionViewWillEnter and ionViewDidEnter on the active tab page when going back to the tabs', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/standalone/tab-lifecycle/home'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('app-tab-lifecycle-outside ion-back-button').click(); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + }); + + test('should not fire leave events again on the tab that was left when returning to a different tab', async ({ + page, + }) => { + await page.goto('/standalone/tab-lifecycle/home'); + await ionPageVisible(page, 'app-tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('#go-to-settings').click(); + await ionPageVisible(page, 'app-tab-lifecycle-settings'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + + expect(await getLifecycleEvents(page, 'home')).toEqual([]); + }); + + test('should fire ionViewWillLeave and ionViewDidLeave on a page in an outlet nested inside the active tab', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/standalone/tab-lifecycle/nested'); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['inner:ionViewWillLeave', 'inner:ionViewDidLeave']); + }); + + test('should fire ionViewWillEnter and ionViewDidEnter on a page in a nested outlet when going back to the tabs', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/standalone/tab-lifecycle/nested'); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('app-tab-lifecycle-outside ion-back-button').click(); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + + await expect.poll(() => getLifecycleEvents(page, 'inner')).toEqual(['inner:ionViewWillEnter', 'inner:ionViewDidEnter']); + }); + + test('should not fire leave events again on a page in a nested outlet when returning to a different tab', async ({ + page, + }) => { + await page.goto('/standalone/tab-lifecycle/nested'); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'app-tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('#go-to-settings').click(); + await ionPageVisible(page, 'app-tab-lifecycle-settings'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + + expect(await getLifecycleEvents(page, 'inner')).toEqual([]); + }); + + test('should fire leave and enter events on a page in a nested outlet when switching tabs', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/standalone/tab-lifecycle/nested'); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await page.locator('ion-tab-button[tab="settings"]').click(); + await ionPageVisible(page, 'app-tab-lifecycle-settings'); + await expect.poll(() => getLifecycleEvents(page, 'inner')).toEqual(['inner:ionViewWillLeave', 'inner:ionViewDidLeave']); + await clearLifecycleEvents(page); + + await page.locator('ion-tab-button[tab="nested"]').click(); + await ionPageVisible(page, 'app-tab-lifecycle-inner'); + await expect.poll(() => getLifecycleEvents(page, 'inner')).toEqual(['inner:ionViewWillEnter', 'inner:ionViewDidEnter']); + }); +}); diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index 337b57b8149..edb79e44158 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -76,6 +76,23 @@ export const routes: Routes = [ { path: 'tab4', loadComponent: () => import('../tabs-search-params/tab4.component').then(c => c.TabsSearchParamsTab4Component) } ] }, + { path: 'tab-lifecycle', redirectTo: '/standalone/tab-lifecycle/home', pathMatch: 'full' }, + { + path: 'tab-lifecycle', + loadComponent: () => import('../tab-lifecycle/tab-lifecycle.component').then(c => c.TabLifecycleComponent), + children: [ + { path: 'home', loadComponent: () => import('../tab-lifecycle/tab-lifecycle-home.component').then(c => c.TabLifecycleHomeComponent) }, + { path: 'settings', loadComponent: () => import('../tab-lifecycle/tab-lifecycle-settings.component').then(c => c.TabLifecycleSettingsComponent) }, + { + path: 'nested', + loadComponent: () => import('../tab-lifecycle/tab-lifecycle-nested.component').then(c => c.TabLifecycleNestedComponent), + children: [ + { path: '', loadComponent: () => import('../tab-lifecycle/tab-lifecycle-inner.component').then(c => c.TabLifecycleInnerComponent) } + ] + } + ] + }, + { path: 'tab-lifecycle-outside', loadComponent: () => import('../tab-lifecycle/tab-lifecycle-outside.component').then(c => c.TabLifecycleOutsideComponent) }, { path: 'swipe-gesture-disabled', loadComponent: () => import('../swipe-gesture-disabled/swipe-gesture-disabled.component').then(c => c.SwipeGestureDisabledComponent), diff --git a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html index 0adb4664e58..60830b84120 100644 --- a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html +++ b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html @@ -104,6 +104,11 @@ Tabs Search Params Test + + + Tab Lifecycle Test + + diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/lifecycle-log.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/lifecycle-log.ts new file mode 100644 index 00000000000..a2a9a09dddf --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/lifecycle-log.ts @@ -0,0 +1,5 @@ +export const logLifecycle = (event: string) => { + const win = window as any; + win.lifecycleEvents = win.lifecycleEvents || []; + win.lifecycleEvents.push(event); +}; diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-home.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-home.component.ts new file mode 100644 index 00000000000..3a614785f01 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-home.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { + IonButton, + IonContent, + IonHeader, + IonRouterLink, + IonTitle, + IonToolbar, + ViewDidEnter, + ViewDidLeave, + ViewWillEnter, + ViewWillLeave, +} from '@ionic/angular'; + +import { logLifecycle } from './lifecycle-log'; + +@Component({ + selector: 'app-tab-lifecycle-home', + template: ` + + + Home Tab + + + + Go Outside Tabs + + `, + standalone: true, + imports: [RouterLink, IonButton, IonContent, IonHeader, IonRouterLink, IonTitle, IonToolbar], +}) +export class TabLifecycleHomeComponent implements ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave { + ionViewWillEnter() { + logLifecycle('home:ionViewWillEnter'); + } + + ionViewDidEnter() { + logLifecycle('home:ionViewDidEnter'); + } + + ionViewWillLeave() { + logLifecycle('home:ionViewWillLeave'); + } + + ionViewDidLeave() { + logLifecycle('home:ionViewDidLeave'); + } +} diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-inner.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-inner.component.ts new file mode 100644 index 00000000000..e6875326fb9 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-inner.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { + IonButton, + IonContent, + IonHeader, + IonRouterLink, + IonTitle, + IonToolbar, + ViewDidEnter, + ViewDidLeave, + ViewWillEnter, + ViewWillLeave, +} from '@ionic/angular'; + +import { logLifecycle } from './lifecycle-log'; + +@Component({ + selector: 'app-tab-lifecycle-inner', + template: ` + + + Inner Page + + + + Go Outside Tabs + + `, + standalone: true, + imports: [RouterLink, IonButton, IonContent, IonHeader, IonRouterLink, IonTitle, IonToolbar], +}) +export class TabLifecycleInnerComponent implements ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave { + ionViewWillEnter() { + logLifecycle('inner:ionViewWillEnter'); + } + + ionViewDidEnter() { + logLifecycle('inner:ionViewDidEnter'); + } + + ionViewWillLeave() { + logLifecycle('inner:ionViewWillLeave'); + } + + ionViewDidLeave() { + logLifecycle('inner:ionViewDidLeave'); + } +} diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-nested.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-nested.component.ts new file mode 100644 index 00000000000..ed74cceab6c --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-nested.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { IonRouterOutlet } from '@ionic/angular'; + +@Component({ + selector: 'app-tab-lifecycle-nested', + template: ``, + standalone: true, + imports: [IonRouterOutlet], +}) +export class TabLifecycleNestedComponent {} diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-outside.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-outside.component.ts new file mode 100644 index 00000000000..18ed12e0d87 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-outside.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { + IonBackButton, + IonButton, + IonButtons, + IonContent, + IonHeader, + IonRouterLink, + IonTitle, + IonToolbar, +} from '@ionic/angular'; + +@Component({ + selector: 'app-tab-lifecycle-outside', + template: ` + + + + + + Outside Tabs + + + +

Outside the tabs

+ Go to Settings Tab +
+ `, + standalone: true, + imports: [RouterLink, IonBackButton, IonButton, IonButtons, IonContent, IonHeader, IonRouterLink, IonTitle, IonToolbar], +}) +export class TabLifecycleOutsideComponent {} diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-settings.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-settings.component.ts new file mode 100644 index 00000000000..067b3d4d0fc --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle-settings.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { + IonButton, + IonContent, + IonHeader, + IonRouterLink, + IonTitle, + IonToolbar, + ViewDidEnter, + ViewDidLeave, + ViewWillEnter, + ViewWillLeave, +} from '@ionic/angular'; + +import { logLifecycle } from './lifecycle-log'; + +@Component({ + selector: 'app-tab-lifecycle-settings', + template: ` + + + Settings Tab + + + + Go Outside Tabs + + `, + standalone: true, + imports: [RouterLink, IonButton, IonContent, IonHeader, IonRouterLink, IonTitle, IonToolbar], +}) +export class TabLifecycleSettingsComponent implements ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave { + ionViewWillEnter() { + logLifecycle('settings:ionViewWillEnter'); + } + + ionViewDidEnter() { + logLifecycle('settings:ionViewDidEnter'); + } + + ionViewWillLeave() { + logLifecycle('settings:ionViewWillLeave'); + } + + ionViewDidLeave() { + logLifecycle('settings:ionViewDidLeave'); + } +} diff --git a/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle.component.ts b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle.component.ts new file mode 100644 index 00000000000..1d488c24ec6 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/tab-lifecycle/tab-lifecycle.component.ts @@ -0,0 +1,80 @@ +import { Component, ViewChild } from '@angular/core'; +import { + IonIcon, + IonLabel, + IonTabBar, + IonTabButton, + IonTabs, + ViewDidEnter, + ViewDidLeave, + ViewWillEnter, + ViewWillLeave, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { square, triangle } from 'ionicons/icons'; + +import { logLifecycle } from './lifecycle-log'; + +addIcons({ square, triangle }); + +@Component({ + selector: 'app-tab-lifecycle', + template: ` + + + + + Home + + + + Settings + + + + Nested + + + + `, + standalone: true, + imports: [IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs], +}) +export class TabLifecycleComponent implements ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave { + @ViewChild(IonTabs) tabs!: IonTabs; + + /** + * The `?propagate` param turns on the workaround from + * https://github.com/ionic-team/ionic-framework/issues/16834, where the tabs + * page re-dispatches its lifecycle events onto the active tab. + */ + private propagate = new URLSearchParams(window.location.search).has('propagate'); + private activeTab?: HTMLElement; + + tabChange() { + this.activeTab = this.tabs.outlet.activatedView?.element; + } + + ionViewWillEnter() { + this.propagateToActiveTab('ionViewWillEnter'); + } + + ionViewDidEnter() { + this.propagateToActiveTab('ionViewDidEnter'); + } + + ionViewWillLeave() { + this.propagateToActiveTab('ionViewWillLeave'); + } + + ionViewDidLeave() { + this.propagateToActiveTab('ionViewDidLeave'); + } + + private propagateToActiveTab(eventName: string) { + if (this.propagate) { + this.activeTab?.dispatchEvent(new CustomEvent(eventName)); + logLifecycle(`tabs:${eventName}`); + } + } +} diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts index 2a53b8d153b..81b4e0fc6e1 100644 --- a/packages/vue/src/components/IonRouterOutlet.ts +++ b/packages/vue/src/components/IonRouterOutlet.ts @@ -16,6 +16,7 @@ import { provide, watch, shallowRef, + onMounted, onUnmounted, } from "vue"; import type { InjectionKey, Ref } from "vue"; @@ -65,6 +66,14 @@ export const IonRouterOutlet = /*@__PURE__*/ defineComponent({ let skipTransition = false; + /** + * The view that was given leave hooks when the page hosting + * this outlet left. It's still visible in this outlet, so + * coming back to a different view would otherwise fire its + * leave hooks a second time. + */ + let leftWithParent: any; + // The base url for this router outlet let parentOutletPath: string; @@ -306,6 +315,10 @@ See https://ionicframework.com/docs/vue/navigation#ionpage for more information. ); } + const leavingViewAlreadyLeft = + leavingViewItem !== undefined && leavingViewItem === leftWithParent; + leftWithParent = undefined; + /** * If the entering view is already * visible, then no transition is needed. @@ -346,11 +359,13 @@ See https://ionicframework.com/docs/vue/navigation#ionpage for more information. let animationBuilder = routerAnimation; const leavingEl = leavingViewItem.ionPageElement; - fireLifecycle( - leavingViewItem.vueComponent, - leavingViewItem.vueComponentRef, - LIFECYCLE_WILL_LEAVE - ); + if (!leavingViewAlreadyLeft) { + fireLifecycle( + leavingViewItem.vueComponent, + leavingViewItem.vueComponentRef, + LIFECYCLE_WILL_LEAVE + ); + } /** * If we are going back from a page that @@ -414,11 +429,13 @@ See https://ionicframework.com/docs/vue/navigation#ionpage for more information. viewStacks.mountIntermediaryViews(id, leavingViewItem, delta); } - fireLifecycle( - leavingViewItem.vueComponent, - leavingViewItem.vueComponentRef, - LIFECYCLE_DID_LEAVE - ); + if (!leavingViewAlreadyLeft) { + fireLifecycle( + leavingViewItem.vueComponent, + leavingViewItem.vueComponentRef, + LIFECYCLE_DID_LEAVE + ); + } } else { /** * If there is no leaving element, just show @@ -504,6 +521,85 @@ See https://ionicframework.com/docs/vue/navigation#ionpage for more information. */ onUnmounted(() => viewStacks.clear(id)); + /** + * The view picked on the parent's `ionViewWillLeave`, so the parent's + * `ionViewDidLeave` fires on the same one. + */ + let leavingWithParent: any; + + /** + * Core dispatches lifecycle events with `bubbles: false` on the page + * the parent outlet is transitioning. When that page hosts this outlet + * (such as a page containing `ion-tabs`), the page visible in this + * outlet never gets leave hooks, so fire them in step with the parent + * page. The events are also dispatched on the page so an outlet + * nested inside it forwards them in turn. + */ + const forwardParentLeave = (ev: Event) => { + if (ev.type === LIFECYCLE_WILL_LEAVE) { + const activeViewItem = viewStacks + .getViewStack(id) + ?.find( + (viewItem: any) => + viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement) + ); + leavingWithParent = + activeViewItem !== leftWithParent ? activeViewItem : undefined; + } + + const viewItem = leavingWithParent; + if (!viewItem) { + return; + } + + const lifecycle = ev.type as + | typeof LIFECYCLE_WILL_LEAVE + | typeof LIFECYCLE_DID_LEAVE; + fireLifecycle(viewItem.vueComponent, viewItem.vueComponentRef, lifecycle); + viewItem.ionPageElement.dispatchEvent( + new CustomEvent(lifecycle, { bubbles: false, cancelable: false }) + ); + + if (lifecycle === LIFECYCLE_DID_LEAVE) { + leftWithParent = viewItem; + leavingWithParent = undefined; + } + }; + + /** + * The page in the parent outlet that contains this outlet, if any. + */ + const getParentPageElement = (): HTMLElement | undefined => { + let el: HTMLElement = ionRouterOutlet.value; + while ( + el.parentElement && + el.parentElement.tagName !== "ION-ROUTER-OUTLET" + ) { + el = el.parentElement; + } + return el.parentElement ? el : undefined; + }; + + let unbindParentLifecycle: (() => void) | undefined; + + onMounted(() => { + const parentPageEl = getParentPageElement(); + if (!parentPageEl) { + return; + } + + const events = [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE]; + events.forEach((eventName) => + parentPageEl.addEventListener(eventName, forwardParentLeave) + ); + unbindParentLifecycle = () => + events.forEach((eventName) => + parentPageEl.removeEventListener(eventName, forwardParentLeave) + ); + }); + + onUnmounted(() => unbindParentLifecycle?.()); + const registerIonPage = (viewItem: any, ionPageEl: HTMLElement) => { const oldIonPageEl = viewItem.ionPageElement; diff --git a/packages/vue/test/base/src/router/index.ts b/packages/vue/test/base/src/router/index.ts index 6419cfa4645..ce5e63de52e 100644 --- a/packages/vue/test/base/src/router/index.ts +++ b/packages/vue/test/base/src/router/index.ts @@ -226,6 +226,38 @@ const routes: Array = [ } ] }, + { + path: '/tab-lifecycle/', + component: () => import('@/views/tab-lifecycle/TabLifecycle.vue'), + children: [ + { + path: '', + redirect: '/tab-lifecycle/home' + }, + { + path: 'home', + component: () => import('@/views/tab-lifecycle/TabLifecycleHome.vue') + }, + { + path: 'settings', + component: () => import('@/views/tab-lifecycle/TabLifecycleSettings.vue') + }, + { + path: 'nested', + component: () => import('@/views/tab-lifecycle/TabLifecycleNested.vue'), + children: [ + { + path: '', + component: () => import('@/views/tab-lifecycle/TabLifecycleInner.vue') + } + ] + } + ] + }, + { + path: '/tab-lifecycle-outside', + component: () => import('@/views/tab-lifecycle/TabLifecycleOutside.vue') + }, { path: '/tabs-similar-prefixes/', component: () => import('@/views/tabs-similar-prefixes/TabsSimilarPrefixes.vue'), diff --git a/packages/vue/test/base/src/views/Home.vue b/packages/vue/test/base/src/views/Home.vue index 1405e720cf8..b8fbfdf04e7 100644 --- a/packages/vue/test/base/src/views/Home.vue +++ b/packages/vue/test/base/src/views/Home.vue @@ -59,6 +59,9 @@ Tabs with Search Params on Href + + Tab Lifecycle + Lifecycle diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycle.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycle.vue new file mode 100644 index 00000000000..aade80e091a --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycle.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleHome.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleHome.vue new file mode 100644 index 00000000000..0dd68f04474 --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleHome.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleInner.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleInner.vue new file mode 100644 index 00000000000..21b7b9f48d0 --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleInner.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleNested.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleNested.vue new file mode 100644 index 00000000000..323b1305bb0 --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleNested.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleOutside.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleOutside.vue new file mode 100644 index 00000000000..27bae14693d --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleOutside.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleSettings.vue b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleSettings.vue new file mode 100644 index 00000000000..40b27f8abc9 --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/TabLifecycleSettings.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/vue/test/base/src/views/tab-lifecycle/lifecycle-log.ts b/packages/vue/test/base/src/views/tab-lifecycle/lifecycle-log.ts new file mode 100644 index 00000000000..a2a9a09dddf --- /dev/null +++ b/packages/vue/test/base/src/views/tab-lifecycle/lifecycle-log.ts @@ -0,0 +1,5 @@ +export const logLifecycle = (event: string) => { + const win = window as any; + win.lifecycleEvents = win.lifecycleEvents || []; + win.lifecycleEvents.push(event); +}; diff --git a/packages/vue/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts b/packages/vue/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts new file mode 100644 index 00000000000..52784ef74c9 --- /dev/null +++ b/packages/vue/test/base/tests/e2e/playwright/tab-lifecycle.spec.ts @@ -0,0 +1,249 @@ +import type { Page } from '@playwright/test'; +import { test, expect, withAnimations } from './utils/test-base'; +import { waitForAnimationsComplete } from './utils/animation-utils'; +import { ionBackClick, ionPageHidden, ionPageVisible, ionSwipeToGoBack, tabClick } from './utils/test-utils'; + +const getLifecycleEvents = async (page: Page, pageName?: string): Promise => { + const events: string[] = await page.evaluate(() => (window as any).lifecycleEvents ?? []); + return pageName ? events.filter((event) => event.startsWith(`${pageName}:`)) : events; +}; + +const clearLifecycleEvents = (page: Page) => page.evaluate(() => ((window as any).lifecycleEvents = [])); + +/** + * Drags far enough from the left edge to start the swipe back gesture, then + * returns to the edge before releasing so the gesture is cancelled. + * Calling `ionSwipeToGoBack(page, false)` won't work because it doesn't move far enough to start it. + */ +const cancelledSwipeBack = async (page: Page) => { + const box = (await page.locator('ion-router-outlet').first().boundingBox())!; + const y = box.y + box.height / 2; + + await page.mouse.move(box.x + 2, y); + await page.mouse.down(); + for (const x of [10, 20, 40, 60, 80, 100, 60, 30, 10, 2]) { + await page.mouse.move(box.x + x, y); + await page.waitForTimeout(25); + } + await page.mouse.up(); +}; + +/** + * Verifies that the active tab's page, and a page in an outlet nested inside + * it, get leave events when navigating out of the tabs, get enter events when + * coming back, and never get leave events twice. + */ +test.describe('Tabs: active tab lifecycle when leaving and returning to the tabs', () => { + test('should fire ionViewWillLeave and ionViewDidLeave on the active tab page', async ({ page }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/tab-lifecycle/home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should fire leave events on the active tab page each time the tabs are left', async ({ page }) => { + await page.goto('/tab-lifecycle/home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await ionBackClick(page, 'tab-lifecycle-outside'); + await ionPageVisible(page, 'tab-lifecycle-home'); + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should not fire leave events on a previously visited tab that is hidden', async ({ page }) => { + await page.goto('/tab-lifecycle/home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await tabClick(page, 'settings'); + await ionPageVisible(page, 'tab-lifecycle-settings'); + await expect + .poll(() => getLifecycleEvents(page, 'settings')) + .toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-settings').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + + await expect + .poll(() => getLifecycleEvents(page)) + .toEqual(['settings:ionViewWillLeave', 'settings:ionViewDidLeave']); + }); + + test('should fire ionViewWillEnter and ionViewDidEnter on the active tab page when going back to the tabs', async ({ + page, + }) => { + await page.goto('/tab-lifecycle/home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await ionBackClick(page, 'tab-lifecycle-outside'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['home:ionViewWillEnter', 'home:ionViewDidEnter']); + }); + + test('should not fire leave events again on the tab that was left when returning to a different tab', async ({ + page, + }) => { + await page.goto('/tab-lifecycle/home'); + await ionPageVisible(page, 'tab-lifecycle-home'); + + await page.locator('#go-outside-home').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('#go-to-settings').click(); + await ionPageVisible(page, 'tab-lifecycle-settings'); + await expect + .poll(() => getLifecycleEvents(page, 'settings')) + .toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + + expect(await getLifecycleEvents(page, 'home')).toEqual([]); + }); + + test('should fire ionViewWillLeave and ionViewDidLeave on a page in an outlet nested inside the active tab', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/tab-lifecycle/nested'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + + await expect.poll(() => getLifecycleEvents(page)).toEqual(['inner:ionViewWillLeave', 'inner:ionViewDidLeave']); + }); + + test('should fire ionViewWillEnter and ionViewDidEnter on a page in a nested outlet when going back to the tabs', async ({ + page, + }) => { + await page.goto('/tab-lifecycle/nested'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await ionBackClick(page, 'tab-lifecycle-outside'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + + await expect + .poll(() => getLifecycleEvents(page, 'inner')) + .toEqual(['inner:ionViewWillEnter', 'inner:ionViewDidEnter']); + }); + + test('should not fire leave events again on a page in a nested outlet when returning to a different tab', async ({ + page, + }) => { + await page.goto('/tab-lifecycle/nested'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + + await page.locator('#go-outside-inner').click(); + await ionPageVisible(page, 'tab-lifecycle-outside'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidLeave'); + await clearLifecycleEvents(page); + + await page.locator('#go-to-settings').click(); + await ionPageVisible(page, 'tab-lifecycle-settings'); + await expect + .poll(() => getLifecycleEvents(page, 'settings')) + .toEqual(['settings:ionViewWillEnter', 'settings:ionViewDidEnter']); + + expect(await getLifecycleEvents(page, 'inner')).toEqual([]); + }); + + test('should fire leave and enter events on a page in a nested outlet when switching tabs', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'FW-7148' }); + + await page.goto('/tab-lifecycle/nested'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + await expect.poll(() => getLifecycleEvents(page)).toContain('inner:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await tabClick(page, 'settings'); + await ionPageVisible(page, 'tab-lifecycle-settings'); + await expect + .poll(() => getLifecycleEvents(page, 'inner')) + .toEqual(['inner:ionViewWillLeave', 'inner:ionViewDidLeave']); + await clearLifecycleEvents(page); + + await tabClick(page, 'nested'); + await ionPageVisible(page, 'tab-lifecycle-inner'); + await expect + .poll(() => getLifecycleEvents(page, 'inner')) + .toEqual(['inner:ionViewWillEnter', 'inner:ionViewDidEnter']); + }); + + test('should fire ionViewWillLeave and ionViewDidLeave once on the active tab page when a swipe back from the tabs completes', async ({ + page, + }) => { + await page.setViewportSize({ width: 320, height: 568 }); + await page.goto(withAnimations('/?ionic:mode=ios')); + await ionPageVisible(page, 'home'); + + await page.locator('ion-item#tab-lifecycle').click(); + await ionPageVisible(page, 'tab-lifecycle-home'); + await ionPageHidden(page, 'home'); + await waitForAnimationsComplete(page, 'div.ion-page[data-pageid="tab-lifecycle"]'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await ionSwipeToGoBack(page, true); + await ionPageVisible(page, 'home'); + await waitForAnimationsComplete(page, 'div.ion-page[data-pageid="home"]'); + + await expect + .poll(() => getLifecycleEvents(page, 'home')) + .toEqual(['home:ionViewWillLeave', 'home:ionViewDidLeave']); + }); + + test('should fire ionViewWillLeave on the active tab page when a swipe back from the tabs is cancelled', async ({ + page, + }) => { + await page.setViewportSize({ width: 320, height: 568 }); + await page.goto(withAnimations('/?ionic:mode=ios')); + await ionPageVisible(page, 'home'); + + await page.locator('ion-item#tab-lifecycle').click(); + await ionPageVisible(page, 'tab-lifecycle-home'); + await ionPageHidden(page, 'home'); + await waitForAnimationsComplete(page, 'div.ion-page[data-pageid="tab-lifecycle"]'); + await expect.poll(() => getLifecycleEvents(page)).toContain('home:ionViewDidEnter'); + await clearLifecycleEvents(page); + + await cancelledSwipeBack(page); + await ionPageVisible(page, 'tab-lifecycle-home'); + await waitForAnimationsComplete(page, 'div.ion-page[data-pageid="tab-lifecycle"]'); + + expect(await getLifecycleEvents(page, 'home')).toEqual(['home:ionViewWillLeave']); + }); +}); diff --git a/packages/vue/test/base/tests/unit/lifecycle.spec.ts b/packages/vue/test/base/tests/unit/lifecycle.spec.ts index 3c1996d5e74..bcf46002605 100644 --- a/packages/vue/test/base/tests/unit/lifecycle.spec.ts +++ b/packages/vue/test/base/tests/unit/lifecycle.spec.ts @@ -167,10 +167,8 @@ describe('Lifecycle Events', () => { expect(TabsPage.ionViewWillLeave).toHaveBeenCalled(); expect(TabsPage.ionViewDidLeave).toHaveBeenCalled(); - // Tab1Page currently does not call leaving hooks - // when navigating out of tabs - //expect(Tab1Page.ionViewWillLeave).toHaveBeenCalled(); - //expect(Tab1Page.ionViewDidLeave).toHaveBeenCalled(); + expect(Tab1Page.ionViewWillLeave).toHaveBeenCalled(); + expect(Tab1Page.ionViewDidLeave).toHaveBeenCalled(); expect(NonTabPage.ionViewWillEnter).toHaveBeenCalled(); expect(NonTabPage.ionViewDidEnter).toHaveBeenCalled(); @@ -197,6 +195,9 @@ describe('Lifecycle Events', () => { expect(TabsPage.ionViewWillLeave).toHaveBeenCalled(); expect(TabsPage.ionViewDidLeave).toHaveBeenCalled(); + expect(Tab1Page.ionViewWillLeave).toHaveBeenCalled(); + expect(Tab1Page.ionViewDidLeave).toHaveBeenCalled(); + expect(NonTabPage.ionViewWillEnter).toHaveBeenCalled(); expect(NonTabPage.ionViewDidEnter).toHaveBeenCalled(); From 55af474e587b62aa248ad629109d21a0fe7fdb20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:53:41 +0000 Subject: [PATCH 19/53] chore(deps): update actions/setup-node action to v7.1.0 (#31547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://redirect.github.com/actions/setup-node) | action | minor | `v7.0.0` → `v7.1.0` | --- ### Release Notes
actions/setup-node (actions/setup-node) ### [`v7.1.0`](https://redirect.github.com/actions/setup-node/releases/tag/v7.1.0) [Compare Source](https://redirect.github.com/actions/setup-node/compare/v7.0.0...v7.1.0) #### What's Changed ##### Enhancements: - Add support for mise.toml file by [@​gperdomor](https://redirect.github.com/gperdomor) in [#​1421](https://redirect.github.com/actions/setup-node/pull/1421) - Support absolute paths in node-version-file by [@​v-mahabaleshwars](https://redirect.github.com/v-mahabaleshwars) in [#​1633](https://redirect.github.com/actions/setup-node/pull/1633) ##### Bug fixes: - Add Node version validation and manifest fetch retry by [@​v-jitenderpalsingh](https://redirect.github.com/v-jitenderpalsingh) in [#​1580](https://redirect.github.com/actions/setup-node/pull/1580) ##### Documentation updates: - README.md: bump actions/checkout to latest version 7 by [@​deining](https://redirect.github.com/deining) in [#​1578](https://redirect.github.com/actions/setup-node/pull/1578) - docs: add V7 ESM migration and NODE_AUTH_TOKEN behavior change by [@​v-gowridurgad](https://redirect.github.com/v-gowridurgad) in [#​1593](https://redirect.github.com/actions/setup-node/pull/1593) - Fixed cache recommendation syntax by [@​SubJunk](https://redirect.github.com/SubJunk) in [#​1603](https://redirect.github.com/actions/setup-node/pull/1603) ##### Dependency updates: - Update workflow pins and migrate Node matrices to modern versions by [@​v-HarithaVattikuti](https://redirect.github.com/v-HarithaVattikuti) in [#​1583](https://redirect.github.com/actions/setup-node/pull/1583) - Upgrade [@​actions/cache](https://redirect.github.com/actions/cache) to 6.2.0 by [@​philip-gai](https://redirect.github.com/philip-gai) in [#​1584](https://redirect.github.com/actions/setup-node/pull/1584) - Pin [@​types/node](https://redirect.github.com/types/node) to v24 by [@​v-priyagupta108](https://redirect.github.com/v-priyagupta108) in [#​1585](https://redirect.github.com/actions/setup-node/pull/1585) - Override brace-expansion to 5.0.8 by [@​v-priyagupta108](https://redirect.github.com/v-priyagupta108) with [@​Copilot](https://redirect.github.com/Copilot) in [#​1599](https://redirect.github.com/actions/setup-node/pull/1599) - Upgrade actions/checkout from 7.0.0 to 7.0.1 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1598](https://redirect.github.com/actions/setup-node/pull/1598) - Upgrade prettier from 3.8.4 to 3.9.6 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1590](https://redirect.github.com/actions/setup-node/pull/1590) - Upgrade [@​vercel/ncc](https://redirect.github.com/vercel/ncc) from 0.44.0 to 0.45.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1610](https://redirect.github.com/actions/setup-node/pull/1610) - Upgrade pnpm/action-setup from 6.0.9 to 6.0.10 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1602](https://redirect.github.com/actions/setup-node/pull/1602) - Upgrade baseline-browser-mapping from 2.10.38 to 2.11.22 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1629](https://redirect.github.com/actions/setup-node/pull/1629) - Upgrade [@​types/semver](https://redirect.github.com/types/semver) from 7.7.1 to 7.8.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1616](https://redirect.github.com/actions/setup-node/pull/1616) - Upgrade [@​typescript-eslint/parser](https://redirect.github.com/typescript-eslint/parser) from 8.62.0 to 8.70.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1615](https://redirect.github.com/actions/setup-node/pull/1615) - Upgrade pnpm/action-setup from 6.0.10 to 6.1.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1631](https://redirect.github.com/actions/setup-node/pull/1631) - Upgrade jest-each from 30.4.1 to 30.5.2 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1636](https://redirect.github.com/actions/setup-node/pull/1636) - Upgrade eslint-plugin-n from 18.1.0 to 18.3.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1638](https://redirect.github.com/actions/setup-node/pull/1638) - Update dependencies and add license files by [@​v-HarithaVattikuti](https://redirect.github.com/v-HarithaVattikuti) in [#​1642](https://redirect.github.com/actions/setup-node/pull/1642) - Upgrade eslint from 10.5.0 to 10.11.0 and package.json from 7.0.0 to 7.1.0 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1635](https://redirect.github.com/actions/setup-node/pull/1635) #### New Contributors - [@​v-jitenderpalsingh](https://redirect.github.com/v-jitenderpalsingh) made their first contribution in [#​1580](https://redirect.github.com/actions/setup-node/pull/1580) - [@​philip-gai](https://redirect.github.com/philip-gai) made their first contribution in [#​1584](https://redirect.github.com/actions/setup-node/pull/1584) - [@​gperdomor](https://redirect.github.com/gperdomor) made their first contribution in [#​1421](https://redirect.github.com/actions/setup-node/pull/1421) - [@​SubJunk](https://redirect.github.com/SubJunk) made their first contribution in [#​1603](https://redirect.github.com/actions/setup-node/pull/1603) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/publish-npm/action.yml | 2 +- .github/workflows/actions/build-angular-server/action.yml | 2 +- .github/workflows/actions/build-angular/action.yml | 2 +- .../workflows/actions/build-core-stencil-prerelease/action.yml | 2 +- .github/workflows/actions/build-core/action.yml | 2 +- .github/workflows/actions/build-react-router/action.yml | 2 +- .github/workflows/actions/build-react/action.yml | 2 +- .github/workflows/actions/build-vue-router/action.yml | 2 +- .github/workflows/actions/build-vue/action.yml | 2 +- .github/workflows/actions/test-angular-e2e/action.yml | 2 +- .github/workflows/actions/test-angular-package/action.yml | 2 +- .github/workflows/actions/test-core-clean-build/action.yml | 2 +- .github/workflows/actions/test-core-lint/action.yml | 2 +- .github/workflows/actions/test-core-screenshot/action.yml | 2 +- .github/workflows/actions/test-core-spec/action.yml | 2 +- .github/workflows/actions/test-react-e2e/action.yml | 2 +- .github/workflows/actions/test-react-router-e2e/action.yml | 2 +- .github/workflows/actions/test-vue-e2e/action.yml | 2 +- .../workflows/actions/update-reference-screenshots/action.yml | 2 +- .github/workflows/migrate.yml | 2 +- .github/workflows/release-migrate.yml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/actions/publish-npm/action.yml b/.github/actions/publish-npm/action.yml index 6913b493ef1..cce9c2c235a 100644 --- a/.github/actions/publish-npm/action.yml +++ b/.github/actions/publish-npm/action.yml @@ -22,7 +22,7 @@ runs: using: 'composite' steps: - name: 🟢 Configure Node for Publish - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ inputs.node-version }} registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index 70c65316883..283a9db0244 100644 --- a/.github/workflows/actions/build-angular-server/action.yml +++ b/.github/workflows/actions/build-angular-server/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Angular Server' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-angular/action.yml b/.github/workflows/actions/build-angular/action.yml index 121fa6f7da9..7771cb0e879 100644 --- a/.github/workflows/actions/build-angular/action.yml +++ b/.github/workflows/actions/build-angular/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Angular' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index f1e33c8b55e..0bd78e4adfe 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -13,7 +13,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index 068eab5c471..d155d64d4f1 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index bb2cbb27eea..1ef7d916c85 100644 --- a/.github/workflows/actions/build-react-router/action.yml +++ b/.github/workflows/actions/build-react-router/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React Router' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index a17e45c5094..5e615935ec7 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue-router/action.yml b/.github/workflows/actions/build-vue-router/action.yml index 68972ef2324..bc068447da3 100644 --- a/.github/workflows/actions/build-vue-router/action.yml +++ b/.github/workflows/actions/build-vue-router/action.yml @@ -3,7 +3,7 @@ description: 'Builds Ionic Vue Router' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue/action.yml b/.github/workflows/actions/build-vue/action.yml index 22c731e99ad..69a55fb0727 100644 --- a/.github/workflows/actions/build-vue/action.yml +++ b/.github/workflows/actions/build-vue/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Vue' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-angular-e2e/action.yml b/.github/workflows/actions/test-angular-e2e/action.yml index 8a15bfb5037..d3cbf772ad4 100644 --- a/.github/workflows/actions/test-angular-e2e/action.yml +++ b/.github/workflows/actions/test-angular-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-angular-package/action.yml b/.github/workflows/actions/test-angular-package/action.yml index 4f6181a2115..296a34baa56 100644 --- a/.github/workflows/actions/test-angular-package/action.yml +++ b/.github/workflows/actions/test-angular-package/action.yml @@ -3,7 +3,7 @@ description: 'Test Ionic Angular Package' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-clean-build/action.yml b/.github/workflows/actions/test-core-clean-build/action.yml index f6275acbee5..169bca12c5c 100644 --- a/.github/workflows/actions/test-core-clean-build/action.yml +++ b/.github/workflows/actions/test-core-clean-build/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Clean Build' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x diff --git a/.github/workflows/actions/test-core-lint/action.yml b/.github/workflows/actions/test-core-lint/action.yml index 66a3036ef04..d9435780f44 100644 --- a/.github/workflows/actions/test-core-lint/action.yml +++ b/.github/workflows/actions/test-core-lint/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Lint' runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/test-core-screenshot/action.yml b/.github/workflows/actions/test-core-screenshot/action.yml index 379ce934edc..e249df7a3e4 100644 --- a/.github/workflows/actions/test-core-screenshot/action.yml +++ b/.github/workflows/actions/test-core-screenshot/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-spec/action.yml b/.github/workflows/actions/test-core-spec/action.yml index f33ab0aa3e3..e2d55586215 100644 --- a/.github/workflows/actions/test-core-spec/action.yml +++ b/.github/workflows/actions/test-core-spec/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/test-react-e2e/action.yml b/.github/workflows/actions/test-react-e2e/action.yml index af6100bdb07..5c1e96e4f88 100644 --- a/.github/workflows/actions/test-react-e2e/action.yml +++ b/.github/workflows/actions/test-react-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml index beea150b549..13e6ef48ebe 100644 --- a/.github/workflows/actions/test-react-router-e2e/action.yml +++ b/.github/workflows/actions/test-react-router-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-vue-e2e/action.yml b/.github/workflows/actions/test-vue-e2e/action.yml index 2316e019a46..c57ba40064f 100644 --- a/.github/workflows/actions/test-vue-e2e/action.yml +++ b/.github/workflows/actions/test-vue-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/update-reference-screenshots/action.yml b/.github/workflows/actions/update-reference-screenshots/action.yml index 844f4e6d2ac..8f6f4f48ce8 100644 --- a/.github/workflows/actions/update-reference-screenshots/action.yml +++ b/.github/workflows/actions/update-reference-screenshots/action.yml @@ -7,7 +7,7 @@ on: runs: using: 'composite' steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index 54990664544..5cd5df3ecb4 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -64,7 +64,7 @@ jobs: working-directory: ./packages/migrate steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x cache: 'npm' diff --git a/.github/workflows/release-migrate.yml b/.github/workflows/release-migrate.yml index d119e6830de..0c083240609 100644 --- a/.github/workflows/release-migrate.yml +++ b/.github/workflows/release-migrate.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🟢 Configure Node for Publish - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.x registry-url: 'https://registry.npmjs.org' From 14676828ca29b16e5cfc4c5a823baa4722ee9062 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:53:59 +0000 Subject: [PATCH 20/53] chore(deps-dev): bump form-data from 4.0.1 to 4.0.6 in /packages/angular/test/apps/ng19 (#31551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [form-data](https://github.com/form-data/form-data) from 4.0.1 to 4.0.6.
Changelog

Sourced from form-data's changelog.

v4.0.6 - 2026-06-12

Commits

  • [Fix] escape CR, LF, and " in field names and filenames 8dff42c
  • [Dev Deps] update @ljharb/eslint-config, auto-changelog, tape f31d21e
  • [Deps] update hasown, mime-types 92ae0eb
  • [Dev Deps] update js-randomness-predictor 67b0f65

v4.0.5 - 2025-11-17

Commits

  • [Tests] Switch to newer v8 prediction library; enable node 24 testing 16e0076
  • [Dev Deps] update @ljharb/eslint-config, eslint 5822467
  • [Fix] set Symbol.toStringTag in the proper place 76d0dee

v4.0.4 - 2025-07-16

Commits

  • [meta] add auto-changelog 811f682
  • [Tests] handle predict-v8-randomness failures in node < 17 and node > 23 1d11a76
  • [Fix] Switch to using crypto random for boundary values 3d17230
  • [Tests] fix linting errors 5e34080
  • [meta] actually ensure the readme backup isn’t published 316c82b
  • [Dev Deps] update @ljharb/eslint-config 58c25d7
  • [meta] fix readme capitalization 2300ca1

v4.0.3 - 2025-06-05

Fixed

Commits

  • [eslint] use a shared config 426ba9a
  • [eslint] fix some spacing issues 2094191
  • [Refactor] use hasown 81ab41b
  • [Fix] validate boundary type in setBoundary() method 8d8e469
  • [Tests] add tests to check the behavior of getBoundary with non-strings 837b8a1
  • [Dev Deps] remove unused deps 870e4e6
  • [meta] remove local commit hooks e6e83cc
  • [Dev Deps] update eslint 4066fd6
  • [meta] fix scripts to use prepublishOnly c4bbb13

v4.0.2 - 2025-02-14

Merged

... (truncated)

Commits
  • 64190db v4.0.6
  • 92ae0eb [Deps] update hasown, mime-types
  • f31d21e [Dev Deps] update @ljharb/eslint-config, auto-changelog, tape
  • 8dff42c [Fix] escape CR, LF, and " in field names and filenames
  • 67b0f65 [Dev Deps] update js-randomness-predictor
  • 68ff7dd v4.0.5
  • 5822467 [Dev Deps] update @ljharb/eslint-config, eslint
  • 76d0dee [Fix] set Symbol.toStringTag in the proper place
  • 16e0076 [Tests] Switch to newer v8 prediction library; enable node 24 testing
  • 41996f5 v4.0.4
  • Additional commits viewable in compare view
Install script changes

This version modifies prepublish script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=form-data&package-manager=npm_and_yarn&previous-version=4.0.1&new-version=4.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ionic-team/ionic-framework/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../angular/test/apps/ng19/package-lock.json | 149 ++++++++++++++---- 1 file changed, 117 insertions(+), 32 deletions(-) diff --git a/packages/angular/test/apps/ng19/package-lock.json b/packages/angular/test/apps/ng19/package-lock.json index 63feeda6270..3a6da3ae346 100644 --- a/packages/angular/test/apps/ng19/package-lock.json +++ b/packages/angular/test/apps/ng19/package-lock.json @@ -7779,6 +7779,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -8869,6 +8882,20 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9044,13 +9071,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -9071,6 +9095,34 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", @@ -9769,15 +9821,17 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -9892,16 +9946,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9910,6 +9969,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -10006,12 +10078,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10058,10 +10130,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10070,11 +10142,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -10083,9 +10159,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11774,6 +11850,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", From 2e29ec28599e175ea0db5114e55428f8f1bd6a55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:54:49 +0000 Subject: [PATCH 21/53] chore(deps-dev): bump handlebars from 4.7.7 to 4.7.10 (#31549) Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.7 to 4.7.10.
Release notes

Sourced from handlebars's releases.

v4.7.10

  • security: Sanitize the source map URL when minifying - f37c599
  • security: Iterate lazily in #each and strip whitespace in linear time - 812c226
  • security: Escape <!-- and <script in precompiled output - 609d1b1 GHSA-xw65-4hp5-5hc7
  • security: Don't trust special properties on context data - ceec388 GHSA-p8wg-vrv2-v86f
  • security: Only compile partials that are template strings - c28ee7a
  • security: Only dispatch known node types in the Compiler and Visitor - 7d501a5
  • security: Validate AST values in the compiler instead of the parser - 703fdcc GHSA-8r5x-fm3f-whwj
  • Clarify that --root does not restrict filesystem access - 0fcf25c
  • Bump minimist to ^1.2.8 - ea8ed82
  • Fix Ruby component publishing documentation - d069c1c
  • Fix Composer component definition - 6714e07

Compatibility notes:

  • {{#each}} iterates iterables such as Map, Set and generators lazily, like for...of, instead of copying them into an array first. Values added to the iterable while the block renders are now visited too.

Commits

v4.7.9

Commits

v4.7.8

  • Make library compatible with workers (#1894) - 3d3796c
  • Don't rely on Node.js global object (#1776) - 2954e7e
  • Fix compiling of each block params in strict mode (#1855) - 30dbf04
  • Fix rollup warning when importing Handlebars as ESM - 03d387b
  • Fix bundler issue with webpack 5 (#1862) - c6c6bbb
  • Use https instead of git for mustache submodule - 88ac068

Commits

Changelog

Sourced from handlebars's changelog.

v4.7.10 - October 5th, 2026

  • security: Sanitize the source map URL when minifying - f37c599
  • security: Iterate lazily in #each and strip whitespace in linear time - 812c226
  • security: Escape <!-- and <script in precompiled output - 609d1b1 GHSA-xw65-4hp5-5hc7
  • security: Don't trust special properties on context data - ceec388 GHSA-p8wg-vrv2-v86f
  • security: Only compile partials that are template strings - c28ee7a
  • security: Only dispatch known node types in the Compiler and Visitor - 7d501a5
  • security: Validate AST values in the compiler instead of the parser - 703fdcc GHSA-8r5x-fm3f-whwj
  • Clarify that --root does not restrict filesystem access - 0fcf25c
  • Bump minimist to ^1.2.8 - ea8ed82
  • Fix Ruby component publishing documentation - d069c1c
  • Fix Composer component definition - 6714e07

Compatibility notes:

  • {{#each}} iterates iterables such as Map, Set and generators lazily, like for...of, instead of copying them into an array first. Values added to the iterable while the block renders are now visited too.

Commits

v4.7.9 - March 26th, 2026

  • fix: enable shell mode for spawn to resolve Windows EINVAL issue - e0137c2
  • fix type "RuntimeOptions" also accepting string partials - eab1d14
  • feat(types): set hash to be a Record<string, any> - de4414d
  • fix non-contiguous program indices - 4512766
  • refactor: rename i to startPartIndex - e497a35
  • security: fix security issues - 68d8df5

Commits

v4.7.8 - July 27th, 2023

  • Make library compatible with workers (#1894) - 3d3796c
  • Don't rely on Node.js global object (#1776) - 2954e7e
  • Fix compiling of each block params in strict mode (#1855) - 30dbf04
  • Fix rollup warning when importing Handlebars as ESM - 03d387b
  • Fix bundler issue with webpack 5 (#1862) - c6c6bbb
  • Use https instead of git for mustache submodule - 88ac068

Commits

Commits
  • 45ce152 v4.7.10
  • e47b236 Update release notes
  • f37c599 Sanitize the source map URL when minifying
  • 812c226 Iterate lazily in #each and strip whitespace in linear time
  • 609d1b1 Escape <!-- and <script in precompiled output
  • ceec388 Don't trust special properties on context data
  • c28ee7a Only compile partials that are template strings
  • 7d501a5 Only dispatch known node types in the Compiler and Visitor
  • 703fdcc Validate AST values in the compiler instead of the parser
  • 0fcf25c Clarify that --root does not restrict filesystem access
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by jaylinski, a new releaser for handlebars since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=handlebars&package-manager=npm_and_yarn&previous-version=4.7.7&new-version=4.7.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ionic-team/ionic-framework/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 39 ++++++++++++++++++-------------- packages/react/package-lock.json | 8 +++---- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index d071e527ba9..8ec921c6bb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3653,13 +3653,14 @@ "dev": true }, "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, + "license": "MIT", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", + "minimist": "^1.2.8", + "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, @@ -5015,10 +5016,14 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minimist-options": { "version": "4.1.0", @@ -10689,13 +10694,13 @@ "dev": true }, "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", + "minimist": "^1.2.8", + "neo-async": "^2.6.2", "source-map": "^0.6.1", "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" @@ -11735,9 +11740,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "minimist-options": { diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index d5747ea6984..bbe3a67414b 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -4620,13 +4620,13 @@ "dev": true }, "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" From d0afd1e012b35b684b0471db1e739017c2e1a32a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:55:19 +0000 Subject: [PATCH 22/53] chore(deps-dev): bump webpack from 5.61.0 to 5.111.1 in /packages/angular/test/apps/ng19 (#31548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.111.1.
Release notes

Sourced from webpack's releases.

v5.111.1

Patch Changes

... (truncated)

Changelog

Sourced from webpack's changelog.

5.111.1

Patch Changes

... (truncated)

Commits
  • 5a10675 chore(release): new release (#22105)
  • 9c448c4 test: consolidate loose harness files under test/harness (#22163)
  • a947604 fix(html): unquote an attribute value carrying a vertical tab (#22162)
  • db556f3 test: run the printer equivalence suite against Gecko in CI (#22155)
  • d174cdf perf(css): drop a fallback every browserslist target reads past (#22158)
  • 39f14f2 refactor(javascript): declare block bindings from the parse (#22161)
  • 0835411 test: move suite drivers into test/templates (#22159)
  • b48cd77 fix(html): minify by what a value says rather than how it was spelled (#22154)
  • a774f10 test: define the webpack major instead of bundling package.json (#22160)
  • 4b2eed0 docs: report only what needs a decision while watching a PR (#22156)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for webpack since your current version.

Install script changes

This version modifies prepare script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.61.0&new-version=5.111.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ionic-team/ionic-framework/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../angular/test/apps/ng19/package-lock.json | 706 +++++++----------- packages/angular/test/apps/ng19/package.json | 2 +- 2 files changed, 290 insertions(+), 418 deletions(-) diff --git a/packages/angular/test/apps/ng19/package-lock.json b/packages/angular/test/apps/ng19/package-lock.json index 3a6da3ae346..731a58c8eb4 100644 --- a/packages/angular/test/apps/ng19/package-lock.json +++ b/packages/angular/test/apps/ng19/package-lock.json @@ -51,7 +51,7 @@ "ts-node": "^8.3.0", "typescript": "^5.6.3", "wait-on": "^8.0.1", - "webpack": "^5.61.0", + "webpack": "^5.111.1", "webpack-cli": "^4.9.2" }, "engines": { @@ -270,167 +270,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -4117,9 +3956,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -6725,73 +6564,73 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -6799,9 +6638,9 @@ } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6809,79 +6648,79 @@ } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -6969,9 +6808,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6980,17 +6819,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "deprecated": "package has been renamed to acorn-import-attributes", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -7403,6 +7231,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -7556,9 +7397,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -7576,10 +7417,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -7802,9 +7644,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001684", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", - "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", "dev": true, "funding": [ { @@ -8141,6 +7983,13 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -8910,9 +8759,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.65", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz", - "integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==", + "version": "1.5.451", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.451.tgz", + "integrity": "sha512-vUNKPrUzw8ByEMOxloq0a71iD/mPuPxVkbDm3G5YhnxTzOw/I6qAg2MUS0cYj/c267lWmw26yW4Vv7foxTxPLg==", "dev": true, "license": "ISC" }, @@ -9089,9 +8938,9 @@ } }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -11039,13 +10888,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", @@ -12100,6 +11942,98 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.13.0.tgz", + "integrity": "sha512-OO1NF2zHj6sf4CNv2AsUWYcS8WEwkVKQDXxxKzlOxx1Xl+WlqYtVC+oNlGNwGkLAOnpj277BSHypWwRtd9w4JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -12530,11 +12464,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nopt": { "version": "7.2.1", @@ -14368,41 +14305,40 @@ "optional": true }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/select-hose": { @@ -15089,13 +15025,17 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -15266,13 +15206,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15816,9 +15749,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { @@ -15837,7 +15770,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -16464,36 +16397,29 @@ "optional": true }, "node_modules/webpack": { - "version": "5.61.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.61.0.tgz", - "integrity": "sha512-fPdTuaYZ/GMGFm4WrPi2KRCqS1vDp773kj9S0iI5Uc//5cszsFEDgHNaX4Rj1vobUiU1dFIV3mA9k1eHeluFpw==", + "version": "5.111.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.111.1.tgz", + "integrity": "sha512-cNypaz0RP+S4cvQVi1M/3bRUkvNP0xZpL0TjFx+c6jg64VbxD+2weWDgY9UnjRI6dhZgtaj8DU76GGFcJ79xPQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", + "enhanced-resolve": "^5.25.0", + "es-module-lexer": "^2.1.0", "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.4", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.0" + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "schema-utils": "^4.5.0", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" }, "bin": { "webpack": "bin/webpack.js" @@ -17001,9 +16927,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", + "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", "dev": true, "license": "MIT", "engines": { @@ -17033,101 +16959,47 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/webpack/node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "graceful-fs": "^4.1.2" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=10.13.0" } }, "node_modules/websocket-driver": { diff --git a/packages/angular/test/apps/ng19/package.json b/packages/angular/test/apps/ng19/package.json index c24dd67b4e2..a417471fb4d 100644 --- a/packages/angular/test/apps/ng19/package.json +++ b/packages/angular/test/apps/ng19/package.json @@ -59,7 +59,7 @@ "ts-node": "^8.3.0", "typescript": "^5.6.3", "wait-on": "^8.0.1", - "webpack": "^5.61.0", + "webpack": "^5.111.1", "webpack-cli": "^4.9.2" }, "overrides": { From 754ea433ad4609741586ffbeea96ce6135856ebd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:55:50 +0000 Subject: [PATCH 23/53] chore(deps): update dependency @capacitor/core to v8.5.3 (#31545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@capacitor/core](https://capacitorjs.com) ([source](https://redirect.github.com/ionic-team/capacitor)) | [`8.5.2` → `8.5.3`](https://renovatebot.com/diffs/npm/@capacitor%2fcore/8.5.2/8.5.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fcore/8.5.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fcore/8.5.2/8.5.3?slim=true) | --- ### Release Notes
ionic-team/capacitor (@​capacitor/core) ### [`v8.5.3`](https://redirect.github.com/ionic-team/capacitor/blob/HEAD/CHANGELOG.md#853-2026-10-07) [Compare Source](https://redirect.github.com/ionic-team/capacitor/compare/8.5.2...8.5.3) ##### Bug Fixes - **cli:** change Cordova AppDelegate.h imports for SPM ([#​8618](https://redirect.github.com/ionic-team/capacitor/issues/8618)) ([145560e](https://redirect.github.com/ionic-team/capacitor/commit/145560ee590d86637dc9dd2e2c9f4c08b0f631c0))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 97ed7e472fe..5d46ce7c9c1 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -633,9 +633,9 @@ "license": "MIT" }, "node_modules/@capacitor/core": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", - "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.3.tgz", + "integrity": "sha512-oBE1oGuYE4ZzYYrulC75OuHdSsD6zLUy7d5o1tFwDLG4govGoubY15cxpiI5l0+q87Lm5p0bhbehMdzJg4aFag==", "dev": true, "license": "MIT", "dependencies": { From 4ae01961e8ed65e26369e09d478fabeffba5ed6d Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 8 Oct 2026 18:13:46 +0000 Subject: [PATCH 24/53] fix(react-router): stop hidden views from re-rendering on every navigation (#31541) Issue number: resolves #31528 --------- ## What is the current behavior? Currently, every mounted view in an `IonRouterOutlet` re-renders on each navigation, hidden ones included. The view stack builds a new `RouteContext` value every time the outlet renders, which happens about 3 times per navigation, and `NavManager` does the same with `NavContext` and `IonRouterContext`. ## What is the new behavior? We now keep each view's `RouteContext` value until its content changes, and a view whose route doesn't match the current location keeps the value it last rendered with. Hidden pages using `useParams` no longer re-render, and the page being shown renders once when its match changes. The `NavManager` also only rebuilds its context values when `routeInfo` changes, so `useIonRouter` consumers render once per navigation instead of twice. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The held value lives in a component under each view instead of on the view item, because two outlets with the same `id` share a view item while sitting under different parent routes. Hidden pages using `useNavigate` or `useLocation` still render once per navigation, because React Router's `useNavigate` reads the location internally. Holding `LocationContext` would fix that, but `useLocation()` in a hidden page would then return a stale location, so I left that alone for now. # Current dev build ``` 9.0.7-dev.11791381676.118a7a15 ``` --- .../src/ReactRouter/IonRouter.tsx | 26 +--- .../src/ReactRouter/ReactRouterViewStack.tsx | 62 +++++++++- .../src/ReactRouter/utils/areParamsEqual.ts | 27 +++++ packages/react-router/test/base/src/App.tsx | 2 + .../react-router/test/base/src/pages/Main.tsx | 3 + .../hidden-view-renders/HiddenViewRenders.tsx | 114 ++++++++++++++++++ .../react-router/test/base/src/utils/index.ts | 1 + .../test/base/src/utils/renderCounts.ts | 5 + .../playwright/hidden-view-renders.spec.ts | 76 ++++++++++++ .../tests/e2e/playwright/utils/test-utils.ts | 25 ++++ packages/react/src/routing/NavManager.tsx | 25 +++- 11 files changed, 337 insertions(+), 29 deletions(-) create mode 100644 packages/react-router/src/ReactRouter/utils/areParamsEqual.ts create mode 100644 packages/react-router/test/base/src/pages/hidden-view-renders/HiddenViewRenders.tsx create mode 100644 packages/react-router/test/base/src/utils/renderCounts.ts create mode 100644 packages/react-router/test/base/tests/e2e/playwright/hidden-view-renders.spec.ts diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index c937f20e4a4..28980ad5dce 100644 --- a/packages/react-router/src/ReactRouter/IonRouter.tsx +++ b/packages/react-router/src/ReactRouter/IonRouter.tsx @@ -23,6 +23,8 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { IonRouteInner } from './IonRouteInner'; import { ReactRouterViewStack } from './ReactRouterViewStack'; import StackManager from './StackManager'; +import { areParamsEqual } from './utils/areParamsEqual'; +import type { RouteParams } from './utils/areParamsEqual'; // Use Location directly - state is typed as `unknown` in history v5 type HistoryLocation = Location; @@ -36,7 +38,6 @@ interface IonRouterProps { registerHistoryListener: (cb: (location: HistoryLocation, action: HistoryAction) => void) => void; } -type RouteParams = Record; type SafeRouteParams = Record; const filterUndefinedParams = (params: RouteParams): SafeRouteParams => { @@ -73,29 +74,6 @@ const checkIsMultiStepBack = ( return false; }; -const areParamsEqual = (a?: RouteParams, b?: RouteParams) => { - const paramsA = a || {}; - const paramsB = b || {}; - const keysA = Object.keys(paramsA); - const keysB = Object.keys(paramsB); - - if (keysA.length !== keysB.length) { - return false; - } - - return keysA.every((key) => { - const valueA = paramsA[key]; - const valueB = paramsB[key]; - if (Array.isArray(valueA) && Array.isArray(valueB)) { - if (valueA.length !== valueB.length) { - return false; - } - return valueA.every((entry, idx) => entry === valueB[idx]); - } - return valueA === valueB; - }); -}; - export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildren) => { const location = useLocation(); const navigate = useNavigate(); diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx index fb78cce7857..c9c72558087 100644 --- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx +++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx @@ -11,6 +11,7 @@ import React from 'react'; import type { PathMatch } from 'react-router'; import { Navigate, UNSAFE_RouteContext as RouteContext } from 'react-router-dom'; +import { areParamsEqual } from './utils/areParamsEqual'; import { analyzeRouteChildren, computeParentPath } from './utils/computeParentPath'; import { derivePathnameToMatch, matchPath } from './utils/pathMatching'; import { normalizePathnameForComparison } from './utils/pathNormalization'; @@ -218,6 +219,54 @@ const resolveIndexRouteMatch = ( return normalizedPathname === normalizedBase ? previousMatch : null; }; +const shallowEqual = (a: object, b: object): boolean => { + const aRecord = a as Record; + const bRecord = b as Record; + const aKeys = Object.keys(aRecord); + return aKeys.length === Object.keys(bRecord).length && aKeys.every((key) => aRecord[key] === bRecord[key]); +}; + +const isSameMatch = (a: RouteContextMatch, b: RouteContextMatch): boolean => + a === b || + (a.pathname === b.pathname && + a.pathnameBase === b.pathnameBase && + areParamsEqual(a.params, b.params) && + (a.route === b.route || shallowEqual(a.route, b.route))); + +/** + * The outlet rebuilds a view's `RouteContext` several times per navigation, so + * this compares by content to tell whether anything actually changed. + */ +const isSameRouteContext = ( + a: React.ContextType, + b: React.ContextType +): boolean => { + const { matches: aMatches, ...aRest } = a; + const { matches: bMatches, ...bRest } = b; + return ( + aMatches.length === bMatches.length && + shallowEqual(aRest, bRest) && + aMatches.every((match, i) => isSameMatch(match as RouteContextMatch, bMatches[i] as RouteContextMatch)) + ); +}; + +/** + * Reuses the last `RouteContext` while its content is the same or `hold` is set, + * so hidden pages that only call `useParams` don't re-render. It lives in + * component state because outlets with the same id share view items. + */ +const ViewRouteContextProvider: React.FC<{ + value: React.ContextType; + hold: boolean; + children: React.ReactNode; +}> = ({ value, hold, children }) => { + const [heldValue, setHeldValue] = React.useState(value); + if (!hold && heldValue !== value && !isSameRouteContext(heldValue, value)) { + setHeldValue(value); + } + return {children}; +}; + export class ReactRouterViewStack extends ViewStacks { /** * Stores the computed parent path for each outlet. @@ -452,6 +501,9 @@ export class ReactRouterViewStack extends ViewStacks { viewItem.routeData.match = match; } + // A more specific sibling owns the pathname, so this view is hidden even though it matches. + let hiddenBySpecificMatch = false; + // Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches. // This prevents "Not found" or fallback pages from showing alongside valid routes. if (routePath === '*' || routePath === '/*' || routePath === '') { @@ -511,6 +563,7 @@ export class ReactRouterViewStack extends ViewStacks { } if (hasSpecificMatch && !isSwipeRevealed(viewItem)) { + hiddenBySpecificMatch = true; // A splat can be the outlet's container page rather than a "not found" fallback. // Pushed over it is the page underneath, so unmounting it destroys its state and // leaves nothing for back to reveal. Hiding it below covers that. A view with no @@ -534,6 +587,11 @@ export class ReactRouterViewStack extends ViewStacks { viewItem.routeData.match = match; } const routeMatch = shouldSkipForDifferentParam ? viewItem.routeData?.match : match || viewItem.routeData?.match; + // Hold the context when a sibling owns the pathname, the view is parked on another param + // instance, or it doesn't match. A pathless default only strictly matches at "/", so no match + // there doesn't mean it's hidden. + const isPathlessDefault = !routePath && !viewItem.routeData?.childProps?.index; + const holdRouteContext = hiddenBySpecificMatch || !!shouldSkipForDifferentParam || (!match && !isPathlessDefault); return ( @@ -581,7 +639,9 @@ export class ReactRouterViewStack extends ViewStacks { mount={viewItem.mount} removeView={() => this.remove(viewItem)} > - {componentElement} + + {componentElement} + ); }} diff --git a/packages/react-router/src/ReactRouter/utils/areParamsEqual.ts b/packages/react-router/src/ReactRouter/utils/areParamsEqual.ts new file mode 100644 index 00000000000..030e5a47d17 --- /dev/null +++ b/packages/react-router/src/ReactRouter/utils/areParamsEqual.ts @@ -0,0 +1,27 @@ +export type RouteParams = Record; + +/** + * Compares two route param objects by value, treating a missing object as empty. + */ +export const areParamsEqual = (a?: RouteParams, b?: RouteParams): boolean => { + const paramsA = a || {}; + const paramsB = b || {}; + const keysA = Object.keys(paramsA); + const keysB = Object.keys(paramsB); + + if (keysA.length !== keysB.length) { + return false; + } + + return keysA.every((key) => { + const valueA = paramsA[key]; + const valueB = paramsB[key]; + if (Array.isArray(valueA) && Array.isArray(valueB)) { + if (valueA.length !== valueB.length) { + return false; + } + return valueA.every((entry, idx) => entry === valueB[idx]); + } + return valueA === valueB; + }); +}; diff --git a/packages/react-router/test/base/src/App.tsx b/packages/react-router/test/base/src/App.tsx index 64dfdffb265..f130c119338 100644 --- a/packages/react-router/test/base/src/App.tsx +++ b/packages/react-router/test/base/src/App.tsx @@ -48,6 +48,7 @@ import DirectionNoneBack from './pages/direction-none-back/DirectionNoneBack'; import TabSearchParams from './pages/tab-search-params/TabSearchParams'; import { Step1, Step2, Step3, Step4 } from './pages/replace-params/ReplaceParams'; import { ParamSwipeBack, ParamSwipeBackB } from './pages/param-swipe-back/ParamSwipeBack'; +import HiddenViewRenders from './pages/hidden-view-renders/HiddenViewRenders'; import TabLifecycle from './pages/tab-lifecycle/TabLifecycle'; import TabLifecycleOutside from './pages/tab-lifecycle/TabLifecycleOutside'; import { @@ -121,6 +122,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/react-router/test/base/src/pages/Main.tsx b/packages/react-router/test/base/src/pages/Main.tsx index f23ceb89a02..f6c14236275 100644 --- a/packages/react-router/test/base/src/pages/Main.tsx +++ b/packages/react-router/test/base/src/pages/Main.tsx @@ -94,6 +94,9 @@ const Main: React.FC = () => { Tab Lifecycle + + Hidden View Renders + diff --git a/packages/react-router/test/base/src/pages/hidden-view-renders/HiddenViewRenders.tsx b/packages/react-router/test/base/src/pages/hidden-view-renders/HiddenViewRenders.tsx new file mode 100644 index 00000000000..494969512f2 --- /dev/null +++ b/packages/react-router/test/base/src/pages/hidden-view-renders/HiddenViewRenders.tsx @@ -0,0 +1,114 @@ +import { + IonTabs, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonLabel, + IonPage, + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonButton, + useIonRouter, +} from '@ionic/react'; +import React, { useEffect } from 'react'; +import { Route, Navigate, useParams } from 'react-router-dom'; + +import TestDescription from '../../components/TestDescription'; +import { countRender } from '../../utils'; + +/** + * Counts committed renders. An effect with no dependency list runs once per + * commit, so a render that bails out isn't counted. + */ +const useCommitCounter = (id: string) => { + useEffect(() => countRender(id)); +}; + +const ParamsReader: React.FC<{ tab: string }> = ({ tab }) => { + useParams(); + useCommitCounter(`${tab}:params`); + return null; +}; + +const IonRouterReader: React.FC<{ tab: string }> = ({ tab }) => { + useIonRouter(); + useCommitCounter(`${tab}:ionRouter`); + return null; +}; + +const TabPage: React.FC<{ tab: string }> = ({ tab }) => { + return ( + + + + Tab {tab.toUpperCase()} + + + + + + + Switch between Tabs A, B and C a few times. Hidden pages that read useParams should not re-render, and ones + that read useIonRouter should re-render once per switch. Counts are in window.renderCounts. + + + + ); +}; + +const SplatPage: React.FC = () => { + const params = useParams(); + useCommitCounter('d:params'); + + return ( + + + + Tab D + + + +
{params['*']}
+ + Go to D Two + + + Tap Go to D Two. The splat param above should change to "two", and window.renderCounts['d:params'] should go + up by 1. + +
+
+ ); +}; + +const HiddenViewRenders: React.FC = () => { + return ( + + + } /> + } /> + } /> + } /> + } /> + + + + Tab A + + + Tab B + + + Tab C + + + Tab D + + + + ); +}; + +export default HiddenViewRenders; diff --git a/packages/react-router/test/base/src/utils/index.ts b/packages/react-router/test/base/src/utils/index.ts index 45db8939265..94b5a23cc04 100644 --- a/packages/react-router/test/base/src/utils/index.ts +++ b/packages/react-router/test/base/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './generateId'; export * from './lifecycleEvents'; export * from './dev'; +export * from './renderCounts'; diff --git a/packages/react-router/test/base/src/utils/renderCounts.ts b/packages/react-router/test/base/src/utils/renderCounts.ts new file mode 100644 index 00000000000..fc932c673ad --- /dev/null +++ b/packages/react-router/test/base/src/utils/renderCounts.ts @@ -0,0 +1,5 @@ +/** Records a committed render under `id` on `window.renderCounts` for a spec to assert on. */ +export const countRender = (id: string) => { + (window as any).renderCounts = (window as any).renderCounts || {}; + (window as any).renderCounts[id] = ((window as any).renderCounts[id] ?? 0) + 1; +}; diff --git a/packages/react-router/test/base/tests/e2e/playwright/hidden-view-renders.spec.ts b/packages/react-router/test/base/tests/e2e/playwright/hidden-view-renders.spec.ts new file mode 100644 index 00000000000..7a4aea9061d --- /dev/null +++ b/packages/react-router/test/base/tests/e2e/playwright/hidden-view-renders.spec.ts @@ -0,0 +1,76 @@ +import { test, expect, type Page } from '@playwright/test'; +import { ionPageVisible, ionTabClick, settledRenderCounts, withTestingMode } from './utils/test-utils'; + +const showTab = async (page: Page, tab: 'b' | 'c') => { + await ionTabClick(page, `Tab ${tab.toUpperCase()}`); + await ionPageVisible(page, `hidden-view-renders-${tab}`); +}; + +/** + * Visits Tabs A, B and C, then switches to B and C again. Returns the render + * counts from before and after that second round. + */ +const switchTabsAfterVisitingAll = async (page: Page) => { + await page.goto(withTestingMode('/hidden-view-renders/a')); + await ionPageVisible(page, 'hidden-view-renders-a'); + await showTab(page, 'b'); + await showTab(page, 'c'); + + const before = await settledRenderCounts(page); + + await showTab(page, 'b'); + await showTab(page, 'c'); + + const after = await settledRenderCounts(page); + + return { before, after }; +}; + +test.describe('Hidden View Renders', () => { + test('should not re-render pages that read useParams when switching tabs', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31528', + }); + + const { before, after } = await switchTabsAfterVisitingAll(page); + + // Tab A stayed hidden for both switches. + expect(after['a:params']).toBe(before['a:params']); + // B and C were shown again at the same pathname. + expect(after['b:params']).toBe(before['b:params']); + expect(after['c:params']).toBe(before['c:params']); + }); + + test('should re-render a hidden page that reads useIonRouter once per navigation', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31528', + }); + + const { before, after } = await switchTabsAfterVisitingAll(page); + + // Since `useIonRouter` exposes the live `routeInfo`, it re-renders once per switch. + expect(after['a:ionRouter'] - before['a:ionRouter']).toBe(2); + }); + + test('should render the shown page once when its route match changes', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31528', + }); + + await page.goto(withTestingMode('/hidden-view-renders/d/one')); + await ionPageVisible(page, 'hidden-view-renders-d'); + await expect(page.locator('#splat-param')).toHaveText('one'); + + const before = await settledRenderCounts(page); + + await page.locator('#go-to-d-two').click(); + await expect(page.locator('#splat-param')).toHaveText('two'); + + const after = await settledRenderCounts(page); + + expect(after['d:params'] - before['d:params']).toBe(1); + }); +}); diff --git a/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts index 7efd5018827..b5fe583d139 100644 --- a/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts +++ b/packages/react-router/test/base/tests/e2e/playwright/utils/test-utils.ts @@ -52,6 +52,31 @@ export async function settledLifecycleEvents(page: Page): Promise { return current; } +/** + * Read `window.renderCounts` once two consecutive reads match, so a render that + * commits late is still counted. + */ +export async function settledRenderCounts(page: Page): Promise> { + let previous: string | undefined; + let current: Record = {}; + + try { + await expect + .poll(async () => { + current = await page.evaluate(() => ({ ...((window as any).renderCounts ?? {}) })); + const serialized = JSON.stringify(current); + const settled = previous !== undefined && serialized === previous; + previous = serialized; + return settled; + }) + .toBe(true); + } catch (error) { + throw new Error(`Render counts never settled. Last read: ${JSON.stringify(current)}`, { cause: error }); + } + + return current; +} + let peakCounterId = 0; /** diff --git a/packages/react/src/routing/NavManager.tsx b/packages/react/src/routing/NavManager.tsx index 9144992b5fe..1f2692c0e24 100644 --- a/packages/react/src/routing/NavManager.tsx +++ b/packages/react/src/routing/NavManager.tsx @@ -120,12 +120,29 @@ export class NavManager extends React.PureComponent