@solidjs/router 2.0.0-next.35 → 2.0.0-next.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -7
- package/dist/claims.d.ts +5 -3
- package/dist/claims.js +14 -14
- package/dist/data/action.js +4 -1
- package/dist/data/events.js +9 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +245 -110
- package/dist/index.jsx +1 -0
- package/dist/paths.d.ts +23 -3
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- package/dist/routers/components.jsx +7 -4
- package/dist/routers/factory.d.ts +17 -2
- package/dist/routers/factory.jsx +22 -13
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +7 -1
- package/dist/routing.js +28 -38
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +27 -10
- package/package.json +6 -6
package/dist/index.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
2
|
export * from "./lifecycle.js";
|
|
3
3
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
|
+
export { pendingLinks } from "./pending.js";
|
|
4
5
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
5
6
|
export { int } from "./paths.js";
|
|
6
7
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
package/dist/paths.d.ts
CHANGED
|
@@ -82,9 +82,29 @@ type TuplePaths<R extends readonly unknown[], PAcc extends Params> = R extends r
|
|
|
82
82
|
...infer T extends readonly unknown[]
|
|
83
83
|
] ? RouteContrib<H, PAcc> & TuplePaths<T, PAcc> : {};
|
|
84
84
|
type PathLeaf<Sch extends SearchTypes, C, PAcc extends Params> = PathEnd<Sch, Flat<PAcc>> & ChildPaths<C, PAcc>;
|
|
85
|
-
|
|
85
|
+
declare const PARAM_NEXT: unique symbol;
|
|
86
|
+
/**
|
|
87
|
+
* Sibling routes that share a param prefix (`/posts/:id`, `/posts/:id/edit`)
|
|
88
|
+
* each contribute a call at that node, and a call on an intersection of
|
|
89
|
+
* function types resolves to its first overload only. So the call reads its
|
|
90
|
+
* result back through `this` — the whole intersection — where the
|
|
91
|
+
* continuations of every sibling with the same arity have merged.
|
|
92
|
+
*/
|
|
93
|
+
interface ParamCall<A extends readonly unknown[]> {
|
|
94
|
+
(...args: A): this extends {
|
|
95
|
+
readonly [PARAM_NEXT]: {
|
|
96
|
+
[K in A["length"]]: infer N;
|
|
97
|
+
};
|
|
98
|
+
} ? N : never;
|
|
99
|
+
}
|
|
100
|
+
type ParamCallTo<A extends readonly unknown[], N> = ParamCall<A> & {
|
|
101
|
+
readonly [PARAM_NEXT]: {
|
|
102
|
+
[K in A["length"]]: N;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
type PathNode<Segs extends readonly string[], F, Sch extends SearchTypes, C, PAcc extends Params> = Segs extends readonly [infer H extends string, ...infer R extends readonly string[]] ? H extends `:${infer N}?` ? ParamCallTo<[ParamArg<N, F>], PathNode<R, F, Sch, C, PAcc & {
|
|
86
106
|
[K in N]?: string;
|
|
87
|
-
}
|
|
107
|
+
}>> & PathNode<R, F, Sch, C, PAcc & {
|
|
88
108
|
[K in N]?: string;
|
|
89
109
|
}> : H extends `:${string}` | `*${string}` ? ParamCallNode<Segs, F, Sch, C, PAcc> : {
|
|
90
110
|
[K in H]: PathNode<R, F, Sch, C, PAcc>;
|
|
@@ -93,7 +113,7 @@ type ParamCallNode<Segs extends readonly string[], F, Sch extends SearchTypes, C
|
|
|
93
113
|
args: infer A extends readonly unknown[];
|
|
94
114
|
params: infer P2 extends Params;
|
|
95
115
|
rest: infer R2 extends readonly string[];
|
|
96
|
-
} ?
|
|
116
|
+
} ? ParamCallTo<A, PathNode<R2, F, Sch, C, PAcc & P2>> & (R2 extends readonly [] ? {
|
|
97
117
|
(...args: [...A, Sch["input"], string?]): string;
|
|
98
118
|
} : {}) & TypedPath<Flat<PAcc & P2>> : never;
|
|
99
119
|
type RouteContrib<Def, PAcc extends Params> = Def extends {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LinksPlugin, LocationChange, RouterContext } from "./types.js";
|
|
2
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
3
|
+
export declare function routingState(router: RouterContext): () => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
6
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pendingTarget(router: RouterContext): LocationChange | undefined;
|
|
9
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
10
|
+
export declare function linkPending(router: RouterContext, to: string | undefined, base: string, end?: boolean): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
13
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
14
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
15
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
20
|
+
*
|
|
21
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
22
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const pendingLinks: LinksPlugin;
|
package/dist/pending.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createMemo, DEV, isPending, latest, NotReadyError, runWithOwner } from "solid-js";
|
|
2
|
+
import { matchLink } from "./utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
5
|
+
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
6
|
+
* and `latest` are UI affordances — the router's own coordination reads its
|
|
7
|
+
* location writes and their `onSettled` instead — so an app that renders
|
|
8
|
+
* none of these never bundles Solid's verdict machinery.
|
|
9
|
+
*/
|
|
10
|
+
const routing = new WeakMap();
|
|
11
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
12
|
+
export function routingState(router) {
|
|
13
|
+
let read = routing.get(router);
|
|
14
|
+
if (!read) {
|
|
15
|
+
const { location, matches, _source: source } = router;
|
|
16
|
+
// `transparent`: created on first use rather than at router setup, so it
|
|
17
|
+
// must not take a hydration id — the server may never create it, or
|
|
18
|
+
// create it at a different point.
|
|
19
|
+
const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
|
|
20
|
+
try {
|
|
21
|
+
matches();
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e instanceof NotReadyError)
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
location.search;
|
|
28
|
+
location.hash;
|
|
29
|
+
}), { transparent: true, ...(DEV && { name: "routingPending" }) }));
|
|
30
|
+
read = () => pending() || isPending(source);
|
|
31
|
+
routing.set(router, read);
|
|
32
|
+
}
|
|
33
|
+
return read;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
37
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
38
|
+
*/
|
|
39
|
+
export function pendingTarget(router) {
|
|
40
|
+
if (!routingState(router)())
|
|
41
|
+
return;
|
|
42
|
+
const target = latest(router._source);
|
|
43
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
46
|
+
export function linkPending(router, to, base, end) {
|
|
47
|
+
const target = pendingTarget(router);
|
|
48
|
+
return !!target && matchLink({ pathname: target.value, search: "" }, to, base, end).active;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
52
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
53
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
54
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
59
|
+
*
|
|
60
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
61
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const pendingLinks = (router, base) => ({
|
|
65
|
+
track: routingState(router),
|
|
66
|
+
pending: target => linkPending(router, target, base)
|
|
67
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createMemo,
|
|
2
|
+
import { createMemo, createOwner, createRoot, onCleanup, runWithOwner, untrack, Show } from "solid-js";
|
|
3
3
|
import { getRequestEvent, isServer } from "@solidjs/web";
|
|
4
4
|
import { createRouteContext, getIntent, getRouteMatches, resolveLazySubtree, RouteContextObj, setInPreloadFn, unresolvedLazyMatches } from "../routing.js";
|
|
5
5
|
import { serverRouteArgs, serverRouteOf } from "../serverRouteShared.js";
|
|
@@ -55,9 +55,12 @@ export function Routes(props) {
|
|
|
55
55
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
56
56
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
57
57
|
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
58
|
-
// are created under
|
|
59
|
-
// computation (which disposes its children every time it re-runs).
|
|
60
|
-
|
|
58
|
+
// are created under an owner of this component rather than the memo's
|
|
59
|
+
// computation (which disposes its children every time it re-runs). A
|
|
60
|
+
// dedicated one, created before the memo: the roots draw hydration ids from
|
|
61
|
+
// its counter, so the ids don't depend on whether the memo first parked on
|
|
62
|
+
// a lazy subtree (the server's render can park where the client's doesn't).
|
|
63
|
+
const owner = createOwner();
|
|
61
64
|
const routeStates = createMemo((prev) => {
|
|
62
65
|
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
63
66
|
// computation parks with it — no route contexts are created against
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { RoutePaths } from "../paths.js";
|
|
3
|
-
import type { DefinedRouteFilters, LazyRouteChildren, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, TypedPath, ValidFilters } from "../types.js";
|
|
3
|
+
import type { DefinedRouteFilters, LazyRouteChildren, LinksPlugin, OutputMatch, Params, RouteDefinition, RouteInfo, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteSectionComponent, RouteSectionProps, StandardSchemaV1, TypedPath, ValidFilters } from "../types.js";
|
|
4
4
|
import type { RouterHistory } from "./history.js";
|
|
5
5
|
/**
|
|
6
6
|
* Identity helper that preserves literal types when the route tree is
|
|
@@ -83,13 +83,28 @@ export interface RouterConfig<R extends readonly RouteDefinition[] = RouteDefini
|
|
|
83
83
|
singleFlight?: boolean;
|
|
84
84
|
actionBase?: string;
|
|
85
85
|
explicitLinks?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Link claims plugin. `aria-current` and `data-active` are automatic; pass
|
|
88
|
+
* `pendingLinks` to also mark links covering the in-flight destination of a
|
|
89
|
+
* link click or `navigate()` with `data-pending`. Client-only.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
94
|
+
*
|
|
95
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
links?: LinksPlugin;
|
|
86
99
|
/** Preload route code/data on link hover and focus. Defaults to `true`. */
|
|
87
100
|
preloadLinks?: boolean;
|
|
88
101
|
/**
|
|
89
102
|
* Explicit scroll restoration for back/forward navigation: positions are
|
|
90
103
|
* saved per history entry and restored once the navigation settles,
|
|
91
104
|
* replacing the browser heuristic that loses offsets when the destination
|
|
92
|
-
* route forces a layout while rendering.
|
|
105
|
+
* route forces a layout while rendering. Reloads of a server-rendered page
|
|
106
|
+
* are left to the browser's native restoration, which waits for the
|
|
107
|
+
* streamed document to finish loading. Defaults to `true` with the
|
|
93
108
|
* default browser history; a custom `history` adapter owns its session and
|
|
94
109
|
* must opt in explicitly.
|
|
95
110
|
*/
|
package/dist/routers/factory.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
|
-
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
|
|
2
|
+
import { createSignal, getOwner, isHydrating, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
|
|
3
3
|
// standalone imports: `DEV` is undefined in solid's production build and
|
|
4
4
|
// `OBSERVE` outside its observe/dev builds, so app bundlers fold the
|
|
5
5
|
// `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
|
|
@@ -118,8 +118,8 @@ function describeInitial(match, location) {
|
|
|
118
118
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
119
119
|
function createIntegration(history, match) {
|
|
120
120
|
let committing = false;
|
|
121
|
-
//
|
|
122
|
-
|
|
121
|
+
// Writes whose transition has landed (see `RouterIntegration.settled`).
|
|
122
|
+
const settled = new WeakSet();
|
|
123
123
|
const wrap = (value) => (typeof value === "string" ? { value } : value);
|
|
124
124
|
const [read, write] = createSignal(wrap(history.get()), {
|
|
125
125
|
equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
|
|
@@ -138,15 +138,16 @@ function createIntegration(history, match) {
|
|
|
138
138
|
// the no-op rule compares against it, so `navigate()` behind another
|
|
139
139
|
// write in one handler sees that write rather than the flushed world.
|
|
140
140
|
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
141
|
-
if (written && written._navigation
|
|
141
|
+
if (written && written._navigation) {
|
|
142
142
|
const next = written;
|
|
143
|
-
inflight = next;
|
|
144
143
|
// Register out of band so a destination error boundary replacing the
|
|
145
|
-
// Router subtree cannot suppress the winning history commit.
|
|
144
|
+
// Router subtree cannot suppress the winning history commit. The
|
|
145
|
+
// settle is tied to the transition carrying the write: it fires when
|
|
146
|
+
// that lands, a rejection included, and a superseded write's settle
|
|
147
|
+
// fires with the transition that absorbed it.
|
|
146
148
|
runWithOwner(null, () => onSettled(() => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (read() !== next)
|
|
149
|
+
settled.add(next);
|
|
150
|
+
if (next._navigation < 0 || read() !== next)
|
|
150
151
|
return;
|
|
151
152
|
committing = true;
|
|
152
153
|
try {
|
|
@@ -182,7 +183,11 @@ function createIntegration(history, match) {
|
|
|
182
183
|
return;
|
|
183
184
|
signal[1]({ ...wrap(value), _navigation: -1 });
|
|
184
185
|
}));
|
|
185
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
signal,
|
|
188
|
+
settled: write => settled.has(write),
|
|
189
|
+
utils: history.utils
|
|
190
|
+
};
|
|
186
191
|
}
|
|
187
192
|
/**
|
|
188
193
|
* Server default: a static view of the request URL — no signal machinery, a
|
|
@@ -246,7 +251,7 @@ export function createRouter(config) {
|
|
|
246
251
|
let restoration;
|
|
247
252
|
let history = config.history;
|
|
248
253
|
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
249
|
-
restoration = createScrollRestoration();
|
|
254
|
+
restoration = createScrollRestoration(isHydrating());
|
|
250
255
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
251
256
|
}
|
|
252
257
|
const integration = isServer
|
|
@@ -270,11 +275,15 @@ export function createRouter(config) {
|
|
|
270
275
|
actionBase: config.actionBase,
|
|
271
276
|
transformUrl: config.transformUrl
|
|
272
277
|
})(routerState);
|
|
273
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
278
|
+
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
274
279
|
if (routerState.singleFlight)
|
|
275
280
|
onCleanup(registerFlightRouter(routerState));
|
|
276
|
-
restoration && restoration.create(
|
|
281
|
+
restoration && restoration.create();
|
|
277
282
|
}
|
|
283
|
+
// Registered on both sides, outside the client-only branch: an owned
|
|
284
|
+
// onSettled takes a hydration id on the server too, so the ids line up.
|
|
285
|
+
// A no-op when the arrival is left to the browser's native restore.
|
|
286
|
+
onSettled(() => restoration && restoration.settled());
|
|
278
287
|
return (<RouterContextObj value={routerState}>
|
|
279
288
|
<Root routerState={routerState} root={root} preload={config.preload}>
|
|
280
289
|
{(context = getOwner()) && null}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { RouterContext } from "../types.js";
|
|
2
1
|
import type { RouterHistory } from "./history.js";
|
|
3
2
|
/**
|
|
4
3
|
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
@@ -13,7 +12,7 @@ import type { RouterHistory } from "./history.js";
|
|
|
13
12
|
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
14
13
|
* otherwise disables.
|
|
15
14
|
*
|
|
16
|
-
* Restoration is a single scroll once
|
|
15
|
+
* Restoration is a single scroll once the traversal settles — the same strategy
|
|
17
16
|
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
18
17
|
* transition commits is what makes the offset reachable; chasing a still-
|
|
19
18
|
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
@@ -23,13 +22,38 @@ import type { RouterHistory } from "./history.js";
|
|
|
23
22
|
* layout changes can feed it back into itself. Content that commits after the
|
|
24
23
|
* transition settles — an image without reserved space, a boundary below the
|
|
25
24
|
* fold — keeps whatever offset the document can hold.
|
|
25
|
+
*
|
|
26
|
+
* A server-rendered document load is the exception: the browser restores it
|
|
27
|
+
* natively once the document — streamed boundaries included — has loaded,
|
|
28
|
+
* which a restore at the first client flush cannot match. So an entry is
|
|
29
|
+
* handed back to the browser (`auto`) whenever its document may unload
|
|
30
|
+
* (pagehide), and a hydrating router arriving on a handed-back entry leaves
|
|
31
|
+
* the restore to the browser: it does not scroll, and it takes the entry back
|
|
32
|
+
* (`manual`) only after load or at the first client navigation, whichever
|
|
33
|
+
* comes first. A programmatic scroll during the load cancels the native
|
|
34
|
+
* restore in Firefox and WebKit, and WebKit restores just after the load
|
|
35
|
+
* event, so a `manual` write before then — inside a load listener included —
|
|
36
|
+
* suppresses it; the write waits a task. Handed-back depths are tracked here
|
|
37
|
+
* rather than read from `history.scrollRestoration`, which Firefox reports as
|
|
38
|
+
* the mode before pagehide after a reload. An entry this router pushed and
|
|
39
|
+
* never handed back stays `manual` and gets no native restore, so the router
|
|
40
|
+
* restores it as on a client-rendered load.
|
|
26
41
|
*/
|
|
27
|
-
export declare function createScrollRestoration(): {
|
|
28
|
-
/**
|
|
42
|
+
export declare function createScrollRestoration(hydrating?: boolean): {
|
|
43
|
+
/**
|
|
44
|
+
* When the adapter notifies a traversal: mark the target and restore once
|
|
45
|
+
* the transition carrying it settles. The adapter notifies right before
|
|
46
|
+
* the location write, in the same tick, so the settle is that write's —
|
|
47
|
+
* a same-URL traversal included, which no location key would see.
|
|
48
|
+
*/
|
|
29
49
|
onPop(): void;
|
|
50
|
+
/** The initial page settled: restore a reload/back_forward arrival. */
|
|
51
|
+
settled: () => void;
|
|
52
|
+
/** Before a client navigation writes history: the entry it creates copies the current mode. */
|
|
53
|
+
beforeWrite: () => void;
|
|
30
54
|
/** After a push: forward entries died, and this depth may be reused. */
|
|
31
55
|
onPush(): void;
|
|
32
|
-
create(
|
|
56
|
+
create(): void;
|
|
33
57
|
};
|
|
34
58
|
export type ScrollRestoration = ReturnType<typeof createScrollRestoration>;
|
|
35
59
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { onCleanup, onSettled, runWithOwner } from "solid-js";
|
|
2
2
|
import { bindEvent, saveCurrentDepth } from "./history.js";
|
|
3
3
|
const STORAGE_KEY = "solid-router:scroll";
|
|
4
|
+
const AUTO_KEY = "solid-router:scroll-auto";
|
|
4
5
|
/**
|
|
5
6
|
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
6
7
|
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
@@ -14,7 +15,7 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
14
15
|
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
15
16
|
* otherwise disables.
|
|
16
17
|
*
|
|
17
|
-
* Restoration is a single scroll once
|
|
18
|
+
* Restoration is a single scroll once the traversal settles — the same strategy
|
|
18
19
|
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
19
20
|
* transition commits is what makes the offset reachable; chasing a still-
|
|
20
21
|
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
@@ -24,18 +25,53 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
24
25
|
* layout changes can feed it back into itself. Content that commits after the
|
|
25
26
|
* transition settles — an image without reserved space, a boundary below the
|
|
26
27
|
* fold — keeps whatever offset the document can hold.
|
|
28
|
+
*
|
|
29
|
+
* A server-rendered document load is the exception: the browser restores it
|
|
30
|
+
* natively once the document — streamed boundaries included — has loaded,
|
|
31
|
+
* which a restore at the first client flush cannot match. So an entry is
|
|
32
|
+
* handed back to the browser (`auto`) whenever its document may unload
|
|
33
|
+
* (pagehide), and a hydrating router arriving on a handed-back entry leaves
|
|
34
|
+
* the restore to the browser: it does not scroll, and it takes the entry back
|
|
35
|
+
* (`manual`) only after load or at the first client navigation, whichever
|
|
36
|
+
* comes first. A programmatic scroll during the load cancels the native
|
|
37
|
+
* restore in Firefox and WebKit, and WebKit restores just after the load
|
|
38
|
+
* event, so a `manual` write before then — inside a load listener included —
|
|
39
|
+
* suppresses it; the write waits a task. Handed-back depths are tracked here
|
|
40
|
+
* rather than read from `history.scrollRestoration`, which Firefox reports as
|
|
41
|
+
* the mode before pagehide after a reload. An entry this router pushed and
|
|
42
|
+
* never handed back stays `manual` and gets no native restore, so the router
|
|
43
|
+
* restores it as on a client-rendered load.
|
|
27
44
|
*/
|
|
28
|
-
export function createScrollRestoration() {
|
|
29
|
-
window.history
|
|
45
|
+
export function createScrollRestoration(hydrating) {
|
|
46
|
+
const h = window.history;
|
|
30
47
|
// the current entry needs its depth stamp for captures to have a key, even
|
|
31
48
|
// if something replaced history.state after the adapter stamped it
|
|
32
49
|
saveCurrentDepth();
|
|
50
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
33
51
|
let positions = {};
|
|
52
|
+
let handedBack = {};
|
|
34
53
|
try {
|
|
35
54
|
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
55
|
+
handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
|
|
36
56
|
}
|
|
37
57
|
catch { }
|
|
38
|
-
const
|
|
58
|
+
const manual = () => {
|
|
59
|
+
h.scrollRestoration = "manual";
|
|
60
|
+
const d = depth();
|
|
61
|
+
if (d != null)
|
|
62
|
+
delete handedBack[d];
|
|
63
|
+
};
|
|
64
|
+
const d = depth();
|
|
65
|
+
let deferred = !!hydrating && (h.scrollRestoration === "auto" || (d != null && !!handedBack[d]));
|
|
66
|
+
if (!deferred)
|
|
67
|
+
manual();
|
|
68
|
+
const claim = () => {
|
|
69
|
+
if (!deferred)
|
|
70
|
+
return;
|
|
71
|
+
deferred = false;
|
|
72
|
+
manual();
|
|
73
|
+
};
|
|
74
|
+
let timer;
|
|
39
75
|
let programmatic = false;
|
|
40
76
|
let pending;
|
|
41
77
|
const unbind = [
|
|
@@ -48,11 +84,22 @@ export function createScrollRestoration() {
|
|
|
48
84
|
pending = undefined;
|
|
49
85
|
}),
|
|
50
86
|
bindEvent(window, "pagehide", () => {
|
|
87
|
+
h.scrollRestoration = "auto";
|
|
88
|
+
const d = depth();
|
|
89
|
+
if (d != null)
|
|
90
|
+
handedBack[d] = 1;
|
|
51
91
|
try {
|
|
52
92
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
93
|
+
sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
|
|
53
94
|
}
|
|
54
95
|
catch { }
|
|
55
|
-
})
|
|
96
|
+
}),
|
|
97
|
+
bindEvent(window, "pageshow", e => {
|
|
98
|
+
if (e.persisted)
|
|
99
|
+
manual();
|
|
100
|
+
}),
|
|
101
|
+
bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))),
|
|
102
|
+
() => clearTimeout(timer)
|
|
56
103
|
];
|
|
57
104
|
const restore = () => {
|
|
58
105
|
if (pending == null)
|
|
@@ -68,43 +115,40 @@ export function createScrollRestoration() {
|
|
|
68
115
|
programmatic = false;
|
|
69
116
|
};
|
|
70
117
|
return {
|
|
71
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* When the adapter notifies a traversal: mark the target and restore once
|
|
120
|
+
* the transition carrying it settles. The adapter notifies right before
|
|
121
|
+
* the location write, in the same tick, so the settle is that write's —
|
|
122
|
+
* a same-URL traversal included, which no location key would see.
|
|
123
|
+
*/
|
|
72
124
|
onPop() {
|
|
125
|
+
claim();
|
|
73
126
|
pending = depth();
|
|
127
|
+
runWithOwner(null, () => onSettled(restore));
|
|
74
128
|
},
|
|
129
|
+
/** The initial page settled: restore a reload/back_forward arrival. */
|
|
130
|
+
settled: () => restore(),
|
|
131
|
+
/** Before a client navigation writes history: the entry it creates copies the current mode. */
|
|
132
|
+
beforeWrite: claim,
|
|
75
133
|
/** After a push: forward entries died, and this depth may be reused. */
|
|
76
134
|
onPush() {
|
|
77
135
|
const d = depth();
|
|
78
|
-
if (d != null)
|
|
136
|
+
if (d != null) {
|
|
79
137
|
for (const k in positions)
|
|
80
138
|
+k >= d && delete positions[k];
|
|
139
|
+
for (const k in handedBack)
|
|
140
|
+
+k >= d && delete handedBack[k];
|
|
141
|
+
}
|
|
81
142
|
},
|
|
82
|
-
create(
|
|
83
|
-
// Restore once the traversal has settled: key on the location (a fully
|
|
84
|
-
// synchronous pop commits without isRouting ever flipping) and on
|
|
85
|
-
// isRouting, which reports in-flight transitions — native pops
|
|
86
|
-
// included — and holds the restore until they commit. restore() no-ops
|
|
87
|
-
// unless a traversal marked a target, so push navigations are inert.
|
|
88
|
-
// `transparent` keeps the effect invisible to the hydration id scheme —
|
|
89
|
-
// same reasoning as the link-claims effect (claims.ts): this setup is
|
|
90
|
-
// client-only, so an id-consuming node here has no server counterpart
|
|
91
|
-
// and every hydration id allocated after it shifts by one child slot.
|
|
92
|
-
// The visible failure is any <Loading> content that settled before the
|
|
93
|
-
// shell flush (a cache hit, a preloaded query): its serialized value and
|
|
94
|
-
// inlined markup are keyed under the server's ids, the shifted client
|
|
95
|
-
// misses both, recomputes, and re-renders the route fresh — duplicating
|
|
96
|
-
// the server DOM and leaving it inert.
|
|
97
|
-
createEffect(() => ({
|
|
98
|
-
url: router.location.pathname + router.location.search + router.location.hash,
|
|
99
|
-
routing: router.isRouting()
|
|
100
|
-
}), current => {
|
|
101
|
-
if (!current.routing)
|
|
102
|
-
restore();
|
|
103
|
-
}, { transparent: true });
|
|
143
|
+
create() {
|
|
104
144
|
onCleanup(() => unbind.forEach(u => u()));
|
|
105
145
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
106
|
-
// navigation starts a new one and belongs at the top); the
|
|
107
|
-
// initial
|
|
146
|
+
// navigation starts a new one and belongs at the top); the router calls
|
|
147
|
+
// `settled` once the initial page has; a deferred arrival is the browser's
|
|
148
|
+
if (deferred) {
|
|
149
|
+
document.readyState === "complete" && claim();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
108
152
|
const [nav] = performance.getEntriesByType?.("navigation");
|
|
109
153
|
if (nav && nav.type !== "navigate")
|
|
110
154
|
pending = depth();
|
|
@@ -121,6 +165,7 @@ export function withScrollRestoration(history, restoration) {
|
|
|
121
165
|
return {
|
|
122
166
|
...history,
|
|
123
167
|
set(next) {
|
|
168
|
+
restoration.beforeWrite();
|
|
124
169
|
history.set(next);
|
|
125
170
|
next.replace || restoration.onPush();
|
|
126
171
|
},
|
package/dist/routing.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare const useLocation: <S = unknown>() => Location<S>;
|
|
|
54
54
|
/**
|
|
55
55
|
* Retrieves a signal that indicates whether the router is currently processing a navigation.
|
|
56
56
|
* Useful for showing pending navigation state while the next route and its data settle.
|
|
57
|
+
* This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
|
|
58
|
+
* For the in-flight destination, read the location with Solid's `isPending`/`latest`.
|
|
57
59
|
*
|
|
58
60
|
* @example
|
|
59
61
|
* ```js
|
|
@@ -177,7 +179,11 @@ export interface LinkState {
|
|
|
177
179
|
* parameter order and hash aside — what `aria-current="page"` reflects.
|
|
178
180
|
*/
|
|
179
181
|
current: () => boolean;
|
|
180
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* This link is the target of an in-flight navigation (back/forward
|
|
184
|
+
* excluded). Works without the `pendingLinks` plugin, which only adds
|
|
185
|
+
* `data-pending` to plain anchors.
|
|
186
|
+
*/
|
|
181
187
|
pending: () => boolean;
|
|
182
188
|
}
|
|
183
189
|
/**
|