diff --git a/.changeset/silver-pigs-report.md b/.changeset/silver-pigs-report.md
new file mode 100644
index 000000000000..669acc786b8b
--- /dev/null
+++ b/.changeset/silver-pigs-report.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: correctly preload data on `mousedown`/`touchstart` if code was preloaded on hover
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 630f035754a1..c1728ca70533 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1636,25 +1636,29 @@ if (import.meta.hot) {
});
}
+/** @typedef {(typeof PRELOAD_PRIORITIES)['hover'] | (typeof PRELOAD_PRIORITIES)['tap']} PreloadDataPriority */
+
function setup_preload() {
/** @type {NodeJS.Timeout} */
let mousemove_timeout;
/** @type {Element} */
let current_a;
+ /** @type {PreloadDataPriority} */
+ let current_priority;
container.addEventListener('mousemove', (event) => {
const target = /** @type {Element} */ (event.target);
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
- void preload(target, 2);
+ void preload(target, PRELOAD_PRIORITIES.hover);
}, 20);
});
/** @param {Event} event */
function tap(event) {
if (event.defaultPrevented) return;
- void preload(/** @type {Element} */ (event.composedPath()[0]), 1);
+ void preload(/** @type {Element} */ (event.composedPath()[0]), PRELOAD_PRIORITIES.tap);
}
container.addEventListener('mousedown', tap);
@@ -1674,11 +1678,14 @@ function setup_preload() {
/**
* @param {Element} element
- * @param {number} priority
+ * @param {PreloadDataPriority} priority
*/
async function preload(element, priority) {
const a = find_anchor(element, container);
- if (!a || a === current_a) return;
+
+ // we don't want to preload data again if the user has already hovered/tapped
+ const interacted = a === current_a && priority >= current_priority;
+ if (!a || interacted) return;
const { url, external, download } = get_link_info(a, base, app.hash);
if (external || download) return;
@@ -1687,31 +1694,34 @@ function setup_preload() {
// we don't want to preload data for a page we're already on
const same_url = url && get_page_key(current.url) === get_page_key(url);
-
- if (!options.reload && !same_url) {
- if (priority <= options.preload_data) {
- current_a = a;
- const intent = await get_navigation_intent(url, false);
- if (intent) {
- if (DEV) {
- void _preload_data(intent).then((result) => {
- if (result.type === 'loaded' && result.state.error) {
- console.warn(
- `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` +
- 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
- 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
- 'See https://svelte.dev/docs/kit/link-options for more info'
- );
- }
- });
- } else {
- void _preload_data(intent);
+ if (options.reload || same_url) return;
+
+ if (priority <= options.preload_data) {
+ current_a = a;
+ // we don't want to preload data again on tap if we've already preloaded it on hover
+ current_priority = PRELOAD_PRIORITIES.tap;
+
+ const intent = await get_navigation_intent(url, false);
+ if (!intent) return;
+
+ if (DEV) {
+ void _preload_data(intent).then((result) => {
+ if (result.type === 'loaded' && result.state.error) {
+ console.warn(
+ `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` +
+ 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
+ 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
+ 'See https://svelte.dev/docs/kit/link-options for more info'
+ );
}
- }
- } else if (priority <= options.preload_code) {
- current_a = a;
- void _preload_code(/** @type {URL} */ (url));
+ });
+ } else {
+ void _preload_data(intent);
}
+ } else if (priority <= options.preload_code) {
+ current_a = a;
+ current_priority = priority;
+ void _preload_code(/** @type {URL} */ (url));
}
}
diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte
index 15d385fb9212..6c932e99140f 100644
--- a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte
@@ -8,3 +8,10 @@
tap
+
+hover for code then tap for data
diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/target/+page.server.js b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/target/+page.server.js
new file mode 100644
index 000000000000..9033be2470e4
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/target/+page.server.js
@@ -0,0 +1,5 @@
+export function load() {
+ return {
+ a: 1
+ };
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index df67597356a2..6175bd3c6c57 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -887,37 +887,44 @@ test.describe('data-sveltekit attributes', () => {
req
.response()
.then(
- (res) => res.text(),
+ (res) => res?.text(),
() => ''
)
- .then((response) => {
- if (response.includes('this string should only appear in this preloaded file')) {
+ .then((text) => {
+ if (text?.includes('this string should only appear in this preloaded file')) {
requests.push(req.url());
}
});
}
+
+ if (req.url().includes('__data.json')) {
+ requests.push(req.url());
+ }
});
await page.goto('/data-sveltekit/preload-data');
await page.locator('#one').hover();
+ await page.locator('#one').dispatchEvent('touchstart');
await Promise.all([
page.waitForTimeout(100), // wait for preloading to start
page.waitForLoadState('networkidle') // wait for preloading to finish
]);
- expect(requests.length).toBe(1);
+ expect(requests.length).toBe(2);
requests.length = 0;
await page.goto('/data-sveltekit/preload-data');
await page.locator('#two').hover();
+ await page.locator('#two').dispatchEvent('touchstart');
await Promise.all([
page.waitForTimeout(100), // wait for preloading to start
page.waitForLoadState('networkidle') // wait for preloading to finish
]);
- expect(requests.length).toBe(1);
+ expect(requests.length).toBe(2);
requests.length = 0;
await page.goto('/data-sveltekit/preload-data');
await page.locator('#three').hover();
+ await page.locator('#three').dispatchEvent('touchstart');
await Promise.all([
page.waitForTimeout(100), // wait for preloading to start
page.waitForLoadState('networkidle') // wait for preloading to finish
@@ -932,7 +939,7 @@ test.describe('data-sveltekit attributes', () => {
page.waitForTimeout(100), // wait for preloading to start
page.waitForLoadState('networkidle') // wait for preloading to finish
]);
- expect(requests.length).toBe(1);
+ expect(requests.length).toBe(2);
});
test('data-sveltekit-preload-data network failure does not trigger navigation', async ({
@@ -1001,6 +1008,47 @@ test.describe('data-sveltekit attributes', () => {
expect(page).toHaveURL('/data-sveltekit/preload-data/offline/slow-navigation');
});
+ test('data-sveltekit-preload-data tap works after data-sveltekit-preload-code hover', async ({
+ page
+ }) => {
+ /** @type {string[]} */
+ const requests = [];
+ page.on('request', (req) => {
+ if (req.resourceType() === 'script') {
+ req
+ .response()
+ .then(
+ (res) => res?.text(),
+ () => ''
+ )
+ .then((text) => {
+ if (text?.includes('this string should only appear in this preloaded file')) {
+ requests.push(req.url());
+ }
+ });
+ }
+
+ if (req.url().includes('__data.json')) {
+ requests.push(req.url());
+ }
+ });
+
+ await page.goto('/data-sveltekit/preload-data');
+ await page.locator('#hover-then-tap').hover();
+ await Promise.all([
+ page.waitForTimeout(100), // wait for preloading to start
+ page.waitForLoadState('networkidle') // wait for preloading to finish
+ ]);
+ expect(requests.length).toBe(1);
+
+ await page.locator('#hover-then-tap').dispatchEvent('touchstart');
+ await Promise.all([
+ page.waitForTimeout(100), // wait for preloading to start
+ page.waitForLoadState('networkidle') // wait for preloading to finish
+ ]);
+ expect(requests.length).toBe(2);
+ });
+
test('data-sveltekit-reload', async ({ baseURL, page, clicknav }) => {
/** @type {string[]} */
const requests = [];