diff --git a/.changeset/two-baboons-prove.md b/.changeset/two-baboons-prove.md new file mode 100644 index 000000000000..7524ec869b75 --- /dev/null +++ b/.changeset/two-baboons-prove.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +feat: added `options` to `Navigation` object, allowing navigation hooks to retrieve options diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 183b85da657c..34d2d7a8c966 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -871,6 +871,20 @@ export interface NavigationTarget { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; +/** Navigation options that are defined when using `goto(..., options)` or link options. See https://kit.svelte.dev/docs/link-options for the equivalent HTML attributes. */ +export interface NavigationOptions { + /** If `true`, will replace the current `history` entry rather than creating a new one with `pushState`. */ + replaceState: boolean; + /** If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation. */ + noScroll: boolean; + /** If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body. */ + keepFocus: boolean; + /** If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. */ + invalidateAll: boolean; + /** An optional object that will be available on the `$page.state` store. */ + state: App.PageState; +} + export interface Navigation { /** * Where navigation was triggered from @@ -902,6 +916,11 @@ export interface Navigation { * fails or is aborted. In the case of a `willUnload` navigation, the promise will never resolve */ complete: Promise; + /** + * Navigation options defined using `goto(..., options)` or inferred from the link that was clicked. + * Only defined for `link` and `goto` navigations. + */ + options?: NavigationOptions; } /** diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 6db8e241d246..bccab0dcbb68 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -350,6 +350,7 @@ async function _goto(url, options, redirect_count, nav_token) { noscroll: options.noScroll, replace_state: options.replaceState, state: options.state, + invalidate_all: options.invalidateAll, redirect_count, nav_token, accept: () => { @@ -1137,12 +1138,13 @@ function get_url_path(pathname) { * type: import('@sveltejs/kit').Navigation["type"]; * intent?: import('./types.js').NavigationIntent; * delta?: number; + * options?: import('@sveltejs/kit').NavigationOptions; * }} opts */ -function _before_navigate({ url, type, intent, delta }) { +function _before_navigate({ url, type, intent, delta, options }) { let should_block = false; - const nav = create_navigation(current, intent, url, type); + const nav = create_navigation(current, intent, url, type, options); if (delta !== undefined) { nav.navigation.delta = delta; @@ -1176,6 +1178,7 @@ function _before_navigate({ url, type, intent, delta }) { * keepfocus?: boolean; * noscroll?: boolean; * replace_state?: boolean; + * invalidate_all?: boolean; * state?: Record; * redirect_count?: number; * nav_token?: {}; @@ -1187,9 +1190,10 @@ async function navigate({ type, url, popped, - keepfocus, - noscroll, - replace_state, + keepfocus = false, + noscroll = false, + replace_state = false, + invalidate_all = false, state = {}, redirect_count = 0, nav_token = {}, @@ -1197,7 +1201,19 @@ async function navigate({ block = noop }) { const intent = get_navigation_intent(url, false); - const nav = _before_navigate({ url, type, delta: popped?.delta, intent }); + const nav = _before_navigate({ + url, + type, + delta: popped?.delta, + intent, + options: { + state, + noScroll: noscroll, + keepFocus: keepfocus, + replaceState: replace_state, + invalidateAll: invalidate_all + } + }); if (!nav) { block(); @@ -1639,12 +1655,7 @@ export function disableScrollHandling() { * For external URLs, use `window.location = url` instead of calling `goto(url)`. * * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. - * @param {Object} [opts] Options related to the navigation - * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` - * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation - * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body - * @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. - * @param {App.PageState} [opts.state] An optional object that will be available on the `$page.state` store + * @param {Partial} [opts] Options related to the navigation * @returns {Promise} */ export function goto(url, opts = {}) { @@ -1925,7 +1936,7 @@ function _start_router() { persist_state(); if (!navigating) { - const nav = create_navigation(current, undefined, null, 'leave'); + const nav = create_navigation(current, undefined, null, 'leave', undefined); // If we're navigating, beforeNavigate was already called. If we end up in here during navigation, // it's due to an external or full-page-reload link, for which we don't want to call the hook again. @@ -2517,8 +2528,9 @@ function reset_focus() { * @param {import('./types.js').NavigationIntent | undefined} intent * @param {URL | null} url * @param {Exclude} type + * @param {import('@sveltejs/kit').NavigationOptions | undefined} options */ -function create_navigation(current, intent, url, type) { +function create_navigation(current, intent, url, type, options) { /** @type {(value: any) => void} */ let fulfil; @@ -2547,7 +2559,8 @@ function create_navigation(current, intent, url, type) { }, willUnload: !intent, type, - complete + complete, + options }; return { diff --git a/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/options/+page.svelte b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/options/+page.svelte new file mode 100644 index 000000000000..6cf303301baa --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/navigation-lifecycle/before-navigate/options/+page.svelte @@ -0,0 +1,37 @@ + + +

+ {type} + {options.noScroll} + {options.keepFocus} + {options.replaceState} + {options.invalidateAll} + {JSON.stringify(options.state)} +

+ + + + + link + diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 19dd898d46be..726d2906b3a5 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -226,6 +226,16 @@ test.describe('Navigation lifecycle functions', () => { expect(await page.innerHTML('pre')).toBe('1 false link'); }); + test('beforeNavigate has an option object', async ({ page }) => { + await page.goto('/navigation-lifecycle/before-navigate/options'); + + await page.click('button'); + expect(await page.innerHTML('p')).toBe('goto true true true true {"active":true}'); + + await page.click('a'); + expect(await page.innerHTML('p')).toBe('link true true true false {}'); + }); + test('afterNavigate calls callback', async ({ page, clicknav }) => { await page.goto('/navigation-lifecycle/after-navigate/a'); expect(await page.textContent('h1')).toBe( diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index a983b4433561..b29598297a0e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -853,6 +853,20 @@ declare module '@sveltejs/kit' { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; + /** Navigation options that are defined when using `goto(..., options)` or link options. See https://kit.svelte.dev/docs/link-options for the equivalent HTML attributes. */ + export interface NavigationOptions { + /** If `true`, will replace the current `history` entry rather than creating a new one with `pushState`. */ + replaceState: boolean; + /** If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation. */ + noScroll: boolean; + /** If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body. */ + keepFocus: boolean; + /** If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. */ + invalidateAll: boolean; + /** An optional object that will be available on the `$page.state` store. */ + state: App.PageState; + } + export interface Navigation { /** * Where navigation was triggered from @@ -884,6 +898,11 @@ declare module '@sveltejs/kit' { * fails or is aborted. In the case of a `willUnload` navigation, the promise will never resolve */ complete: Promise; + /** + * Navigation options defined using `goto(..., options)` or inferred from the link that was clicked. + * Only defined for `link` and `goto` navigations. + */ + options?: NavigationOptions; } /** @@ -1992,15 +2011,9 @@ declare module '$app/navigation' { * For external URLs, use `window.location = url` instead of calling `goto(url)`. * * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. - * @param {Object} opts Options related to the navigation + * @param opts Options related to the navigation * */ - export function goto(url: string | URL, opts?: { - replaceState?: boolean | undefined; - noScroll?: boolean | undefined; - keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; - state?: App.PageState | undefined; - } | undefined): Promise; + export function goto(url: string | URL, opts?: Partial | undefined): Promise; /** * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. *