Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## [8.8.12](https://github.com/ionic-team/ionic-framework/compare/v8.8.11...v8.8.12) (2026-06-24)


### Bug Fixes

* **angular:** honor modifier-click on routerLink ([#31230](https://github.com/ionic-team/ionic-framework/issues/31230)) ([2ac9851](https://github.com/ionic-team/ionic-framework/commit/2ac98512c78f6d47e3a6a17ff0159047ba14a5cd)), closes [#26394](https://github.com/ionic-team/ionic-framework/issues/26394)
* **angular:** run change detection on attached overlay views ([#31235](https://github.com/ionic-team/ionic-framework/issues/31235)) ([8e76fd0](https://github.com/ionic-team/ionic-framework/commit/8e76fd0cceeb878ad4b071515acb593892512111)), closes [#31220](https://github.com/ionic-team/ionic-framework/issues/31220)





## [8.8.11](https://github.com/ionic-team/ionic-framework/compare/v8.8.10...v8.8.11) (2026-06-17)


Expand Down
8 changes: 8 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## [8.8.12](https://github.com/ionic-team/ionic-framework/compare/v8.8.11...v8.8.12) (2026-06-24)

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





## [8.8.11](https://github.com/ionic-team/ionic-framework/compare/v8.8.10...v8.8.11) (2026-06-17)


Expand Down
10 changes: 5 additions & 5 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "8.8.11",
"version": "8.8.12",
"description": "Base components for Ionic",
"engines": {
"node": ">= 16"
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"core",
"packages/*"
],
"version": "8.8.11"
"version": "8.8.12"
}
8 changes: 8 additions & 0 deletions packages/angular-server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## [8.8.12](https://github.com/ionic-team/ionic-framework/compare/v8.8.11...v8.8.12) (2026-06-24)

**Note:** Version bump only for package @ionic/angular-server





## [8.8.11](https://github.com/ionic-team/ionic-framework/compare/v8.8.10...v8.8.11) (2026-06-17)

**Note:** Version bump only for package @ionic/angular-server
Expand Down
18 changes: 9 additions & 9 deletions packages/angular-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/angular-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "8.8.11",
"version": "8.8.12",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
Expand Down Expand Up @@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^8.8.11"
"@ionic/core": "^8.8.12"
}
}
12 changes: 12 additions & 0 deletions packages/angular/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

## [8.8.12](https://github.com/ionic-team/ionic-framework/compare/v8.8.11...v8.8.12) (2026-06-24)


### Bug Fixes

* **angular:** honor modifier-click on routerLink ([#31230](https://github.com/ionic-team/ionic-framework/issues/31230)) ([2ac9851](https://github.com/ionic-team/ionic-framework/commit/2ac98512c78f6d47e3a6a17ff0159047ba14a5cd)), closes [#26394](https://github.com/ionic-team/ionic-framework/issues/26394)
* **angular:** run change detection on attached overlay views ([#31235](https://github.com/ionic-team/ionic-framework/issues/31235)) ([8e76fd0](https://github.com/ionic-team/ionic-framework/commit/8e76fd0cceeb878ad4b071515acb593892512111)), closes [#31220](https://github.com/ionic-team/ionic-framework/issues/31220)





## [8.8.11](https://github.com/ionic-team/ionic-framework/compare/v8.8.10...v8.8.11) (2026-06-17)

**Note:** Version bump only for package @ionic/angular
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LocationStrategy } from '@angular/common';
import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
import { ElementRef, OnChanges, OnDestroy, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import type { AnimationBuilder, RouterDirection } from '@ionic/core/components';

Expand All @@ -14,7 +14,7 @@ import { NavController } from '../../providers/nav-controller';
@Directive({
selector: ':not(a):not(area)[routerLink]',
})
export class RouterLinkDelegateDirective implements OnInit, OnChanges {
export class RouterLinkDelegateDirective implements OnInit, OnChanges, OnDestroy {
@Input()
routerDirection: RouterDirection = 'forward';

Expand All @@ -32,12 +32,51 @@ export class RouterLinkDelegateDirective implements OnInit, OnChanges {
ngOnInit(): void {
this.updateTargetUrlAndHref();
this.updateTabindex();

/**
* Ionic components like `ion-item` render a native anchor in their shadow DOM,
* so a modifier click (ctrl/meta/shift/alt) or a non-`_self` target should let
* the browser handle the navigation natively (new tab, new window, download)
* instead of navigating in-app.
*
* We listen in the capture phase so this runs before Angular's `RouterLink`
* handler and our own bubble-phase `onClick`. On a native-navigation intent it
* stops propagation to cancel the in-app navigation, but leaves `preventDefault`
* alone so the native anchor can still act.
*/
this.elementRef.nativeElement.addEventListener('click', this.onCaptureClick, { capture: true });
}

ngOnChanges(): void {
this.updateTargetUrlAndHref();
}

ngOnDestroy(): void {
this.elementRef.nativeElement.removeEventListener('click', this.onCaptureClick, { capture: true });
}

private onCaptureClick = (ev: Event): void => {
if (this.opensNatively(ev)) {
ev.stopImmediatePropagation();
}
};

/**
* True when the browser should handle the click natively instead of routing
* in-app: a modifier was held (ctrl/meta/shift/alt), or the host targets
* something other than `_self`. This mirrors the modifier set Angular's own
* `RouterLink` guards on, so an Ionic `routerLink` behaves like a plain anchor
* for new-tab, new-window, and download intents.
*/
private opensNatively(ev: Event): boolean {
if (ev instanceof MouseEvent && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) {
return true;
}

const target = this.elementRef.nativeElement.target;
return target != null && target !== '' && target !== '_self';
}

/**
* The `tabindex` is set to `0` by default on the host element when
* the `routerLink` directive is used. This causes issues with Ionic
Expand Down
8 changes: 8 additions & 0 deletions packages/angular/common/src/providers/angular-delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ export const attachView = (

applicationRef.attachView(componentRef.hostView);

/**
* Run change detection so template bindings (e.g. `<ion-nav [root]="rootPage">`)
* apply during this synchronous pass, before the web component runs its load
* lifecycle. `createComponent` only runs the creation pass, so without this the
* binding lands after the element has loaded, too late for `ion-nav` to read it.
*/
componentRef.changeDetectorRef.detectChanges();

elRefMap.set(hostElement, componentRef);
elEventsMap.set(hostElement, unbindEvents);
return hostElement;
Expand Down
12 changes: 6 additions & 6 deletions packages/angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "8.8.11",
"version": "8.8.12",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
Expand Down Expand Up @@ -48,7 +48,7 @@
}
},
"dependencies": {
"@ionic/core": "^8.8.11",
"@ionic/core": "^8.8.12",
"ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from '@playwright/test';

test.describe('Nav: root binding inside a modal (standalone)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/standalone/nav-modal-root');
});

test('should render the nav root when bound from componentProps', async ({ page }) => {
await page.locator('#open-nav-modal').click();

await expect(page.locator('ion-modal')).toBeVisible();
await expect(page.locator('ion-modal ion-nav')).toBeVisible();
await expect(page.locator('#nav-modal-root-content')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test, expect, type Page } from '@playwright/test';

/**
* Issue #26394: a modifier click (ctrl/meta/shift/alt) or a non-`_self` target on
* a `routerLink` over a non-anchor Ionic component (ion-item, ion-button) must let
* the browser handle the navigation natively (new tab, window, download) instead
* of navigating the current page in-app.
*
* Playwright headless can't dispatch a real modifier-click or observe the
* browser's native new-tab default, so we dispatch a synthetic click on the
* host (not the shadow anchor, which would fire its own default navigation) and
* assert the JS handler chain leaves the page unchanged with `preventDefault`
* uncalled.
*/
test.describe('RouterLink: modifier click', () => {
const dispatchClick = (page: Page, selector: string, modifiers: Record<string, boolean>) =>
page.evaluate(
({ selector, mods }: { selector: string; mods: Record<string, boolean> }) => {
const item = document.querySelector(selector) as HTMLElement;
const ev = new MouseEvent('click', { bubbles: true, composed: true, cancelable: true, button: 0, ...mods });
item.dispatchEvent(ev);
return { defaultPrevented: ev.defaultPrevented };
},
{ selector, mods: modifiers }
);

test.beforeEach(async ({ page }) => {
await page.goto('/standalone/router-link');
});

test('normal click navigates the current page in-app', async ({ page }) => {
const { defaultPrevented } = await dispatchClick(page, '#modifier-item', {});

await expect(page).toHaveURL(/.*\/standalone\/popover/);
// The delegate prevents the default to stop a hard page reload.
expect(defaultPrevented).toBe(true);
});

for (const modifier of ['ctrlKey', 'metaKey', 'shiftKey', 'altKey']) {
test(`${modifier}+click does not navigate the current page and allows native handling`, async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/26394',
});

const { defaultPrevented } = await dispatchClick(page, '#modifier-item', { [modifier]: true });

// Give any in-app navigation a chance to run before asserting it did not.
await page.waitForTimeout(300);
await expect(page).toHaveURL(/.*\/standalone\/router-link/);
// Default is left intact so the browser can handle the link natively
// (new tab, new window, or download, depending on the browser and OS).
expect(defaultPrevented).toBe(false);
});
}

test('click on a non-_self target does not navigate the current page and allows a native new tab', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/26394',
});

const { defaultPrevented } = await dispatchClick(page, '#target-item', {});

// Give any in-app navigation a chance to run before asserting it did not.
await page.waitForTimeout(300);
await expect(page).toHaveURL(/.*\/standalone\/router-link/);
// Default is left intact so the browser can open the link in a new tab.
expect(defaultPrevented).toBe(false);
});
});
Loading
Loading