@jsenv/navi 0.29.418 → 0.29.419
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 +1 -1
- package/dist/dev/jsenv_navi.js +192 -43
- package/dist/dev/jsenv_navi.js.map +10 -10
- package/dist/jsenv_navi.js +190 -41
- package/dist/jsenv_navi.js.map +10 -10
- package/docs/AI_INSTRUCTIONS.md +177 -261
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +115 -174
- package/docs/drag_to_travel.md +10 -9
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +6 -4
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -482
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -335
- package/docs/state_binding.md +2 -1
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/navigation.md
CHANGED
|
@@ -22,9 +22,7 @@ linking to them, and turning them into tabs.
|
|
|
22
22
|
- [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
23
23
|
- [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
|
|
24
24
|
- [A layer over the screen: what its address may say](#a-layer-over-the-screen-what-its-address-may-say)
|
|
25
|
-
- [Why a layer is a routing question at all](#why-a-layer-is-a-routing-question-at-all)
|
|
26
25
|
- [`/me/settings` names a place the reader is not at](#mesettings-names-a-place-the-reader-is-not-at)
|
|
27
|
-
- [It is not "use a search param"](#it-is-not-use-a-search-param)
|
|
28
26
|
- [What the search param buys: the way back is in the address](#what-the-search-param-buys-the-way-back-is-in-the-address)
|
|
29
27
|
- [The wiring](#the-wiring)
|
|
30
28
|
- [Places inside the layer](#places-inside-the-layer)
|
|
@@ -34,30 +32,23 @@ linking to them, and turning them into tabs.
|
|
|
34
32
|
|
|
35
33
|
Where the user is — which section, which tab, which sub-page — is state. Put it
|
|
36
34
|
in the URL unless there is a reason not to. What that buys, none of which can be
|
|
37
|
-
retrofitted later:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- the place is shareable and bookmarkable — someone can send a link to exactly
|
|
42
|
-
what they are looking at;
|
|
43
|
-
- the place is **targetable**: anything, anywhere in the app, can send the user
|
|
44
|
-
there with a `<Link route={…}>`, without knowing anything about the component
|
|
45
|
-
that displays it;
|
|
46
|
-
- a reload lands where the user was.
|
|
35
|
+
retrofitted later: back and forward work, each place being a history entry; the
|
|
36
|
+
place is shareable and bookmarkable; it is **targetable** — anything in the app
|
|
37
|
+
can send the user there with a `<Link route={…}>`, knowing nothing of the
|
|
38
|
+
component that displays it; and a reload lands where the user was.
|
|
47
39
|
|
|
48
40
|
So the default shape of a tab row is routes: `<Nav>` + `<Link route>` +
|
|
49
|
-
`<RouteTravel>`. `SlideContainer` is the exception, not the starting point
|
|
50
|
-
[Tabs that are not routes](#tabs-that-are-not-routes)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
without a route and without a history entry per step.
|
|
41
|
+
`<RouteTravel>`. `SlideContainer` is the exception, not the starting point (see
|
|
42
|
+
[Tabs that are not routes](#tabs-that-are-not-routes)), with a middle answer: a
|
|
43
|
+
position READ from the URL and restored on reload, without a history entry per
|
|
44
|
+
step
|
|
45
|
+
([A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)).
|
|
55
46
|
|
|
56
47
|
## Declaring routes
|
|
57
48
|
|
|
58
|
-
Every route is created with `route()` and
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
Every route is created with `route()` and all are declared to `setupRoutes()` in
|
|
50
|
+
one call: the routing system resolves specificity and signal ownership across the
|
|
51
|
+
whole set, so it has to see the whole set.
|
|
61
52
|
|
|
62
53
|
```js
|
|
63
54
|
// routes.js
|
|
@@ -71,50 +62,23 @@ setupRoutes([HOME_ROUTE, GAMES_ROUTE, GAME_ROUTE]);
|
|
|
71
62
|
```
|
|
72
63
|
|
|
73
64
|
Named exports from one module, on purpose: the file is the map of the
|
|
74
|
-
application, and an import line says which places a component deals with.
|
|
75
|
-
|
|
76
|
-
`
|
|
77
|
-
declared apart from the JSX that renders them.
|
|
65
|
+
application, and an import line says which places a component deals with. Routes
|
|
66
|
+
are plain objects usable outside any component (`buildUrl()`, `navTo()`,
|
|
67
|
+
`redirectTo()`, `matching`), which is why they are declared apart from the JSX.
|
|
78
68
|
|
|
79
69
|
### A document that is not at the root: `setBaseUrl`
|
|
80
70
|
|
|
81
71
|
A second document in the same site — an admin panel at `/admin/admin.html`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
setBaseUrl("/admin/admin.html");
|
|
90
|
-
|
|
91
|
-
export const PLACES_ROUTE = route("/places");
|
|
92
|
-
export const PLACE_ROUTE = route("/places/:placeId");
|
|
93
|
-
|
|
94
|
-
setupRoutes([PLACES_ROUTE, PLACE_ROUTE]);
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`/admin/places/42` matches `PLACE_ROUTE`, and `buildUrl` puts the prefix back —
|
|
98
|
-
nothing else in the document ever spells `/admin` again, so the panel can be
|
|
99
|
-
moved by changing this one line.
|
|
100
|
-
|
|
101
|
-
Two things about it:
|
|
102
|
-
|
|
103
|
-
- **it belongs in the routes module, above `setupRoutes()`** — the base url
|
|
104
|
-
is read whenever an address is matched or built, and the first match happens
|
|
105
|
-
right there, as the routes are set up against the current address. In the
|
|
106
|
-
entry point it would be too late: the import of the routes module runs
|
|
107
|
-
first, `setupRoutes()` with it, and an import sorter would put the call after
|
|
108
|
-
anyway;
|
|
109
|
-
- **the server has to answer the document for every address below it.** jsenv's
|
|
110
|
-
dev server already does: an address it finds no file for is answered with the
|
|
111
|
-
closest html file above it, `<dirname>.html` included, so `/admin/places/42`
|
|
112
|
-
is served `/admin/admin.html`. Any other host needs the same rule.
|
|
72
|
+
beside the app at `/` — writes its routes as if its directory were the root,
|
|
73
|
+
after `setBaseUrl("/admin/admin.html")` in its routes module, above
|
|
74
|
+
`setupRoutes()` (see its JSDoc). **The server has to answer the document for
|
|
75
|
+
every address below it.** jsenv's dev server already does — an address it finds
|
|
76
|
+
no file for is answered with the closest html file above it, `<dirname>.html`
|
|
77
|
+
included, so `/admin/places/42` is served `/admin/admin.html` — and any other
|
|
78
|
+
host needs the same rule.
|
|
113
79
|
|
|
114
80
|
### A section is allowed to be a route of its own
|
|
115
81
|
|
|
116
|
-
This is the most commonly missed point.
|
|
117
|
-
|
|
118
82
|
When a segment can take a **finite, known set of values**, declare one literal
|
|
119
83
|
route per value rather than one parameterized route you pass params to:
|
|
120
84
|
|
|
@@ -125,49 +89,24 @@ export const CANDIDATE_GAMES_ROUTE = route("/games/candidates");
|
|
|
125
89
|
export const FINISHED_GAMES_ROUTE = route("/games/finished");
|
|
126
90
|
```
|
|
127
91
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
92
|
+
The routes are then listable — `routes.js` shows the places the application has,
|
|
93
|
+
where `/games/:section` shows one and hides three; call sites need no
|
|
94
|
+
`routeParams`, so a wrong section is a missing import rather than a string nobody
|
|
95
|
+
checks; and each section can carry search params of its own. Params stay for what
|
|
96
|
+
the code cannot enumerate (an id, a date), and for a finite set handled
|
|
97
|
+
**uniformly** — a row built by `.map()` over the sections — bound to a signal for
|
|
98
|
+
validation and a default.
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
same for `<Route>`. A wrong section is then a missing import rather than a
|
|
140
|
-
string nobody checks.
|
|
141
|
-
- **Each section can carry its own search params.** `/games/finished` may have a
|
|
142
|
-
`sort` the other sections have no business knowing about.
|
|
143
|
-
|
|
144
|
-
Params stay for what is genuinely dynamic — a value the code cannot enumerate:
|
|
145
|
-
|
|
146
|
-
```js
|
|
147
|
-
export const GAME_ROUTE = route("/games/:gameId"); // ✅ an id
|
|
148
|
-
export const DAY_ROUTE = route("/planning/:day"); // ✅ any date
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
A parameterized route also remains right for a finite set that must be handled
|
|
152
|
-
**uniformly** — a row built by `.map()` over a list of sections, where writing
|
|
153
|
-
one branch per section would be writing the same branch N times. Bind the param
|
|
154
|
-
to a signal to get validation and a default:
|
|
155
|
-
|
|
156
|
-
```js
|
|
157
|
-
import { stateSignal } from "@jsenv/navi";
|
|
158
|
-
|
|
159
|
-
const sectionSignal = stateSignal("to_come", {
|
|
160
|
-
id: "games_section",
|
|
161
|
-
oneOf: ["candidate", "to_come", "done"],
|
|
162
|
-
autoFix: true,
|
|
163
|
-
});
|
|
164
|
-
export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`);
|
|
165
|
-
```
|
|
100
|
+
A literal and a parameterized route can sit on the same segment, and both then
|
|
101
|
+
match: `/games/my_games` is also a `/games/:section`, whose signal is written
|
|
102
|
+
`"my_games"`. Where both exist,
|
|
103
|
+
the literal branch goes first (see [Rendering routes](#rendering-routes)) and the
|
|
104
|
+
param is constrained to the values it really takes (see
|
|
105
|
+
[Which values a param accepts](#which-values-a-param-accepts)).
|
|
166
106
|
|
|
167
107
|
#### Declaring the sections is what makes them places
|
|
168
108
|
|
|
169
|
-
Both forms
|
|
170
|
-
why declaring the literals is not decoration:
|
|
109
|
+
Both forms written together say different things:
|
|
171
110
|
|
|
172
111
|
```js
|
|
173
112
|
export const MY_GAMES_ROUTE = route(`/games/me/:section=${sectionSignal}`);
|
|
@@ -176,50 +115,29 @@ export const MY_GAMES_CANDIDATE_ROUTE = route("/games/me/candidate");
|
|
|
176
115
|
export const MY_GAMES_DONE_ROUTE = route("/games/me/done");
|
|
177
116
|
```
|
|
178
117
|
|
|
179
|
-
|
|
118
|
+
On `/games/me/done`, `MY_GAMES_ROUTE.buildUrl()` gives `/games/me/done`: the
|
|
119
|
+
parameterized route reads its signal, so a link to "my games" from the bottom bar
|
|
120
|
+
**reopens the section you were looking at**. `MY_GAMES_TO_COME_ROUTE.buildUrl()`
|
|
121
|
+
gives `/games/me`, always: a tab must point at its own section, never at the one
|
|
122
|
+
already open — a tab pointing at the current page cannot be clicked. The default
|
|
123
|
+
section has no segment, so its literal route is the **parent** of the
|
|
124
|
+
parameterized one, and still does not inherit the param.
|
|
180
125
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
you were looking at**. That is what the signal is for, and `persists` makes it
|
|
184
|
-
survive the night;
|
|
185
|
-
- `MY_GAMES_TO_COME_ROUTE.buildUrl()` → `/games/me`, always. A tab must point at
|
|
186
|
-
its own section, never at the one already open — a tab pointing at the current
|
|
187
|
-
page is a tab that cannot be clicked.
|
|
188
|
-
|
|
189
|
-
The default section is the delicate one: it has no segment of its own, so its
|
|
190
|
-
literal route is the **parent** of the parameterized one. It still means the
|
|
191
|
-
default section and does not inherit the param.
|
|
192
|
-
|
|
193
|
-
What tells navi these values name pages rather than qualify one is precisely
|
|
194
|
-
that the literal routes exist. Where no literal is declared, the value stays a
|
|
195
|
-
qualifier and an ancestor url keeps it:
|
|
126
|
+
That the literals exist is what tells navi these values name pages. Where none is
|
|
127
|
+
declared, the value stays a qualifier, carried by an ancestor url:
|
|
196
128
|
|
|
197
129
|
```js
|
|
198
|
-
const tabSignal = stateSignal("general", { id: "settings_tab" });
|
|
199
130
|
export const ADMIN_ROUTE = route(`/admin/:section=${sectionSignal}/`);
|
|
200
131
|
export const ADMIN_SETTINGS_ROUTE = route(`/admin/settings/:tab=${tabSignal}`);
|
|
201
132
|
// nobody declared /admin/settings/advanced, so on tab "advanced":
|
|
202
133
|
// ADMIN_ROUTE.buildUrl() → /admin/settings/advanced — "admin, where you left it"
|
|
203
134
|
```
|
|
204
135
|
|
|
205
|
-
So the rule is the one you would want: name a section and it becomes a place;
|
|
206
|
-
leave it unnamed and it stays a setting carried along.
|
|
207
|
-
|
|
208
136
|
### Which values a param accepts
|
|
209
137
|
|
|
210
138
|
A param says which segments it accepts, and a segment it declines is not a
|
|
211
139
|
half-match to be sorted out later — the route simply does not match:
|
|
212
140
|
|
|
213
|
-
```js
|
|
214
|
-
export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
|
|
215
|
-
params: { gameId: /^W-[A-Z0-9]{8}$/i },
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
A constraint is a regexp, the list of accepted values, or a `(value) => boolean`
|
|
220
|
-
— the list is compared as strings, so it can be the very `oneOf` given to the
|
|
221
|
-
signal bound to that param:
|
|
222
|
-
|
|
223
141
|
```js
|
|
224
142
|
const SECTIONS = ["candidate", "to_come", "done"];
|
|
225
143
|
const sectionSignal = stateSignal("to_come", {
|
|
@@ -229,27 +147,25 @@ const sectionSignal = stateSignal("to_come", {
|
|
|
229
147
|
export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`, {
|
|
230
148
|
params: { section: SECTIONS },
|
|
231
149
|
});
|
|
150
|
+
export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
|
|
151
|
+
params: { gameId: /^W-[A-Z0-9]{8}$/i },
|
|
152
|
+
});
|
|
232
153
|
```
|
|
233
154
|
|
|
234
|
-
This is what
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
A constrained param is also **required** — no segment is not one of the values
|
|
240
|
-
it accepts — so `/:gameId` does not match `/`. The address with no segment is a
|
|
241
|
-
route of its own, which is the shape you want anyway.
|
|
155
|
+
This is what lets a param sit at the root without swallowing every single-segment
|
|
156
|
+
address: `/cgu` stays another route's url, `<Route fallback>` is reachable, and
|
|
157
|
+
no signal is written for a url this route has nothing to do with. A constrained
|
|
158
|
+
param is also **required**: `/:gameId` does not match `/`. The accepted forms are
|
|
159
|
+
in `route()`'s JSDoc.
|
|
242
160
|
|
|
243
161
|
#### Constrain the shape, never the existence
|
|
244
162
|
|
|
245
163
|
A constraint answers one question: **is this segment addressed to this route?**
|
|
246
164
|
It is decided on the url alone, before anything is written, so it can only be
|
|
247
165
|
about shape — that a segment looks like a game code, not that the game exists.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
route action's data. That question belongs to a route that **did** match, with a
|
|
252
|
-
page free to repair itself, show a not-found screen, offer a way out:
|
|
166
|
+
Whether the value is any good is asked later, by the signal's validation and the
|
|
167
|
+
route action's data, on a route that **did** match, with a page free to show a
|
|
168
|
+
not-found screen and offer a way out:
|
|
253
169
|
|
|
254
170
|
```js
|
|
255
171
|
// ✅ /W-ZZZZZZZZ matches, the action 404s, the page says so
|
|
@@ -257,122 +173,52 @@ page free to repair itself, show a not-found screen, offer a way out:
|
|
|
257
173
|
// and "no route matched" is a worse answer than "this game is gone"
|
|
258
174
|
```
|
|
259
175
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
When several routes match one url and bind the **same signal** on a param of the
|
|
266
|
-
same name, they all write it, in declaration order — the last one wins:
|
|
267
|
-
|
|
268
|
-
```js
|
|
269
|
-
route(`/games/:gameId=${gameIdSignal}`); // declared first
|
|
270
|
-
route(`/:gameId=${gameIdSignal}/:state`); // declared later
|
|
271
|
-
// on /games/W-ABC234PQ the second one matches too and writes "games"
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
Constraining `gameId` removes that second match entirely, which is the fix.
|
|
275
|
-
Where a param genuinely cannot be constrained, the routes must not share a
|
|
276
|
-
signal.
|
|
176
|
+
Constraining is also the fix when several routes match one url and bind the
|
|
177
|
+
**same signal** on a param of the same name: they all write it, in declaration
|
|
178
|
+
order, the last one winning (`/:gameId/:state` matches `/games/W-ABC234PQ` and
|
|
179
|
+
writes `"games"` into a `gameIdSignal` it shares). Where a param genuinely cannot
|
|
180
|
+
be constrained, the routes must not share a signal.
|
|
277
181
|
|
|
278
182
|
### An address that only sends elsewhere
|
|
279
183
|
|
|
280
|
-
Some addresses are not pages
|
|
281
|
-
games », the old address of a section that moved,
|
|
282
|
-
|
|
283
|
-
route says so itself:
|
|
184
|
+
Some addresses are not pages — the root of an app whose home screen is « my
|
|
185
|
+
games », the old address of a section that moved, a share link carrying a
|
|
186
|
+
segment only WhatsApp cares about — and the route says so itself:
|
|
284
187
|
|
|
285
188
|
```js
|
|
286
189
|
export const HOME_ROUTE = route("/", { redirectRoute: MY_GAMES_ROUTE });
|
|
287
|
-
export const GAME_SHARED_ROUTE = route("/:gameId/:shareState", {
|
|
288
|
-
redirectRoute: GAME_ROUTE,
|
|
289
|
-
});
|
|
290
190
|
```
|
|
291
191
|
|
|
292
|
-
The params found in the url carry over to the ones the target route declares
|
|
293
|
-
under the same name — `gameId` above needs no help — and what it cannot place
|
|
294
|
-
is left behind, `shareState` included. `redirectRouteParams` says the rest:
|
|
295
|
-
|
|
296
|
-
```js
|
|
297
|
-
// renaming, when the two routes do not call it the same thing
|
|
298
|
-
route("/partie/:id", {
|
|
299
|
-
redirectRoute: GAME_ROUTE,
|
|
300
|
-
redirectRouteParams: ({ id }) => ({ gameId: id }),
|
|
301
|
-
});
|
|
302
|
-
// dropping one, keeping the others
|
|
303
|
-
route("/:gameId/invite", {
|
|
304
|
-
redirectRoute: MY_GAMES_ROUTE,
|
|
305
|
-
redirectRouteParams: { gameId: undefined },
|
|
306
|
-
});
|
|
307
|
-
// carrying nothing over
|
|
308
|
-
route("/tri", { redirectRoute: MY_GAMES_ROUTE, redirectRouteParams: null });
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
#### Why it is not a page rendering `null`
|
|
312
|
-
|
|
313
192
|
The redirection is resolved at the door of the navigation, before the url is
|
|
314
|
-
written anywhere
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
with the address and never appears in the `<Route>` tree at all.
|
|
324
|
-
|
|
325
|
-
It fires on the route's own address only. `/` catches everything below it when
|
|
326
|
-
it renders a container, and would carry `/cgu` away with it if redirecting
|
|
327
|
-
followed the same reading — so redirecting asks the stricter question: is this
|
|
328
|
-
url exactly that route's address?
|
|
329
|
-
|
|
330
|
-
Where several redirecting routes answer for one url, the more specific wins —
|
|
331
|
-
`/:gameId/invite` over `/:gameId/:shareState`, the same reading the rest of the
|
|
332
|
-
router uses. Chains collapse into one navigation, and a cycle throws naming the
|
|
333
|
-
addresses it goes through.
|
|
193
|
+
written anywhere: no history entry, no route action, nothing mounted, and going
|
|
194
|
+
back lands on the page before it. A page redirecting in an effect gets none of
|
|
195
|
+
that — the address exists, its action runs, and a screen nobody should see is
|
|
196
|
+
painted, one a route transition can even animate _to_ — so a redirection is
|
|
197
|
+
declared on the route, **never as a page rendering `null`**. It fires on the
|
|
198
|
+
route's own address only (`/` matches everything below it for rendering, and
|
|
199
|
+
would carry `/cgu` away); of several redirecting routes the more specific wins,
|
|
200
|
+
chains collapse into one navigation, and a cycle throws. Which params carry over:
|
|
201
|
+
the JSDoc of `redirectRoute` and `redirectRouteParams`.
|
|
334
202
|
|
|
335
203
|
#### A search param only the link carries: `dropSearchParams`
|
|
336
204
|
|
|
337
|
-
A share link can carry a param that is not for the app at all
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
the
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
export const GAME_ROUTE = route(`/games/:gameId=${gameIdSignal}`, {
|
|
345
|
-
searchParams: { weather: weatherSignal },
|
|
346
|
-
dropSearchParams: ["v"],
|
|
347
|
-
});
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
`/games/W-ABC234PQ?weather&v=k3f9x2` becomes `/games/W-ABC234PQ?weather` at the
|
|
351
|
-
door, exactly like a redirection: no history entry, no route action, no signal
|
|
352
|
-
written. The other params stay as they were written. Like a redirection, it
|
|
353
|
-
applies to the route's own address only.
|
|
354
|
-
|
|
355
|
-
A redirecting route on `/games/:gameId` cannot do this. A search param never
|
|
356
|
-
makes a pattern fail to match, so that route also matches the address without
|
|
357
|
-
`v`. It then redirects the address to itself, which does nothing, and it
|
|
358
|
-
reports `matching` next to `GAME_ROUTE` on every game page.
|
|
205
|
+
A share link can carry a param that is not for the app at all — `?v=k3f9x2`,
|
|
206
|
+
there because WhatsApp caches one link preview per address — and left in the bar
|
|
207
|
+
it gets copied and shared again, stale. The route that owns the address drops it
|
|
208
|
+
at the door, like a redirection, with `dropSearchParams: ["v"]`. A redirecting
|
|
209
|
+
route cannot do this: a search param never makes a pattern fail to match, so it
|
|
210
|
+
would match the address without `v` too, redirect it to itself, and report
|
|
211
|
+
`matching` next to the real route on every page.
|
|
359
212
|
|
|
360
213
|
#### When the destination depends on data
|
|
361
214
|
|
|
362
|
-
`/admin` sends the reader to the first section their permissions allow:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
What it is, is a **landing that loads**, so it is declared the way every load is:
|
|
368
|
-
a route action, and the redirection once the answer is there.
|
|
215
|
+
`/admin` sends the reader to the first section their permissions allow: a target
|
|
216
|
+
not known until `GET /me` has answered. That is not a redirection — the door
|
|
217
|
+
resolves an address from the url alone — it is a **landing that loads**: a route
|
|
218
|
+
action, and the redirection once the answer is there.
|
|
369
219
|
|
|
370
220
|
```js
|
|
371
|
-
export const ADMIN_ROUTE = route("/admin");
|
|
372
|
-
const anySectionMatchingSignal = anyMatchingRouteSignal([
|
|
373
|
-
PLACES_ROUTE,
|
|
374
|
-
USERS_ROUTE,
|
|
375
|
-
]);
|
|
221
|
+
export const ADMIN_ROUTE = route("/admin"); // no trailing slash: the landing alone
|
|
376
222
|
|
|
377
223
|
routeAction(
|
|
378
224
|
ADMIN_ROUTE,
|
|
@@ -380,31 +226,19 @@ routeAction(
|
|
|
380
226
|
const me = await ME.GET.run();
|
|
381
227
|
firstSectionAllowed(me).redirectTo();
|
|
382
228
|
},
|
|
383
|
-
//
|
|
384
|
-
() =>
|
|
229
|
+
// params, even constant ones: an action without them is prerun on a hover
|
|
230
|
+
() => true,
|
|
385
231
|
);
|
|
386
232
|
```
|
|
387
233
|
|
|
388
|
-
`redirectTo()` replaces the entry
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
the pointer reaches the link (see [dynamic_import.md](./dynamic_import.md)),
|
|
397
|
-
which for a read is a head start and for a navigation is the navigation
|
|
398
|
-
happening on a hover. Params say the action asks something of the address, and
|
|
399
|
-
that is what keeps it from being prerun.
|
|
400
|
-
|
|
401
|
-
What it costs, and there is no way around it: the address exists, and something
|
|
402
|
-
is on screen while the request is out. That is honest — the reader IS waiting —
|
|
403
|
-
so give `/admin` the screen a wait deserves (the section frame, a `<Loading>`)
|
|
404
|
-
rather than a page rendering `null`. What must not happen is the rest of the
|
|
405
|
-
list: the wait belongs to the route action, so no page below it runs, and the
|
|
406
|
-
destination is decided in one place instead of in an effect that fires again on
|
|
407
|
-
every render.
|
|
234
|
+
`redirectTo()` replaces the entry, so the back button still leaves by where the
|
|
235
|
+
reader came in. The params function is not optional: a route action declared
|
|
236
|
+
without one is prerun when the pointer reaches a link to it (see
|
|
237
|
+
[dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)) — for a read
|
|
238
|
+
a head start, for a navigation the navigation happening on a hover. The cost: the
|
|
239
|
+
address exists, and something is on screen while the request is out — give it
|
|
240
|
+
the screen a wait deserves (the section frame, a `<Loading>`), not a page
|
|
241
|
+
rendering `null`.
|
|
408
242
|
|
|
409
243
|
### Search params
|
|
410
244
|
|
|
@@ -419,44 +253,29 @@ const vueSignal = stateSignal("liste", {
|
|
|
419
253
|
export const HOME_ROUTE = route("/", { searchParams: { vue: vueSignal } });
|
|
420
254
|
```
|
|
421
255
|
|
|
422
|
-
The signal and the URL are the same state:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
Declared on the **root route**, a search param is a position that holds wherever
|
|
427
|
-
one is in the application — a view mode that survives moving from page to page.
|
|
428
|
-
Declared on one route, it exists only there.
|
|
256
|
+
The signal and the URL are the same state: never keep a `useState` beside a route
|
|
257
|
+
param for the same fact. Declared on the **root route**, a search param holds
|
|
258
|
+
wherever one is in the application; declared on one route, it exists only there.
|
|
429
259
|
|
|
430
260
|
Writing it AMENDS the history entry one is on rather than stacking a new one: a
|
|
431
261
|
param that qualifies a screen is not a place, and one entry per write turns a
|
|
432
|
-
single back-press into as many as the user moved. A state whose values ARE
|
|
433
|
-
|
|
262
|
+
single back-press into as many as the user moved. A state whose values ARE places
|
|
263
|
+
says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
|
|
434
264
|
|
|
435
265
|
#### A value written per frame
|
|
436
266
|
|
|
437
|
-
Every write
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
- **A gesture with a release** — a disc turned, a slider dragged: hold the value
|
|
450
|
-
in the component while the pointer is down, and write the signal on release.
|
|
451
|
-
Re-reading the signal meanwhile would pull the control back mid-gesture, so
|
|
452
|
-
the held value is the one drawn.
|
|
453
|
-
- **A movement with no release** — a wheel zooming, a fly-to animation: write
|
|
454
|
-
the signal when the movement has settled (a short idle timer, or the end of
|
|
455
|
-
the animation).
|
|
456
|
-
|
|
457
|
-
When every intermediate value IS a valid address — the centre of a panned map,
|
|
458
|
-
where each frame is a place one could reload into — the state can ask navi to
|
|
459
|
-
debounce the browser's address instead, in milliseconds:
|
|
267
|
+
Every write reaches the address, synchronously, and browsers refuse an address
|
|
268
|
+
written too often: Safari throws a `SecurityError` past 100 writes per 10
|
|
269
|
+
seconds, and the state then runs ahead of an address that stopped following
|
|
270
|
+
(Chromium and Firefox drop the writes silently; navi warns in dev).
|
|
271
|
+
|
|
272
|
+
**An address is not a recording of a gesture.** It records where the value was
|
|
273
|
+
PUT DOWN, not the path the finger took: a value one drags is held in the
|
|
274
|
+
component while the pointer is down and written on release — re-reading the
|
|
275
|
+
signal meanwhile would pull the control back mid-gesture — and a movement with no
|
|
276
|
+
release (a wheel zooming, a fly-to) is written once it has settled. When every
|
|
277
|
+
intermediate value IS a valid address — the centre of a panned map — the state
|
|
278
|
+
can ask navi to debounce the browser's copy of the address instead:
|
|
460
279
|
|
|
461
280
|
```js
|
|
462
281
|
const eastSignal = stateSignal(0, {
|
|
@@ -466,12 +285,9 @@ const eastSignal = stateSignal(0, {
|
|
|
466
285
|
});
|
|
467
286
|
```
|
|
468
287
|
|
|
469
|
-
The routes, the document url and the renders still follow every write; only
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
debounced, and a push or a state written onto the entry flushes what is pending
|
|
473
|
-
first. A back pressed before the write keeps the entry at the last address
|
|
474
|
-
written: the drag in progress, and nothing else.
|
|
288
|
+
The routes, the document url and the renders still follow every write; only
|
|
289
|
+
`window.location` waits for the state to stay still that long, and a back pressed
|
|
290
|
+
before the write keeps the last address written. A push is never debounced.
|
|
475
291
|
|
|
476
292
|
## Rendering routes
|
|
477
293
|
|
|
@@ -479,6 +295,15 @@ written: the drag in progress, and nothing else.
|
|
|
479
295
|
the branch matching the URL; with a `route` it is a branch; with `fallback` it is
|
|
480
296
|
the branch taken when no sibling matches.
|
|
481
297
|
|
|
298
|
+
A route matches its own address and nothing below it — `/games/:gameId` does not
|
|
299
|
+
match `/games/2/edit` — except `/` and a pattern ending in `/`, which match
|
|
300
|
+
everything below them too (`route("/games/")` matches `/games/2/edit`): that is
|
|
301
|
+
what keeps a section's container on screen while one is inside it. Two patterns
|
|
302
|
+
can still match one url — `/games/my_games` is also a `/games/:gameId` — and a
|
|
303
|
+
container renders the **first** branch that matches, in the order written, not
|
|
304
|
+
the most specific. So the branches go from the most precise to the widest, and
|
|
305
|
+
the pages of a `RouteTravel` row alike:
|
|
306
|
+
|
|
482
307
|
```jsx
|
|
483
308
|
<Route>
|
|
484
309
|
<Route route={MY_GAMES_ROUTE} element={MyGamesPage} />
|
|
@@ -488,18 +313,11 @@ the branch taken when no sibling matches.
|
|
|
488
313
|
</Route>
|
|
489
314
|
```
|
|
490
315
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
- **A section with a shared prefix owns its own sub-router.** One leaf
|
|
497
|
-
`<Route route={DASHBOARD_SECTION_ROUTE} element={DashboardSection} />` at the
|
|
498
|
-
top, and `DashboardSection` renders its own `<Route>` tree plus whatever chrome
|
|
499
|
-
it has. Everything about the section is in one file.
|
|
500
|
-
- **Pages sharing a layout but no prefix** (`/profile` and `/settings` inside an
|
|
501
|
-
authenticated shell) use a container `<Route element={AuthLayout}>`: the active
|
|
502
|
-
child is injected into the layout as its children.
|
|
316
|
+
A section with a shared prefix owns its own sub-router: one leaf on
|
|
317
|
+
`/dashboard/` whose element renders its own `<Route>` tree and chrome, handing its
|
|
318
|
+
local state down through `elementProps`. Pages sharing a layout but no prefix
|
|
319
|
+
(`/profile`, `/settings`) use a container `<Route element={AuthLayout}>`, which
|
|
320
|
+
injects the active child as its children.
|
|
503
321
|
|
|
504
322
|
### Loading data
|
|
505
323
|
|
|
@@ -525,115 +343,82 @@ section of pages shares one:
|
|
|
525
343
|
</Route>
|
|
526
344
|
```
|
|
527
345
|
|
|
528
|
-
The order matters: the boundary goes **outside** the `<Loading>`. A page
|
|
529
|
-
|
|
530
|
-
|
|
346
|
+
The order matters: the boundary goes **outside** the `<Loading>`. A page suspends
|
|
347
|
+
first and fails second, and a boundary placed under the `Suspense` it suspended
|
|
348
|
+
in is part of the tree being held.
|
|
531
349
|
|
|
532
350
|
A branch selected inside a wrapper keeps it — the container renders the active
|
|
533
|
-
branch alone, wrapper included
|
|
534
|
-
subset of the branches rather than the whole router. What happens to a failure
|
|
535
|
-
|
|
351
|
+
branch alone, wrapper included — so `<Loading>`/`<ErrorBoundary>` can bracket a
|
|
352
|
+
subset of the branches rather than the whole router. What happens to a failure no
|
|
353
|
+
boundary takes: [error_handling.md](./error_handling.md).
|
|
536
354
|
|
|
537
355
|
## Links and tab rows
|
|
538
356
|
|
|
539
|
-
`<Link route={…}>` builds its href from the route
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
for the whole row, where the bar that marks the current tab goes:
|
|
357
|
+
`<Link route={…}>` builds its href from the route, knows on its own whether it is
|
|
358
|
+
the current one — that is what draws the current-tab state — and fetches the code
|
|
359
|
+
of where it leads when the pointer or the focus reaches it (see
|
|
360
|
+
[dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)). `<Nav>`
|
|
361
|
+
says once, for the whole row, where the bar that marks the current tab goes:
|
|
544
362
|
|
|
545
363
|
```jsx
|
|
546
364
|
<Nav currentIndicator>
|
|
547
|
-
<Link route={MY_GAMES_ROUTE} variant="tab">
|
|
365
|
+
<Link route={MY_GAMES_ROUTE} variant="tab" replace>
|
|
548
366
|
Mes parties
|
|
549
367
|
</Link>
|
|
550
|
-
<Link route={CANDIDATE_GAMES_ROUTE} variant="tab">
|
|
368
|
+
<Link route={CANDIDATE_GAMES_ROUTE} variant="tab" replace>
|
|
551
369
|
Candidatures
|
|
552
370
|
</Link>
|
|
553
371
|
</Nav>
|
|
554
372
|
```
|
|
555
373
|
|
|
556
|
-
The bar
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
374
|
+
The bar glides from one tab to the next when the change between them plays as a
|
|
375
|
+
view transition — a `RouteTravel` swipe, a route transition — because `<Nav>`
|
|
376
|
+
names it for that movement (`currentIndicatorSlides`, on by default), and only
|
|
377
|
+
for a movement between two tabs of its own row: during any other route movement
|
|
378
|
+
the bar leaves or arrives with its row, a named element inside a page that moves
|
|
379
|
+
standing still (see
|
|
380
|
+
[view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)).
|
|
381
|
+
With no transition at all, it jumps.
|
|
382
|
+
|
|
383
|
+
`replace` is what a row of tabs wants: the neighbour is a lateral move, not a step
|
|
384
|
+
deeper, so the destination takes the place of the current history entry — the
|
|
385
|
+
whole row weighs one entry, and the back button (the arrow at the top, the
|
|
386
|
+
phone's own) leaves by where the reader came in. The link stays a link (an
|
|
387
|
+
address, a middle click, `aria-current`); the same word is
|
|
388
|
+
`navTo(url, { replace: true })`, `route.redirectTo()`, `<Button replace>`. A
|
|
389
|
+
replaced entry inherits the state of the one it takes the place of: **an entry's
|
|
390
|
+
state does not say how it arrived** — only the navigation being applied does, and
|
|
391
|
+
navi is the one applying it.
|
|
392
|
+
|
|
393
|
+
An entry usually stands for a whole section: "Lieux" stays lit on `/places/42`
|
|
394
|
+
with `currentAlso={[PLACE_ROUTE]}`, and `currentExcept` names a place under the
|
|
395
|
+
section it does NOT stand for. Both amend what a link claims about itself and
|
|
396
|
+
nothing else — neither is an answer to a route that lies about where the reader
|
|
575
397
|
is (see [what its address may say](#a-layer-over-the-screen-what-its-address-may-say)).
|
|
576
398
|
|
|
577
|
-
A row of tabs is a lateral move: the neighbour is one finger away, and going
|
|
578
|
-
there is not going one step deeper. `replace` says exactly that — the
|
|
579
|
-
destination takes the place of the current history entry instead of stacking
|
|
580
|
-
onto it, so the whole row weighs one entry and the back button (the arrow at the
|
|
581
|
-
top, the phone's own) leaves by where the reader came in:
|
|
582
|
-
|
|
583
|
-
```jsx
|
|
584
|
-
<Link route={CANDIDATE_GAMES_ROUTE} variant="tab" replace>
|
|
585
|
-
Candidatures
|
|
586
|
-
</Link>
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
The link stays a link — an address, a middle click, the keyboard, `aria-current`
|
|
590
|
-
— only the way there changes. It is the same word as `navTo(url, { replace:
|
|
591
|
-
true })` and `route.redirectTo()` — and as `<Button replace>`, on an `href` or
|
|
592
|
-
on a `--navi-nav-to` command.
|
|
593
|
-
|
|
594
|
-
A replaced entry inherits the state of the one it takes the place of (so does
|
|
595
|
-
`route.redirectTo()`): **an entry's state does not say how it arrived**. Only
|
|
596
|
-
the navigation being applied says that, and navi is the one applying it — see
|
|
597
|
-
the back arrow below, which is what that fact is usually needed for.
|
|
598
|
-
|
|
599
399
|
## The back arrow: `navBack`
|
|
600
400
|
|
|
601
401
|
An arrow drawn inside the app promises the screen the reader came from — never
|
|
602
|
-
the page
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
neither can an entry's state, for the reason just above. What answers is a count
|
|
609
|
-
of how many entries of THIS document stand underneath, kept as the navigations
|
|
610
|
-
are applied and written into each entry so it survives a reload mid-stack. navi
|
|
611
|
-
keeps it: an app that kept its own would have to be told about every single
|
|
612
|
-
`replace` it performs, and the one it forgets shows up only on a cold-opened
|
|
613
|
-
screen after a precise gesture.
|
|
402
|
+
the page before the app, which is what `history.back()` gives on a screen opened
|
|
403
|
+
cold from a shared link, a bookmark, a notification. `window.history.length`
|
|
404
|
+
counts the whole tab and cannot tell the two cases apart, and neither can an
|
|
405
|
+
entry's state (above). navi counts the entries of THIS document underneath,
|
|
406
|
+
through every push and replace it applies — an app keeping its own count forgets
|
|
407
|
+
one, and it shows only on a cold-opened screen after a precise gesture.
|
|
614
408
|
|
|
615
409
|
```jsx
|
|
616
|
-
const
|
|
617
|
-
const canNavBack = useCanNavBack();
|
|
618
|
-
...
|
|
619
|
-
};
|
|
410
|
+
const canNavBack = useCanNavBack(); // reactive; canNavBackSignal outside components
|
|
620
411
|
```
|
|
621
412
|
|
|
622
|
-
`useCanNavBack()` (or `canNavBackSignal` outside a component) is reactive: the
|
|
623
|
-
arrow appears and disappears as the stack moves, it is not decided at mount.
|
|
624
|
-
|
|
625
413
|
```js
|
|
626
414
|
navBack({ fallback: USER_ME_ROUTE.buildUrl() });
|
|
627
415
|
```
|
|
628
416
|
|
|
629
417
|
The `fallback` takes the place of the current entry rather than stacking on it:
|
|
630
418
|
pushed, it would put the screen just left one press ahead, and the phone's own
|
|
631
|
-
back button would walk straight back into it — a loop with no way out of the
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
Said by a button, it is a command, the fallback being its argument the way
|
|
636
|
-
`--navi-nav-to` carries its destination:
|
|
419
|
+
back button would walk straight back into it — a loop with no way out of the app.
|
|
420
|
+
Without a `fallback`, a `navBack()` with nothing of ours behind does nothing. Said
|
|
421
|
+
by a button, it is a command carrying the fallback:
|
|
637
422
|
|
|
638
423
|
```jsx
|
|
639
424
|
<Button command={`--navi-nav-back:${USER_ME_ROUTE.buildUrl()}`}>←</Button>
|
|
@@ -643,7 +428,7 @@ Said by a button, it is a command, the fallback being its argument the way
|
|
|
643
428
|
|
|
644
429
|
`<RouteTravel>` wraps the `<Route>` tree of a row of tabs and makes every change
|
|
645
430
|
between them a movement — a tab pressed, a key, the back button, and a thumb
|
|
646
|
-
dragging the pages
|
|
431
|
+
dragging the pages:
|
|
647
432
|
|
|
648
433
|
```jsx
|
|
649
434
|
<SectionNav />
|
|
@@ -656,215 +441,144 @@ dragging the pages.
|
|
|
656
441
|
</RouteTravel>
|
|
657
442
|
```
|
|
658
443
|
|
|
659
|
-
The router still mounts only the branch that matches
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
towards the same neighbour must not write two different histories. `onTravel`
|
|
678
|
-
decides otherwise. A replace normally leaves the scroll where it is; a row of
|
|
679
|
-
tabs is the exception, and gives each tab back the offset it was read at — see
|
|
680
|
-
[A row of tabs, where a replace IS an arrival](#a-row-of-tabs-where-a-replace-is-an-arrival).
|
|
681
|
-
|
|
682
|
-
Several `RouteTravel` boxes may live on one page — a section of the path and a
|
|
683
|
-
search param of the root route are two rows of tabs, both live — and only the one
|
|
684
|
-
actually travelling is captured.
|
|
685
|
-
|
|
686
|
-
Demo: [../src/nav/demos/route_travel/route_travel.html](../src/nav/demos/route_travel/route_travel.html)
|
|
687
|
-
and [../src/nav/demos/tabs/tabs.html](../src/nav/demos/tabs/tabs.html). The full
|
|
688
|
-
spec of the gesture is [drag_to_travel.md](./drag_to_travel.md).
|
|
689
|
-
|
|
690
|
-
`RouteTravel` is for pages that form a ROW the finger can push. Pages related
|
|
691
|
-
pair by pair without being a row — a game opened from several places, a
|
|
692
|
-
settings page rising over whatever showed it — are animated with
|
|
693
|
-
`defineRouteTransition` instead, and a given pair must be animated by one of
|
|
694
|
-
the two, never both: see [route_transitions.md](./route_transitions.md).
|
|
444
|
+
The router still mounts only the branch that matches: the page being left is the
|
|
445
|
+
picture the browser keeps of it, and the page arriving mounts during the gesture,
|
|
446
|
+
filling in under the finger as its own loading state. The order of the tabs —
|
|
447
|
+
"one step that way", which no URL says — is the order the children are written;
|
|
448
|
+
the row is on the **first** of its pages that matches, like a `<Route>`; a page
|
|
449
|
+
left out of the row does not travel. A swipe **replaces** the current entry, as
|
|
450
|
+
`<Link replace>` does for a tab pressed: two gestures towards the same neighbour
|
|
451
|
+
must not write two different histories — and a row of tabs is the one place a
|
|
452
|
+
replace moves the scroll
|
|
453
|
+
([below](#a-row-of-tabs-where-a-replace-is-an-arrival)). Several `RouteTravel`
|
|
454
|
+
boxes may live on one page, and only the one travelling is captured.
|
|
455
|
+
|
|
456
|
+
`RouteTravel` is for pages that form a ROW the finger can push; pages related
|
|
457
|
+
pair by pair are animated with `defineRouteTransition`, and a given pair by one
|
|
458
|
+
of the two, never both (see
|
|
459
|
+
[route_transitions.md](./route_transitions.md#route-transitions-and-routetravel--one-pair-one-system)).
|
|
460
|
+
The gesture itself: [drag_to_travel.md](./drag_to_travel.md). Demos:
|
|
461
|
+
`src/nav/demos/route_travel/route_travel.html`, `src/nav/demos/tabs/tabs.html`.
|
|
695
462
|
|
|
696
463
|
## Where a navigation lands: the scroll
|
|
697
464
|
|
|
698
465
|
Five cases, and they are not a policy to configure but five different facts:
|
|
699
466
|
|
|
700
|
-
- **Going somewhere new** (a
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
A push that keeps the path is not one: it stacks an entry over the page the
|
|
705
|
-
reader is scrolled in — a [layer](#a-layer-over-the-screen-what-its-address-may-say)
|
|
467
|
+
- **Going somewhere new** (a push to another path) lands at the top. It is an
|
|
468
|
+
arrival: left alone, the new entry would be born holding the previous page's
|
|
469
|
+
offset, which the browser would then hand back as this page's own. A push that
|
|
470
|
+
keeps the path — a [layer](#a-layer-over-the-screen-what-its-address-may-say)
|
|
706
471
|
opening, a [state whose values are places](#a-state-whose-values-are-places-history-push)
|
|
707
|
-
|
|
708
|
-
- **Going somewhere new, to an element** (`/places/le-set#tournaments`) lands
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
cannot do: it restores at the instant the entry changes, when the document
|
|
716
|
-
still holds the page being left, so anything further down than that page is
|
|
717
|
-
tall is clamped away.
|
|
472
|
+
— stacks an entry over the page the reader is scrolled in, and moves nothing.
|
|
473
|
+
- **Going somewhere new, to an element** (`/places/le-set#tournaments`) lands on
|
|
474
|
+
the element once it is rendered (see
|
|
475
|
+
[Landing on an element](#landing-on-an-element-the-fragment)).
|
|
476
|
+
- **Going back or forward** lands where that page was left, put back once the
|
|
477
|
+
page is really rendered. The browser restores at the instant the entry changes,
|
|
478
|
+
while the document still holds the page being left, and clamps away anything
|
|
479
|
+
further down than that page is tall.
|
|
718
480
|
- **Replacing the entry** (`<Link replace>`, `route.redirectTo()`, a param
|
|
719
|
-
settling
|
|
720
|
-
|
|
721
|
-
tabs is the one shape where that reading is wrong, see below.
|
|
481
|
+
settling) moves nothing: the same place said differently — except in a row of
|
|
482
|
+
tabs, below.
|
|
722
483
|
- **A reload** lands where one was, as it would have without navi.
|
|
723
484
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
just left resumes it where it was. Only towards entries of this document (a
|
|
731
|
-
traversal to another one would be a full page load no link asked for), and
|
|
732
|
-
never when the push carries explicit state.
|
|
733
|
-
|
|
734
|
-
Everywhere else a `<Link>` is an arrival and lands at the top. A back arrow
|
|
735
|
-
that must ALWAYS behave as a back — even far from the entry it targets, even
|
|
736
|
-
in a browser with no Navigation API — is `navBack()`. Where there may be
|
|
737
|
-
nothing to go back to (a shared link opened cold), decide what the arrow does
|
|
738
|
-
from the history, not from the link.
|
|
485
|
+
Where the browser exposes its stack (the Navigation API), **a `<Link>` whose
|
|
486
|
+
destination is the entry right next to the current one becomes a real
|
|
487
|
+
traversal**: a "back" link to the page one just came from behaves as a back — no
|
|
488
|
+
A, B, A, B… growth, and the scroll comes back — and one step forward too. Only
|
|
489
|
+
towards entries of this document, and never when the push carries explicit
|
|
490
|
+
state. An arrow that must ALWAYS behave as a back is `navBack()`.
|
|
739
491
|
|
|
740
492
|
### Landing on an element: the fragment
|
|
741
493
|
|
|
742
494
|
A link meant to bring one element under the reader's eyes — a notification
|
|
743
|
-
pointing at a section several screens down, a shared link to a comment, a row
|
|
744
|
-
|
|
495
|
+
pointing at a section several screens down, a shared link to a comment, a row in
|
|
496
|
+
a list — is an `id` on the element and a `#id` in the link. Nothing else:
|
|
745
497
|
|
|
746
498
|
```jsx
|
|
747
499
|
<Box id="tournaments">…</Box>
|
|
748
500
|
|
|
749
|
-
// anywhere else
|
|
501
|
+
// anywhere else — <Link route> builds no fragment, so the link spells its href
|
|
750
502
|
<Link href={`${PLACE_ROUTE.buildUrl({ slug })}#tournaments`}>…</Link>
|
|
751
503
|
```
|
|
752
504
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
- **
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
and the attribute disagree.
|
|
790
|
-
- **Adjusting it**: `setUrlTargetOptions()`, once, at the app's start — the
|
|
791
|
-
alignment (`block`), `behavior`, `markDuration`, `graceAfterIdle`, `maxWait`.
|
|
792
|
-
|
|
793
|
-
**`:target` is the lasting "this one"**, and `data-url-target` the moment it
|
|
794
|
-
arrived. navi sets `:target` on the element as it lands on it — the browser
|
|
795
|
-
alone never would after a navigation navi routes, nor for an element that
|
|
796
|
-
arrived after the load — and takes it off once the URL stops naming the
|
|
797
|
-
element, which the browser alone would not either: a search param written on
|
|
798
|
-
the page rebuilds the address without its fragment, a back returns to the page
|
|
799
|
-
without one. It stays on the element navi landed on; a node rendered in its
|
|
800
|
-
place later is another one. In JS, `useUrlTargetId()` is the id the hash
|
|
801
|
-
designates, `""` when there is none, re-rendering when it changes.
|
|
802
|
-
|
|
803
|
-
#### A fragment, or a search param
|
|
505
|
+
The browser answers a fragment when the document finishes loading and on a
|
|
506
|
+
fragment navigation within the page — both before the data has drawn the element
|
|
507
|
+
— and a navigation navi routes is not a fragment navigation at all. So navi
|
|
508
|
+
answers every navigation it routes to a URL carrying a hash (a `#id` link to the
|
|
509
|
+
page one is already on stays the browser's own, without the wait or the ring):
|
|
510
|
+
|
|
511
|
+
- **it waits for the element, and for the page.** The element must exist and
|
|
512
|
+
show something (`checkVisibility`): one inside a closed tab or a folded
|
|
513
|
+
`<details>` has not arrived, and navi opens nothing to reach it. And no route
|
|
514
|
+
or action may be loading: what sits above it may still push it down, and a
|
|
515
|
+
node reused from the page being left is there before its own data;
|
|
516
|
+
- **it gives up** once the document has stopped working for a moment (or after a
|
|
517
|
+
maximum wait, for one that never does): a link to an element that is gone lands
|
|
518
|
+
where a link without a fragment would, with no mark;
|
|
519
|
+
- **a return is not an arrival**: back, forward and a reload land where the page
|
|
520
|
+
was left, and only put `:target` on the element — no scroll, no ring, no focus;
|
|
521
|
+
- **it answers once per arrival**: a search param written while the reader is
|
|
522
|
+
there does not throw them back to the element; pressing the very link one is on
|
|
523
|
+
answers again.
|
|
524
|
+
|
|
525
|
+
What the reader gets: the element against the top edge, instantly; the keyboard
|
|
526
|
+
focus on it when it is itself focusable — an `id` on the link or the button, not
|
|
527
|
+
on a box around one; and a fading ring, the element carrying `data-url-target`.
|
|
528
|
+
The ring is coloured by `--navi-url-target-color` and lives in `@layer navi`: an
|
|
529
|
+
unlayered `[data-url-target] { … }` replaces it, `animation: none` removes it.
|
|
530
|
+
Its length is `markDuration`, set with `setUrlTargetOptions()` along with the
|
|
531
|
+
alignment and the waits; it also publishes `--navi-url-target-duration`, and
|
|
532
|
+
setting only the variable makes the ring and the attribute disagree.
|
|
533
|
+
|
|
534
|
+
**`:target` is the lasting "this one"**, `data-url-target` the moment it arrived.
|
|
535
|
+
navi sets `:target` as it lands on the element — the browser never would after a
|
|
536
|
+
navigation navi routes, nor for an element arriving after the load — and takes
|
|
537
|
+
it off once the URL stops naming it: a search param a route writes rebuilds the
|
|
538
|
+
address without its fragment, a back returns to the page without one. A node
|
|
539
|
+
rendered in its place later is another element. `useUrlTargetId()` is the id the
|
|
540
|
+
hash designates, reactive.
|
|
804
541
|
|
|
805
542
|
A fragment says where to look; a search param says what to show. When the page
|
|
806
543
|
draws something differently for the value — a filter, a selected row, an open
|
|
807
|
-
panel — it is a [search param](#search-params)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
A search param plus a `scrollIntoView` in an effect rebuilds this, less well:
|
|
812
|
-
the effect fires at mount, before what sits above the element has laid out,
|
|
813
|
-
and nothing ever tells it to stop waiting.
|
|
544
|
+
panel — it is a [search param](#search-params); when only the reader's eyes
|
|
545
|
+
should go somewhere, a fragment, down to a single row (`#tournament_42`) — never
|
|
546
|
+
a search param plus a `scrollIntoView` in an effect, which fires before what sits
|
|
547
|
+
above the element has laid out, with nothing to tell it how long to wait.
|
|
814
548
|
|
|
815
549
|
### A row of tabs, where a replace IS an arrival
|
|
816
550
|
|
|
817
551
|
The tabs of a `<RouteTravel>` navigate by replacing, and yet each one is another
|
|
818
|
-
route
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
- the arriving tab is given back the offset it was read at, once it is really
|
|
828
|
-
rendered;
|
|
829
|
-
- a tab never read opens at its **top**, rather than wherever its neighbour
|
|
830
|
-
happened to be;
|
|
831
|
-
- the clamp itself is never recorded. It is not the reader scrolling, and the
|
|
832
|
-
url it would be written against is already the arriving tab's — recorded, it
|
|
833
|
-
destroys that tab's own position, which is then what a later back or forward
|
|
834
|
-
hands out.
|
|
552
|
+
route, sharing one scrollport — the document — that the tab on screen makes
|
|
553
|
+
tall: the moment the arriving tab is shorter, the browser clamps, and the
|
|
554
|
+
reader's place is gone. The row is the only thing that knows this, so on every
|
|
555
|
+
travel — a tab pressed, a thumb, a wheel, a travel put back — it gives the
|
|
556
|
+
arriving tab the offset it was read at, once it is really rendered; opens a tab
|
|
557
|
+
never read at its **top**, not wherever its neighbour happened to be; and never
|
|
558
|
+
records the clamp, which, recorded against the arriving tab's url, would destroy
|
|
559
|
+
that tab's own position.
|
|
835
560
|
|
|
836
561
|
**Only where the row owns the document**: nothing between the travelling box and
|
|
837
|
-
the viewport may scroll
|
|
838
|
-
|
|
839
|
-
the
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
that angle first.
|
|
845
|
-
|
|
846
|
-
Pages that scroll inside themselves rather than scrolling the document are not
|
|
847
|
-
concerned either way: each one brings its own scrollport, which goes away with
|
|
848
|
-
the page and has nothing to give back.
|
|
849
|
-
|
|
850
|
-
What is not covered: a page whose height depends on something still loading is
|
|
851
|
-
not tall enough at the moment its position is put back, so a deep position is
|
|
852
|
-
clamped as it was before. Only the page knows when it is whole.
|
|
562
|
+
the viewport may scroll, and a row inside a scroller of its own leaves that
|
|
563
|
+
offset alone. An `overflow: hidden` on any ancestor is enough to take the row out
|
|
564
|
+
of the document's scrollport — it holds an offset even without a scrollbar, where
|
|
565
|
+
`overflow: clip` holds none — so it is the first thing to look for when a row
|
|
566
|
+
does not give positions back. And a page whose height depends on something still
|
|
567
|
+
loading is not tall enough when its position is put back: only the page knows
|
|
568
|
+
when it is whole.
|
|
853
569
|
|
|
854
570
|
## Creating something, then editing it
|
|
855
571
|
|
|
856
572
|
The create screen, the page of what was created, the edit screen — three routes,
|
|
857
|
-
one form, and a movement between them
|
|
858
|
-
[create_and_edit.md](./create_and_edit.md)
|
|
859
|
-
rules that decide the shape of the `<Route>` tree are spelled out (several routes
|
|
860
|
-
match at once; the first matching branch wins).
|
|
573
|
+
one form, and a movement between them — are assembled in
|
|
574
|
+
[create_and_edit.md](./create_and_edit.md).
|
|
861
575
|
|
|
862
576
|
## Tabs that are not routes
|
|
863
577
|
|
|
864
578
|
`SlideContainer` holds slides that replace one another in one box, with the same
|
|
865
|
-
gestures and the same travelling bar, and — unless
|
|
866
|
-
|
|
867
|
-
|
|
579
|
+
gestures and the same travelling bar, and — unless its signal is one the URL
|
|
580
|
+
holds, see below — nothing written to the URL. Use it when the position genuinely
|
|
581
|
+
is not a place one should be able to link to:
|
|
868
582
|
|
|
869
583
|
- the steps of a wizard, or the screens of a picker, inside a dialog or a popover
|
|
870
584
|
— a popup is promoted to the browser's top layer, so no container can hold two
|
|
@@ -886,33 +600,29 @@ route.
|
|
|
886
600
|
</SlideContainer>
|
|
887
601
|
```
|
|
888
602
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
and behaves like a button: this is not a link to anywhere.
|
|
603
|
+
The row names its container by id, so it can sit anywhere on the page, and its
|
|
604
|
+
bar follows the slides, a finger dragging them included. `<Link slide>` has no
|
|
605
|
+
href and behaves like a button: this is not a link to anywhere.
|
|
893
606
|
|
|
894
607
|
### A `SlideContainer` in the URL: a position that is not a place one came from
|
|
895
608
|
|
|
896
609
|
"Should a link be able to open the app on this?" has a third answer, and a wizard
|
|
897
|
-
is exactly it: **yes for reading and for reloading, no for history.** The step
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
There is nothing to invent for it: a search param already IS a position in the
|
|
906
|
-
URL that replaces rather than pushes ([Search params](#search-params)). Declare
|
|
907
|
-
the step as one, and hand its signal to the container:
|
|
610
|
+
is exactly it: **yes for reading and for reloading, no for history.** The step
|
|
611
|
+
should be legible in the address bar and survive a reload, and should NOT stack
|
|
612
|
+
an entry per step: the back arrow of a form means "leave this form", not "one
|
|
613
|
+
question back", and four steps that each push walk the reader backwards through a
|
|
614
|
+
form they thought they had left. A search param already IS a position in the URL
|
|
615
|
+
that replaces rather than pushes; declare the step as one, and hand its signal to
|
|
616
|
+
the container:
|
|
908
617
|
|
|
909
618
|
```js
|
|
910
619
|
const stepSignal = stateSignal("when", {
|
|
911
620
|
id: "step",
|
|
912
621
|
oneOf: ["when", "where", "who", "recap", "done"],
|
|
622
|
+
autoFix: true, // a value outside oneOf is repaired, and the address with it
|
|
913
623
|
// the step qualifies THIS visit, not the screen: a link built to the editor
|
|
914
|
-
// does not inherit
|
|
915
|
-
//
|
|
624
|
+
// does not inherit it, and it goes back to the default when the route stops
|
|
625
|
+
// matching
|
|
916
626
|
weak: true,
|
|
917
627
|
});
|
|
918
628
|
export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
|
|
@@ -924,31 +634,20 @@ export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
|
|
|
924
634
|
<SlideContainer signal={stepSignal}>
|
|
925
635
|
```
|
|
926
636
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
way would ask it, and the first one that holds is where one stops. `?step=done`
|
|
942
|
-
cannot open a confirmation screen for something nobody sent. The signal is then
|
|
943
|
-
written with the area actually shown, so the address never says one is
|
|
944
|
-
somewhere one is not;
|
|
945
|
-
- **only the travels that HAPPENED are written.** A travel a lock refused, or one
|
|
946
|
-
`onCurrentChange` refused late, never reaches the signal — or is written back
|
|
947
|
-
when it does.
|
|
948
|
-
|
|
949
|
-
A container remembers nothing across a reload, so a step whose `required` the app
|
|
950
|
-
knows is already satisfied says so itself (`required={!alreadyFilled}`); the same
|
|
951
|
-
holds for a hold that a finished job lifts (`preventNavNext={!published}`).
|
|
637
|
+
Where it opens is the state's own default — a bound container does not own its
|
|
638
|
+
position, so one place says where the step starts, the same place a reset goes
|
|
639
|
+
back to — and the param is absent from the address while the step IS that
|
|
640
|
+
default. The container **walks** to the step the address asks for rather than
|
|
641
|
+
jumping to it: the address comes from outside the box (typed, shared, kept from a
|
|
642
|
+
session that has moved on), so every slide on the way is asked to let go the way
|
|
643
|
+
a key would ask it, and the first one that holds is where one stops —
|
|
644
|
+
`?step=done` cannot open a confirmation for something nobody sent. The signal is
|
|
645
|
+
then written with the area actually shown, and only the travels that HAPPENED are
|
|
646
|
+
written (one a lock or `onCurrentChange` refused never reaches it, or is written
|
|
647
|
+
back). A value no slide carries is left as asked: refusing it is the state's job,
|
|
648
|
+
`oneOf` + `autoFix`. A container remembers nothing across a reload, so a step the
|
|
649
|
+
app knows is already satisfied says so itself (`required={!alreadyFilled}`,
|
|
650
|
+
`preventNavNext={!published}`).
|
|
952
651
|
|
|
953
652
|
**A start that depends on the page one is on** — a wizard creating something
|
|
954
653
|
opens on its first question, the same wizard editing something opens on its
|
|
@@ -963,72 +662,44 @@ route("/alerts/:alertId/edit", {
|
|
|
963
662
|
```
|
|
964
663
|
|
|
965
664
|
Everything that asks "is this the default" then asks the route one is on: both
|
|
966
|
-
addresses stay clean on the step they open on
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
Not a `computed` over the route's `matchingSignal`: a state and the route that
|
|
971
|
-
declares it cannot name each other — whichever is written first, the other is
|
|
972
|
-
not initialised yet, and a dynamic default is read while `stateSignal` is being
|
|
973
|
-
built. The route is the right place regardless; it is the thing that knows which
|
|
974
|
-
page one is on.
|
|
975
|
-
|
|
976
|
-
Two containers on one screen are two signals, and that is the whole answer to
|
|
977
|
-
"which one owns the param": the one holding the route's signal. A gallery of
|
|
978
|
-
seven wizards side by side hands each of them a `useSignal` of its own, and
|
|
979
|
-
nothing goes into the address.
|
|
665
|
+
addresses stay clean on the step they open on, and going from one to the other
|
|
666
|
+
moves the step with it. Two containers on one screen are two signals — the one
|
|
667
|
+
holding the route's signal owns the param; a gallery of wizards side by side
|
|
668
|
+
hands each a `useSignal` of its own, and nothing goes into the address.
|
|
980
669
|
|
|
981
670
|
### A state whose values ARE places: `history: "push"`
|
|
982
671
|
|
|
983
672
|
Replacement is the default because most URL-held state qualifies the screen one
|
|
984
673
|
is on. A state whose values are places one came from — the photo being looked at
|
|
985
|
-
in a gallery — says so where
|
|
674
|
+
in a gallery — says so where it is declared, and one write can still say
|
|
675
|
+
otherwise:
|
|
986
676
|
|
|
987
677
|
```js
|
|
988
678
|
const photoSignal = stateSignal(undefined, { id: "photo", history: "push" });
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
Every write of it then stacks an entry, wherever the write comes from. Except
|
|
992
|
-
where the writer knows this particular move is not one: a slide reached by
|
|
993
|
-
DRAGGING replaces even in a container that pushes, because swiping back and forth
|
|
994
|
-
with a thumb is browsing, not a trail one wants to walk home along. That is said
|
|
995
|
-
at the write rather than declared:
|
|
996
|
-
|
|
997
|
-
```js
|
|
998
679
|
photoSignal.set(nextPhoto, { history: "replace" });
|
|
999
680
|
```
|
|
1000
681
|
|
|
1001
|
-
|
|
1002
|
-
|
|
682
|
+
A slide reached by DRAGGING is such a write — swiping back and forth with a thumb
|
|
683
|
+
is browsing, not a trail one wants to walk home along — and `SlideContainer`
|
|
684
|
+
replaces for its own drags even when its state pushes.
|
|
1003
685
|
|
|
1004
686
|
### What this is not
|
|
1005
687
|
|
|
1006
|
-
It is not a route.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
stands. A position several parts of the app must react to is still a route.
|
|
1011
|
-
|
|
1012
|
-
Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
|
|
688
|
+
It is not a route. A search param bound to a `SlideContainer` moves the box, not
|
|
689
|
+
a page: nothing is declared for the steps, nothing matches on them, and no route
|
|
690
|
+
transition is written for them. A position several parts of the app must react
|
|
691
|
+
to is still a route. Demo: `src/layout/demos/8_slide_container_demo.html`.
|
|
1013
692
|
|
|
1014
693
|
## A layer over the screen: what its address may say
|
|
1015
694
|
|
|
1016
695
|
Some things are opened from everywhere and drawn OVER whatever the reader was
|
|
1017
696
|
looking at: the settings behind a gear in the top bar, a photo, a help sheet.
|
|
1018
|
-
Closing one puts the reader back exactly where they were —
|
|
1019
|
-
|
|
697
|
+
Closing one puts the reader back exactly where they were — on the page they had,
|
|
698
|
+
not one the app picked — and the address has to hold it, for the same reasons
|
|
1020
699
|
every other position does: a reload lands on it, a link opens on it, the back
|
|
1021
|
-
button closes it.
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
Layout should not decide routing. But routing decides a great deal of what is on
|
|
1026
|
-
screen: which page is mounted, whose data loads, which bar entry lights up, which
|
|
1027
|
-
movement plays between two screens. An address is not a label — it is read by
|
|
1028
|
-
everything — so a layer, which is a pure fact of layout, cannot be given just any
|
|
1029
|
-
address.
|
|
1030
|
-
|
|
1031
|
-
The rule that falls out of it, and the whole of this section:
|
|
700
|
+
button closes it. Routing decides a great deal of what is on screen — which page
|
|
701
|
+
is mounted, whose data loads, which bar entry lights up, which movement plays —
|
|
702
|
+
so a layer, a pure fact of layout, cannot be given just any address:
|
|
1032
703
|
|
|
1033
704
|
> The URL may say what is drawn OVER the screen. It must never name a place the
|
|
1034
705
|
> reader is not at.
|
|
@@ -1040,43 +711,29 @@ when they conceptually belong to "me". Written `/me/settings`, the router
|
|
|
1040
711
|
believes otherwise, and everything that reads the router inherits the belief:
|
|
1041
712
|
|
|
1042
713
|
- the bar entry for the "me" section lights up while the settings cover a game;
|
|
1043
|
-
-
|
|
1044
|
-
fiction that lasts as long as the animation
|
|
714
|
+
- the screen the reader was on is REPLACED by a page of the "me" section: the
|
|
715
|
+
covering is a fiction that lasts as long as the animation;
|
|
1045
716
|
- a route transition plays a crossing between two pages that never crossed, and
|
|
1046
717
|
its back half plays on every way out — the sheet lifts to reveal a page that
|
|
1047
718
|
was never underneath;
|
|
1048
|
-
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
These do not get fixed one at a time. `currentExcept` stops one link from
|
|
1052
|
-
claiming "you are here"; a `"none"` relation silences one crossing; a hand-rolled
|
|
1053
|
-
`active` replaces one reading. Each patches a reader of the lie without removing
|
|
1054
|
-
it, and the next reader arrives quietly wrong.
|
|
719
|
+
- closing does not return to `/me`, it returns to wherever the reader was, which
|
|
720
|
+
the address never said.
|
|
1055
721
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
about not being there, and every symptom above goes with it. What it still is,
|
|
1060
|
-
though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
|
|
1061
|
-
has nowhere written to return to. `whatever/settings` is the same answer in a
|
|
1062
|
-
longer word.
|
|
1063
|
-
|
|
1064
|
-
So the rule is not about search params. It is about not claiming a position. A
|
|
1065
|
-
path can be honest; what a path cannot do is carry what is underneath.
|
|
722
|
+
These do not get fixed one at a time: `currentExcept`, a `"none"` relation, a
|
|
723
|
+
hand-rolled `active` each patch one reader of the lie, and the next reader
|
|
724
|
+
arrives quietly wrong.
|
|
1066
725
|
|
|
1067
726
|
### What the search param buys: the way back is in the address
|
|
1068
727
|
|
|
1069
|
-
`/
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
`/settings` has to remember instead
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
which is what makes the param shape the right model rather than the convenient
|
|
1079
|
-
one.
|
|
728
|
+
`/settings` at the top level is honest about not being inside `/me`, but it is
|
|
729
|
+
still a PAGE: the screen beneath is gone, and closing has nowhere written to
|
|
730
|
+
return to — so the rule is about not claiming a position, not about search
|
|
731
|
+
params. `/places?settings` says both which screen the reader is on and what is
|
|
732
|
+
drawn over it, so closing needs no memory at all: exact after a reload, from a
|
|
733
|
+
link someone sent, in a new tab. `/settings` has to remember instead — the
|
|
734
|
+
browser's history of the tab often covers for it after a reload, but a link
|
|
735
|
+
someone else opens has none of it, and neither does a bookmark. Only the address
|
|
736
|
+
travels.
|
|
1080
737
|
|
|
1081
738
|
### The wiring
|
|
1082
739
|
|
|
@@ -1095,31 +752,35 @@ export const ROOT_ROUTE = route("/", {
|
|
|
1095
752
|
```jsx
|
|
1096
753
|
<Button command="--navi-toggle" commandFor="settings_panel">⚙</Button>
|
|
1097
754
|
…
|
|
1098
|
-
<SidePanel
|
|
755
|
+
<SidePanel
|
|
756
|
+
id="settings_panel"
|
|
757
|
+
signal={settingsSignal}
|
|
758
|
+
side="top"
|
|
759
|
+
expandY
|
|
760
|
+
animation
|
|
761
|
+
/>
|
|
1099
762
|
```
|
|
1100
763
|
|
|
1101
764
|
Every piece of it answers something:
|
|
1102
765
|
|
|
1103
|
-
- **declared on the ROOT route**, so the layer opens over every screen
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
mounted while closed so it has somewhere to animate from;
|
|
766
|
+
- **declared on the ROOT route**, so the layer opens over every screen, where its
|
|
767
|
+
door in the furniture is;
|
|
768
|
+
- **`weak`**: a link built to a page never inherits a layer that happens to be
|
|
769
|
+
open — a layer qualifies one visit, it is not part of anyone's address;
|
|
770
|
+
- **`history: "push"`**: the layer is a place one came from, and the back button
|
|
771
|
+
closes it. The closing is never an entry of its own —
|
|
772
|
+
[popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways) owns what a
|
|
773
|
+
`signal` bound to a URL writes on open, on close, and on cancel;
|
|
774
|
+
- **the panel is rendered outside the page area**, next to the router: over the
|
|
775
|
+
pages and the fixed bars alike, mounted while closed, `animation` giving it an
|
|
776
|
+
entrance of its own (a `SidePanel` has none by default);
|
|
1115
777
|
- **`expandY`**, because a sheet is content-tall by default and a layer covers
|
|
1116
|
-
the screen.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
- **no `defineRouteTransition` for it.**
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
([route_transitions.md](./route_transitions.md)).
|
|
778
|
+
the screen. A tab row inside it must NOT get `expand`: that is both axes, and
|
|
779
|
+
in the panel's column the row eats the height and pushes everything below it
|
|
780
|
+
off screen;
|
|
781
|
+
- **no `defineRouteTransition` for it.** The page beneath does not change, so
|
|
782
|
+
there is no pair of pages to move between — route transitions are for pages
|
|
783
|
+
replacing pages ([route_transitions.md](./route_transitions.md)).
|
|
1123
784
|
|
|
1124
785
|
### Places inside the layer
|
|
1125
786
|
|
|
@@ -1127,9 +788,9 @@ A layer big enough to have tabs holds them the way a wizard does — a search pa
|
|
|
1127
788
|
of its own, `oneOf` its pages, replacing rather than pushing, so the back button
|
|
1128
789
|
closes the layer instead of stepping back one tab. Everything in
|
|
1129
790
|
[A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
1130
|
-
applies unchanged, and a layer is exactly the case
|
|
1131
|
-
|
|
1132
|
-
|
|
791
|
+
applies unchanged, and a layer is exactly the case
|
|
792
|
+
[Tabs that are not routes](#tabs-that-are-not-routes) names for `SlideContainer`
|
|
793
|
+
over `RouteTravel`.
|
|
1133
794
|
|
|
1134
795
|
```js
|
|
1135
796
|
const settingsTabSignal = stateSignal("account", {
|
|
@@ -1141,16 +802,11 @@ const settingsTabSignal = stateSignal("account", {
|
|
|
1141
802
|
|
|
1142
803
|
The address then grows and shrinks with what the reader does: `/me` closed,
|
|
1143
804
|
`/me?settings` open on the tab it opens on, `/me?settings&settings_tab=alerts`
|
|
1144
|
-
one tab further, and back to `/me?settings` returning to the first.
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
(
|
|
1149
|
-
hold it: it is open on exactly one value of its signal (`value`, see
|
|
1150
|
-
[popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)), so a tab
|
|
1151
|
-
changing under it would read as the layer closing. `value` is for the other
|
|
1152
|
-
question — WHICH of several popups is open, one sheet per card under a single
|
|
1153
|
-
`?seat=<gameId>` — not for what varies inside one.
|
|
805
|
+
one tab further, and back to `/me?settings` returning to the first. Two params,
|
|
806
|
+
because they answer two questions — is the layer there, and which of its pages
|
|
807
|
+
is shown. A popup's `value` answers another one, WHICH of several popups is open
|
|
808
|
+
(one sheet per card under a single `?seat=<gameId>`), not what varies inside one
|
|
809
|
+
(see [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)).
|
|
1154
810
|
|
|
1155
811
|
### What it costs
|
|
1156
812
|
|
|
@@ -1161,14 +817,12 @@ This is a compromise, and it is worth stating rather than discovering:
|
|
|
1161
817
|
a route "you are covered", and nothing should be inferred from a layer being
|
|
1162
818
|
open — the reader is still on that page, and will be back on it in a moment;
|
|
1163
819
|
- **the router knows nothing about layers.** `<Route>` renders pages; the layer is
|
|
1164
|
-
drawn beside them by the application
|
|
1165
|
-
|
|
1166
|
-
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
Demo:
|
|
1172
|
-
[../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html)
|
|
820
|
+
drawn beside them by the application, and "over" is not a routing concept;
|
|
821
|
+
- **the app really is on the page underneath**, which is right for
|
|
822
|
+
`aria-current`, for the bar, for a link built while the layer is open — and
|
|
823
|
+
wrong for anything wanting "the settings are what is current": that reads the
|
|
824
|
+
layer's own state, not the router.
|
|
825
|
+
|
|
826
|
+
Demo: `src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html`
|
|
1173
827
|
— a bar holding both doors side by side: notifications, a page that replaces the
|
|
1174
828
|
screen with a route transition, and settings, a layer that covers it.
|