Repository: sveltejs/kit (js)

Issue:
SvelteKit should not set tabIndex=-1
### Describe the bug

SvelteKit adds tabIndex=-1 to the document body in order to be able to manage focus upon navigation: https://github.com/sveltejs/kit/issues/307

This is not a "safe" change, in that it may impact the behavior of components on the page. In particular, this change means that some actions that would normally not trigger a `focus` event (like mousedown on some text) will now fire a `focus` event on the document body. These extra `focus` events may confuse code that does its own focus management (for example, a focus trap). They were the source of two different user-reported bugs in my component library: https://github.com/rgossiaux/svelte-headlessui/issues/36 and https://github.com/rgossiaux/svelte-headlessui/issues/39 

Looking at the motivation for introducing this originally, I don't see why it can't be replaced with something better. Can SvelteKit clean up after itself after navigation by removing the `tabIndex`? Alternatively, can it do something that doesn't require setting this at all? For example, Headless UI has a function to focus on the first focusable element within a container: https://github.com/tailwindlabs/headlessui/blob/cae976a18b7955670cd28fb1067a49917bda2c09/packages/%40headlessui-react/src/utils/focus-management.ts#L105 This works by using a CSS selector to identify potentially-focusable elements, then attempting to focus them one by one until a success.

### Reproduction

Not sure this needs a special test repro since this happens on every SvelteKit app, but I can create one if you disagree. 

The bugs that this caused can be reproduced at this commit: https://github.com/rgossiaux/svelte-headlessui/tree/ac1f86ac156a94c86e81647c55d8bb56c035edc6

### Logs

_No response_

### System Info

```shell
System:
    OS: macOS 12.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 4.11 GB / 64.00 GB
    Shell: 3.3.1 - /opt/homebrew/bin/fish
  Binaries:
    Node: 16.8.0 - /var/folders/rn/2kdgb0jj25q2v656j3qc_ywr0000gn/T/fnm_multishells/23841_1640400023703/bin/node
    Yarn: 1.22.17 - /opt/homebrew/bin/yarn
    npm: 7.21.0 - /var/folders/rn/2kdgb0jj25q2v656j3qc_ywr0000gn/T/fnm_multishells/23841_1640400023703/bin/npm
  Browsers:
    Chrome: 97.0.4692.99
    Safari: 15.2
```


### Severity

serious, but I can work around it

### Additional Information

_No response_

Pull request description:
restore `tabindex` after navigation
Fixes #3501.

As noted in the issue, adding `tabindex="-1"` to `<body>` causes a few focus management problems with body "stealing" input from other elements (eg. scrolling overflown children).

Since it's only needed to reset the page's focus during navigation, we can safely [?] remove it afterwards.

[?]: I'm not able to test this on Apple devices.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpx changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/honest-islands-flash.md b/.changeset/honest-islands-flash.md
new file mode 100644
index 000000000..b87fbad1a
--- /dev/null
+++ b/.changeset/honest-islands-flash.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] `tabindex="-1"` is no longer added to `<body>`; `<html>` only briefly receives it during navigation
diff --git a/.changeset/many-pigs-deliver.md b/.changeset/many-pigs-deliver.md
new file mode 100644
index 000000000..4617dfa1a
--- /dev/null
+++ b/.changeset/many-pigs-deliver.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-netlify': patch
+---
+
+Add Netlify Functions context as `event.platform.context`
diff --git a/.changeset/purple-readers-build.md b/.changeset/purple-readers-build.md
new file mode 100644
index 000000000..762698421
--- /dev/null
+++ b/.changeset/purple-readers-build.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+`RequestHandlerOutput` accepts body when it has or maybe is `undefined`
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 07035a2c9..c4d594fd6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,7 +27,7 @@ jobs:
       - run: pnpm check
   Tests:
     runs-on: ${{ matrix.os }}
-    timeout-minutes: 15
+    timeout-minutes: 20
     strategy:
       fail-fast: false
       matrix:
diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md
index faca2bcc0..199cd5e78 100644
--- a/documentation/docs/03-loading.md
+++ b/documentation/docs/03-loading.md
@@ -26,7 +26,7 @@ If the data for a page comes from its endpoint, you may not need a `load` functi
 
 > Note the `<script context="module">` — this is necessary because `load` runs before the component is rendered. Code that is per-component instance should go into a second `<script>` tag.
 
-`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client. In the example above, if a user clicks on a link to this page the data will be fetched from `cms.example.com` without going via our server.
+`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that `load` runs on both the server and the client. In the example above, if a user clicks on a link to this page the data will be fetched from `cms.example.com` without going via our server.
 
 If `load` returns `{fallthrough: true}`, SvelteKit will [fall through](/docs/routing#advanced-routing-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.
 
diff --git a/documentation/docs/13-configuration.md b/documentation/docs/13-configuration.md
index 54f77a5ef..0bc7ff71b 100644
--- a/documentation/docs/13-configuration.md
+++ b/documentation/docs/13-configuration.md
@@ -132,7 +132,7 @@ export default config;
 
 When pages are prerendered, the CSP header is added via a `<meta http-equiv>` tag (note that in this case, `frame-ancestors`, `report-uri` and `sandbox` directives will be ignored).
 
-> When `mode` is `'auto'`, SvelteKit will use nonces for dynamically rendered pages and hashes for prerendered pages. Using nonces with prerendered pages is insecure and therefore forbiddem.
+> When `mode` is `'auto'`, SvelteKit will use nonces for dynamically rendered pages and hashes for prerendered pages. Using nonces with prerendered pages is insecure and therefore forbidden.
 
 ### files
 
diff --git a/packages/adapter-netlify/README.md b/packages/adapter-netlify/README.md
index 52979b7bc..8ba42157d 100644
--- a/packages/adapter-netlify/README.md
+++ b/packages/adapter-netlify/README.md
@@ -57,7 +57,9 @@ During compilation, redirect rules are automatically appended to your `_redirect
 
 ### Using Netlify Functions
 
-[Netlify Functions](https://docs.netlify.com/functions/overview/) can be used alongside your SvelteKit routes. If you would like to add them to your site, you should create a directory for them and add the configuration to your `netlify.toml` file. For example:
+With this adapter, SvelteKit endpoints are hosted as [Netlify Functions](https://docs.netlify.com/functions/overview/). Netlify function handlers have additional context, including [Netlify Identity](https://docs.netlify.com/visitor-access/identity/) information. You can access this context via the `event.platform.context` field inside your hooks and endpoints.
+
+Additionally, you can add your own Netlify functions by creating a directory for them and adding the configuration to your `netlify.toml` file. For example:
 
 ```toml
 [build]
diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js
index 13eb9175c..160706c1b 100644
--- a/packages/adapter-netlify/src/handler.js
+++ b/packages/adapter-netlify/src/handler.js
@@ -9,8 +9,8 @@ import { split_headers } from './headers';
 export function init(manifest) {
 	const server = new Server(manifest);
 
-	return async (event) => {
-		const rendered = await server.respond(to_request(event));
+	return async (event, context) => {
+		const rendered = await server.respond(to_request(event), { platform: { context } });
 
 		const partial_response = {
 			statusCode: rendered.status,
diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js
index 3c0db40a6..9732283b6 100644
--- a/packages/kit/src/runtime/client/renderer.js
+++ b/packages/kit/src/runtime/client/renderer.js
@@ -369,8 +369,24 @@ export class Renderer {
 			const { scroll, keepfocus } = opts;
 
 			if (!keepfocus) {
+				// Reset page selection and focus
+				// We try to mimick browsers' behaviour as closely as possible by targeting the
+				// viewport, but unfortunately it's not a perfect match — e.g. shift-tabbing won't
+				// immediately cycle from the end of the page
+				// See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
+				const root = document.documentElement;
+				const tabindex = root.getAttribute('tabindex');
+
 				getSelection()?.removeAllRanges();
-				document.body.focus();
+				root.tabIndex = -1;
+				root.focus();
+
+				// restore `tabindex` as to prevent the document from stealing input from elements
+				if (tabindex !== null) {
+					root.setAttribute('tabindex', tabindex);
+				} else {
+					root.removeAttribute('tabindex');
+				}
 			}
 
 			// need to render the DOM before we can scroll to the rendered elements
diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js
index 1f2351f35..4c581d90b 100644
--- a/packages/kit/src/runtime/client/router.js
+++ b/packages/kit/src/runtime/client/router.js
@@ -73,9 +73,6 @@ export class Router {
 		this.enabled = true;
 		this.initialized = false;
 
-		// make it possible to reset focus
-		document.body.setAttribute('tabindex', '-1');
-
 		// keeping track of the history index in order to prevent popstate navigation events if needed
 		this.current_history_index = history.state?.['sveltekit:index'] ?? 0;
 
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 885c960f3..ff8f0fd7f 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -125,7 +125,7 @@ declare module '$app/navigation' {
 	 * If no argument is given, all routes will be fetched, otherwise you can specify routes by any matching pathname
 	 * such as `/about` (to match `src/routes/about.svelte`) or `/blog/*` (to match `src/routes/blog/[slug].svelte`).
 	 *
-	 * Unlike prefetch, this won't call preload for individual pages.
+	 * Unlike prefetch, this won't call load for individual pages.
 	 * Returns a Promise that resolves when the routes have been prefetched.
 	 */
 	export function prefetchRoutes(routes?: string[]): Promise<void>;
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index d51d7864e..33b8d487f 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -208,7 +208,15 @@ export type HttpMethod = 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch';
 
 export type JSONObject = { [key: string]: JSONValue };
 
-export type JSONValue = string | number | boolean | null | ToJSON | JSONValue[] | JSONObject;
+export type JSONValue =
+	| string
+	| number
+	| boolean
+	| null
+	| undefined
+	| ToJSON
+	| JSONValue[]
+	| JSONObject;
 
 export interface LoadInput<Params = Record<string, string>> {
 	url: URL;
diff --git a/sites/kit.svelte.dev/src/lib/search/SearchBox.svelte b/sites/kit.svelte.dev/src/lib/search/SearchBox.svelte
index b42c7e929..8cf93195a 100644
--- a/sites/kit.svelte.dev/src/lib/search/SearchBox.svelte
+++ b/sites/kit.svelte.dev/src/lib/search/SearchBox.svelte
@@ -57,7 +57,9 @@
 			const scroll = -parseInt(document.body.style.top || '0');
 			document.body.style.position = '';
 			document.body.style.top = '';
+			document.body.tabIndex = -1;
 			document.body.focus();
+			document.body.removeAttribute('tabindex');
 			window.scrollTo(0, scroll);
 		}
 	}


Test patch:
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index ec1305efa..7c99311be 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -25,6 +25,8 @@ test.describe.parallel('a11y', () => {
 		await page.keyboard.press('Tab');
 		expect(await page.evaluate(() => (document.activeElement || {}).nodeName)).toBe('A');
 		expect(await page.evaluate(() => (document.activeElement || {}).textContent)).toBe('a');
+
+		expect(await page.evaluate(() => document.documentElement.getAttribute('tabindex'))).toBe(null);
 	});
 
 	test('announces client-side navigation', async ({ page, clicknav, javaScriptEnabled }) => {
diff --git a/packages/kit/test/typings/endpoint.test.ts b/packages/kit/test/typings/endpoint.test.ts
index f805a811d..9f5af8ec3 100644
--- a/packages/kit/test/typings/endpoint.test.ts
+++ b/packages/kit/test/typings/endpoint.test.ts
@@ -5,10 +5,11 @@ const valid_body = {
 	num: 12345,
 	bool: true,
 	null: null,
+	maybe: Math.random() < 0.5 ? undefined : true,
 	custom: {
 		toJSON: () => 'custom toJSON function'
 	},
-	list: ['string', 12345, false, null],
+	list: ['string', 12345, false, null, undefined],
 	nested: {
 		another: 'string',
 		big_num: 98765,
@@ -84,13 +85,6 @@ export const differential_headers_assignment: RequestHandler = () => {
 
 // --- invalid cases ---
 
-// @ts-expect-error - should not have undefined (should it not?)
-export const error_no_undefined: RequestHandler = () => {
-	return {
-		body: { no: Math.random() < 0.5 ? undefined : 'something' }
-	};
-};
-
 // @ts-expect-error - body must be JSON serializable
 export const error_body_must_be_serializable: RequestHandler = () => {
 	return {