@jsenv/navi 0.29.68 → 0.29.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +347 -76
- package/dist/jsenv_navi.js.map +32 -23
- package/docs/AI_INSTRUCTIONS.md +9 -3
- package/docs/list_refresh.md +76 -4
- package/docs/navigation.md +15 -5
- package/docs/route_transitions.md +144 -7
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -165,9 +165,15 @@ consistency across the app, not from any single call site.
|
|
|
165
165
|
- `docs/route_transitions.md` — how pages move against each other on
|
|
166
166
|
navigation (`defineRouteTransition`): a transition states a relation the
|
|
167
167
|
user reads as a map, which movement fits which relation, when a global
|
|
168
|
-
default is right,
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
default is right, how one link or one `navTo` overrides what the pair says
|
|
169
|
+
for the length of a single navigation (`<Link routeTransition>`), marking the page
|
|
170
|
+
area between fixed bars, and why a pair of routes is animated by
|
|
171
|
+
`RouteTravel` or by a route transition but never both. It also says why a
|
|
172
|
+
test must wait for the page arriving rather than for its address — a back
|
|
173
|
+
taken before the page has rendered returns to a page that never left, which
|
|
174
|
+
looks exactly like the arriving page failing to load or refresh. Read it
|
|
175
|
+
before animating any navigation, and before believing a symptom that only
|
|
176
|
+
appears once a pair is animated.
|
|
171
177
|
- `docs/navigation.md` — how to build navigation: declaring routes
|
|
172
178
|
(`route()` / `setupRoutes()`), when a section is a route of its own rather
|
|
173
179
|
than a param, search params bound to signals, rendering with `<Route>`,
|
package/docs/list_refresh.md
CHANGED
|
@@ -4,6 +4,9 @@ When a write touches one item of a list, two questions decide what the user
|
|
|
4
4
|
sees: **what goes back to the network**, and **what stays on screen meanwhile**.
|
|
5
5
|
Getting either wrong turns "pause one row" into a full page reload.
|
|
6
6
|
|
|
7
|
+
The same two questions decide what a list does when the user leaves it and
|
|
8
|
+
comes back, which is the second half of this file.
|
|
9
|
+
|
|
7
10
|
The short answer:
|
|
8
11
|
|
|
9
12
|
- A write that returns the modified item fixes every list containing it, with
|
|
@@ -11,6 +14,8 @@ The short answer:
|
|
|
11
14
|
- `useAsyncData(action, { loading: true })` keeps returning the previous data
|
|
12
15
|
while the action re-runs — the list is never taken away unless the component
|
|
13
16
|
throws it away.
|
|
17
|
+
- On the way back, the **source** decides whether anything is asked again — not
|
|
18
|
+
the navigation, and not a transition playing between the two pages.
|
|
14
19
|
|
|
15
20
|
## `loading: true` returns the previous value
|
|
16
21
|
|
|
@@ -170,11 +175,75 @@ list is walked through. A transition states a relation between two pages (see
|
|
|
170
175
|
rendering hold for the one frame the browser needs to photograph it, and gives
|
|
171
176
|
it back. It never decides what the page arriving is allowed to ask for. Held by
|
|
172
177
|
`tests/route_transition_list_revisit/`, which mounts the same app twice — with
|
|
173
|
-
and without a relation on the pair — and
|
|
178
|
+
and without a relation on the pair — and walks the way back ten times on each,
|
|
179
|
+
counting what goes out at every revisit.
|
|
180
|
+
|
|
181
|
+
When a list stops refreshing after a transition was added, two things account
|
|
182
|
+
for it, in this order:
|
|
183
|
+
|
|
184
|
+
1. **The revisit did not happen.** A back taken before the page being opened was
|
|
185
|
+
ever on screen returns to a list that never left, and a page that never left
|
|
186
|
+
has nothing to come back from (see
|
|
187
|
+
[route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)).
|
|
188
|
+
This is what an automated walk does by default, and it is the answer far more
|
|
189
|
+
often than the next one.
|
|
190
|
+
2. **The list is on the first row of the table.** A `GET_MANY` list never
|
|
191
|
+
refreshed on its own; what changed is whatever else in the app was doing the
|
|
192
|
+
re-read.
|
|
193
|
+
|
|
194
|
+
### Watching what the run asks for
|
|
195
|
+
|
|
196
|
+
A run that decides **not** to ask is invisible from the application's side: it
|
|
197
|
+
sends nothing and changes no state, so the network is silent and
|
|
198
|
+
`onRequestStateChange` — which reports what a request is doing — has no request
|
|
199
|
+
to report. A run that declined and a run that was never mounted look identical.
|
|
200
|
+
`debugScroll` is where that difference is visible; it carries the render window
|
|
201
|
+
and the run's asking, which are one subject.
|
|
202
|
+
|
|
203
|
+
```jsx
|
|
204
|
+
window.askLog = [];
|
|
205
|
+
<NaviDebug debugScroll={(...args) => window.askLog.push(args.join(" "))}>
|
|
206
|
+
<MyList />
|
|
207
|
+
</NaviDebug>;
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
One line per pass of the run, whatever the outcome:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
ask 0-49: sent (revalidating=true holdPending=false count=412)
|
|
214
|
+
ask -1--1: nothing missing (revalidating=false holdPending=false count=412)
|
|
215
|
+
ask 0-49: held on a row not reached yet (revalidating=true holdPending=true count=412)
|
|
216
|
+
```
|
|
174
217
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
218
|
+
| outcome | what it means |
|
|
219
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `sent` | the range on the line went out |
|
|
221
|
+
| `nothing missing` | the run holds every row it draws and has nothing to revalidate |
|
|
222
|
+
| `held on a row not reached yet` | the list is on its way somewhere the window does not frame; only the row it is held on can be asked for |
|
|
223
|
+
| `already revalidating` | a revalidation for this window is in flight |
|
|
224
|
+
| `a request still covers this window` | what is in flight is still what the list would draw |
|
|
225
|
+
| `this range was asked for already` | asking again could only produce the same answer |
|
|
226
|
+
|
|
227
|
+
The state that decides is on the line rather than left to be inferred:
|
|
228
|
+
|
|
229
|
+
- **`revalidating`** — the run knows what it holds is from before. A revisit
|
|
230
|
+
showing `revalidating=false` never restored a composition, which is a
|
|
231
|
+
different problem from one showing `revalidating=true` and no `sent`.
|
|
232
|
+
- **`holdPending`** — the list is held somewhere it has not reached.
|
|
233
|
+
- **`count`** — how many rows the run stands for, `undefined` before its first
|
|
234
|
+
answer.
|
|
235
|
+
|
|
236
|
+
**Record, do not print.** The sink is called during rendering: push into an
|
|
237
|
+
array. Formatting an object in a console costs far more than what it measures,
|
|
238
|
+
and a timing-sensitive symptom moves under one — a list that fails to refresh on
|
|
239
|
+
the first revisit can start refreshing on the first two as soon as a
|
|
240
|
+
`console.log` is in the way, which makes the log a report about the log.
|
|
241
|
+
|
|
242
|
+
An absence in the trace is a fact too: no line for a revisit means the run never
|
|
243
|
+
rendered, which is about the list being mounted, not about what it asked for.
|
|
244
|
+
The usual cause is a revisit that never happened — a back taken before the page
|
|
245
|
+
being opened was ever on screen, which returns to a list that never left (see
|
|
246
|
+
[route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)).
|
|
178
247
|
|
|
179
248
|
## `rerunOn`, verb by verb
|
|
180
249
|
|
|
@@ -237,6 +306,9 @@ runs it again.
|
|
|
237
306
|
|
|
238
307
|
## See also
|
|
239
308
|
|
|
309
|
+
- [route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)
|
|
310
|
+
— why a walk away and back has to wait for the page arriving, and what a list
|
|
311
|
+
does when it did not
|
|
240
312
|
- [resource.md](./resource.md) — `resource()`, relations, callback return
|
|
241
313
|
contracts
|
|
242
314
|
- [resource_dependencies.md](./resource_dependencies.md) — invalidating a
|
package/docs/navigation.md
CHANGED
|
@@ -310,11 +310,21 @@ Three cases, and they are not a policy to configure but three different facts:
|
|
|
310
310
|
tall is clamped away.
|
|
311
311
|
- **A reload** lands where one was, as it would have without navi.
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
One consequence is softened where the browser exposes its stack (the
|
|
314
|
+
Navigation API — everywhere but Firefox today): **a `<Link>` whose destination
|
|
315
|
+
is the entry right next to the current one becomes a real traversal** instead
|
|
316
|
+
of a push. A "back" link to the page one just came from therefore behaves as a
|
|
317
|
+
back — the stack stays what the reader thinks it is (no A, B, A, B… growth)
|
|
318
|
+
and the scroll comes back; one step forward too, so returning to the page one
|
|
319
|
+
just left resumes it where it was. Only towards entries of this document (a
|
|
320
|
+
traversal to another one would be a full page load no link asked for), and
|
|
321
|
+
never when the push carries explicit state.
|
|
322
|
+
|
|
323
|
+
Everywhere else a `<Link>` is an arrival and lands at the top. A back arrow
|
|
324
|
+
that must ALWAYS behave as a back — even far from the entry it targets, even
|
|
325
|
+
in a browser with no Navigation API — is `navBack()`. Where there may be
|
|
326
|
+
nothing to go back to (a shared link opened cold), decide what the arrow does
|
|
327
|
+
from the history, not from the link.
|
|
318
328
|
|
|
319
329
|
What is not covered: a page whose height depends on something still loading is
|
|
320
330
|
not tall enough at the moment its position is put back, so a deep position is
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Route transitions
|
|
2
2
|
|
|
3
3
|
How pages of an app move against each other when the user navigates —
|
|
4
|
-
`defineRouteTransition`, `defineRouteDefaultTransition`,
|
|
5
|
-
|
|
4
|
+
`defineRouteTransition`, `defineRouteDefaultTransition`, what one link or one
|
|
5
|
+
`navTo` may ask for on top of them, and the thinking that decides which
|
|
6
|
+
movement (if any) a navigation deserves. The API grammar itself
|
|
6
7
|
(accepted forms, shipped type names) lives in the JSDoc of
|
|
7
8
|
`defineRouteTransition`; this file holds what a signature cannot say.
|
|
8
9
|
|
|
@@ -77,6 +78,77 @@ A default has no direction (nothing says which of two arbitrary pages is
|
|
|
77
78
|
"before" the other), so only directionless movements make sense there. Written
|
|
78
79
|
relations, and `"none"`, always win over it.
|
|
79
80
|
|
|
81
|
+
## When one navigation knows better
|
|
82
|
+
|
|
83
|
+
A relation is written on a PAIR, so it holds for every way of reaching the
|
|
84
|
+
page — and some ways are walked against the map. Two pairs that are travelled
|
|
85
|
+
in both directions, with one direction common and one rare:
|
|
86
|
+
|
|
87
|
+
- a game ↔ a player's profile: the name of a player, tapped from the game, is
|
|
88
|
+
the common way in; a badge on a profile that leads back to the game it was
|
|
89
|
+
won in is the rare one;
|
|
90
|
+
- a profile ↔ the cards it describes: going down into the cards is the
|
|
91
|
+
structural descent; a card that leads up to the player it describes goes back
|
|
92
|
+
out.
|
|
93
|
+
|
|
94
|
+
Written for the common direction, the rare one plays backwards. And `"none"`
|
|
95
|
+
cannot fix it: navi does not tell a link from the back button, so silencing the
|
|
96
|
+
bad direction silences the good one too.
|
|
97
|
+
|
|
98
|
+
So the navigation itself may ask, and what it asks holds for **that navigation
|
|
99
|
+
and no other**:
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
// The rare way round: the pair's movement, turned round.
|
|
103
|
+
<Link
|
|
104
|
+
route={GAME_ROUTE}
|
|
105
|
+
routeParams={{ id }}
|
|
106
|
+
transition={{ direction: "back" }}
|
|
107
|
+
>
|
|
108
|
+
{badge.gameName}
|
|
109
|
+
</Link>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// The same thing said by a call rather than by an element.
|
|
114
|
+
navTo(GAME_ROUTE.buildUrl({ id }), { routeTransition: { direction: "back" } });
|
|
115
|
+
GAME_ROUTE.navTo({ id }, { routeTransition: { direction: "back" } });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The request is `"slide-x"`-style shorthand or `{ type, duration, direction }`,
|
|
119
|
+
the same forms `defineRouteTransition` takes, plus `direction`. It overrides
|
|
120
|
+
**field by field**: what it does not name, the relation (or the default) still
|
|
121
|
+
answers for. So:
|
|
122
|
+
|
|
123
|
+
- `{ direction: "back" }` keeps the pair's movement and only turns it round;
|
|
124
|
+
- `"zoom"` swaps the movement, keeping nothing else;
|
|
125
|
+
- `"none"` cuts, where the pair — or the default — would have played;
|
|
126
|
+
- `{ duration: 500 }` re-times what was already going to play.
|
|
127
|
+
|
|
128
|
+
A pair no relation was ever written for answers the same way: silence is what
|
|
129
|
+
the routes say, and a link that asks for a movement gets it, forward unless it
|
|
130
|
+
says otherwise. That is the whole shape of the control:
|
|
131
|
+
`defineRouteTransition` is what the app's map says and applies by default; a
|
|
132
|
+
link, or a programmatic `navTo`, overrides it for the length of one navigation.
|
|
133
|
+
Navigate again by any other means and the relation is back in charge — nothing
|
|
134
|
+
is remembered.
|
|
135
|
+
|
|
136
|
+
The link wears what it asks as an attribute, so a plain `<a>` says it too (a
|
|
137
|
+
type name, or the object as JSON):
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<a href="/game/42" data-navi-route-transition-request='{"direction":"back"}'
|
|
141
|
+
>…</a
|
|
142
|
+
>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Not yet: a movement chosen by HOW one navigated.** Playing one movement for
|
|
146
|
+
the back button and another for a link would be written on the same request —
|
|
147
|
+
`{ back: "slide-x", forward: "slide-y" }` — but the History API does not say
|
|
148
|
+
which way a traversal went, and navi runs on it today (see
|
|
149
|
+
`browser_integration/via_history.js`). The Navigation API does; the notation is
|
|
150
|
+
kept in mind for the day navi navigates through it.
|
|
151
|
+
|
|
80
152
|
## Pages between fixed bars: the transition area
|
|
81
153
|
|
|
82
154
|
By default the movement plays on the document itself — right when pages are the
|
|
@@ -151,6 +223,20 @@ movement against the view transition pseudo-elements — of the document, or of
|
|
|
151
223
|
the marked area. See the JSDoc of `defineRouteTransition` for the selector
|
|
152
224
|
shape, and the `spin` type in the demo for a working one.
|
|
153
225
|
|
|
226
|
+
What is left to write is the `animation-name`s and nothing else: navi attaches
|
|
227
|
+
to ANY named type what makes a movement look like one — each picture at the
|
|
228
|
+
size it was taken at (a page half the height of the one it crosses would
|
|
229
|
+
otherwise be seen inflating over the length of the movement), two solid pages
|
|
230
|
+
rather than two panes of glass, and the animation held where it ends. The
|
|
231
|
+
untyped cross-fade keeps the browser's defaults, since scaling one picture into
|
|
232
|
+
the other is the whole idea there.
|
|
233
|
+
|
|
234
|
+
The one knob a custom movement may want back: a movement that animates ONE of
|
|
235
|
+
its two sides leaves the other on the browser's fade, and a fade needs its two
|
|
236
|
+
half-transparent pictures to add up rather than cover each other
|
|
237
|
+
(`mix-blend-mode: plus-lighter`, as the shipped `zoom` and the demo's `spin`
|
|
238
|
+
both do). A movement where both pages move wants what navi poses.
|
|
239
|
+
|
|
154
240
|
## Route transitions and `RouteTravel` — one pair, one system
|
|
155
241
|
|
|
156
242
|
`RouteTravel` and `defineRouteTransition` answer different questions:
|
|
@@ -198,14 +284,65 @@ browser needs to photograph the page being left (see `rendering_hold.js`), and
|
|
|
198
284
|
it gives it back in the same callback. The hold is about a picture, not about
|
|
199
285
|
data: nothing waits on it, nothing is skipped because of it.
|
|
200
286
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
|
|
205
|
-
|
|
287
|
+
That is the design, and it is held by `tests/route_transition_list_revisit/`:
|
|
288
|
+
one app mounted twice, the two mounts differing by a single
|
|
289
|
+
`defineRouteTransition` line, walked back and forth **ten times each** with what
|
|
290
|
+
goes to the network counted on every revisit. The loop is the point — a
|
|
291
|
+
difference that came and went would pass a single comparison often enough to
|
|
292
|
+
look like an invariant. Three decors, chosen to be opposites: a long list
|
|
293
|
+
virtualized against its scroller, the same one held on a row the url names, and
|
|
294
|
+
a one-row list against the document scroller with its count known from
|
|
295
|
+
elsewhere.
|
|
296
|
+
|
|
297
|
+
One thing does make a page lose its revisit under a movement and not without
|
|
298
|
+
one, and it is not about data either: a back taken before the page being opened
|
|
299
|
+
has rendered, which returns to a page that never left. It has its own section
|
|
300
|
+
below. See
|
|
206
301
|
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)
|
|
207
302
|
for which source refreshes on a revisit and which does not.
|
|
208
303
|
|
|
304
|
+
## Waiting for a navigation: the address is not the page
|
|
305
|
+
|
|
306
|
+
A navigation changes the URL first and the screen after — always. Under a
|
|
307
|
+
transition the gap is wider on purpose: the rendering hold above spans the frame
|
|
308
|
+
the picture is taken in, so for that frame the address says one page and the
|
|
309
|
+
screen still shows the other. That is what makes the picture honest, and it is
|
|
310
|
+
also long enough to be walked through by mistake.
|
|
311
|
+
|
|
312
|
+
So a test that waits on the URL has not waited for anything to happen:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
// ✗ resolves while the page being left is still the page on screen
|
|
316
|
+
await page.getByTestId("game_card").click();
|
|
317
|
+
await page.waitForURL(/\/games\//);
|
|
318
|
+
await page.goBack();
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
That back does not come back from anywhere. Nothing was unmounted, so nothing
|
|
322
|
+
remounts — the list the user "returns to" is the element that never left, with
|
|
323
|
+
no first load, no revisit, and no re-read (see
|
|
324
|
+
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)).
|
|
325
|
+
Every symptom of the arriving page being wrong follows from a walk that never
|
|
326
|
+
took place.
|
|
327
|
+
|
|
328
|
+
Wait for the page instead — anything only it can show:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
await page.getByTestId("game_card").click();
|
|
332
|
+
await expect(page.getByTestId("game_edit_link")).toBeVisible();
|
|
333
|
+
await page.goBack();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The window is **one frame**: the hold is given back inside the view
|
|
337
|
+
transition's callback, which the browser runs at its next rendering
|
|
338
|
+
opportunity. `tests/route_transition_list_revisit/` walks it the wrong way on
|
|
339
|
+
purpose in its last case — under a movement the list that comes back is the same
|
|
340
|
+
DOM element ten times out of ten and asks for nothing, while the same walk
|
|
341
|
+
without a movement is a real one — and the window measured there is a back at 0
|
|
342
|
+
or 8ms losing every round trip, at 16ms losing only the first, and past 64ms
|
|
343
|
+
losing none. No thumb moves in one frame; an automated click continues in the
|
|
344
|
+
same millisecond. This is a testing trap, not a user-facing behaviour.
|
|
345
|
+
|
|
209
346
|
## The rest, briefly
|
|
210
347
|
|
|
211
348
|
- Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone;
|