@solidjs/router 0.16.2 → 0.16.3
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/dist/index.js +124 -17
- package/dist/routers/Router.d.ts +8 -0
- package/dist/routers/Router.js +24 -10
- package/dist/routers/scrollRestoration.d.ts +24 -0
- package/dist/routers/scrollRestoration.js +106 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
|
|
2
|
-
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, splitProps, createResource, catchError } from 'solid-js';
|
|
2
|
+
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, createEffect, splitProps, createResource, catchError } from 'solid-js';
|
|
3
3
|
import { createStore, reconcile, unwrap } from 'solid-js/store';
|
|
4
4
|
|
|
5
5
|
function createBeforeLeave() {
|
|
@@ -1550,8 +1550,105 @@ function setupNativeEvents({
|
|
|
1550
1550
|
};
|
|
1551
1551
|
}
|
|
1552
1552
|
|
|
1553
|
+
const STORAGE_KEY = "solid-router:scroll";
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
1557
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
1558
|
+
* if the destination route forces a layout while the document is still short,
|
|
1559
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
1560
|
+
*
|
|
1561
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
1562
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
1563
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
1564
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
1565
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
1566
|
+
* otherwise disables. Restoration runs once routing settles; if the document
|
|
1567
|
+
* is still shorter than the target (a boundary below the fold hasn't
|
|
1568
|
+
* resolved), a ResizeObserver retries as content grows, cancelled by the
|
|
1569
|
+
* first user scroll.
|
|
1570
|
+
*/
|
|
1571
|
+
function createScrollRestoration() {
|
|
1572
|
+
window.history.scrollRestoration = "manual";
|
|
1573
|
+
// the current entry needs its depth stamp for captures to have a key, even
|
|
1574
|
+
// if something replaced history.state after the lifecycle module loaded
|
|
1575
|
+
saveCurrentDepth();
|
|
1576
|
+
let positions = {};
|
|
1577
|
+
try {
|
|
1578
|
+
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
1579
|
+
} catch {}
|
|
1580
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
1581
|
+
let programmatic = false;
|
|
1582
|
+
let pending;
|
|
1583
|
+
let disconnect;
|
|
1584
|
+
const cancelGuard = () => {
|
|
1585
|
+
disconnect && disconnect();
|
|
1586
|
+
disconnect = undefined;
|
|
1587
|
+
};
|
|
1588
|
+
const unbind = [bindEvent(window, "scroll", () => {
|
|
1589
|
+
const d = depth();
|
|
1590
|
+
if (d != null) positions[d] = window.scrollY;
|
|
1591
|
+
if (!programmatic) {
|
|
1592
|
+
// the user took over — a pending or chasing restore would yank them
|
|
1593
|
+
pending = undefined;
|
|
1594
|
+
cancelGuard();
|
|
1595
|
+
}
|
|
1596
|
+
}), bindEvent(window, "pagehide", () => {
|
|
1597
|
+
try {
|
|
1598
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
1599
|
+
} catch {}
|
|
1600
|
+
})];
|
|
1601
|
+
const restore = () => {
|
|
1602
|
+
if (pending == null) return;
|
|
1603
|
+
const y = positions[pending];
|
|
1604
|
+
pending = undefined;
|
|
1605
|
+
if (y == null) return;
|
|
1606
|
+
cancelGuard();
|
|
1607
|
+
const attempt = () => {
|
|
1608
|
+
programmatic = true;
|
|
1609
|
+
window.scrollTo(0, y);
|
|
1610
|
+
programmatic = false;
|
|
1611
|
+
// reachable once the document is tall enough to hold the offset
|
|
1612
|
+
return document.documentElement.scrollHeight - window.innerHeight >= y;
|
|
1613
|
+
};
|
|
1614
|
+
if (!attempt() && typeof ResizeObserver !== "undefined") {
|
|
1615
|
+
const observer = new ResizeObserver(() => attempt() && cancelGuard());
|
|
1616
|
+
observer.observe(document.documentElement);
|
|
1617
|
+
disconnect = () => observer.disconnect();
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
return {
|
|
1621
|
+
/** Before the router reacts to a popstate: mark the traversal target. */
|
|
1622
|
+
onPop() {
|
|
1623
|
+
pending = depth();
|
|
1624
|
+
},
|
|
1625
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
1626
|
+
onPush() {
|
|
1627
|
+
const d = depth();
|
|
1628
|
+
if (d != null) for (const k in positions) +k >= d && delete positions[k];
|
|
1629
|
+
},
|
|
1630
|
+
create(router) {
|
|
1631
|
+
createEffect(on(router.isRouting, routing => routing || restore(), {
|
|
1632
|
+
defer: true
|
|
1633
|
+
}));
|
|
1634
|
+
onCleanup(() => {
|
|
1635
|
+
unbind.forEach(u => u());
|
|
1636
|
+
cancelGuard();
|
|
1637
|
+
});
|
|
1638
|
+
// reload/back_forward document loads land on an existing entry; a fresh
|
|
1639
|
+
// navigation starts a new one and belongs at the top
|
|
1640
|
+
const [nav] = performance.getEntriesByType && performance.getEntriesByType("navigation");
|
|
1641
|
+
if (nav && nav.type !== "navigate") {
|
|
1642
|
+
pending = depth();
|
|
1643
|
+
restore();
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1553
1649
|
function Router(props) {
|
|
1554
1650
|
if (isServer) return StaticRouter(props);
|
|
1651
|
+
const restoration = props.scrollRestoration ? createScrollRestoration() : undefined;
|
|
1555
1652
|
const getSource = () => {
|
|
1556
1653
|
const url = window.location.pathname + window.location.search;
|
|
1557
1654
|
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
@@ -1576,23 +1673,33 @@ function Router(props) {
|
|
|
1576
1673
|
}
|
|
1577
1674
|
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
1578
1675
|
saveCurrentDepth();
|
|
1676
|
+
restoration && !replace && restoration.onPush();
|
|
1677
|
+
},
|
|
1678
|
+
init: notify => {
|
|
1679
|
+
const handler = notifyIfNotBlocked(notify, delta => {
|
|
1680
|
+
if (delta) {
|
|
1681
|
+
return !beforeLeave.confirm(delta);
|
|
1682
|
+
} else {
|
|
1683
|
+
const s = getSource();
|
|
1684
|
+
return !beforeLeave.confirm(s.value, {
|
|
1685
|
+
state: s.state
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
return bindEvent(window, "popstate", restoration ? () => {
|
|
1690
|
+
restoration.onPop();
|
|
1691
|
+
handler();
|
|
1692
|
+
} : handler);
|
|
1693
|
+
},
|
|
1694
|
+
create: router => {
|
|
1695
|
+
setupNativeEvents({
|
|
1696
|
+
preload: props.preload,
|
|
1697
|
+
explicitLinks: props.explicitLinks,
|
|
1698
|
+
actionBase: props.actionBase,
|
|
1699
|
+
transformUrl: props.transformUrl
|
|
1700
|
+
})(router);
|
|
1701
|
+
restoration && restoration.create(router);
|
|
1579
1702
|
},
|
|
1580
|
-
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
1581
|
-
if (delta) {
|
|
1582
|
-
return !beforeLeave.confirm(delta);
|
|
1583
|
-
} else {
|
|
1584
|
-
const s = getSource();
|
|
1585
|
-
return !beforeLeave.confirm(s.value, {
|
|
1586
|
-
state: s.state
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
})),
|
|
1590
|
-
create: setupNativeEvents({
|
|
1591
|
-
preload: props.preload,
|
|
1592
|
-
explicitLinks: props.explicitLinks,
|
|
1593
|
-
actionBase: props.actionBase,
|
|
1594
|
-
transformUrl: props.transformUrl
|
|
1595
|
-
}),
|
|
1596
1703
|
utils: {
|
|
1597
1704
|
go: delta => window.history.go(delta),
|
|
1598
1705
|
beforeLeave
|
package/dist/routers/Router.d.ts
CHANGED
|
@@ -5,5 +5,13 @@ export type RouterProps = BaseRouterProps & {
|
|
|
5
5
|
actionBase?: string;
|
|
6
6
|
explicitLinks?: boolean;
|
|
7
7
|
preload?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Opt-in explicit scroll restoration for back/forward navigation (read
|
|
10
|
+
* once, not reactive). Sets `history.scrollRestoration = "manual"` and
|
|
11
|
+
* restores saved positions after the navigation settles, replacing the
|
|
12
|
+
* browser heuristic that loses offsets when the destination route forces
|
|
13
|
+
* a layout while rendering (#577).
|
|
14
|
+
*/
|
|
15
|
+
scrollRestoration?: boolean;
|
|
8
16
|
};
|
|
9
17
|
export declare function Router(props: RouterProps): JSX.Element;
|
package/dist/routers/Router.js
CHANGED
|
@@ -3,9 +3,11 @@ import { createRouter, scrollToHash, bindEvent } from "./createRouter.js";
|
|
|
3
3
|
import { StaticRouter } from "./StaticRouter.js";
|
|
4
4
|
import { setupNativeEvents } from "../data/events.js";
|
|
5
5
|
import { createBeforeLeave, keepDepth, notifyIfNotBlocked, saveCurrentDepth } from "../lifecycle.js";
|
|
6
|
+
import { createScrollRestoration } from "./scrollRestoration.js";
|
|
6
7
|
export function Router(props) {
|
|
7
8
|
if (isServer)
|
|
8
9
|
return StaticRouter(props);
|
|
10
|
+
const restoration = props.scrollRestoration ? createScrollRestoration() : undefined;
|
|
9
11
|
const getSource = () => {
|
|
10
12
|
const url = window.location.pathname + window.location.search;
|
|
11
13
|
const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
|
|
@@ -26,17 +28,29 @@ export function Router(props) {
|
|
|
26
28
|
}
|
|
27
29
|
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
28
30
|
saveCurrentDepth();
|
|
31
|
+
restoration && !replace && restoration.onPush();
|
|
32
|
+
},
|
|
33
|
+
init: notify => {
|
|
34
|
+
const handler = notifyIfNotBlocked(notify, delta => {
|
|
35
|
+
if (delta) {
|
|
36
|
+
return !beforeLeave.confirm(delta);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const s = getSource();
|
|
40
|
+
return !beforeLeave.confirm(s.value, { state: s.state });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return bindEvent(window, "popstate", restoration
|
|
44
|
+
? () => {
|
|
45
|
+
restoration.onPop();
|
|
46
|
+
handler();
|
|
47
|
+
}
|
|
48
|
+
: handler);
|
|
49
|
+
},
|
|
50
|
+
create: router => {
|
|
51
|
+
setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase, transformUrl: props.transformUrl })(router);
|
|
52
|
+
restoration && restoration.create(router);
|
|
29
53
|
},
|
|
30
|
-
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
31
|
-
if (delta) {
|
|
32
|
-
return !beforeLeave.confirm(delta);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
const s = getSource();
|
|
36
|
-
return !beforeLeave.confirm(s.value, { state: s.state });
|
|
37
|
-
}
|
|
38
|
-
})),
|
|
39
|
-
create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase, transformUrl: props.transformUrl }),
|
|
40
54
|
utils: {
|
|
41
55
|
go: delta => window.history.go(delta),
|
|
42
56
|
beforeLeave
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RouterContext } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
4
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
5
|
+
* if the destination route forces a layout while the document is still short,
|
|
6
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
7
|
+
*
|
|
8
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
9
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
10
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
11
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
12
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
13
|
+
* otherwise disables. Restoration runs once routing settles; if the document
|
|
14
|
+
* is still shorter than the target (a boundary below the fold hasn't
|
|
15
|
+
* resolved), a ResizeObserver retries as content grows, cancelled by the
|
|
16
|
+
* first user scroll.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createScrollRestoration(): {
|
|
19
|
+
/** Before the router reacts to a popstate: mark the traversal target. */
|
|
20
|
+
onPop(): void;
|
|
21
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
22
|
+
onPush(): void;
|
|
23
|
+
create(router: RouterContext): void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createEffect, on, onCleanup } from "solid-js";
|
|
2
|
+
import { saveCurrentDepth } from "../lifecycle.js";
|
|
3
|
+
import { bindEvent } from "./createRouter.js";
|
|
4
|
+
const STORAGE_KEY = "solid-router:scroll";
|
|
5
|
+
/**
|
|
6
|
+
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
7
|
+
* native same-document heuristic is unreliable for suspense-driven rendering:
|
|
8
|
+
* if the destination route forces a layout while the document is still short,
|
|
9
|
+
* the saved offset for the previous entry is clamped and lost (#577).
|
|
10
|
+
*
|
|
11
|
+
* Positions are captured continuously from the scroll event, keyed by the
|
|
12
|
+
* `_depth` the router already stamps on every history entry — capturing at
|
|
13
|
+
* scroll time (rather than at exit) stays correct through `useBeforeLeave`
|
|
14
|
+
* blocked/reverted traversals. The map persists to sessionStorage on pagehide
|
|
15
|
+
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
16
|
+
* otherwise disables. Restoration runs once routing settles; if the document
|
|
17
|
+
* is still shorter than the target (a boundary below the fold hasn't
|
|
18
|
+
* resolved), a ResizeObserver retries as content grows, cancelled by the
|
|
19
|
+
* first user scroll.
|
|
20
|
+
*/
|
|
21
|
+
export function createScrollRestoration() {
|
|
22
|
+
window.history.scrollRestoration = "manual";
|
|
23
|
+
// the current entry needs its depth stamp for captures to have a key, even
|
|
24
|
+
// if something replaced history.state after the lifecycle module loaded
|
|
25
|
+
saveCurrentDepth();
|
|
26
|
+
let positions = {};
|
|
27
|
+
try {
|
|
28
|
+
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
32
|
+
let programmatic = false;
|
|
33
|
+
let pending;
|
|
34
|
+
let disconnect;
|
|
35
|
+
const cancelGuard = () => {
|
|
36
|
+
disconnect && disconnect();
|
|
37
|
+
disconnect = undefined;
|
|
38
|
+
};
|
|
39
|
+
const unbind = [
|
|
40
|
+
bindEvent(window, "scroll", () => {
|
|
41
|
+
const d = depth();
|
|
42
|
+
if (d != null)
|
|
43
|
+
positions[d] = window.scrollY;
|
|
44
|
+
if (!programmatic) {
|
|
45
|
+
// the user took over — a pending or chasing restore would yank them
|
|
46
|
+
pending = undefined;
|
|
47
|
+
cancelGuard();
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
bindEvent(window, "pagehide", () => {
|
|
51
|
+
try {
|
|
52
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
})
|
|
56
|
+
];
|
|
57
|
+
const restore = () => {
|
|
58
|
+
if (pending == null)
|
|
59
|
+
return;
|
|
60
|
+
const y = positions[pending];
|
|
61
|
+
pending = undefined;
|
|
62
|
+
if (y == null)
|
|
63
|
+
return;
|
|
64
|
+
cancelGuard();
|
|
65
|
+
const attempt = () => {
|
|
66
|
+
programmatic = true;
|
|
67
|
+
window.scrollTo(0, y);
|
|
68
|
+
programmatic = false;
|
|
69
|
+
// reachable once the document is tall enough to hold the offset
|
|
70
|
+
return document.documentElement.scrollHeight - window.innerHeight >= y;
|
|
71
|
+
};
|
|
72
|
+
if (!attempt() && typeof ResizeObserver !== "undefined") {
|
|
73
|
+
const observer = new ResizeObserver(() => attempt() && cancelGuard());
|
|
74
|
+
observer.observe(document.documentElement);
|
|
75
|
+
disconnect = () => observer.disconnect();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
/** Before the router reacts to a popstate: mark the traversal target. */
|
|
80
|
+
onPop() {
|
|
81
|
+
pending = depth();
|
|
82
|
+
},
|
|
83
|
+
/** After a push: forward entries died, and this depth may be reused. */
|
|
84
|
+
onPush() {
|
|
85
|
+
const d = depth();
|
|
86
|
+
if (d != null)
|
|
87
|
+
for (const k in positions)
|
|
88
|
+
+k >= d && delete positions[k];
|
|
89
|
+
},
|
|
90
|
+
create(router) {
|
|
91
|
+
createEffect(on(router.isRouting, routing => routing || restore(), { defer: true }));
|
|
92
|
+
onCleanup(() => {
|
|
93
|
+
unbind.forEach(u => u());
|
|
94
|
+
cancelGuard();
|
|
95
|
+
});
|
|
96
|
+
// reload/back_forward document loads land on an existing entry; a fresh
|
|
97
|
+
// navigation starts a new one and belongs at the top
|
|
98
|
+
const [nav] = (performance.getEntriesByType &&
|
|
99
|
+
performance.getEntriesByType("navigation"));
|
|
100
|
+
if (nav && nav.type !== "navigate") {
|
|
101
|
+
pending = depth();
|
|
102
|
+
restore();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|