@real-router/solid 0.3.1 → 0.4.1
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 +11 -0
- package/dist/cjs/index.js +124 -75
- package/dist/esm/index.mjs +126 -77
- package/dist/types/RouterProvider.d.ts +1 -0
- package/dist/types/RouterProvider.d.ts.map +1 -1
- package/dist/types/components/Link.d.ts.map +1 -1
- package/dist/types/components/RouteView/components.d.ts.map +1 -1
- package/dist/types/components/RouteView/helpers.d.ts +2 -4
- package/dist/types/components/RouteView/helpers.d.ts.map +1 -1
- package/dist/types/components/RouterErrorBoundary.d.ts.map +1 -1
- package/dist/types/createSignalFromSource.d.ts.map +1 -1
- package/dist/types/createStoreFromSource.d.ts.map +1 -1
- package/dist/types/dom-utils/index.d.ts +1 -1
- package/dist/types/dom-utils/index.d.ts.map +1 -1
- package/dist/types/dom-utils/link-utils.d.ts +2 -1
- package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
- package/dist/types/dom-utils/route-announcer.d.ts.map +1 -1
- package/dist/types/hooks/useRouteNode.d.ts.map +1 -1
- package/dist/types/hooks/useRouteStore.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/RouterProvider.tsx +1 -1
- package/src/components/Link.tsx +6 -3
- package/src/components/RouteView/RouteView.tsx +13 -13
- package/src/components/RouteView/components.tsx +12 -6
- package/src/components/RouteView/helpers.tsx +19 -17
- package/src/components/RouterErrorBoundary.tsx +9 -17
- package/src/createSignalFromSource.ts +9 -1
- package/src/createStoreFromSource.ts +1 -3
- package/src/hooks/useRouteNode.tsx +1 -2
- package/src/hooks/useRouteStore.tsx +0 -8
- package/src/hooks/useRouterTransition.tsx +3 -3
- package/dist/types/hooks/useRouterError.d.ts +0 -4
- package/dist/types/hooks/useRouterError.d.ts.map +0 -1
- package/src/hooks/useRouterError.tsx +0 -23
package/README.md
CHANGED
|
@@ -90,6 +90,17 @@ function UserProfile() {
|
|
|
90
90
|
|
|
91
91
|
Signal-based hooks (`useRoute`, `useRouteNode`) remain available for simpler use cases.
|
|
92
92
|
|
|
93
|
+
### Primitives
|
|
94
|
+
|
|
95
|
+
Two low-level bridges convert `@real-router/sources` `RouterSource<T>` instances into Solid reactive primitives. Use them when you build custom hooks on top of `@real-router/sources`:
|
|
96
|
+
|
|
97
|
+
| Primitive | Returns | Description |
|
|
98
|
+
| ------------------------ | ------------------ | ------------------------------------------------------------- |
|
|
99
|
+
| `createSignalFromSource` | `Accessor<T>` | Bridges a source to a Solid signal. Calls `onCleanup`. |
|
|
100
|
+
| `createStoreFromSource` | `T` (Solid store) | Bridges a source to a Solid store via `createStore + reconcile`. |
|
|
101
|
+
|
|
102
|
+
Both must be called inside a reactive owner (component body or `createRoot`).
|
|
103
|
+
|
|
93
104
|
```tsx
|
|
94
105
|
// useRouteNode — updates only when "users.*" changes
|
|
95
106
|
function UsersLayout() {
|
package/dist/cjs/index.js
CHANGED
|
@@ -8,8 +8,10 @@ var routeUtils = require('@real-router/route-utils');
|
|
|
8
8
|
var store = require('solid-js/store');
|
|
9
9
|
var core = require('@real-router/core');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Local (non-global) Symbols — Symbol.for() would expose markers to spoofing
|
|
12
|
+
// via the global Symbol registry. See Gotchas section "RouteView Marker Objects".
|
|
13
|
+
const MATCH_MARKER = Symbol("RouteView.Match");
|
|
14
|
+
const NOT_FOUND_MARKER = Symbol("RouteView.NotFound");
|
|
13
15
|
function Match(props) {
|
|
14
16
|
const result = {
|
|
15
17
|
$$type: MATCH_MARKER,
|
|
@@ -20,6 +22,10 @@ function Match(props) {
|
|
|
20
22
|
return props.children;
|
|
21
23
|
}
|
|
22
24
|
};
|
|
25
|
+
|
|
26
|
+
// Marker object is identified by $$type Symbol in RouteView/helpers.tsx,
|
|
27
|
+
// not rendered as JSX. Cast required because JSX.Element does not include
|
|
28
|
+
// arbitrary marker shapes.
|
|
23
29
|
return result;
|
|
24
30
|
}
|
|
25
31
|
Match.displayName = "RouteView.Match";
|
|
@@ -30,6 +36,8 @@ function NotFound(props) {
|
|
|
30
36
|
return props.children;
|
|
31
37
|
}
|
|
32
38
|
};
|
|
39
|
+
|
|
40
|
+
// See Match for the marker-pattern rationale.
|
|
33
41
|
return result;
|
|
34
42
|
}
|
|
35
43
|
NotFound.displayName = "RouteView.NotFound";
|
|
@@ -69,37 +77,44 @@ function buildRenderList(elements, routeName, nodeName) {
|
|
|
69
77
|
notFoundChildren = child.children;
|
|
70
78
|
continue;
|
|
71
79
|
}
|
|
80
|
+
if (activeMatchFound) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
72
83
|
const {
|
|
73
84
|
segment,
|
|
74
85
|
exact,
|
|
75
86
|
fallback
|
|
76
87
|
} = child;
|
|
77
88
|
const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
activeMatchFound = true;
|
|
81
|
-
const matchContent = child.children;
|
|
82
|
-
const content = fallback === undefined ? matchContent : web.createComponent(solidJs.Suspense, {
|
|
83
|
-
fallback: fallback,
|
|
84
|
-
children: matchContent
|
|
85
|
-
});
|
|
86
|
-
rendered.push(content);
|
|
89
|
+
if (!isSegmentMatch(routeName, fullSegmentName, exact)) {
|
|
90
|
+
continue;
|
|
87
91
|
}
|
|
92
|
+
activeMatchFound = true;
|
|
93
|
+
rendered.push(fallback === undefined ? child.children : web.createComponent(solidJs.Suspense, {
|
|
94
|
+
fallback: fallback,
|
|
95
|
+
get children() {
|
|
96
|
+
return child.children;
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
88
99
|
}
|
|
89
100
|
if (!activeMatchFound && routeName === core.UNKNOWN_ROUTE && notFoundChildren !== null) {
|
|
90
101
|
rendered.push(notFoundChildren);
|
|
91
102
|
}
|
|
92
|
-
return
|
|
93
|
-
rendered,
|
|
94
|
-
activeMatchFound
|
|
95
|
-
};
|
|
103
|
+
return rendered;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
function createSignalFromSource(source) {
|
|
99
107
|
const [value, setValue] = solidJs.createSignal(source.getSnapshot());
|
|
108
|
+
const sync = () => source.getSnapshot();
|
|
100
109
|
const unsubscribe = source.subscribe(() => {
|
|
101
|
-
setValue(
|
|
110
|
+
setValue(sync);
|
|
102
111
|
});
|
|
112
|
+
|
|
113
|
+
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
114
|
+
// onFirstSubscribe (when reused after disconnect via cache). Listener is not
|
|
115
|
+
// notified for that internal update, so we must sync the signal manually.
|
|
116
|
+
// No-op when snapshot is unchanged (signal equality check).
|
|
117
|
+
setValue(sync);
|
|
103
118
|
solidJs.onCleanup(() => {
|
|
104
119
|
unsubscribe();
|
|
105
120
|
});
|
|
@@ -119,27 +134,24 @@ const useRouter = () => {
|
|
|
119
134
|
|
|
120
135
|
function useRouteNode(nodeName) {
|
|
121
136
|
const router = useRouter();
|
|
122
|
-
|
|
123
|
-
return createSignalFromSource(store);
|
|
137
|
+
return createSignalFromSource(sources.createRouteNodeSource(router, nodeName));
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
function RouteViewRoot(props) {
|
|
127
141
|
const routeState = useRouteNode(props.nodeName);
|
|
128
142
|
const resolved = solidJs.children(() => props.children);
|
|
143
|
+
const elements = solidJs.createMemo(() => {
|
|
144
|
+
const arr = [];
|
|
145
|
+
collectElements(resolved(), arr);
|
|
146
|
+
return arr;
|
|
147
|
+
});
|
|
129
148
|
return web.memo(() => {
|
|
130
149
|
const state = routeState();
|
|
131
150
|
if (!state.route) {
|
|
132
151
|
return null;
|
|
133
152
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const {
|
|
137
|
-
rendered
|
|
138
|
-
} = buildRenderList(elements, state.route.name, props.nodeName);
|
|
139
|
-
if (rendered.length > 0) {
|
|
140
|
-
return rendered;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
153
|
+
const rendered = buildRenderList(elements(), state.route.name, props.nodeName);
|
|
154
|
+
return rendered.length > 0 ? rendered : null;
|
|
143
155
|
});
|
|
144
156
|
}
|
|
145
157
|
RouteViewRoot.displayName = "RouteView";
|
|
@@ -165,14 +177,36 @@ const INTERNAL_ROUTE_PREFIX = "@@";
|
|
|
165
177
|
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
|
|
166
178
|
function createRouteAnnouncer(router, options) {
|
|
167
179
|
const prefix = "Navigated to ";
|
|
180
|
+
const getCustomText = options?.getAnnouncementText;
|
|
168
181
|
let isInitialNavigation = true;
|
|
169
182
|
let isReady = false;
|
|
170
183
|
let isDestroyed = false;
|
|
171
184
|
let lastAnnouncedText = "";
|
|
185
|
+
let pendingText = null;
|
|
172
186
|
let clearTimeoutId;
|
|
173
187
|
const announcer = getOrCreateAnnouncer();
|
|
188
|
+
const doAnnounce = (text, h1) => {
|
|
189
|
+
lastAnnouncedText = text;
|
|
190
|
+
clearTimeout(clearTimeoutId);
|
|
191
|
+
announcer.textContent = text;
|
|
192
|
+
clearTimeoutId = setTimeout(() => {
|
|
193
|
+
announcer.textContent = "";
|
|
194
|
+
lastAnnouncedText = "";
|
|
195
|
+
}, CLEAR_DELAY);
|
|
196
|
+
manageFocus(h1);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
200
|
+
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
201
|
+
// before marking the announcer "ready" — any navigation during that window is
|
|
202
|
+
// buffered in pendingText and flushed once the delay expires.
|
|
174
203
|
const safariTimeoutId = setTimeout(() => {
|
|
175
204
|
isReady = true;
|
|
205
|
+
if (pendingText !== null && !isDestroyed) {
|
|
206
|
+
const text = pendingText;
|
|
207
|
+
pendingText = null;
|
|
208
|
+
doAnnounce(text, document.querySelector("h1"));
|
|
209
|
+
}
|
|
176
210
|
}, SAFARI_READY_DELAY);
|
|
177
211
|
const unsubscribe = router.subscribe(({
|
|
178
212
|
route
|
|
@@ -181,22 +215,28 @@ function createRouteAnnouncer(router, options) {
|
|
|
181
215
|
isInitialNavigation = false;
|
|
182
216
|
return;
|
|
183
217
|
}
|
|
218
|
+
|
|
219
|
+
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
220
|
+
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
221
|
+
// Single rAF fires before the new route's template has been attached,
|
|
222
|
+
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
223
|
+
// document.title / route.name prematurely.
|
|
184
224
|
requestAnimationFrame(() => {
|
|
185
225
|
requestAnimationFrame(() => {
|
|
186
226
|
if (isDestroyed) {
|
|
187
227
|
return;
|
|
188
228
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
manageFocus();
|
|
229
|
+
const h1 = document.querySelector("h1");
|
|
230
|
+
const text = resolveText(route, prefix, getCustomText, h1);
|
|
231
|
+
if (!text || text === lastAnnouncedText) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!isReady) {
|
|
235
|
+
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
236
|
+
pendingText = text;
|
|
237
|
+
return;
|
|
199
238
|
}
|
|
239
|
+
doAnnounce(text, h1);
|
|
200
240
|
});
|
|
201
241
|
});
|
|
202
242
|
});
|
|
@@ -226,15 +266,13 @@ function getOrCreateAnnouncer() {
|
|
|
226
266
|
function removeAnnouncer() {
|
|
227
267
|
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
228
268
|
}
|
|
229
|
-
function resolveText(route, prefix, getCustomText) {
|
|
230
|
-
const h1 = document.querySelector("h1");
|
|
269
|
+
function resolveText(route, prefix, getCustomText, h1) {
|
|
231
270
|
const h1Text = h1?.textContent.trim() ?? "";
|
|
232
271
|
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX) ? "" : route.name;
|
|
233
272
|
const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
|
|
234
273
|
return `${prefix}${rawText}`;
|
|
235
274
|
}
|
|
236
|
-
function manageFocus() {
|
|
237
|
-
const h1 = document.querySelector("h1");
|
|
275
|
+
function manageFocus(h1) {
|
|
238
276
|
if (!h1) {
|
|
239
277
|
return;
|
|
240
278
|
}
|
|
@@ -253,7 +291,10 @@ function buildHref(router, routeName, routeParams) {
|
|
|
253
291
|
try {
|
|
254
292
|
const buildUrl = router.buildUrl;
|
|
255
293
|
if (buildUrl) {
|
|
256
|
-
|
|
294
|
+
const url = buildUrl(routeName, routeParams);
|
|
295
|
+
if (url !== undefined) {
|
|
296
|
+
return url;
|
|
297
|
+
}
|
|
257
298
|
}
|
|
258
299
|
return router.buildPath(routeName, routeParams);
|
|
259
300
|
} catch {
|
|
@@ -261,20 +302,41 @@ function buildHref(router, routeName, routeParams) {
|
|
|
261
302
|
return undefined;
|
|
262
303
|
}
|
|
263
304
|
}
|
|
305
|
+
function parseTokens(value) {
|
|
306
|
+
return value ? value.match(/\S+/g) ?? [] : [];
|
|
307
|
+
}
|
|
264
308
|
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
265
309
|
if (isActive && activeClassName) {
|
|
266
|
-
|
|
310
|
+
const activeTokens = parseTokens(activeClassName);
|
|
311
|
+
if (activeTokens.length === 0) {
|
|
312
|
+
return baseClassName ?? undefined;
|
|
313
|
+
}
|
|
314
|
+
if (!baseClassName) {
|
|
315
|
+
return activeTokens.join(" ");
|
|
316
|
+
}
|
|
317
|
+
const baseTokens = parseTokens(baseClassName);
|
|
318
|
+
const seen = new Set(baseTokens);
|
|
319
|
+
for (const token of activeTokens) {
|
|
320
|
+
if (!seen.has(token)) {
|
|
321
|
+
seen.add(token);
|
|
322
|
+
baseTokens.push(token);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return baseTokens.join(" ");
|
|
267
326
|
}
|
|
268
327
|
return baseClassName ?? undefined;
|
|
269
328
|
}
|
|
270
329
|
function applyLinkA11y(element) {
|
|
330
|
+
if (!element) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
271
333
|
if (element instanceof HTMLAnchorElement || element instanceof HTMLButtonElement) {
|
|
272
334
|
return;
|
|
273
335
|
}
|
|
274
|
-
if (!element.
|
|
336
|
+
if (!element.hasAttribute("role")) {
|
|
275
337
|
element.setAttribute("role", "link");
|
|
276
338
|
}
|
|
277
|
-
if (!element.
|
|
339
|
+
if (!element.hasAttribute("tabindex")) {
|
|
278
340
|
element.setAttribute("tabindex", "0");
|
|
279
341
|
}
|
|
280
342
|
}
|
|
@@ -289,9 +351,12 @@ function Link(props) {
|
|
|
289
351
|
ignoreQueryParams: true
|
|
290
352
|
}, props);
|
|
291
353
|
const [local, rest] = solidJs.splitProps(merged, ["routeName", "routeParams", "routeOptions", "activeClassName", "activeStrict", "ignoreQueryParams", "onClick", "target", "class", "children"]);
|
|
292
|
-
const router = useRouter();
|
|
293
354
|
const ctx = solidJs.useContext(RouterContext);
|
|
294
|
-
|
|
355
|
+
if (!ctx) {
|
|
356
|
+
throw new Error("Link must be used within a RouterProvider");
|
|
357
|
+
}
|
|
358
|
+
const router = ctx.router;
|
|
359
|
+
const useFastPath = !local.activeStrict && local.ignoreQueryParams && local.routeParams === EMPTY_PARAMS;
|
|
295
360
|
const isActive = useFastPath ? () => ctx.routeSelector(local.routeName) : createSignalFromSource(sources.createActiveRouteSource(router, local.routeName, local.routeParams, {
|
|
296
361
|
strict: local.activeStrict,
|
|
297
362
|
ignoreQueryParams: local.ignoreQueryParams
|
|
@@ -327,34 +392,20 @@ function Link(props) {
|
|
|
327
392
|
})();
|
|
328
393
|
}
|
|
329
394
|
|
|
330
|
-
const cache = new WeakMap();
|
|
331
|
-
function useRouterError() {
|
|
332
|
-
const router = useRouter();
|
|
333
|
-
let source = cache.get(router);
|
|
334
|
-
if (!source) {
|
|
335
|
-
source = sources.createErrorSource(router);
|
|
336
|
-
cache.set(router, source);
|
|
337
|
-
}
|
|
338
|
-
return createSignalFromSource(source);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
395
|
function RouterErrorBoundary(props) {
|
|
342
|
-
const
|
|
343
|
-
const
|
|
396
|
+
const router = useRouter();
|
|
397
|
+
const snapshot = createSignalFromSource(sources.createDismissableError(router));
|
|
344
398
|
solidJs.createEffect(() => {
|
|
345
399
|
const snap = snapshot();
|
|
346
400
|
if (snap.error) {
|
|
347
401
|
props.onError?.(snap.error, snap.toRoute, snap.fromRoute);
|
|
348
402
|
}
|
|
349
403
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return [web.memo(() => props.children), web.memo(() => {
|
|
356
|
-
const error = visibleError();
|
|
357
|
-
return error ? props.fallback(error, resetError) : null;
|
|
404
|
+
return [web.memo(() => props.children), web.createComponent(solidJs.Show, {
|
|
405
|
+
get when() {
|
|
406
|
+
return snapshot().error;
|
|
407
|
+
},
|
|
408
|
+
children: error => props.fallback(error(), snapshot().resetError)
|
|
358
409
|
})];
|
|
359
410
|
}
|
|
360
411
|
|
|
@@ -424,7 +475,9 @@ const useRoute = () => {
|
|
|
424
475
|
};
|
|
425
476
|
|
|
426
477
|
function createStoreFromSource(source) {
|
|
427
|
-
const [state, setState] = store.createStore(
|
|
478
|
+
const [state, setState] = store.createStore({
|
|
479
|
+
...source.getSnapshot()
|
|
480
|
+
});
|
|
428
481
|
const unsubscribe = source.subscribe(() => {
|
|
429
482
|
setState(store.reconcile(source.getSnapshot()));
|
|
430
483
|
});
|
|
@@ -433,10 +486,6 @@ function createStoreFromSource(source) {
|
|
|
433
486
|
}
|
|
434
487
|
|
|
435
488
|
function useRouteStore() {
|
|
436
|
-
const ctx = solidJs.useContext(RouteContext);
|
|
437
|
-
if (!ctx) {
|
|
438
|
-
throw new Error("useRouteStore must be used within a RouterProvider");
|
|
439
|
-
}
|
|
440
489
|
const router = useRouter();
|
|
441
490
|
return createStoreFromSource(sources.createRouteSource(router));
|
|
442
491
|
}
|
|
@@ -448,8 +497,8 @@ function useRouteNodeStore(nodeName) {
|
|
|
448
497
|
|
|
449
498
|
function useRouterTransition() {
|
|
450
499
|
const router = useRouter();
|
|
451
|
-
const
|
|
452
|
-
return createSignalFromSource(
|
|
500
|
+
const source = sources.getTransitionSource(router);
|
|
501
|
+
return createSignalFromSource(source);
|
|
453
502
|
}
|
|
454
503
|
|
|
455
504
|
function isRouteActive(linkRouteName, currentRouteName) {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createComponent, memo, spread, mergeProps as mergeProps$1, insert, template } from 'solid-js/web';
|
|
2
|
-
import { createRouteNodeSource, createActiveRouteSource,
|
|
3
|
-
import { Suspense, createSignal, onCleanup, createContext, useContext, children, mergeProps, splitProps,
|
|
2
|
+
import { createRouteNodeSource, createActiveRouteSource, createDismissableError, createRouteSource, getTransitionSource } from '@real-router/sources';
|
|
3
|
+
import { Suspense, createSignal, onCleanup, createContext, useContext, children, createMemo, mergeProps, splitProps, createEffect, Show, onMount, createSelector } from 'solid-js';
|
|
4
4
|
import { getPluginApi } from '@real-router/core/api';
|
|
5
5
|
import { startsWithSegment, getRouteUtils } from '@real-router/route-utils';
|
|
6
6
|
import { createStore, reconcile } from 'solid-js/store';
|
|
7
7
|
import { UNKNOWN_ROUTE, getNavigator } from '@real-router/core';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Local (non-global) Symbols — Symbol.for() would expose markers to spoofing
|
|
10
|
+
// via the global Symbol registry. See Gotchas section "RouteView Marker Objects".
|
|
11
|
+
const MATCH_MARKER = Symbol("RouteView.Match");
|
|
12
|
+
const NOT_FOUND_MARKER = Symbol("RouteView.NotFound");
|
|
11
13
|
function Match(props) {
|
|
12
14
|
const result = {
|
|
13
15
|
$$type: MATCH_MARKER,
|
|
@@ -18,6 +20,10 @@ function Match(props) {
|
|
|
18
20
|
return props.children;
|
|
19
21
|
}
|
|
20
22
|
};
|
|
23
|
+
|
|
24
|
+
// Marker object is identified by $$type Symbol in RouteView/helpers.tsx,
|
|
25
|
+
// not rendered as JSX. Cast required because JSX.Element does not include
|
|
26
|
+
// arbitrary marker shapes.
|
|
21
27
|
return result;
|
|
22
28
|
}
|
|
23
29
|
Match.displayName = "RouteView.Match";
|
|
@@ -28,6 +34,8 @@ function NotFound(props) {
|
|
|
28
34
|
return props.children;
|
|
29
35
|
}
|
|
30
36
|
};
|
|
37
|
+
|
|
38
|
+
// See Match for the marker-pattern rationale.
|
|
31
39
|
return result;
|
|
32
40
|
}
|
|
33
41
|
NotFound.displayName = "RouteView.NotFound";
|
|
@@ -67,37 +75,44 @@ function buildRenderList(elements, routeName, nodeName) {
|
|
|
67
75
|
notFoundChildren = child.children;
|
|
68
76
|
continue;
|
|
69
77
|
}
|
|
78
|
+
if (activeMatchFound) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
70
81
|
const {
|
|
71
82
|
segment,
|
|
72
83
|
exact,
|
|
73
84
|
fallback
|
|
74
85
|
} = child;
|
|
75
86
|
const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
activeMatchFound = true;
|
|
79
|
-
const matchContent = child.children;
|
|
80
|
-
const content = fallback === undefined ? matchContent : createComponent(Suspense, {
|
|
81
|
-
fallback: fallback,
|
|
82
|
-
children: matchContent
|
|
83
|
-
});
|
|
84
|
-
rendered.push(content);
|
|
87
|
+
if (!isSegmentMatch(routeName, fullSegmentName, exact)) {
|
|
88
|
+
continue;
|
|
85
89
|
}
|
|
90
|
+
activeMatchFound = true;
|
|
91
|
+
rendered.push(fallback === undefined ? child.children : createComponent(Suspense, {
|
|
92
|
+
fallback: fallback,
|
|
93
|
+
get children() {
|
|
94
|
+
return child.children;
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
86
97
|
}
|
|
87
98
|
if (!activeMatchFound && routeName === UNKNOWN_ROUTE && notFoundChildren !== null) {
|
|
88
99
|
rendered.push(notFoundChildren);
|
|
89
100
|
}
|
|
90
|
-
return
|
|
91
|
-
rendered,
|
|
92
|
-
activeMatchFound
|
|
93
|
-
};
|
|
101
|
+
return rendered;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
function createSignalFromSource(source) {
|
|
97
105
|
const [value, setValue] = createSignal(source.getSnapshot());
|
|
106
|
+
const sync = () => source.getSnapshot();
|
|
98
107
|
const unsubscribe = source.subscribe(() => {
|
|
99
|
-
setValue(
|
|
108
|
+
setValue(sync);
|
|
100
109
|
});
|
|
110
|
+
|
|
111
|
+
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
112
|
+
// onFirstSubscribe (when reused after disconnect via cache). Listener is not
|
|
113
|
+
// notified for that internal update, so we must sync the signal manually.
|
|
114
|
+
// No-op when snapshot is unchanged (signal equality check).
|
|
115
|
+
setValue(sync);
|
|
101
116
|
onCleanup(() => {
|
|
102
117
|
unsubscribe();
|
|
103
118
|
});
|
|
@@ -117,27 +132,24 @@ const useRouter = () => {
|
|
|
117
132
|
|
|
118
133
|
function useRouteNode(nodeName) {
|
|
119
134
|
const router = useRouter();
|
|
120
|
-
|
|
121
|
-
return createSignalFromSource(store);
|
|
135
|
+
return createSignalFromSource(createRouteNodeSource(router, nodeName));
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
function RouteViewRoot(props) {
|
|
125
139
|
const routeState = useRouteNode(props.nodeName);
|
|
126
140
|
const resolved = children(() => props.children);
|
|
141
|
+
const elements = createMemo(() => {
|
|
142
|
+
const arr = [];
|
|
143
|
+
collectElements(resolved(), arr);
|
|
144
|
+
return arr;
|
|
145
|
+
});
|
|
127
146
|
return memo(() => {
|
|
128
147
|
const state = routeState();
|
|
129
148
|
if (!state.route) {
|
|
130
149
|
return null;
|
|
131
150
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const {
|
|
135
|
-
rendered
|
|
136
|
-
} = buildRenderList(elements, state.route.name, props.nodeName);
|
|
137
|
-
if (rendered.length > 0) {
|
|
138
|
-
return rendered;
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
151
|
+
const rendered = buildRenderList(elements(), state.route.name, props.nodeName);
|
|
152
|
+
return rendered.length > 0 ? rendered : null;
|
|
141
153
|
});
|
|
142
154
|
}
|
|
143
155
|
RouteViewRoot.displayName = "RouteView";
|
|
@@ -163,14 +175,36 @@ const INTERNAL_ROUTE_PREFIX = "@@";
|
|
|
163
175
|
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
|
|
164
176
|
function createRouteAnnouncer(router, options) {
|
|
165
177
|
const prefix = "Navigated to ";
|
|
178
|
+
const getCustomText = options?.getAnnouncementText;
|
|
166
179
|
let isInitialNavigation = true;
|
|
167
180
|
let isReady = false;
|
|
168
181
|
let isDestroyed = false;
|
|
169
182
|
let lastAnnouncedText = "";
|
|
183
|
+
let pendingText = null;
|
|
170
184
|
let clearTimeoutId;
|
|
171
185
|
const announcer = getOrCreateAnnouncer();
|
|
186
|
+
const doAnnounce = (text, h1) => {
|
|
187
|
+
lastAnnouncedText = text;
|
|
188
|
+
clearTimeout(clearTimeoutId);
|
|
189
|
+
announcer.textContent = text;
|
|
190
|
+
clearTimeoutId = setTimeout(() => {
|
|
191
|
+
announcer.textContent = "";
|
|
192
|
+
lastAnnouncedText = "";
|
|
193
|
+
}, CLEAR_DELAY);
|
|
194
|
+
manageFocus(h1);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
198
|
+
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
199
|
+
// before marking the announcer "ready" — any navigation during that window is
|
|
200
|
+
// buffered in pendingText and flushed once the delay expires.
|
|
172
201
|
const safariTimeoutId = setTimeout(() => {
|
|
173
202
|
isReady = true;
|
|
203
|
+
if (pendingText !== null && !isDestroyed) {
|
|
204
|
+
const text = pendingText;
|
|
205
|
+
pendingText = null;
|
|
206
|
+
doAnnounce(text, document.querySelector("h1"));
|
|
207
|
+
}
|
|
174
208
|
}, SAFARI_READY_DELAY);
|
|
175
209
|
const unsubscribe = router.subscribe(({
|
|
176
210
|
route
|
|
@@ -179,22 +213,28 @@ function createRouteAnnouncer(router, options) {
|
|
|
179
213
|
isInitialNavigation = false;
|
|
180
214
|
return;
|
|
181
215
|
}
|
|
216
|
+
|
|
217
|
+
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
218
|
+
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
219
|
+
// Single rAF fires before the new route's template has been attached,
|
|
220
|
+
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
221
|
+
// document.title / route.name prematurely.
|
|
182
222
|
requestAnimationFrame(() => {
|
|
183
223
|
requestAnimationFrame(() => {
|
|
184
224
|
if (isDestroyed) {
|
|
185
225
|
return;
|
|
186
226
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
manageFocus();
|
|
227
|
+
const h1 = document.querySelector("h1");
|
|
228
|
+
const text = resolveText(route, prefix, getCustomText, h1);
|
|
229
|
+
if (!text || text === lastAnnouncedText) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!isReady) {
|
|
233
|
+
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
234
|
+
pendingText = text;
|
|
235
|
+
return;
|
|
197
236
|
}
|
|
237
|
+
doAnnounce(text, h1);
|
|
198
238
|
});
|
|
199
239
|
});
|
|
200
240
|
});
|
|
@@ -224,15 +264,13 @@ function getOrCreateAnnouncer() {
|
|
|
224
264
|
function removeAnnouncer() {
|
|
225
265
|
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
226
266
|
}
|
|
227
|
-
function resolveText(route, prefix, getCustomText) {
|
|
228
|
-
const h1 = document.querySelector("h1");
|
|
267
|
+
function resolveText(route, prefix, getCustomText, h1) {
|
|
229
268
|
const h1Text = h1?.textContent.trim() ?? "";
|
|
230
269
|
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX) ? "" : route.name;
|
|
231
270
|
const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
|
|
232
271
|
return `${prefix}${rawText}`;
|
|
233
272
|
}
|
|
234
|
-
function manageFocus() {
|
|
235
|
-
const h1 = document.querySelector("h1");
|
|
273
|
+
function manageFocus(h1) {
|
|
236
274
|
if (!h1) {
|
|
237
275
|
return;
|
|
238
276
|
}
|
|
@@ -251,7 +289,10 @@ function buildHref(router, routeName, routeParams) {
|
|
|
251
289
|
try {
|
|
252
290
|
const buildUrl = router.buildUrl;
|
|
253
291
|
if (buildUrl) {
|
|
254
|
-
|
|
292
|
+
const url = buildUrl(routeName, routeParams);
|
|
293
|
+
if (url !== undefined) {
|
|
294
|
+
return url;
|
|
295
|
+
}
|
|
255
296
|
}
|
|
256
297
|
return router.buildPath(routeName, routeParams);
|
|
257
298
|
} catch {
|
|
@@ -259,20 +300,41 @@ function buildHref(router, routeName, routeParams) {
|
|
|
259
300
|
return undefined;
|
|
260
301
|
}
|
|
261
302
|
}
|
|
303
|
+
function parseTokens(value) {
|
|
304
|
+
return value ? value.match(/\S+/g) ?? [] : [];
|
|
305
|
+
}
|
|
262
306
|
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
263
307
|
if (isActive && activeClassName) {
|
|
264
|
-
|
|
308
|
+
const activeTokens = parseTokens(activeClassName);
|
|
309
|
+
if (activeTokens.length === 0) {
|
|
310
|
+
return baseClassName ?? undefined;
|
|
311
|
+
}
|
|
312
|
+
if (!baseClassName) {
|
|
313
|
+
return activeTokens.join(" ");
|
|
314
|
+
}
|
|
315
|
+
const baseTokens = parseTokens(baseClassName);
|
|
316
|
+
const seen = new Set(baseTokens);
|
|
317
|
+
for (const token of activeTokens) {
|
|
318
|
+
if (!seen.has(token)) {
|
|
319
|
+
seen.add(token);
|
|
320
|
+
baseTokens.push(token);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return baseTokens.join(" ");
|
|
265
324
|
}
|
|
266
325
|
return baseClassName ?? undefined;
|
|
267
326
|
}
|
|
268
327
|
function applyLinkA11y(element) {
|
|
328
|
+
if (!element) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
269
331
|
if (element instanceof HTMLAnchorElement || element instanceof HTMLButtonElement) {
|
|
270
332
|
return;
|
|
271
333
|
}
|
|
272
|
-
if (!element.
|
|
334
|
+
if (!element.hasAttribute("role")) {
|
|
273
335
|
element.setAttribute("role", "link");
|
|
274
336
|
}
|
|
275
|
-
if (!element.
|
|
337
|
+
if (!element.hasAttribute("tabindex")) {
|
|
276
338
|
element.setAttribute("tabindex", "0");
|
|
277
339
|
}
|
|
278
340
|
}
|
|
@@ -287,9 +349,12 @@ function Link(props) {
|
|
|
287
349
|
ignoreQueryParams: true
|
|
288
350
|
}, props);
|
|
289
351
|
const [local, rest] = splitProps(merged, ["routeName", "routeParams", "routeOptions", "activeClassName", "activeStrict", "ignoreQueryParams", "onClick", "target", "class", "children"]);
|
|
290
|
-
const router = useRouter();
|
|
291
352
|
const ctx = useContext(RouterContext);
|
|
292
|
-
|
|
353
|
+
if (!ctx) {
|
|
354
|
+
throw new Error("Link must be used within a RouterProvider");
|
|
355
|
+
}
|
|
356
|
+
const router = ctx.router;
|
|
357
|
+
const useFastPath = !local.activeStrict && local.ignoreQueryParams && local.routeParams === EMPTY_PARAMS;
|
|
293
358
|
const isActive = useFastPath ? () => ctx.routeSelector(local.routeName) : createSignalFromSource(createActiveRouteSource(router, local.routeName, local.routeParams, {
|
|
294
359
|
strict: local.activeStrict,
|
|
295
360
|
ignoreQueryParams: local.ignoreQueryParams
|
|
@@ -325,34 +390,20 @@ function Link(props) {
|
|
|
325
390
|
})();
|
|
326
391
|
}
|
|
327
392
|
|
|
328
|
-
const cache = new WeakMap();
|
|
329
|
-
function useRouterError() {
|
|
330
|
-
const router = useRouter();
|
|
331
|
-
let source = cache.get(router);
|
|
332
|
-
if (!source) {
|
|
333
|
-
source = createErrorSource(router);
|
|
334
|
-
cache.set(router, source);
|
|
335
|
-
}
|
|
336
|
-
return createSignalFromSource(source);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
393
|
function RouterErrorBoundary(props) {
|
|
340
|
-
const
|
|
341
|
-
const
|
|
394
|
+
const router = useRouter();
|
|
395
|
+
const snapshot = createSignalFromSource(createDismissableError(router));
|
|
342
396
|
createEffect(() => {
|
|
343
397
|
const snap = snapshot();
|
|
344
398
|
if (snap.error) {
|
|
345
399
|
props.onError?.(snap.error, snap.toRoute, snap.fromRoute);
|
|
346
400
|
}
|
|
347
401
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return [memo(() => props.children), memo(() => {
|
|
354
|
-
const error = visibleError();
|
|
355
|
-
return error ? props.fallback(error, resetError) : null;
|
|
402
|
+
return [memo(() => props.children), createComponent(Show, {
|
|
403
|
+
get when() {
|
|
404
|
+
return snapshot().error;
|
|
405
|
+
},
|
|
406
|
+
children: error => props.fallback(error(), snapshot().resetError)
|
|
356
407
|
})];
|
|
357
408
|
}
|
|
358
409
|
|
|
@@ -422,7 +473,9 @@ const useRoute = () => {
|
|
|
422
473
|
};
|
|
423
474
|
|
|
424
475
|
function createStoreFromSource(source) {
|
|
425
|
-
const [state, setState] = createStore(
|
|
476
|
+
const [state, setState] = createStore({
|
|
477
|
+
...source.getSnapshot()
|
|
478
|
+
});
|
|
426
479
|
const unsubscribe = source.subscribe(() => {
|
|
427
480
|
setState(reconcile(source.getSnapshot()));
|
|
428
481
|
});
|
|
@@ -431,10 +484,6 @@ function createStoreFromSource(source) {
|
|
|
431
484
|
}
|
|
432
485
|
|
|
433
486
|
function useRouteStore() {
|
|
434
|
-
const ctx = useContext(RouteContext);
|
|
435
|
-
if (!ctx) {
|
|
436
|
-
throw new Error("useRouteStore must be used within a RouterProvider");
|
|
437
|
-
}
|
|
438
487
|
const router = useRouter();
|
|
439
488
|
return createStoreFromSource(createRouteSource(router));
|
|
440
489
|
}
|
|
@@ -446,8 +495,8 @@ function useRouteNodeStore(nodeName) {
|
|
|
446
495
|
|
|
447
496
|
function useRouterTransition() {
|
|
448
497
|
const router = useRouter();
|
|
449
|
-
const
|
|
450
|
-
return createSignalFromSource(
|
|
498
|
+
const source = getTransitionSource(router);
|
|
499
|
+
return createSignalFromSource(source);
|
|
451
500
|
}
|
|
452
501
|
|
|
453
502
|
function isRouteActive(linkRouteName, currentRouteName) {
|
|
@@ -4,5 +4,6 @@ export interface RouteProviderProps {
|
|
|
4
4
|
router: Router;
|
|
5
5
|
announceNavigation?: boolean;
|
|
6
6
|
}
|
|
7
|
+
export declare function isRouteActive(linkRouteName: string, currentRouteName: string): boolean;
|
|
7
8
|
export declare function RouterProvider(props: ParentProps<RouteProviderProps>): JSX.Element;
|
|
8
9
|
//# sourceMappingURL=RouterProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouterProvider.d.ts","sourceRoot":"","sources":["../../src/RouterProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"RouterProvider.d.ts","sourceRoot":"","sources":["../../src/RouterProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,CAAC,kBAAkB,CAAC,GACrC,GAAG,CAAC,OAAO,CA+Bb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/components/Link.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/components/Link.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC5C,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAC5B,GAAG,CAAC,OAAO,CAoFb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../../src/components/RouteView/components.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../../src/components/RouteView/components.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAIpC,eAAO,MAAM,YAAY,eAA4B,CAAC;AAEtD,eAAO,MAAM,gBAAgB,eAA+B,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,YAAY,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACvB,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,gBAAgB,CAAC;IAChC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,cAAc,CAAC;AAE3D,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CAepD;yBAfe,KAAK;;;AAmBrB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,GAAG,CAAC,OAAO,CAU1D;yBAVe,QAAQ"}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { RouteViewMarker } from "./components";
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
|
+
export declare function isSegmentMatch(routeName: string, fullSegmentName: string, exact: boolean): boolean;
|
|
3
4
|
export declare function collectElements(children: unknown, result: RouteViewMarker[]): void;
|
|
4
|
-
export declare function buildRenderList(elements: RouteViewMarker[], routeName: string, nodeName: string):
|
|
5
|
-
rendered: JSX.Element[];
|
|
6
|
-
activeMatchFound: boolean;
|
|
7
|
-
};
|
|
5
|
+
export declare function buildRenderList(elements: RouteViewMarker[], routeName: string, nodeName: string): JSX.Element[];
|
|
8
6
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/components/RouteView/helpers.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/components/RouteView/helpers.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,OAAO,GACb,OAAO,CAMT;AAoBD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,eAAe,EAAE,GACxB,IAAI,CAgBN;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,eAAe,EAAE,EAC3B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,GAAG,CAAC,OAAO,EAAE,CAyCf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouterErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/RouterErrorBoundary.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RouterErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/RouterErrorBoundary.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,MAAM,IAAI,KACnB,GAAG,CAAC,OAAO,CAAC;IACjB,QAAQ,CAAC,OAAO,CAAC,EAAE,CACjB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,KAAK,GAAG,IAAI,EACrB,SAAS,EAAE,KAAK,GAAG,IAAI,KACpB,IAAI,CAAC;CACX;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,GAAG,CAAC,OAAO,CAoBb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSignalFromSource.d.ts","sourceRoot":"","sources":["../../src/createSignalFromSource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,QAAQ,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"createSignalFromSource.d.ts","sourceRoot":"","sources":["../../src/createSignalFromSource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,QAAQ,CAAC,CAAC,CAAC,CAoBb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createStoreFromSource.d.ts","sourceRoot":"","sources":["../../src/createStoreFromSource.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,CAAC,
|
|
1
|
+
{"version":3,"file":"createStoreFromSource.d.ts","sourceRoot":"","sources":["../../src/createStoreFromSource.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,CAAC,CAUH"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createRouteAnnouncer } from "./route-announcer.js";
|
|
2
|
-
export { shouldNavigate, buildHref, buildActiveClassName, applyLinkA11y, } from "./link-utils.js";
|
|
2
|
+
export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
|
|
3
3
|
export type { RouteAnnouncerOptions } from "./route-announcer.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EACL,cAAc,EACd,SAAS,EACT,oBAAoB,EACpB,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EACL,cAAc,EACd,SAAS,EACT,oBAAoB,EACpB,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -2,5 +2,6 @@ import type { Router, Params } from "@real-router/core";
|
|
|
2
2
|
export declare function shouldNavigate(evt: MouseEvent): boolean;
|
|
3
3
|
export declare function buildHref(router: Router, routeName: string, routeParams: Params): string | undefined;
|
|
4
4
|
export declare function buildActiveClassName(isActive: boolean, activeClassName: string | undefined, baseClassName: string | undefined): string | undefined;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function shallowEqual(prev: object | undefined, next: object | undefined): boolean;
|
|
6
|
+
export declare function applyLinkA11y(element: HTMLElement | null | undefined): void;
|
|
6
7
|
//# sourceMappingURL=link-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-utils.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/link-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAExD,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAQvD;AAID,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"link-utils.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/link-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAExD,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAQvD;AAID,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAoBpB;AAMD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EACjB,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CAyBpB;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,OAAO,CAwBT;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAgB3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-announcer.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/route-announcer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AASvD,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAChD;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,qBAAqB,GAC9B;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"route-announcer.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/route-announcer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AASvD,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAChD;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,qBAAqB,GAC9B;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAsFzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRouteNode.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteNode.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"useRouteNode.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteNode.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,CAInE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRouteStore.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteStore.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useRouteStore.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteStore.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,wBAAgB,aAAa,IAAI,UAAU,CAI1C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Solid.js integration for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@real-router/core": "^0.48.0",
|
|
55
55
|
"@real-router/route-utils": "^0.2.1",
|
|
56
|
-
"@real-router/sources": "^0.
|
|
56
|
+
"@real-router/sources": "^0.6.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/core": "7.29.0",
|
|
@@ -71,19 +71,19 @@
|
|
|
71
71
|
"solid-js": "1.9.12",
|
|
72
72
|
"vite-plugin-solid": "2.11.11",
|
|
73
73
|
"vitest": "4.1.0",
|
|
74
|
-
"@real-router/browser-plugin": "^0.
|
|
74
|
+
"@real-router/browser-plugin": "^0.13.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"solid-js": ">=1.7.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
|
-
"build": "rimraf dist && tsc -p tsconfig.build.json && rollup -c rollup.config.mjs",
|
|
81
80
|
"test": "vitest",
|
|
82
81
|
"test:properties": "vitest run --config vitest.config.properties.mts",
|
|
83
82
|
"test:stress": "vitest run --config vitest.config.stress.mts",
|
|
84
83
|
"type-check": "tsc --noEmit",
|
|
85
84
|
"lint": "eslint --cache --ext .ts,.tsx src/ tests/ --fix --max-warnings 0",
|
|
86
85
|
"lint:package": "publint",
|
|
87
|
-
"lint:types": "attw --pack ."
|
|
86
|
+
"lint:types": "attw --pack .",
|
|
87
|
+
"bundle": "rimraf dist && tsc -p tsconfig.build.json && rollup -c rollup.config.mjs"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/RouterProvider.tsx
CHANGED
package/src/components/Link.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
buildHref,
|
|
10
10
|
buildActiveClassName,
|
|
11
11
|
} from "../dom-utils/index.js";
|
|
12
|
-
import { useRouter } from "../hooks/useRouter";
|
|
13
12
|
|
|
14
13
|
import type { LinkProps } from "../types";
|
|
15
14
|
import type { Params } from "@real-router/core";
|
|
@@ -42,11 +41,15 @@ export function Link<P extends Params = Params>(
|
|
|
42
41
|
"children",
|
|
43
42
|
]);
|
|
44
43
|
|
|
45
|
-
const router = useRouter();
|
|
46
44
|
const ctx = useContext(RouterContext);
|
|
47
45
|
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
throw new Error("Link must be used within a RouterProvider");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const router = ctx.router;
|
|
51
|
+
|
|
48
52
|
const useFastPath =
|
|
49
|
-
ctx?.routeSelector &&
|
|
50
53
|
!local.activeStrict &&
|
|
51
54
|
local.ignoreQueryParams &&
|
|
52
55
|
local.routeParams === EMPTY_PARAMS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { children as resolveChildren } from "solid-js";
|
|
1
|
+
import { children as resolveChildren, createMemo } from "solid-js";
|
|
2
2
|
|
|
3
3
|
import { Match, NotFound } from "./components";
|
|
4
4
|
import { buildRenderList, collectElements } from "./helpers";
|
|
@@ -13,6 +13,14 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
|
|
|
13
13
|
|
|
14
14
|
const resolved = resolveChildren(() => props.children);
|
|
15
15
|
|
|
16
|
+
const elements = createMemo(() => {
|
|
17
|
+
const arr: RouteViewMarker[] = [];
|
|
18
|
+
|
|
19
|
+
collectElements(resolved(), arr);
|
|
20
|
+
|
|
21
|
+
return arr;
|
|
22
|
+
});
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
25
|
<>
|
|
18
26
|
{(() => {
|
|
@@ -22,24 +30,16 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
|
|
|
22
30
|
return null;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
collectElements(resolved(), elements);
|
|
28
|
-
|
|
29
|
-
const { rendered } = buildRenderList(
|
|
30
|
-
elements,
|
|
33
|
+
const rendered = buildRenderList(
|
|
34
|
+
elements(),
|
|
31
35
|
state.route.name,
|
|
32
36
|
props.nodeName,
|
|
33
37
|
);
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
return rendered;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return null;
|
|
39
|
+
return rendered.length > 0 ? rendered : null;
|
|
40
40
|
})()}
|
|
41
41
|
</>
|
|
42
|
-
)
|
|
42
|
+
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
RouteViewRoot.displayName = "RouteView";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { MatchProps, NotFoundProps } from "./types";
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Local (non-global) Symbols — Symbol.for() would expose markers to spoofing
|
|
5
|
+
// via the global Symbol registry. See Gotchas section "RouteView Marker Objects".
|
|
6
|
+
export const MATCH_MARKER = Symbol("RouteView.Match");
|
|
5
7
|
|
|
6
|
-
export const NOT_FOUND_MARKER = Symbol
|
|
8
|
+
export const NOT_FOUND_MARKER = Symbol("RouteView.NotFound");
|
|
7
9
|
|
|
8
10
|
export interface MatchMarker {
|
|
9
11
|
$$type: typeof MATCH_MARKER;
|
|
@@ -21,7 +23,7 @@ export interface NotFoundMarker {
|
|
|
21
23
|
export type RouteViewMarker = MatchMarker | NotFoundMarker;
|
|
22
24
|
|
|
23
25
|
export function Match(props: MatchProps): JSX.Element {
|
|
24
|
-
const result = {
|
|
26
|
+
const result: MatchMarker = {
|
|
25
27
|
$$type: MATCH_MARKER,
|
|
26
28
|
segment: props.segment,
|
|
27
29
|
exact: props.exact ?? false,
|
|
@@ -29,21 +31,25 @@ export function Match(props: MatchProps): JSX.Element {
|
|
|
29
31
|
get children(): JSX.Element {
|
|
30
32
|
return props.children;
|
|
31
33
|
},
|
|
32
|
-
}
|
|
34
|
+
};
|
|
33
35
|
|
|
36
|
+
// Marker object is identified by $$type Symbol in RouteView/helpers.tsx,
|
|
37
|
+
// not rendered as JSX. Cast required because JSX.Element does not include
|
|
38
|
+
// arbitrary marker shapes.
|
|
34
39
|
return result as unknown as JSX.Element;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
Match.displayName = "RouteView.Match";
|
|
38
43
|
|
|
39
44
|
export function NotFound(props: NotFoundProps): JSX.Element {
|
|
40
|
-
const result = {
|
|
45
|
+
const result: NotFoundMarker = {
|
|
41
46
|
$$type: NOT_FOUND_MARKER,
|
|
42
47
|
get children(): JSX.Element {
|
|
43
48
|
return props.children;
|
|
44
49
|
},
|
|
45
|
-
}
|
|
50
|
+
};
|
|
46
51
|
|
|
52
|
+
// See Match for the marker-pattern rationale.
|
|
47
53
|
return result as unknown as JSX.Element;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "./components";
|
|
12
12
|
import type { JSX } from "solid-js";
|
|
13
13
|
|
|
14
|
-
function isSegmentMatch(
|
|
14
|
+
export function isSegmentMatch(
|
|
15
15
|
routeName: string,
|
|
16
16
|
fullSegmentName: string,
|
|
17
17
|
exact: boolean,
|
|
@@ -66,7 +66,7 @@ export function buildRenderList(
|
|
|
66
66
|
elements: RouteViewMarker[],
|
|
67
67
|
routeName: string,
|
|
68
68
|
nodeName: string,
|
|
69
|
-
):
|
|
69
|
+
): JSX.Element[] {
|
|
70
70
|
let notFoundChildren: JSX.Element | null = null;
|
|
71
71
|
let activeMatchFound = false;
|
|
72
72
|
const rendered: JSX.Element[] = [];
|
|
@@ -77,23 +77,25 @@ export function buildRenderList(
|
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (activeMatchFound) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
const { segment, exact, fallback } = child;
|
|
81
85
|
const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (isActive) {
|
|
86
|
-
activeMatchFound = true;
|
|
87
|
-
const matchContent = child.children;
|
|
88
|
-
const content =
|
|
89
|
-
fallback === undefined ? (
|
|
90
|
-
matchContent
|
|
91
|
-
) : (
|
|
92
|
-
<Suspense fallback={fallback}>{matchContent}</Suspense>
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
rendered.push(content);
|
|
86
|
+
|
|
87
|
+
if (!isSegmentMatch(routeName, fullSegmentName, exact)) {
|
|
88
|
+
continue;
|
|
96
89
|
}
|
|
90
|
+
|
|
91
|
+
activeMatchFound = true;
|
|
92
|
+
rendered.push(
|
|
93
|
+
fallback === undefined ? (
|
|
94
|
+
child.children
|
|
95
|
+
) : (
|
|
96
|
+
<Suspense fallback={fallback}>{child.children}</Suspense>
|
|
97
|
+
),
|
|
98
|
+
);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
if (
|
|
@@ -104,5 +106,5 @@ export function buildRenderList(
|
|
|
104
106
|
rendered.push(notFoundChildren);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
return
|
|
109
|
+
return rendered;
|
|
108
110
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createDismissableError } from "@real-router/sources";
|
|
2
|
+
import { createEffect, Show } from "solid-js";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { createSignalFromSource } from "../createSignalFromSource";
|
|
5
|
+
import { useRouter } from "../hooks/useRouter";
|
|
4
6
|
|
|
5
7
|
import type { RouterError, State } from "@real-router/core";
|
|
6
8
|
import type { JSX } from "solid-js";
|
|
@@ -21,8 +23,8 @@ export interface RouterErrorBoundaryProps {
|
|
|
21
23
|
export function RouterErrorBoundary(
|
|
22
24
|
props: RouterErrorBoundaryProps,
|
|
23
25
|
): JSX.Element {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const snapshot = createSignalFromSource(createDismissableError(router));
|
|
26
28
|
|
|
27
29
|
createEffect(() => {
|
|
28
30
|
const snap = snapshot();
|
|
@@ -32,22 +34,12 @@ export function RouterErrorBoundary(
|
|
|
32
34
|
}
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
const visibleError = createMemo(() => {
|
|
36
|
-
const snap = snapshot();
|
|
37
|
-
|
|
38
|
-
return snap.version > dismissedVersion() ? snap.error : null;
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const resetError = () => setDismissedVersion(snapshot().version);
|
|
42
|
-
|
|
43
37
|
return (
|
|
44
38
|
<>
|
|
45
39
|
{props.children}
|
|
46
|
-
{(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return error ? props.fallback(error, resetError) : null;
|
|
50
|
-
})()}
|
|
40
|
+
<Show when={snapshot().error}>
|
|
41
|
+
{(error) => props.fallback(error(), snapshot().resetError)}
|
|
42
|
+
</Show>
|
|
51
43
|
</>
|
|
52
44
|
);
|
|
53
45
|
}
|
|
@@ -8,10 +8,18 @@ export function createSignalFromSource<T>(
|
|
|
8
8
|
): Accessor<T> {
|
|
9
9
|
const [value, setValue] = createSignal<T>(source.getSnapshot());
|
|
10
10
|
|
|
11
|
+
const sync = (): T => source.getSnapshot();
|
|
12
|
+
|
|
11
13
|
const unsubscribe = source.subscribe(() => {
|
|
12
|
-
setValue(
|
|
14
|
+
setValue(sync);
|
|
13
15
|
});
|
|
14
16
|
|
|
17
|
+
// Re-read after subscribe: lazy sources reconcile their snapshot in
|
|
18
|
+
// onFirstSubscribe (when reused after disconnect via cache). Listener is not
|
|
19
|
+
// notified for that internal update, so we must sync the signal manually.
|
|
20
|
+
// No-op when snapshot is unchanged (signal equality check).
|
|
21
|
+
setValue(sync);
|
|
22
|
+
|
|
15
23
|
onCleanup(() => {
|
|
16
24
|
unsubscribe();
|
|
17
25
|
});
|
|
@@ -6,9 +6,7 @@ import type { RouterSource } from "@real-router/sources";
|
|
|
6
6
|
export function createStoreFromSource<T extends object>(
|
|
7
7
|
source: RouterSource<T>,
|
|
8
8
|
): T {
|
|
9
|
-
const [state, setState] = createStore<T>(
|
|
10
|
-
structuredClone(source.getSnapshot()),
|
|
11
|
-
);
|
|
9
|
+
const [state, setState] = createStore<T>({ ...source.getSnapshot() });
|
|
12
10
|
|
|
13
11
|
const unsubscribe = source.subscribe(() => {
|
|
14
12
|
setState(reconcile(source.getSnapshot()));
|
|
@@ -8,7 +8,6 @@ import type { Accessor } from "solid-js";
|
|
|
8
8
|
|
|
9
9
|
export function useRouteNode(nodeName: string): Accessor<RouteState> {
|
|
10
10
|
const router = useRouter();
|
|
11
|
-
const store = createRouteNodeSource(router, nodeName);
|
|
12
11
|
|
|
13
|
-
return createSignalFromSource(
|
|
12
|
+
return createSignalFromSource(createRouteNodeSource(router, nodeName));
|
|
14
13
|
}
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { createRouteSource } from "@real-router/sources";
|
|
2
|
-
import { useContext } from "solid-js";
|
|
3
2
|
|
|
4
|
-
import { RouteContext } from "../context";
|
|
5
3
|
import { createStoreFromSource } from "../createStoreFromSource";
|
|
6
4
|
import { useRouter } from "./useRouter";
|
|
7
5
|
|
|
8
6
|
import type { RouteState } from "../types";
|
|
9
7
|
|
|
10
8
|
export function useRouteStore(): RouteState {
|
|
11
|
-
const ctx = useContext(RouteContext);
|
|
12
|
-
|
|
13
|
-
if (!ctx) {
|
|
14
|
-
throw new Error("useRouteStore must be used within a RouterProvider");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
9
|
const router = useRouter();
|
|
18
10
|
|
|
19
11
|
return createStoreFromSource(createRouteSource(router));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTransitionSource } from "@real-router/sources";
|
|
2
2
|
|
|
3
3
|
import { createSignalFromSource } from "../createSignalFromSource";
|
|
4
4
|
import { useRouter } from "./useRouter";
|
|
@@ -8,7 +8,7 @@ import type { Accessor } from "solid-js";
|
|
|
8
8
|
|
|
9
9
|
export function useRouterTransition(): Accessor<RouterTransitionSnapshot> {
|
|
10
10
|
const router = useRouter();
|
|
11
|
-
const
|
|
11
|
+
const source = getTransitionSource(router);
|
|
12
12
|
|
|
13
|
-
return createSignalFromSource(
|
|
13
|
+
return createSignalFromSource(source);
|
|
14
14
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useRouterError.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouterError.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAgB,MAAM,sBAAsB,CAAC;AAC9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIzC,wBAAgB,cAAc,IAAI,QAAQ,CAAC,mBAAmB,CAAC,CAW9D"}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createErrorSource } from "@real-router/sources";
|
|
2
|
-
|
|
3
|
-
import { createSignalFromSource } from "../createSignalFromSource";
|
|
4
|
-
import { useRouter } from "./useRouter";
|
|
5
|
-
|
|
6
|
-
import type { Router } from "@real-router/core";
|
|
7
|
-
import type { RouterErrorSnapshot, RouterSource } from "@real-router/sources";
|
|
8
|
-
import type { Accessor } from "solid-js";
|
|
9
|
-
|
|
10
|
-
const cache = new WeakMap<Router, RouterSource<RouterErrorSnapshot>>();
|
|
11
|
-
|
|
12
|
-
export function useRouterError(): Accessor<RouterErrorSnapshot> {
|
|
13
|
-
const router = useRouter();
|
|
14
|
-
|
|
15
|
-
let source = cache.get(router);
|
|
16
|
-
|
|
17
|
-
if (!source) {
|
|
18
|
-
source = createErrorSource(router);
|
|
19
|
-
cache.set(router, source);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return createSignalFromSource(source);
|
|
23
|
-
}
|