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 = [];