Skip to content

Commit

Permalink
feat: Use composedPath to find anchor elements (#2769)
Browse files Browse the repository at this point in the history
* feat: Use `composedPath` to find anchor elements

The client-side router registers event handlers on mouse movements and
clicks that search anchor elements in an event's target's ancestors to
perform prefetching or routing. The current implementation uses
`parentNode` which "skips" anchor elements in shadow doms.

This change searches instead the `event.composedPath()`, which includes
nodes in open shadow trees.

* refactor: `find_anchor` to receive a full event

* test: Add integration tests

* docs: Add changeset
  • Loading branch information
andreavaccari authored Nov 23, 2021
1 parent f075f05 commit c90c8aa
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-phones-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] Use `event.composedPath` to find anchors for prefetching and routing
16 changes: 9 additions & 7 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ function scroll_state() {
}

/**
* @param {Node | null} node
* @returns {HTMLAnchorElement | SVGAElement | null}
* @param {Event} event
* @returns {HTMLAnchorElement | SVGAElement | undefined}
*/
function find_anchor(node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return /** @type {HTMLAnchorElement | SVGAElement} */ (node);
function find_anchor(event) {
const node = event
.composedPath()
.find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
}

/**
Expand Down Expand Up @@ -94,7 +96,7 @@ export class Router {

/** @param {MouseEvent|TouchEvent} event */
const trigger_prefetch = (event) => {
const a = find_anchor(/** @type {Node} */ (event.target));
const a = find_anchor(event);
if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
this.prefetch(get_href(a));
}
Expand Down Expand Up @@ -124,7 +126,7 @@ export class Router {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
if (event.defaultPrevented) return;

const a = find_anchor(/** @type {Node} */ (event.target));
const a = find_anchor(event);
if (!a) return;

if (!a.href) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as assert from 'uvu/assert';

/** @type {import('test').TestMaker} */
export default function (test) {
test(
'client router captures anchors in shadow dom',
'/routing/shadow-dom',
async ({ app, capture_requests, page, clicknav, js }) => {
if (js) {
await app.prefetchRoutes(['/routing/a']).catch((e) => {
// from error handler tests; ignore
if (!e.message.includes('Crashing now')) throw e;
});

const requests = await capture_requests(async () => {
await clicknav('div[id="clickme"]');
assert.equal(await page.textContent('h1'), 'a');
});

assert.equal(requests, []);
}
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
import { onMount } from 'svelte';
/** @type {HTMLDivElement} */
let elem;
onMount(() => {
const shadow = elem.attachShadow({ mode: 'open' });
const anchor = document.createElement('a');
anchor.href = '/routing/a';
anchor.innerHTML = '<slot>';
shadow.appendChild(anchor);
});
</script>

<div bind:this={elem}>
<div id="clickme">Hello world</div>
</div>

0 comments on commit c90c8aa

Please sign in to comment.