Skip to content

Commit

Permalink
fix: SVG in links breaking partials (#2681)
Browse files Browse the repository at this point in the history
Fixes #2239
  • Loading branch information
marvinhagemeister authored Oct 4, 2024
1 parent 5f65747 commit a596a04
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 26 deletions.
44 changes: 18 additions & 26 deletions src/runtime/client/partials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ function maybeUpdateHistory(nextUrl: URL) {

document.addEventListener("click", async (e) => {
let el = e.target;
if (el && el instanceof HTMLElement) {
if (el && (el instanceof HTMLElement || el instanceof SVGElement)) {
const originalEl = el;

// Check if we clicked inside an anchor link
if (el.nodeName !== "A") {
el = el.closest("a");
}
if (el === null) {
el = originalEl.closest("button");
}

if (
// Check that we're still dealing with an anchor tag
Expand Down Expand Up @@ -132,33 +135,22 @@ document.addEventListener("click", async (e) => {
indicator.value = false;
}
}
} else {
let button: HTMLButtonElement | HTMLElement | null = originalEl;
// Check if we clicked on a button
if (button.nodeName !== "A") {
button = button.closest("button");
}

if (
button !== null && button instanceof HTMLButtonElement &&
(button.type !== "submit" || button.form === null)
) {
const partial = button.getAttribute(PARTIAL_ATTR);

// Check if the user opted out of client side navigation.
if (
partial === null ||
!checkClientNavEnabled(button)
) {
return;
}
} else if (
el && el instanceof HTMLButtonElement &&
(el.type !== "submit" || el.form === null)
) {
const partial = el.getAttribute(PARTIAL_ATTR);

const partialUrl = new URL(
partial,
location.href,
);
await fetchPartials(partialUrl, partialUrl);
// Check if the user opted out of client side navigation.
if (partial === null || !checkClientNavEnabled(el)) {
return;
}

const partialUrl = new URL(
partial,
location.href,
);
await fetchPartials(partialUrl, partialUrl);
}
}
});
Expand Down
172 changes: 172 additions & 0 deletions tests/partials_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,178 @@ Deno.test({
sanitizeOps: false,
});

Deno.test({
name: "partials - with SVG in link",
fn: async () => {
const app = testApp()
.get("/partial", (ctx) => {
return ctx.render(
<Doc>
<Partial name="foo">
<p class="done">done</p>
<SelfCounter />
</Partial>
</Doc>,
);
})
.get("/", (ctx) => {
return ctx.render(
<Doc>
<div f-client-nav>
<a href="/foo" f-partial="/partial">
<svg
width="100"
height="100"
viewBox="-256 -256 512 512"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<path
class="update"
d="M0,-256 221.7025033688164,-128 221.7025033688164,128 0,256 -221.7025033688164,128 -221.7025033688164,-128z"
fill="#673ab8"
/>
<ellipse
cx="0"
cy="0"
stroke-width="16px"
rx="75px"
ry="196px"
fill="none"
stroke="white"
transform="rotate(52.5)"
/>
<ellipse
cx="0"
cy="0"
stroke-width="16px"
rx="75px"
ry="196px"
fill="none"
stroke="white"
transform="rotate(-52.5)"
/>
<circle cx="0" cy="0" r="34" fill="white" />
</svg>
update
</a>
<Partial name="foo">
<p class="init">init</p>
<SelfCounter />
</Partial>
</div>
</Doc>,
);
});

await withBrowserApp(app, async (page, address) => {
await page.goto(address, { waitUntil: "load" });
await page.locator(".ready").wait();

await page.locator(".increment").click();
await waitForText(page, ".output", "1");

await page.locator(".update").click();

await page.waitForFunction(() => {
const url = new URL(window.location.href);
return url.pathname === "/foo";
});
await page.locator(".done").wait();
await waitForText(page, ".output", "1");
});
},
sanitizeResources: false,
sanitizeOps: false,
});

Deno.test({
name: "partials - with SVG in button",
fn: async () => {
const app = testApp()
.get("/partial", (ctx) => {
return ctx.render(
<Doc>
<Partial name="foo">
<p class="done">done</p>
<SelfCounter />
</Partial>
</Doc>,
);
})
.get("/", (ctx) => {
return ctx.render(
<Doc>
<div f-client-nav>
<button f-partial="/partial">
<svg
width="100"
height="100"
viewBox="-256 -256 512 512"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<path
class="update"
d="M0,-256 221.7025033688164,-128 221.7025033688164,128 0,256 -221.7025033688164,128 -221.7025033688164,-128z"
fill="#673ab8"
/>
<ellipse
cx="0"
cy="0"
stroke-width="16px"
rx="75px"
ry="196px"
fill="none"
stroke="white"
transform="rotate(52.5)"
/>
<ellipse
cx="0"
cy="0"
stroke-width="16px"
rx="75px"
ry="196px"
fill="none"
stroke="white"
transform="rotate(-52.5)"
/>
<circle cx="0" cy="0" r="34" fill="white" />
</svg>
update
</button>
<Partial name="foo">
<p class="init">init</p>
<SelfCounter />
</Partial>
</div>
</Doc>,
);
});

await withBrowserApp(app, async (page, address) => {
await page.goto(address, { waitUntil: "load" });
await page.locator(".ready").wait();

await page.locator(".increment").click();
await waitForText(page, ".output", "1");

await page.locator(".update").click();

await page.waitForFunction(() => {
const url = new URL(window.location.href);
return url.pathname === "/partial";
});
await page.locator(".done").wait();
await waitForText(page, ".output", "1");
});
},
sanitizeResources: false,
sanitizeOps: false,
});

Deno.test({
name: "partials - opt out of partial navigation",
fn: async () => {
Expand Down

0 comments on commit a596a04

Please sign in to comment.