From 9c219e69e8552c0ac40c5835bff2de8b9fa225a4 Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 26 Oct 2026 00:45:09 +0500 Subject: [PATCH] plugin/router: leave external links to the browser x-router:link intercepted every click and navigated to the link's path, ignoring where the link leads. A link to another origin opened the same path within the application, and "mailto:" and "tel:" links navigated to their address or number as a path. $active compared only the path as well, so an external link to "/" was active on the home page. Treat a link whose protocol or host differs from the page's as external: the click handler leaves it to the browser, and $active is false for it. The origin is not compared, since browsers report it differently for file: URLs. --- src/plugins/router/router.js | 15 ++++++-- tests/playwright/assets/page.html | 1 + tests/playwright/x-router.spec.js | 60 +++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/playwright/x-router.spec.js diff --git a/src/plugins/router/router.js b/src/plugins/router/router.js index 37e143c..3a1c5af 100644 --- a/src/plugins/router/router.js +++ b/src/plugins/router/router.js @@ -137,7 +137,8 @@ export default function({ $data, addScopeToNode, directive, magic, reactive }) { || e.shiftKey || e.defaultPrevented || e.button > 0 - || is_blank) { + || is_blank + || is_external(link)) { return; } @@ -191,7 +192,7 @@ export default function({ $data, addScopeToNode, directive, magic, reactive }) { // if (link) { - return router.history.resolve(link.href) === router.values.path; + return !is_external(link) && router.history.resolve(link.href) === router.values.path; } if (el._r_routerlink_init) { @@ -215,6 +216,16 @@ function is_anchor_element(el) { return el.tagName.toUpperCase() === "A"; } +// +// A link to another origin or a non-HTTP scheme, such as "mailto:" or "tel:", +// leads outside the application and is left to the browser. +// The protocol and host are compared instead of the origin, +// which browsers report differently for file: URLs ("null" or "file://"). +// +function is_external(link) { + return link.protocol !== location.protocol || link.host !== location.host; +} + function get_anchor_element(el) { if (is_anchor_element(el)) { return el; diff --git a/tests/playwright/assets/page.html b/tests/playwright/assets/page.html index b7bbe68..e39714e 100644 --- a/tests/playwright/assets/page.html +++ b/tests/playwright/assets/page.html @@ -20,6 +20,7 @@ + diff --git a/tests/playwright/x-router.spec.js b/tests/playwright/x-router.spec.js new file mode 100644 index 0000000..d2250d3 --- /dev/null +++ b/tests/playwright/x-router.spec.js @@ -0,0 +1,60 @@ +import { expect, test } from "@playwright/test"; +import { set_html } from "./assets/utils"; + +// The page is opened from a file: URL, where the html5 history cannot change the path, +// so the tests use the hash history + +// Records whether the router took over the click, and keeps the browser from following the link +const capture_clicks = page => page.evaluate(() => { + document.addEventListener("click", e => { + window.prevented = e.defaultPrevented; + e.preventDefault(); + }); +}); + +test.describe("x-router: link", () => { + test("external links are left to the browser", async ({ page }) => { + await set_html(page, ` +
+ Example + Mail + Phone +
`); + + await capture_clicks(page); + + for (let link of await page.locator("a").all()) { + await link.click(); + expect(await page.evaluate(() => window.prevented)).toBe(false); + expect(await page.evaluate(() => location.hash)).toBe(""); + } + }); + + test("internal link is handled by the router", async ({ page }) => { + await set_html(page, ` +
+ About +
`); + + await capture_clicks(page); + + await page.locator("a").click(); + expect(await page.evaluate(() => window.prevented)).toBe(true); + expect(await page.evaluate(() => location.hash)).toBe("#/about"); + }); +}); + +test.describe("x-router: $active", () => { + test("external link with the current path is not active", async ({ page }) => { + await set_html(page, ` +
+ + Home + Example +
`); + + // Wait until the router activates the current route + await expect(page.locator("#home")).toHaveClass("active"); + await expect(page.locator("#example")).not.toHaveClass("active"); + }); +});