@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
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
How pages of an app move against each other when the user navigates —
|
|
4
4
|
`defineRouteTransition`, `defineRouteDefaultTransition`, what one link or one
|
|
5
|
-
`navTo` may ask for on top of them, and the thinking that decides which
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
`navTo` may ask for on top of them, and the thinking that decides which movement
|
|
6
|
+
(if any) a navigation deserves. The API grammar itself (accepted forms, shipped
|
|
7
|
+
type names) lives in the JSDoc of `defineRouteTransition`; this file holds what a
|
|
8
|
+
signature cannot say.
|
|
9
9
|
|
|
10
|
-
Demos
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Demos, in `src/nav/demos/`: `route_transition/route_transition.html` (the
|
|
11
|
+
movements), `route_transition/route_transition_default.html` (a default
|
|
12
|
+
transition), `route_transition_fixed_bars/route_transition_fixed_bars.html`
|
|
13
|
+
(pages between fixed bars), `route_transition/route_transition_with_travel.html`
|
|
14
|
+
(with a `RouteTravel` inside).
|
|
14
15
|
|
|
15
16
|
- [What a transition is for](#what-a-transition-is-for)
|
|
16
17
|
- [A page opened from anywhere](#a-page-opened-from-anywhere)
|
|
@@ -31,13 +32,12 @@ Demos: [the movements](../src/nav/demos/route_transition/route_transition.html),
|
|
|
31
32
|
## What a transition is for
|
|
32
33
|
|
|
33
34
|
A transition is not decoration: it states a **relation** between two pages, and
|
|
34
|
-
the user reads it as a map
|
|
35
|
-
deeper, the way back is to the left"
|
|
36
|
-
inevitable rather than learned. A movement
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
So the unit of declaration is the pair, not the app:
|
|
35
|
+
the user reads it as a map — a page sliding in from the right says "this place is
|
|
36
|
+
deeper, the way back is to the left", which is why the back arrow then feels
|
|
37
|
+
inevitable rather than learned. A movement stating a relation the app does not
|
|
38
|
+
have (a slide between two sibling tabs) teaches a false map, and a false map is
|
|
39
|
+
worse than no animation at all. So the unit of declaration is the pair, not the
|
|
40
|
+
app:
|
|
41
41
|
|
|
42
42
|
```js
|
|
43
43
|
defineRouteTransition(MY_GAMES_PAGE, GAME_PAGE, "slide-x");
|
|
@@ -45,119 +45,88 @@ defineRouteTransition(RADAR_PAGE, GAME_PAGE, "slide-x");
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
and two pages never written in the same relation play **nothing** between each
|
|
48
|
-
other. That silence is a statement too: "Mes parties" and "Radars" are two tabs
|
|
49
|
-
|
|
50
|
-
rendering of that fact.
|
|
51
|
-
declare the relations that exist and let the rest cut.
|
|
48
|
+
other. That silence is a statement too: "Mes parties" and "Radars" are two tabs of
|
|
49
|
+
a bottom bar, side by side, neither before the other — a cut is the honest
|
|
50
|
+
rendering of that fact. Declare the relations that exist and let the rest cut.
|
|
52
51
|
|
|
53
52
|
## A page opened from anywhere
|
|
54
53
|
|
|
55
|
-
One shape of page has no pair to write
|
|
56
|
-
gear in the top bar, a "+" in the tab bar — so it is opened from every screen
|
|
57
|
-
|
|
58
|
-
real and it is a single sentence ("the settings come down over the screen you
|
|
59
|
-
were on, and go back up"), but there are twenty-five `from` pages and not one of
|
|
60
|
-
them is the point.
|
|
61
|
-
|
|
62
|
-
Leave the `from` out:
|
|
54
|
+
One shape of page has no pair to write: its door is in the fixed furniture — a
|
|
55
|
+
gear in the top bar, a "+" in the tab bar — so it is opened from every screen and
|
|
56
|
+
closed back onto whichever one the reader was on. Leave the `from` out:
|
|
63
57
|
|
|
64
58
|
```js
|
|
65
59
|
defineRouteTransition(null, SETTINGS_PAGE, "cover-top");
|
|
66
60
|
```
|
|
67
61
|
|
|
68
|
-
Arriving there plays forward from wherever, leaving plays back to wherever.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
more.
|
|
80
|
-
|
|
81
|
-
**Such a page is still a PAGE**, and that is the question to settle before
|
|
82
|
-
writing the relation: it takes the screen's place, and the reader comes back to
|
|
83
|
-
whatever the router puts there — not necessarily to what they were on. A door in
|
|
84
|
-
the furniture whose destination must be drawn OVER the screen, and must give it
|
|
85
|
-
back exactly on closing, is not a page at all: it is a layer, its address says so
|
|
86
|
-
differently, and no relation is written for it. See
|
|
87
|
-
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
|
|
62
|
+
Arriving there plays forward from wherever, leaving plays back to wherever. It is
|
|
63
|
+
tried last — a pair naming the same destination still owns its crossing — and it
|
|
64
|
+
is not `defineRouteDefaultTransition`, which is about every navigation nothing
|
|
65
|
+
was said about. Write it only for a door that really is furniture: a page reached from a
|
|
66
|
+
screen has that screen to be paired with, and the pair says more.
|
|
67
|
+
|
|
68
|
+
**Such a page is still a PAGE**: it takes the screen's place, and the reader comes
|
|
69
|
+
back to whatever the router puts there. A destination that must be drawn OVER the
|
|
70
|
+
screen and give it back exactly on closing is a layer, not a page — its address
|
|
71
|
+
says so differently, and no relation is written for it (see
|
|
72
|
+
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say)).
|
|
88
73
|
|
|
89
74
|
## Choosing a movement
|
|
90
75
|
|
|
91
76
|
- **`slide-x`** — going INTO something: a list item opened, a card followed, a
|
|
92
77
|
notification tapped. The page is deeper on the same plane; leaving it slides
|
|
93
|
-
back out. The most common relation in an app
|
|
94
|
-
taught.
|
|
78
|
+
back out. The most common relation in an app.
|
|
95
79
|
- **`slide-y`** — the same relation on a vertical arrangement, when the layout
|
|
96
80
|
genuinely reads as a column.
|
|
97
81
|
- **`cover-right` / `cover-left` / `cover-bottom` / `cover-top`** — a page that
|
|
98
|
-
INTERRUPTS rather than continues: settings, a composer, anything modal-like
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
settings pulled down from a top bar cover from the top.
|
|
82
|
+
INTERRUPTS rather than continues: settings, a composer, anything modal-like that
|
|
83
|
+
one returns from to find the page beneath unchanged — the covered page holding
|
|
84
|
+
still says "you are not leaving, this is on top". The edge named is the one the
|
|
85
|
+
page comes IN from, and the one to name is where its door is: a composer opened
|
|
86
|
+
from a bottom bar covers from the bottom.
|
|
104
87
|
- **`zoom`** — a detail brought closer: a photo, a card expanded into a page.
|
|
105
|
-
- **`cross-fade`** — a soft change with no spatial claim
|
|
106
|
-
|
|
107
|
-
- **`none`** — silence, written down. Needed only to override: one way of a
|
|
108
|
-
|
|
88
|
+
- **`cross-fade`** — a soft change with no spatial claim, where a cut feels harsh
|
|
89
|
+
but no direction would be true.
|
|
90
|
+
- **`none`** — silence, written down. Needed only to override: one way of a pair,
|
|
91
|
+
or the default.
|
|
109
92
|
|
|
110
93
|
Two recommendations that matter more than the individual choices:
|
|
111
94
|
|
|
112
|
-
- **One movement per KIND of relation, app-wide.** If opening a game slides
|
|
113
|
-
|
|
114
|
-
|
|
95
|
+
- **One movement per KIND of relation, app-wide.** If opening a game slides from
|
|
96
|
+
the right, opening a profile should too — the user learns one grammar, not one
|
|
97
|
+
rule per page.
|
|
115
98
|
- **Keep reciprocity.** The way back being the same movement reversed is what
|
|
116
|
-
makes the map hold together
|
|
99
|
+
makes the map hold together. It is the default, and breaking it (a relation
|
|
117
100
|
written for the exact way travelled wins over being the reverse of another)
|
|
118
|
-
should answer a real asymmetry
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
navi warns when it sees that pair defined.
|
|
101
|
+
should answer a real asymmetry, not a styling whim: write the way back only to
|
|
102
|
+
say something DIFFERENT — another movement, or `"none"`. Written with the same
|
|
103
|
+
one, both crossings play forward and the pair can never say "back" again, the
|
|
104
|
+
back button included; navi warns when it sees that pair.
|
|
123
105
|
|
|
124
106
|
## A default transition — when
|
|
125
107
|
|
|
126
|
-
`defineRouteDefaultTransition("cross-fade")` plays on every navigation no
|
|
127
|
-
|
|
108
|
+
`defineRouteDefaultTransition("cross-fade")` plays on every navigation no relation
|
|
109
|
+
was written for. Two situations, two answers:
|
|
128
110
|
|
|
129
|
-
- **App-shaped UI** (bars, tabs, pages one goes into): don't. The silence
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
deserves a direction. This is the case the export exists for.
|
|
111
|
+
- **App-shaped UI** (bars, tabs, pages one goes into): don't. The silence between
|
|
112
|
+
sibling tabs is part of the grammar, and a default erases it.
|
|
113
|
+
- **Content-shaped site** (documents, articles, browsing): a global cross-fade can
|
|
114
|
+
be right — every navigation is a soft change of subject. This is the case the
|
|
115
|
+
export exists for.
|
|
135
116
|
|
|
136
117
|
A default has no direction (nothing says which of two arbitrary pages is
|
|
137
|
-
"before" the other), so
|
|
138
|
-
|
|
139
|
-
play nothing. Written relations, and `"none"`, always win over it.
|
|
118
|
+
"before" the other), so navi's `slide-*` and `cover-*`, written on the direction,
|
|
119
|
+
play nothing there. Written relations, and `"none"`, always win over it.
|
|
140
120
|
|
|
141
121
|
## When one navigation knows better
|
|
142
122
|
|
|
143
|
-
A relation is written on a PAIR, so it holds for every way of reaching the
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- a profile ↔ the cards it describes: going down into the cards is the
|
|
151
|
-
structural descent; a card that leads up to the player it describes goes back
|
|
152
|
-
out.
|
|
153
|
-
|
|
154
|
-
Written for the common direction, the rare one plays backwards. And `"none"`
|
|
155
|
-
cannot fix it: a relation written for a way holds for every crossing of it,
|
|
156
|
-
the back button's included (see below), so silencing the bad direction
|
|
157
|
-
silences the good one too.
|
|
158
|
-
|
|
159
|
-
So the navigation itself may ask, and what it asks holds for **that navigation
|
|
160
|
-
and no other**:
|
|
123
|
+
A relation is written on a PAIR, so it holds for every way of reaching the page —
|
|
124
|
+
and some ways are walked against the map: a player's name tapped from a game is
|
|
125
|
+
the common way into a profile, a badge on that profile leading back to the game
|
|
126
|
+
it was won in is the rare one, and plays backwards. `"none"` cannot fix it: a
|
|
127
|
+
relation written for a way holds for every crossing of it, the back button's
|
|
128
|
+
included. So the navigation itself may ask, and what it asks holds for **that
|
|
129
|
+
navigation and no other**:
|
|
161
130
|
|
|
162
131
|
```jsx
|
|
163
132
|
// The rare way round: the pair's movement, turned round.
|
|
@@ -171,56 +140,37 @@ and no other**:
|
|
|
171
140
|
```
|
|
172
141
|
|
|
173
142
|
```js
|
|
174
|
-
// The same thing said by a call rather than by an element.
|
|
175
143
|
navTo(GAME_ROUTE.buildUrl({ id }), { routeTransition: { direction: "back" } });
|
|
176
144
|
GAME_ROUTE.navTo({ id }, { routeTransition: { direction: "back" } });
|
|
177
145
|
```
|
|
178
146
|
|
|
179
|
-
The request
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
answers for. So:
|
|
147
|
+
The request takes the forms `defineRouteTransition` takes, plus `direction`, and
|
|
148
|
+
overrides **field by field** — what it does not name, the relation (or the
|
|
149
|
+
default) still answers for:
|
|
183
150
|
|
|
184
151
|
- `{ direction: "back" }` keeps the pair's movement and only turns it round;
|
|
185
|
-
- `"zoom"` swaps the movement; the way round and the pace are still the
|
|
186
|
-
pair's;
|
|
152
|
+
- `"zoom"` swaps the movement; the way round and the pace are still the pair's;
|
|
187
153
|
- `"none"` cuts, where the pair — or the default — would have played;
|
|
188
154
|
- `{ duration: 500 }` re-times what was already going to play.
|
|
189
155
|
|
|
190
|
-
A pair no relation was
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
that navigation plays what it asked for, reversed (next section).
|
|
198
|
-
|
|
199
|
-
The link wears what it asks as an attribute, so a plain `<a>` says it too (a
|
|
200
|
-
type name, or the object as JSON):
|
|
201
|
-
|
|
202
|
-
```html
|
|
203
|
-
<a href="/game/42" data-navi-route-transition-request='{"direction":"back"}'
|
|
204
|
-
>…</a
|
|
205
|
-
>
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Not yet: a movement chosen by HOW one navigated.** Playing one movement for
|
|
209
|
-
the back button and another for a link would be written on the same request —
|
|
210
|
-
`{ back: "slide-x", forward: "slide-y" }`. The notation is kept in mind for the
|
|
211
|
-
day it is asked for.
|
|
156
|
+
A pair no relation was written for answers the same way: a link asking for a
|
|
157
|
+
movement gets it, forward unless it says otherwise. Navigate again by any other
|
|
158
|
+
means and the relations are back in charge — except the history traversal undoing
|
|
159
|
+
that navigation, which plays what it asked for, reversed (next section). The link
|
|
160
|
+
wears its request as an attribute, so a plain `<a>` says it too:
|
|
161
|
+
`data-navi-route-transition-request` holds a type name, or the object as JSON
|
|
162
|
+
(`'{"direction":"back"}'`).
|
|
212
163
|
|
|
213
164
|
## A traversal retraces its crossing
|
|
214
165
|
|
|
215
|
-
The back button, `navBack()`, `history.back()`, the browser's "next": a
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
page being left plays it again as it was.
|
|
166
|
+
The back button, `navBack()`, `history.back()`, the browser's "next": a traversal
|
|
167
|
+
is not a walk on the map, it undoes one (or redoes one). The entry a push creates
|
|
168
|
+
remembers the crossing that created it — what played, which way, from which url
|
|
169
|
+
— so a back onto the page that crossing came from plays it reversed, and a
|
|
170
|
+
forward plays it again as it was.
|
|
221
171
|
|
|
222
|
-
That replay outranks everything the relations **deduce**, and nothing the
|
|
223
|
-
|
|
172
|
+
That replay outranks everything the relations **deduce**, and nothing the author
|
|
173
|
+
**wrote** for that exact way:
|
|
224
174
|
|
|
225
175
|
| back from B to A | plays |
|
|
226
176
|
| ----------------------------------------------- | ----------------------------- |
|
|
@@ -231,39 +181,27 @@ author **wrote** for that exact way:
|
|
|
231
181
|
| `defineRouteTransition(B, A, "none")` | nothing |
|
|
232
182
|
| `defineRouteTransition(B, A, "flip")` | flip, forward |
|
|
233
183
|
|
|
234
|
-
The line is drawn there because the
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
What this settles:
|
|
244
|
-
|
|
245
|
-
- a page reached from anywhere whose links walk the map the other way (the
|
|
246
|
-
author of a place's sheet, from the place): the link says
|
|
247
|
-
`routeTransition={{ direction: "forward" }}`, and the back undoes exactly
|
|
248
|
-
that — there is no reverse pair to write, and none that would collide with
|
|
249
|
-
the other pages reaching the place;
|
|
250
|
-
- a `routeTransition` request covers its own way back: a request is about one
|
|
251
|
-
crossing, so nothing written for the pair outranks its replay;
|
|
252
|
-
- the "two ways of a pair" trade-off above is about pairs written in one
|
|
253
|
-
direction: the reverse is deduced, and the traversal wins over it.
|
|
184
|
+
The line is drawn there because writing the way back by hand is the one tool for
|
|
185
|
+
breaking reciprocity, so it must reach the back button: a `"none"` written for
|
|
186
|
+
`B → A` silences every return from B to A, whatever pressed it. Everything else
|
|
187
|
+
is a deduction from a sentence about something else, and a traversal that knows
|
|
188
|
+
the crossing it undoes knows better. So a link walking the map the other way from
|
|
189
|
+
a page reached from anywhere (the author of a place's sheet, from the place) says
|
|
190
|
+
`routeTransition={{ direction: "forward" }}`, and the back undoes exactly that —
|
|
191
|
+
no reverse pair to write, none to collide with the other pages reaching the
|
|
192
|
+
place.
|
|
254
193
|
|
|
255
194
|
The relations alone answer a traversal that retraces no remembered crossing:
|
|
256
|
-
several entries at once, or an entry another document wrote.
|
|
257
|
-
|
|
258
|
-
(
|
|
259
|
-
so it retraces as the button does — under the same rule.
|
|
195
|
+
several entries at once, or an entry another document wrote. A link to the page
|
|
196
|
+
one just came from is a traversal too, where the browser exposes its stack (see
|
|
197
|
+
[navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)).
|
|
260
198
|
|
|
261
199
|
## Pages between fixed bars: the transition area
|
|
262
200
|
|
|
263
201
|
By default the movement plays on the document itself — right when pages are the
|
|
264
|
-
whole viewport. With fixed bars it is not: the root
|
|
265
|
-
|
|
266
|
-
|
|
202
|
+
whole viewport. With fixed bars it is not: the root picture spans the viewport and
|
|
203
|
+
the bars' regions are blank in it, so the moving picture drags a blank band across
|
|
204
|
+
the screen where they stand. Wrap the pages instead:
|
|
267
205
|
|
|
268
206
|
```jsx
|
|
269
207
|
<RouteTransitionArea className="app">
|
|
@@ -271,157 +209,143 @@ across the screen where they stand. Wrap the pages instead:
|
|
|
271
209
|
</RouteTransitionArea>
|
|
272
210
|
```
|
|
273
211
|
|
|
274
|
-
The movement then plays on that region's own pictures, and the bars never move
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
would be watched sliding over
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
photographed on its own and travels with its page, or holds where it stands if
|
|
334
|
-
both states have it. Its **wall** cannot be photographed (see
|
|
212
|
+
The movement then plays on that region's own pictures, and the bars never move —
|
|
213
|
+
without being named one by one. For an app with fixed bars this is part of
|
|
214
|
+
declaring transitions at all, not an option. It is a `Box`, so the layout the
|
|
215
|
+
pages need is written on it directly; an app that already has an element holding
|
|
216
|
+
its pages marks that one with `data-navi-route-transition-area` instead.
|
|
217
|
+
|
|
218
|
+
The pictures are drawn in the top layer, above everything the document can clip,
|
|
219
|
+
so they are cut: at the area's own bounds, at the app's rectangle (the glass
|
|
220
|
+
beside an app narrowed with `--navi-app-max-width`), and at the bands the
|
|
221
|
+
furniture keeps free — the area runs under the fixed bars by design, and uncut, a
|
|
222
|
+
page taller than the screen would be watched sliding over them. The bands are
|
|
223
|
+
read, never declared, so furniture that grows, shrinks or unmounts mid-movement
|
|
224
|
+
is followed: the app's **safe area** ([safe_area.md](./safe_area.md)) for
|
|
225
|
+
everything pinned to the window's edges, and **`useTransitionCover(ref, edge)`**
|
|
226
|
+
for what covers the pages from INSIDE the document, which the safe area does not
|
|
227
|
+
count — a sticky row of tabs above them, a header pinned to the top of a scroller
|
|
228
|
+
(`edge` is `"top"` by default). The pictures of a route transition or a travel
|
|
229
|
+
are then cut at that element instead of sliding over it.
|
|
230
|
+
|
|
231
|
+
A page travels the **window** it is seen through, not its own height, and a page
|
|
232
|
+
that was **scrolled** is photographed, and travels, from where the reader was.
|
|
233
|
+
|
|
234
|
+
Each **fixed bar around the area is photographed on its own**, and whether it is
|
|
235
|
+
the frame or part of what changes is derived from the pair of states: a bar
|
|
236
|
+
**both states have** holds where it stands while the pages move behind it (a bar
|
|
237
|
+
whose content changes with the route cross-fades, unnamed); a bar **one state
|
|
238
|
+
has** travels with the page that has it, by the keyframes that page leaves or
|
|
239
|
+
arrives by, instead of appearing or vanishing in a frame — a full-screen wizard
|
|
240
|
+
whose banner is its own header. Every bar stands over the pages, as at rest, so
|
|
241
|
+
what it paints outside its box (a button standing up out of a tab bar, a shadow)
|
|
242
|
+
is seen for the length of the movement — except over a page **covered** by the
|
|
243
|
+
other one (`cover-*`): the sheet comes over that page's furniture too, so its bars
|
|
244
|
+
go under the pages, and what they paint outside their box is cut by their page for
|
|
245
|
+
those few hundred milliseconds.
|
|
246
|
+
|
|
247
|
+
A bar the application names itself keeps its name and its own keyframes — navi
|
|
248
|
+
names only what is unnamed — but plays on the movement's clock, under the pages.
|
|
249
|
+
Its name must be worn before the movement starts: navi reads it before its
|
|
250
|
+
attributes go on the root, so a name written only under
|
|
251
|
+
`:root[data-navi-route-transition]` is not seen, and one of navi's takes its
|
|
252
|
+
place.
|
|
253
|
+
|
|
254
|
+
Everything photographed goes deaf to the pointer for the length of the movement
|
|
255
|
+
(see
|
|
256
|
+
[view_transitions.md](./view_transitions.md#an-element-captured-in-a-view-transition-cannot-be-pointed-at)),
|
|
257
|
+
which is what the pages want: a press on a picture would be an accident. The
|
|
258
|
+
**door** in a bar the two states share is the exception — it stands where its
|
|
259
|
+
picture is drawn, and the press that closes the page it is still opening is aimed
|
|
260
|
+
at what the reader sees. `pressableDuringRouteTransition` on its `<Link>` or
|
|
261
|
+
`<Button>` (`data-navi-route-transition-pressable` on anything else) keeps it
|
|
262
|
+
answering: the click is caught at the document and handed to it when it fell
|
|
263
|
+
inside its rectangle, and a navigation that is the exact way back of the movement
|
|
264
|
+
playing turns that movement round. Only on furniture that stands still: a control
|
|
265
|
+
travelling with the pages has a rectangle where it will be, not where it is seen
|
|
266
|
+
(navi warns about one inside the area). `RouteTravel` is the opposite case — a
|
|
267
|
+
finger is on the box — and leaves the bars live.
|
|
268
|
+
|
|
269
|
+
A **modal `Dialog` open on a page** is furniture of the same kind: it travels with
|
|
270
|
+
its page, or holds if both states have it. Its wall cannot be photographed (see
|
|
335
271
|
[view_transitions.md](./view_transitions.md#the-top-layer-is-painted-through-the-roots-picture)),
|
|
336
|
-
and dropping it for the movement makes the
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
(the browser also aborts the transition). This is measured behaviour, not a
|
|
356
|
-
precaution.
|
|
357
|
-
|
|
358
|
-
Three misconfigurations are silent enough to be worth a console warning, each
|
|
359
|
-
said once: an area that was not captured (the case above), several elements
|
|
360
|
-
marked at once (they would share one `view-transition-name`, and the browser
|
|
361
|
-
then refuses **every** view transition of the document), and pages travelling
|
|
362
|
-
on the document while something else is captured on its own — the blank band.
|
|
363
|
-
A warning here is a bug to fix, not a mechanism to lean on.
|
|
272
|
+
and dropping it for the movement makes the window blink under the press, so navi
|
|
273
|
+
paints it into the pictures: a page leaving from under a wall leaves dimmed, one
|
|
274
|
+
arriving under a wall arrives dimmed, a wall both states have holds — and no frame
|
|
275
|
+
shows two walls. Demo:
|
|
276
|
+
`src/nav/demos/route_transition_dialog/route_transition_dialog.html`.
|
|
277
|
+
|
|
278
|
+
**The area is a real box, and it has to be**: what gets photographed and clipped
|
|
279
|
+
IS its rectangle. An element with `display: contents` is never captured, and the
|
|
280
|
+
movement then plays on nothing.
|
|
281
|
+
|
|
282
|
+
Four misconfigurations are silent enough to be worth a console warning, each said
|
|
283
|
+
once: an area that was not captured (the case above); several elements marked at
|
|
284
|
+
once — they share one `view-transition-name`, and the browser refuses every route
|
|
285
|
+
transition while they do; pages travelling on the document while something else
|
|
286
|
+
is captured on its own — the blank band; and, on a browser without nested groups,
|
|
287
|
+
a `view-transition-name` written inside the area, whose element escapes the pages'
|
|
288
|
+
picture (see
|
|
289
|
+
[view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)). A
|
|
290
|
+
warning here is a bug to fix, not a mechanism to lean on.
|
|
364
291
|
|
|
365
292
|
## Custom movements
|
|
366
293
|
|
|
367
|
-
A type navi does not ship belongs to the application: the name is written on
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
What is left to write is the `animation-name`s and nothing else: navi attaches
|
|
376
|
-
to ANY named type what makes a movement look like one — each picture at the
|
|
377
|
-
size it was taken at (a page half the height of the one it crosses would
|
|
378
|
-
otherwise be seen inflating over the length of the movement), two solid pages
|
|
379
|
-
rather than two panes of glass, and the animation held where it ends. The
|
|
380
|
-
untyped cross-fade keeps the browser's defaults, since scaling one picture into
|
|
381
|
-
the other is the whole idea there.
|
|
382
|
-
|
|
383
|
-
The one knob a custom movement may want back: a movement that animates ONE of
|
|
384
|
-
its two sides leaves the other on the browser's fade, and a fade needs its two
|
|
385
|
-
half-transparent pictures to add up rather than cover each other
|
|
386
|
-
(`mix-blend-mode: plus-lighter`, as the shipped `zoom` and the demo's `spin`
|
|
387
|
-
both do). A movement where both pages move wants what navi poses.
|
|
388
|
-
|
|
389
|
-
A custom type can also say its two keyframes **as values**, next to the rules
|
|
390
|
-
that play them:
|
|
294
|
+
A type navi does not ship belongs to the application: the name is written on the
|
|
295
|
+
root for the length of the transition (`data-navi-route-transition-type="<type>"`,
|
|
296
|
+
next to `data-navi-route-transition="forward"|"back"`), and the app's CSS defines
|
|
297
|
+
the movement against the view transition pseudo-elements. Write it for both
|
|
298
|
+
groups, as navi does for its own: `root` when the document travels,
|
|
299
|
+
`navi-route-transition` when an area is marked — the root is then unnamed, and a
|
|
300
|
+
rule written for `(root)` alone never applies. The demo's `spin` type is one.
|
|
391
301
|
|
|
392
302
|
```css
|
|
393
303
|
:root[data-navi-route-transition-type="spin"][data-navi-route-transition="forward"] {
|
|
394
|
-
|
|
395
|
-
|
|
304
|
+
&::view-transition-new(root),
|
|
305
|
+
&::view-transition-new(navi-route-transition) {
|
|
306
|
+
animation-name: spin-in;
|
|
307
|
+
}
|
|
396
308
|
}
|
|
397
309
|
```
|
|
398
310
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
311
|
+
What is left to write is the `animation-name`s: navi gives ANY named type what
|
|
312
|
+
makes a movement look like one — each picture at the size it was taken at, two
|
|
313
|
+
solid pages rather than two panes of glass, the animation held where it ends. The
|
|
314
|
+
untyped cross-fade keeps the browser's defaults.
|
|
402
315
|
|
|
403
|
-
|
|
404
|
-
|
|
316
|
+
The one knob a custom movement may want back: a movement that animates ONE of its
|
|
317
|
+
two sides leaves the other on the browser's fade, and a fade needs its two
|
|
318
|
+
half-transparent pictures to add up rather than cover each other
|
|
319
|
+
(`mix-blend-mode: plus-lighter`, as the shipped `zoom` does). navi poses
|
|
320
|
+
`normal` from `:root[data-navi-route-transition][data-navi-route-transition-type]`,
|
|
321
|
+
in a sheet adopted after the document's own, so the rule taking it back must weigh
|
|
322
|
+
more than that selector
|
|
323
|
+
(`html:root[data-navi-route-transition][data-navi-route-transition-type="spin"]`
|
|
324
|
+
does); written on the type attribute alone, it is silently overruled.
|
|
325
|
+
|
|
326
|
+
A custom type can also say its two keyframes **as values** — what a fixed bar
|
|
327
|
+
belonging to one of the two states is given to travel with its page (a type that
|
|
328
|
+
publishes nothing leaves such a bar to the browser's fade) — and, when it plays
|
|
329
|
+
one page **over** the other, which one is covered: `old` when the page arriving
|
|
330
|
+
comes over the page being left, `new` when the page being left slides off the page
|
|
331
|
+
arriving. navi then draws the covered page under the other one, cut at its own
|
|
332
|
+
band, with its furniture under the pages, as the shipped `cover-*` types do:
|
|
405
333
|
|
|
406
334
|
```css
|
|
407
|
-
:root[data-navi-route-transition-type="
|
|
408
|
-
--navi-route-transition-
|
|
335
|
+
:root[data-navi-route-transition-type="spin"][data-navi-route-transition="forward"] {
|
|
336
|
+
--navi-route-transition-leave: spin-out;
|
|
337
|
+
--navi-route-transition-enter: spin-in;
|
|
409
338
|
}
|
|
410
|
-
:root[data-navi-route-transition-type="sheet"][data-navi-route-transition="
|
|
411
|
-
--navi-route-transition-covered: new
|
|
339
|
+
:root[data-navi-route-transition-type="sheet"][data-navi-route-transition="forward"] {
|
|
340
|
+
--navi-route-transition-covered: old; /* and "new" on the way back */
|
|
412
341
|
}
|
|
413
342
|
```
|
|
414
343
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
other publishes nothing.
|
|
421
|
-
|
|
422
|
-
Those three values are read by navi from the root, so they are written there,
|
|
423
|
-
and navi registers them as not inherited: writing them costs nothing. Any other
|
|
424
|
-
value a custom movement's pictures read goes on `::view-transition` instead:
|
|
344
|
+
Those three values are read by navi from the root, so they are written there, and
|
|
345
|
+
navi registers them as not inherited: writing them costs nothing. Any other value
|
|
346
|
+
a custom movement's pictures read goes on `::view-transition`, never on the root
|
|
347
|
+
(see
|
|
348
|
+
[view_transitions.md](./view_transitions.md#what-makes-a-transition-restyle-the-whole-document)):
|
|
425
349
|
|
|
426
350
|
```css
|
|
427
351
|
:root[data-navi-route-transition-type="spin"] {
|
|
@@ -431,17 +355,12 @@ value a custom movement's pictures read goes on `::view-transition` instead:
|
|
|
431
355
|
}
|
|
432
356
|
```
|
|
433
357
|
|
|
434
|
-
The pictures inherit it from there, and no element of the document does.
|
|
435
|
-
Written on the root, every element would, and the page being left would be
|
|
436
|
-
restyled in full in the frame it is photographed (see
|
|
437
|
-
[What a transition costs](#what-a-transition-costs)).
|
|
438
|
-
|
|
439
358
|
## Two routes matching one url
|
|
440
359
|
|
|
441
|
-
A relation is
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
360
|
+
A relation is resolved through **which page is current** — not through the url,
|
|
361
|
+
and not through the branch the router renders. So a url claimed by two of the
|
|
362
|
+
routes named in relations makes the movement depend on the order the relations
|
|
363
|
+
were declared in:
|
|
445
364
|
|
|
446
365
|
```js
|
|
447
366
|
const ALERTS_ROUTE = route("/me/alerts");
|
|
@@ -453,12 +372,11 @@ defineRouteTransition(ALERTS_ROUTE, ALERT_CREATE_ROUTE, "cover-bottom");
|
|
|
453
372
|
```
|
|
454
373
|
|
|
455
374
|
On `/me/alerts/create` both detail and create are current; the page mentioned
|
|
456
|
-
first wins, and the composer slides in from the right instead of covering.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
(see [navigation.md](./navigation.md#which-values-a-param-accepts)).
|
|
375
|
+
first wins, and the composer slides in from the right instead of covering.
|
|
376
|
+
Nothing on screen says so — a wrong movement is still a movement, and reads as a
|
|
377
|
+
deliberate choice. The fix is on the route, not on the relation: make one of them
|
|
378
|
+
decline the url (see
|
|
379
|
+
[navigation.md](./navigation.md#which-values-a-param-accepts)).
|
|
462
380
|
|
|
463
381
|
```js
|
|
464
382
|
const ALERT_DETAIL_ROUTE = route("/me/alerts/:alertId", {
|
|
@@ -466,129 +384,100 @@ const ALERT_DETAIL_ROUTE = route("/me/alerts/:alertId", {
|
|
|
466
384
|
});
|
|
467
385
|
```
|
|
468
386
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
naming both routes.
|
|
387
|
+
Worth doing whether or not a transition is involved: two routes matching one url
|
|
388
|
+
also run two route actions, and compete for the branch the router renders (see
|
|
389
|
+
[navigation.md](./navigation.md#rendering-routes)). Left in place, navi says it
|
|
390
|
+
once in the console, naming both routes.
|
|
473
391
|
|
|
474
392
|
## Route transitions and `RouteTravel` — one pair, one system
|
|
475
393
|
|
|
476
|
-
`RouteTravel` and `defineRouteTransition` answer different questions:
|
|
477
|
-
|
|
478
394
|
- **`RouteTravel`** is a ROW: a total order of tabs, plus the drag gesture that
|
|
479
395
|
walks it. Use it when the pages are genuinely a row the finger should push.
|
|
480
|
-
- **`defineRouteTransition`** declares individual relations, with no gesture
|
|
481
|
-
|
|
396
|
+
- **`defineRouteTransition`** declares individual relations, with no gesture and
|
|
397
|
+
no order beyond each pair.
|
|
482
398
|
|
|
483
|
-
A given PAIR of routes must be animated by one of the two, never both: a
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
399
|
+
A given PAIR of routes must be animated by one of the two, never both: a travel's
|
|
400
|
+
pictures can be under a finger, and a transition starting on top would skip them
|
|
401
|
+
mid-slide — and a page one can drag has promised a translation, which a
|
|
402
|
+
cross-fade would break. A travel in flight wins and the route transition is
|
|
403
|
+
skipped with a console warning: the sign of a misconfiguration to fix, not a
|
|
404
|
+
mechanism to rely on.
|
|
489
405
|
|
|
490
406
|
The two DO live together in one app, on different pairs, including a
|
|
491
407
|
`<RouteTravel>` nested inside a `<RouteTransitionArea>` — a row of sections the
|
|
492
408
|
thumb pushes, inside pages one goes into. Write the relations on the routes the
|
|
493
|
-
row does not own: the tabs
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
branch — its tabs as params of one route is the usual shape.
|
|
409
|
+
row does not own: the tabs travel, and opening something from any of them plays
|
|
410
|
+
its own movement. A relation written on a bare route covers every one of its
|
|
411
|
+
params at once, so "from any section" is one line rather than one per tab. Demo:
|
|
412
|
+
`src/nav/demos/route_transition/route_transition_with_travel.html`.
|
|
413
|
+
|
|
414
|
+
One trap that belongs to `RouteTravel` but bites here first: the travelling box
|
|
415
|
+
must stay MOUNTED across the changes it animates. A `<RouteTravel>` rendered
|
|
416
|
+
inside the `element` of each of the routes it travels between is destroyed
|
|
417
|
+
mid-travel by the router. Give the row a single branch — its tabs as params of one
|
|
418
|
+
route is the usual shape.
|
|
504
419
|
|
|
505
420
|
## A transition says nothing about data
|
|
506
421
|
|
|
507
|
-
A relation is about the map of the app
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
it gives it back in the same callback. The hold is about a picture, not about
|
|
519
|
-
data: nothing waits on it, nothing is skipped because of it.
|
|
520
|
-
|
|
521
|
-
That is the design, and it is held by `tests/route_transition_list_revisit/`:
|
|
522
|
-
one app mounted twice, the two mounts differing by a single
|
|
523
|
-
`defineRouteTransition` line, walked back and forth **ten times each** with what
|
|
524
|
-
goes to the network counted on every revisit. The loop is the point — a
|
|
525
|
-
difference that came and went would pass a single comparison often enough to
|
|
526
|
-
look like an invariant. Three decors, chosen to be opposites: a long list
|
|
527
|
-
virtualized against its scroller, the same one held on a row the url names, and
|
|
528
|
-
a one-row list against the document scroller with its count known from
|
|
529
|
-
elsewhere.
|
|
530
|
-
|
|
531
|
-
One thing does make a page lose its revisit under a movement and not without
|
|
532
|
-
one, and it is not about data either: a back taken before the page being opened
|
|
533
|
-
has rendered, which returns to a page that never left. It has its own section
|
|
534
|
-
below. See
|
|
535
|
-
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)
|
|
536
|
-
for which source refreshes on a revisit and which does not.
|
|
422
|
+
A relation is about the map of the app and nothing else: it does not change what
|
|
423
|
+
the page arriving loads, reloads or keeps. An action that `COMPLETED` still holds
|
|
424
|
+
its response, on the way back as on the way in — a page wanting fresh data says
|
|
425
|
+
`.rerun()`, with or without a movement — and a `<List.Items>` reading through
|
|
426
|
+
`GET_RANGE` still revalidates the window it draws when mounted again (see
|
|
427
|
+
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)).
|
|
428
|
+
What a transition holds is the document's **rendering**, while the page being
|
|
429
|
+
left is photographed, and it gives it back in the same callback: nothing waits on
|
|
430
|
+
it, nothing is skipped because of it. The one thing that makes a revisit differ
|
|
431
|
+
under a movement is a back taken before the page being opened has rendered — see
|
|
432
|
+
[below](#waiting-for-a-navigation-the-address-is-not-the-page).
|
|
537
433
|
|
|
538
434
|
## The page being left stays until its movement is over
|
|
539
435
|
|
|
540
|
-
The picture of the page being left is taken before the change; the page
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
callback keeps the page it was showing, hidden (`display: none`), and takes it
|
|
548
|
-
down once the movement is over (`keepLeavingPages` in `nav/route.jsx`).
|
|
549
|
-
Measured on that page, first frame of the movement: 63 → 45 ms at full speed,
|
|
550
|
-
211 → 188 ms at 4×.
|
|
436
|
+
The picture of the page being left is taken before the change; the page arriving
|
|
437
|
+
is built inside the transition's update callback, and everything done there delays
|
|
438
|
+
the first frame of the movement. Taking a page down — every hook of every
|
|
439
|
+
component cleaned up — is a large part of it, and the page's picture already
|
|
440
|
+
exists, so it only has to stay out of the second one: a container that changes
|
|
441
|
+
page during the callback keeps the page it was showing, hidden (`display: none`),
|
|
442
|
+
and takes it down once the movement is over.
|
|
551
443
|
|
|
552
444
|
What that means for a page and for the application around it:
|
|
553
445
|
|
|
554
446
|
- **It is still mounted for the length of the movement.** Its effects keep
|
|
555
|
-
running, and
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
`useAsyncData` does not: navi leaves a page alone as soon as its container
|
|
559
|
-
would no longer show it.
|
|
447
|
+
running, and a component reading the URL, a route or a signal directly
|
|
448
|
+
re-renders in it too, hidden. What it reads through `useAsyncData` does not:
|
|
449
|
+
navi leaves a page alone as soon as its container would no longer show it.
|
|
560
450
|
- **It is taken down after the page arriving is up.** A cleanup that puts back
|
|
561
|
-
what it found — a document title, a class on `<body>`, a value in a shared
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
- **What it fills outside its own nodes leaves at once.** A `SlotFill` in a
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
- **Its popups leave with it.** A popup it holds open is no longer rendered: it
|
|
569
|
-
|
|
570
|
-
|
|
451
|
+
what it found — a document title, a class on `<body>`, a value in a shared store
|
|
452
|
+
— puts a stale value back over the new page's. Such a registration is written as
|
|
453
|
+
"the last to arrive wins": navi's `<Head>` keeps its titles as a stack, and a
|
|
454
|
+
slot shows the last `SlotFill` to arrive.
|
|
455
|
+
- **What it fills outside its own nodes leaves at once.** A `SlotFill` in a page
|
|
456
|
+
kept while leaving steps out of its slot, so a bar each page fills shows the
|
|
457
|
+
page arriving — or nothing, when that page fills none.
|
|
458
|
+
- **Its popups leave with it.** A popup it holds open is no longer rendered: it is
|
|
459
|
+
photographed on the old side only, leaves with its page, and is closed when the
|
|
460
|
+
page is taken down (see
|
|
571
461
|
[view_transitions.md](./view_transitions.md#a-hidden-element-is-not-photographed-even-in-the-top-layer)).
|
|
572
|
-
- **The focus it holds goes where its removal would have sent it** — to no
|
|
573
|
-
|
|
574
|
-
|
|
462
|
+
- **The focus it holds goes where its removal would have sent it** — to no element
|
|
463
|
+
— when it is hidden: a hidden element keeps the focus otherwise, and would
|
|
464
|
+
receive the keys pressed during the movement.
|
|
575
465
|
- **Never while a modal dialog is open.** An open modal keeps everything but
|
|
576
|
-
itself out of reach — the page arriving could not even take the focus — and
|
|
577
|
-
|
|
578
|
-
|
|
466
|
+
itself out of reach — the page arriving could not even take the focus — and one
|
|
467
|
+
in a page kept while leaving would stay open for the whole movement. The page is
|
|
468
|
+
then taken down at once, as without a movement.
|
|
579
469
|
- **Only where the page would have been taken down anyway.** A page rendered by
|
|
580
470
|
the same element on both sides — two routes, one component — is the same
|
|
581
|
-
instance carried over
|
|
471
|
+
instance carried over.
|
|
582
472
|
|
|
583
473
|
## Waiting for a navigation: the address is not the page
|
|
584
474
|
|
|
585
475
|
A navigation changes the URL first and the screen after — always. Under a
|
|
586
|
-
transition the gap is wider on purpose: the rendering hold
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
So a test that waits on the URL has not waited for anything to happen:
|
|
476
|
+
transition the gap is wider on purpose: the rendering hold spans the frame the
|
|
477
|
+
picture is taken in, so for that frame the address says one page and the screen
|
|
478
|
+
still shows the other. That is what makes the picture honest, and it is long
|
|
479
|
+
enough to be walked through by mistake. A test that waits on the URL has not
|
|
480
|
+
waited for anything to happen:
|
|
592
481
|
|
|
593
482
|
```js
|
|
594
483
|
// ✗ resolves while the page being left is still the page on screen
|
|
@@ -598,12 +487,9 @@ await page.goBack();
|
|
|
598
487
|
```
|
|
599
488
|
|
|
600
489
|
That back does not come back from anywhere. Nothing was unmounted, so nothing
|
|
601
|
-
remounts — the list the user "returns to" is the element that never left, with
|
|
602
|
-
|
|
490
|
+
remounts — the list the user "returns to" is the element that never left, with no
|
|
491
|
+
first load, no revisit, and no re-read (see
|
|
603
492
|
[list_refresh.md](./list_refresh.md#who-decides-the-re-read--and-who-does-not)).
|
|
604
|
-
Every symptom of the arriving page being wrong follows from a walk that never
|
|
605
|
-
took place.
|
|
606
|
-
|
|
607
493
|
Wait for the page instead — anything only it can show:
|
|
608
494
|
|
|
609
495
|
```js
|
|
@@ -612,90 +498,55 @@ await expect(page.getByTestId("game_edit_link")).toBeVisible();
|
|
|
612
498
|
await page.goBack();
|
|
613
499
|
```
|
|
614
500
|
|
|
615
|
-
The window is **one frame**:
|
|
616
|
-
|
|
617
|
-
opportunity. `tests/route_transition_list_revisit/` walks it the wrong way on
|
|
618
|
-
purpose in its last case — a back pressed the instant the URL changes — and
|
|
619
|
-
which side of that frame a cycle lands on belongs to the machine's load: under
|
|
620
|
-
a movement the list that comes back is either the element that never left,
|
|
621
|
-
asking for nothing, or one truly remounted, asking once, while the same walk
|
|
622
|
-
without a movement is a real one every time. No thumb moves in one frame; an
|
|
623
|
-
automated click continues in the same millisecond. This is a testing trap, not
|
|
624
|
-
a user-facing behaviour.
|
|
501
|
+
The window is **one frame**: no thumb moves in one frame, but an automated click
|
|
502
|
+
continues in the same millisecond. A testing trap, not a user-facing behaviour.
|
|
625
503
|
|
|
626
504
|
## What a transition costs
|
|
627
505
|
|
|
628
506
|
A transition adds one frame to a navigation: the browser photographs the page
|
|
629
|
-
being left at its next rendering opportunity, and the page arriving is built
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
17–21 ms at full speed, 60–90 ms at 4× CPU. Two browser facts cause it: a
|
|
636
|
-
`::highlight()` rule every element matches, and a custom property changed on
|
|
637
|
-
the root as the movement starts (see
|
|
507
|
+
being left at its next rendering opportunity, and the page arriving is built only
|
|
508
|
+
then, inside the update callback. Anything beyond that is avoidable:
|
|
509
|
+
|
|
510
|
+
- **Restyling the whole page being left in the frame it is photographed** — a
|
|
511
|
+
`::highlight()` rule every element matches, or a custom property changed on the
|
|
512
|
+
root as the movement starts (see
|
|
638
513
|
[view_transitions.md](./view_transitions.md#what-makes-a-transition-restyle-the-whole-document)).
|
|
639
|
-
navi's movements declare nothing inheritable on the root; a custom type
|
|
640
|
-
|
|
641
|
-
[Custom movements](#custom-movements)).
|
|
514
|
+
navi's movements declare nothing inheritable on the root; a custom type writes
|
|
515
|
+
its values on `::view-transition`.
|
|
642
516
|
- **The page being left reacting to what the navigation changed.** Every
|
|
643
517
|
component in it that re-renders on the change does so inside the update
|
|
644
|
-
callback, next to the page arriving being built. A
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
of its own into the same store leaves the cards of the page being left as
|
|
651
|
-
they were. What moves last is the page's own reading of its route's action
|
|
652
|
-
— its params, its data and its state all change as the route stops
|
|
653
|
-
matching. `useAsyncData` gives no render to a page its container no longer
|
|
654
|
-
shows: the page is about to be taken down, or hidden until the movement is
|
|
655
|
-
over (on the list of thirty cards: about 450 component renders gone from the
|
|
656
|
-
callback, first frame 10–20 ms sooner at 4× CPU, nothing measurable at full
|
|
657
|
-
speed). A component reading the action's signals itself
|
|
658
|
-
(`action.dataSignal.value`) does not get that, and re-renders in the hidden
|
|
659
|
-
page.
|
|
518
|
+
callback, next to the page arriving being built. A `<Link route>` or
|
|
519
|
+
`<Button route>` reads one flag — whether it is current — and stays still unless
|
|
520
|
+
that flips; a row read with `RESOURCE.useById` follows that row only; and
|
|
521
|
+
`useAsyncData` gives no render to a page its container no longer shows. A
|
|
522
|
+
component reading an action's signals itself (`action.dataSignal.value`) gets
|
|
523
|
+
none of that, and re-renders in the hidden page.
|
|
660
524
|
- **Restyling the page arriving more than once.** Built inside the update
|
|
661
525
|
callback, it is styled as it lands; anything written afterwards that its
|
|
662
|
-
elements inherit, or that re-selects them, restyles all of them again
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
main thread builds the rows. On the list of thirty cards, handed its whole
|
|
679
|
-
position back: the movement starts 70–85 ms after the click instead of
|
|
680
|
-
185–270.
|
|
681
|
-
- **Re-rendering what did not change, on the press itself.** A component that
|
|
682
|
-
only wants a yes or a no reads a computed flag, not the signal it is derived
|
|
683
|
-
from: a list whose rows each read the count of runs in flight re-rendered
|
|
684
|
-
all thirty cards twice when a link in one of them was pressed — 774
|
|
685
|
-
component renders before the navigation even started, 18 once the rows read
|
|
686
|
-
whether the list is full instead.
|
|
687
|
-
- **Nothing is gained by starting at the press.** The click's own work is the
|
|
688
|
-
navigation, and the picture can only be taken on the frame after it; work
|
|
689
|
-
moved to the pointer-down is paid again, for nothing, by every press that
|
|
690
|
-
turns into a scroll.
|
|
526
|
+
elements inherit, or that re-selects them, restyles all of them again — the room
|
|
527
|
+
a fixed bar takes (`--navi-fixed-bar-space-*`, feeding the inherited
|
|
528
|
+
`--navi-safe-area-inset-*`) changing on the root as a bar arrives or leaves with
|
|
529
|
+
the page, an app's `:has()` rule on the list container over its rows (an
|
|
530
|
+
attribute the app sets on the container says the same thing for nothing).
|
|
531
|
+
- **Building rows the first picture does not show.** A list coming back where it
|
|
532
|
+
was draws, in the update callback, only the rows that were on screen then (the
|
|
533
|
+
`visibleCount` of the position it is handed back, see
|
|
534
|
+
[scroll.md](./scroll.md#where-the-list-opens-and-where-it-is)), and the rest of
|
|
535
|
+
its window once the movement has started — which plays on the compositor, and
|
|
536
|
+
does not wait for them.
|
|
537
|
+
- **Re-rendering what did not change, on the press itself.** A component that only
|
|
538
|
+
wants a yes or a no reads a computed flag, not the signal it is derived from.
|
|
539
|
+
- **Nothing is gained by starting at the press.** The picture can only be taken on
|
|
540
|
+
the frame after the click; work moved to the pointer-down is paid again, for
|
|
541
|
+
nothing, by every press that turns into a scroll.
|
|
691
542
|
|
|
692
543
|
## The rest, briefly
|
|
693
544
|
|
|
694
|
-
- Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone;
|
|
695
|
-
|
|
696
|
-
|
|
545
|
+
- Pace: `--navi-route-transition-duration` (CSS, default 300ms) for everyone; a
|
|
546
|
+
per-relation `{ type, duration }` for one relation, worn by that movement's
|
|
547
|
+
pictures only.
|
|
697
548
|
- The URL leads: transitions play on navigations somebody else started (a
|
|
698
549
|
`<Link>`, the back button, `history.back()`). Nothing here navigates.
|
|
699
|
-
- A browser without view transitions navigates with a cut. The app
|
|
700
|
-
|
|
701
|
-
|
|
550
|
+
- A browser without view transitions navigates with a cut. The app must remain
|
|
551
|
+
fully usable that way — which it is, if the transitions state relations rather
|
|
552
|
+
than carry information.
|