@jsenv/navi 0.29.21 → 0.29.23
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 +2 -0
- package/dist/jsenv_navi.js +2275 -454
- package/dist/jsenv_navi.js.map +255 -118
- package/docs/AI_INSTRUCTIONS.md +22 -0
- package/docs/drag_to_travel.md +371 -0
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -49,6 +49,9 @@ consistency across the app, not from any single call site.
|
|
|
49
49
|
values navi's own popups/bars/tables use. Read it before writing a `z-index`.
|
|
50
50
|
- `docs/MOBILE_LAYOUT_PITFALLS.md` — mobile-specific layout gotchas (viewport
|
|
51
51
|
units, virtual keyboard, safe areas).
|
|
52
|
+
- `src/nav/route_ui.md` — routes as UI: layout/section patterns, and
|
|
53
|
+
`RouteTravel` (swiping between pages that are URLs — the tabs of a page,
|
|
54
|
+
driven by thumb, wheel, or a link).
|
|
52
55
|
- Source code on GitHub: https://github.com/jsenv/core/tree/main/packages/frontend/navi/src
|
|
53
56
|
— worth checking if the JSDoc on an export genuinely doesn't answer your
|
|
54
57
|
question.
|
|
@@ -73,6 +76,25 @@ consistency across the app, not from any single call site.
|
|
|
73
76
|
- **Field components** (`Input`, `Select`, `Checkbox`, etc.) take an `action`
|
|
74
77
|
prop to respond to interaction — this is the standard wiring, not
|
|
75
78
|
`onChange` + manual state.
|
|
79
|
+
- **View transitions**: navi components animate their own changes
|
|
80
|
+
(`itemTransition` on `List`, `RouteTravel` for routes) and never decide for
|
|
81
|
+
the whole document. Two things are the application's call, not navi's:
|
|
82
|
+
- a `view-transition-name` must be unique per document (a duplicate aborts
|
|
83
|
+
the transition) — scope any name your app adds;
|
|
84
|
+
- list/grid transitions rely on nested groups
|
|
85
|
+
(`view-transition-group: contain`, Chrome/Edge 140+). On browsers without
|
|
86
|
+
it nothing is named, so an unconditional `startViewTransition` falls back
|
|
87
|
+
to a full-page cross-fade. If that fade is unwanted in your app, the app —
|
|
88
|
+
not a component — writes:
|
|
89
|
+
`@supports not (view-transition-group: contain) { :root { view-transition-name: none } }`.
|
|
90
|
+
Only the application knows whether a page-wide fade is a decent default or
|
|
91
|
+
a glitch there.
|
|
92
|
+
- a bonus that costs nothing: any element given its own
|
|
93
|
+
`view-transition-name` (a tab underline, a header) is animated by the
|
|
94
|
+
browser from where it was to where it is during any transition — `Nav`
|
|
95
|
+
does this for its current-tab indicator automatically
|
|
96
|
+
(`currentIndicator`), which is why the bar follows a `RouteTravel` swipe
|
|
97
|
+
with no wiring.
|
|
76
98
|
|
|
77
99
|
If unsure which export solves a problem, check `README.md` first — the
|
|
78
100
|
`src/` tree on GitHub is there too if a specific export's own JSDoc doesn't
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Travelling by drag, and who owns the gesture
|
|
2
|
+
|
|
3
|
+
A drag-travel is a pointer pushing a whole screen aside to bring in the next
|
|
4
|
+
one: slides inside a box (`SlideContainer`), pages that are URLs
|
|
5
|
+
(`RouteTravel`). One module answers what such a gesture IS —
|
|
6
|
+
[@jsenv/dom's drag_to_travel.js](../../dom/src/interaction/drag/drag_to_travel.js) — and both read it, so
|
|
7
|
+
a hand never has to learn two sets of numbers.
|
|
8
|
+
|
|
9
|
+
This file is the spec of the travel gesture and of what its two consumers do
|
|
10
|
+
with it. The general rules it leans on — who owns state while something
|
|
11
|
+
animates, the view-transition pitfalls, the compositor traps — live in the
|
|
12
|
+
animations skill
|
|
13
|
+
([.agents/skills/animations/SKILL.md](../../../../.agents/skills/animations/SKILL.md))
|
|
14
|
+
and are referenced from here rather than restated.
|
|
15
|
+
|
|
16
|
+
## What the rules are
|
|
17
|
+
|
|
18
|
+
- A press is not a gesture until it has wandered ~10px, and the axis it leans on
|
|
19
|
+
then is the axis it walks, for good. **Except on something already moving**:
|
|
20
|
+
there the hand said what it wanted by reaching for it, so the gesture answers
|
|
21
|
+
from its first pixel and owes it every one of them — asking it to cross a
|
|
22
|
+
threshold is asking twice, and over those pixels the thing it is holding
|
|
23
|
+
answers to nobody. A diagonal would ask for two travels at
|
|
24
|
+
once and only one screen can arrive.
|
|
25
|
+
- Letting go carries on when about a third of a box has been pulled, or on a
|
|
26
|
+
flick, whatever the distance — a gesture that has clearly begun is an
|
|
27
|
+
intention, and asking for a screen to be dragged all the way across turns a
|
|
28
|
+
travel into work.
|
|
29
|
+
- **A hand still moving says where it is going, and it says it both ways.** Away
|
|
30
|
+
from what was being brought in, it means "put it back" whatever the distance
|
|
31
|
+
already covered — otherwise a screen caught in flight and thrown back still
|
|
32
|
+
arrives, because the picture alone decided. A slow nudge is not a throw: there
|
|
33
|
+
the distance decides, as usual.
|
|
34
|
+
- Pulling towards nothing follows the finger at a fraction of its distance and
|
|
35
|
+
comes back: a wall one can lean on, never walk through.
|
|
36
|
+
- A hand can go further than one box, and those extra pixels are not owed back:
|
|
37
|
+
once the end is reached the gesture is measured from where the finger IS, so
|
|
38
|
+
turning around moves the picture at once. Measured from the origin instead, a
|
|
39
|
+
hand that came in fast would push against a screen that does not answer for as
|
|
40
|
+
many pixels as it went too far.
|
|
41
|
+
|
|
42
|
+
## Two inputs, one travel
|
|
43
|
+
|
|
44
|
+
A thumb dragging the page and a wheel pushing it sideways both ask to travel,
|
|
45
|
+
and both consumers take both. But they do not ask for the same thing, and this
|
|
46
|
+
is the one place where the two really part:
|
|
47
|
+
|
|
48
|
+
- **a hand HOLDS a screen** and says where to put it. It is owed every pixel, it
|
|
49
|
+
may change its mind halfway, and letting go is a question with an answer
|
|
50
|
+
(`onStart`/`onPull`/`onEnd`);
|
|
51
|
+
- **a wheel POINTS at the next screen** and says "that one". What travels is a
|
|
52
|
+
row of slides, not a long strip one stops in the middle of, so one push moves
|
|
53
|
+
one slide — `onStep`, and the travel that follows plays at its own pace,
|
|
54
|
+
exactly as it would from a tab pressed or an arrow key.
|
|
55
|
+
|
|
56
|
+
A wheel gesture also has **no press and no release**: it is a stream that begins
|
|
57
|
+
with its first event and ends in silence — a gap long enough to mean the hand is
|
|
58
|
+
gone, and long enough to survive the busiest frames of a travel (a navigation, a
|
|
59
|
+
render, a picture being taken).
|
|
60
|
+
|
|
61
|
+
Cutting that stream into pushes is the whole difficulty, because momentum keeps
|
|
62
|
+
arriving with the fingers gone, and counted it turns one flick into five slides.
|
|
63
|
+
Two things say "the hand asked again", and a stream only ever has one of them:
|
|
64
|
+
|
|
65
|
+
- **a gap.** A mouse spends tens of milliseconds between two notches; a
|
|
66
|
+
trackpad, which sends whether or not the fingers are still there, never does.
|
|
67
|
+
So a notch is a step, however fast the wheel is spun;
|
|
68
|
+
- **a number that grows after having shrunk.** Momentum only ever weakens, so a
|
|
69
|
+
trackpad picking up again is a hand pushing again. One flick — rise, peak,
|
|
70
|
+
decay — is therefore one step, and a second flick over the tail of the first
|
|
71
|
+
is heard as its own.
|
|
72
|
+
|
|
73
|
+
Taking it is also the only way to stop the browser from answering it: on a
|
|
74
|
+
laptop a horizontal two-finger swipe IS the back-navigation gesture, and a
|
|
75
|
+
region that neither takes it nor lets it go is the worst of the three — the page
|
|
76
|
+
rocks and nothing happens.
|
|
77
|
+
|
|
78
|
+
Not the same thing as the drag gesture it sits beside
|
|
79
|
+
([drag_gesture.js](../../dom/src/interaction/drag/drag_gesture.js)): that one is
|
|
80
|
+
for **carrying an object** across the page — it lays a backdrop over the
|
|
81
|
+
document, makes everything else `inert`, takes the focus and blocks the scroll
|
|
82
|
+
keys. Here nothing is picked up and the page must keep its focus and its
|
|
83
|
+
scrolling while a screen slides. Same word, other gesture.
|
|
84
|
+
|
|
85
|
+
## Who owns a gesture
|
|
86
|
+
|
|
87
|
+
Two things can claim a pointer that landed on a travelling box, and both are
|
|
88
|
+
read before the box moves:
|
|
89
|
+
|
|
90
|
+
1. **What says so itself.** A field, a `contenteditable`, or anything carrying
|
|
91
|
+
`data-no-drag-travel`.
|
|
92
|
+
2. **A scroller in between with room left that way.** It keeps the gesture until
|
|
93
|
+
it has no room left, and only then hands the travel over — so a row that
|
|
94
|
+
scrolls sideways inside a page still scrolls sideways.
|
|
95
|
+
|
|
96
|
+
### The browser also wants to answer the gesture
|
|
97
|
+
|
|
98
|
+
A gesture that is already answered — something is being dragged — must not be
|
|
99
|
+
answered a second time by the browser. Two of its answers show up as "the whole
|
|
100
|
+
page moved a little, and it looked wrong":
|
|
101
|
+
|
|
102
|
+
- **the leftovers of a scroll**, handed up the chain until something moves: a
|
|
103
|
+
list inside the box reaches its end and the page scrolls behind the travel.
|
|
104
|
+
`overscroll-behavior-<axis>: contain !important` on the travelling box and
|
|
105
|
+
everything inside it — and **written once and for all, never while the finger
|
|
106
|
+
is down**: a browser decides what a gesture may do when the gesture BEGINS (at
|
|
107
|
+
the touchstart, at the first wheel event), so a property written after that
|
|
108
|
+
decision arrives too late for the gesture it was meant for. That is what
|
|
109
|
+
"usually it does not move, sometimes it does" is made of. On the travelling
|
|
110
|
+
axis only — the other one is the content's own scrolling, and containing does
|
|
111
|
+
not stop scrolling anyway, it stops spilling;
|
|
112
|
+
- **the elastic bounce** at the end of a page, and the swipe that goes back in
|
|
113
|
+
history with it: `overscroll-behavior: none` on the document while a finger is
|
|
114
|
+
down. Same lateness applies, so this is a last resort behind the rule above
|
|
115
|
+
rather than the thing that does the work;
|
|
116
|
+
- **the selection** a drag paints across the text it crosses: `user-select:
|
|
117
|
+
none`, but only once the press has become a travel — a press on text IS how
|
|
118
|
+
one selects it, and nothing has said otherwise yet.
|
|
119
|
+
|
|
120
|
+
Both are written by the gesture itself (`data-drag-travel-gesture` and
|
|
121
|
+
`data-drag-travel-walking` on `:root`), so a page that bounces the rest of the
|
|
122
|
+
time goes on bouncing. `preventDefault()` on each move says the same thing to
|
|
123
|
+
the browser for what those two properties do not cover.
|
|
124
|
+
|
|
125
|
+
### A hand reaching for something still moving is reaching for THAT thing
|
|
126
|
+
|
|
127
|
+
A gesture arriving while a travel is playing takes **that travel** over — it does
|
|
128
|
+
not ask for a new one, and it is not refused. Refusing it is what makes a page
|
|
129
|
+
rock: a gesture given back to the browser is answered by the browser, over a
|
|
130
|
+
travel that is already moving.
|
|
131
|
+
|
|
132
|
+
**Touching it stops it, at the press** — in both consumers — and not at the first
|
|
133
|
+
pixels that decide an axis. A hand landing on something that is moving expects it to obey at once;
|
|
134
|
+
waiting for a threshold lets the pages travel on under a finger already resting
|
|
135
|
+
on them, which is the one moment a gesture must not ask for proof. A press that
|
|
136
|
+
turns out to be nothing lets go again and the travel carries on from where it
|
|
137
|
+
was caught, over what is left of it.
|
|
138
|
+
|
|
139
|
+
Position alone does not say it is working: caught late, the picture is already
|
|
140
|
+
where the finger is and nothing looks wrong. What gives it away is SPEED — a
|
|
141
|
+
travel that keeps its pace under a resting finger, then is pinned to a hand
|
|
142
|
+
moving at another one. Measure the position frame by frame across the press: it
|
|
143
|
+
must stop on the frame the finger lands, not on the one where the axis is
|
|
144
|
+
decided.
|
|
145
|
+
|
|
146
|
+
Taking over means the pictures stop where they are and answer the finger again,
|
|
147
|
+
from where they stand (`slack`) rather than from zero. Only one box is in hand,
|
|
148
|
+
and walking out of either of its ends is a travel of its own — see below.
|
|
149
|
+
|
|
150
|
+
### A hand that does not stop at the end of a page is asking for the next one
|
|
151
|
+
|
|
152
|
+
One travel brings in one neighbour, but a gesture is not over because a travel
|
|
153
|
+
is: reaching an end and carrying on says "and the one after that", and being
|
|
154
|
+
made to let go and press again to say it is a wall in the middle of a movement.
|
|
155
|
+
So the gesture asks for another box at the end it reached (`onEdge`), and the
|
|
156
|
+
pixels past that end are the new box's first ones — nothing is spent twice.
|
|
157
|
+
|
|
158
|
+
The two ends cost differently, and it is worth knowing which one is being felt:
|
|
159
|
+
|
|
160
|
+
- **out the far end** — the page arrived, and the page it was leaving is gone:
|
|
161
|
+
there is no pair left to travel with, so the next travel wants pictures of its
|
|
162
|
+
own. A navigation, a render, a snapshot — and over those frames nothing
|
|
163
|
+
follows the finger before catching up with it. At the start of a gesture that
|
|
164
|
+
gap is invisible, the hand has barely moved; here the hand is at full speed;
|
|
165
|
+
- **back out of the start** — the pair in hand is already the right one: the
|
|
166
|
+
still it starts from is the same page, and what is being brought in is LIVE,
|
|
167
|
+
so pointing the router at the other neighbour is enough for it to show that
|
|
168
|
+
one instead. The travel turns around where it stands, on the same transition,
|
|
169
|
+
and there is no gap at all.
|
|
170
|
+
|
|
171
|
+
What the browser will not turn around with it is everything ELSE the
|
|
172
|
+
transition carries — see "One gesture that bar cannot follow" at the end of
|
|
173
|
+
this file.
|
|
174
|
+
|
|
175
|
+
Two things a travel in hand must never lose:
|
|
176
|
+
|
|
177
|
+
- **a travel being undone is not up for grabs.** Its end is already decided;
|
|
178
|
+
held again mid-revert, its animations never finish, the wait for them never
|
|
179
|
+
resolves — and the pictures stand where they are, over a page that cannot be
|
|
180
|
+
touched anymore;
|
|
181
|
+
- **a held travel is let go of before anything else animates.** A hold is
|
|
182
|
+
written in CSS against whatever transition is running (see the animations
|
|
183
|
+
skill), so a transition starting while a finger holds ours would be born
|
|
184
|
+
paused with nobody holding it — it never finishes, and the page freezes under
|
|
185
|
+
its pictures. Every transition navi starts passes through one funnel, which
|
|
186
|
+
releases the hold first;
|
|
187
|
+
- **the hold belongs to a travel, not to the page.** Only the travel that took
|
|
188
|
+
it may give it back — and it must give it back even when it ends after
|
|
189
|
+
something else has replaced it, or the hold survives its owner;
|
|
190
|
+
- **a gesture must hear its own end wherever it is delivered.** A pointer can be
|
|
191
|
+
cancelled somewhere the box is not on the path (the document root, during a
|
|
192
|
+
transition): missed, the gesture never ends, and whatever it was holding stays
|
|
193
|
+
held. The end is listened for on the window too, filtered by pointer id;
|
|
194
|
+
- **a box's own navigations are not somebody changing the route.** Routing is
|
|
195
|
+
asynchronous: a travel's navigation lands well after the travel decided
|
|
196
|
+
anything about it — sometimes after it was undone. Read back as "the route
|
|
197
|
+
changed", it starts a second travel nobody asked for, over pictures already
|
|
198
|
+
showing something else. So the box remembers what it asked for and recognises
|
|
199
|
+
its own answer when it arrives;
|
|
200
|
+
- **a travel ENDS, whatever happened on the way.** Whoever set the hold lifts it,
|
|
201
|
+
and the "a travel is playing" state is cleared in a `finally`. A travel left in
|
|
202
|
+
flight is not a small leak: it freezes the page under its own pictures, and
|
|
203
|
+
every gesture after it finds the box busy.
|
|
204
|
+
|
|
205
|
+
One browser fact makes this harder than it reads: **an element captured in a
|
|
206
|
+
view transition cannot be pointed at.** It is not painted where it stands
|
|
207
|
+
anymore, so nothing hit-tests to it — the press falls through to the nearest
|
|
208
|
+
ancestor still being painted, whatever the pseudo-elements are told about
|
|
209
|
+
`pointer-events`. Both readings answer it the same way: the event is caught at
|
|
210
|
+
the document and handed to the box when it fell inside its rectangle, which is
|
|
211
|
+
where the hand thinks it is.
|
|
212
|
+
|
|
213
|
+
### Two ways of holding a render still, and why only one is global
|
|
214
|
+
|
|
215
|
+
Nothing may reach the DOM between the moment a transition is asked for and the
|
|
216
|
+
moment the browser photographs the page — the photograph is taken a frame later,
|
|
217
|
+
and Preact renders sooner than that. So a navigation holds rendering from its
|
|
218
|
+
very first write until the transition's own callback: **all of it**, because a
|
|
219
|
+
view transition photographs the whole document. Hold only the routes and the tab
|
|
220
|
+
row updates first — the bar is then photographed already under the tab one is
|
|
221
|
+
going to, and it has nowhere to slide from. It lasts one frame.
|
|
222
|
+
|
|
223
|
+
The other hold is the revert above, and it can last a whole travel. There
|
|
224
|
+
nothing is being photographed: the pictures exist, and the only thing that must
|
|
225
|
+
not change is what the LIVE one shows. So only the pages are held
|
|
226
|
+
(`freezeRouteRender`), by the containers themselves — a route container keeps
|
|
227
|
+
returning the branch it returned last time — and the rest of the document goes
|
|
228
|
+
on rendering.
|
|
229
|
+
|
|
230
|
+
The same browser fact decides something bigger: **`RouteTravel` opts the page
|
|
231
|
+
OUT of the transition** (`view-transition-name: none` on the root, against the browser's
|
|
232
|
+
default). Captured, the whole page would be unpointable for the length of every
|
|
233
|
+
travel — a tab row beside the box stops highlighting, the cursor goes back to an
|
|
234
|
+
arrow, and a press on the tab one has just changed one's mind about goes
|
|
235
|
+
nowhere. Left live it answers as it always did, and nothing shows through where
|
|
236
|
+
the pages are: the box itself IS captured, so it paints nothing, and the two
|
|
237
|
+
pictures cover its rectangle between them at every moment of the travel.
|
|
238
|
+
|
|
239
|
+
It costs more for a wheel than for a press, because a press is one event and a
|
|
240
|
+
wheel gesture is a stream: heard on the box alone, a wheel that sets a travel
|
|
241
|
+
off loses every event after the first. What one sees then is a page nudged a
|
|
242
|
+
few pixels, going quiet, being put back — and scrolling behind the travel with
|
|
243
|
+
everything that was not taken.
|
|
244
|
+
|
|
245
|
+
### On a touchscreen, the browser takes the gesture unless it is refused
|
|
246
|
+
|
|
247
|
+
A `pointermove` is a report; a **`touchmove` is the decision**. Left alone, the
|
|
248
|
+
browser consumes the touch to scroll with — it latches on the first move, and a
|
|
249
|
+
touch it has taken is a pointer stream it CANCELS. The gesture then dies at its
|
|
250
|
+
second pixel: the finger is still down, nothing reads it anymore, and whatever
|
|
251
|
+
travel had started finishes without anyone. It is invisible with a mouse, which
|
|
252
|
+
is why it survives a whole session of desktop testing.
|
|
253
|
+
|
|
254
|
+
So a travel that has become ours refuses the `touchmove` (`preventDefault`), and
|
|
255
|
+
only then — a finger that means to scroll must still scroll. Two details make it
|
|
256
|
+
hold:
|
|
257
|
+
|
|
258
|
+
- the listener sits on the element the touch LANDED on as well as on the box: a
|
|
259
|
+
touch keeps being dispatched at the node it started on, and a travel may
|
|
260
|
+
replace the DOM under the finger (a page that travels navigates), after which
|
|
261
|
+
that node no longer passes through the box on its way up;
|
|
262
|
+
- the pointer is captured **before** the caller is told the gesture started, and
|
|
263
|
+
on the BOX rather than on what the finger landed on, for the same reason: what
|
|
264
|
+
the caller does may take that target away, and a capture whose element leaves
|
|
265
|
+
the document is a capture the browser drops.
|
|
266
|
+
|
|
267
|
+
And the refusal has to be _listened for_ from the grab, even though it only
|
|
268
|
+
refuses later: whether a touchmove can be refused at all is decided when the
|
|
269
|
+
touch begins, from the non-passive listeners present at that moment. Registered
|
|
270
|
+
afterwards, the listener is handed events that are already `cancelable: false` —
|
|
271
|
+
it runs, it calls `preventDefault`, and nothing happens.
|
|
272
|
+
|
|
273
|
+
### A navi component that reads the pointer marks ITSELF
|
|
274
|
+
|
|
275
|
+
`data-no-drag-travel` is written by the component that takes the pointer, never
|
|
276
|
+
by whoever puts it in a page:
|
|
277
|
+
|
|
278
|
+
```jsx
|
|
279
|
+
<div className="navi_wheel_viewport" data-no-drag-travel="">
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The caller cannot know — a `Table` whose columns can be dragged, a `Wheel` spun
|
|
283
|
+
with a thumb, a canvas one draws on all look like ordinary content from
|
|
284
|
+
outside — and will not find out until they watch a page leave under their
|
|
285
|
+
finger. The component knows, so the component says it.
|
|
286
|
+
|
|
287
|
+
_Currently marked: the wheel viewport, the table resize handles, the cells of a
|
|
288
|
+
table whose columns can be dragged._
|
|
289
|
+
|
|
290
|
+
## The two consumers
|
|
291
|
+
|
|
292
|
+
| | `SlideContainer` | `RouteTravel` |
|
|
293
|
+
| ----------------------- | ---------------------------------- | ---------------------------------- |
|
|
294
|
+
| what the screens are | `<Slide>`s in one box, all mounted | routes — one mounted, ever |
|
|
295
|
+
| what says which is here | `current` / a command | the URL |
|
|
296
|
+
| what the finger moves | a translated track | the pictures of a view transition |
|
|
297
|
+
| letting go too early | the track comes back | the transition is played backwards |
|
|
298
|
+
| what says the order | the layout map | the `<Route>` children, in order |
|
|
299
|
+
| what one wheel push is | `move(±1)`, as an arrow key | one travel, as a tab pressed |
|
|
300
|
+
|
|
301
|
+
Both expose how far the travel has come, and the way to read it differs because
|
|
302
|
+
what draws an indicator differs: `SlideContainer` writes
|
|
303
|
+
`--slide-travel-progress` on its box (a number, declared with `@property`, so it
|
|
304
|
+
interpolates) for anything drawn inside it; `RouteTravel` leaves it to the
|
|
305
|
+
browser — give the indicator a `view-transition-name` of its own and it is
|
|
306
|
+
animated from where it was to where it is, even from outside the box.
|
|
307
|
+
|
|
308
|
+
A `<Nav>` does that for its own bar without being told anything about the box
|
|
309
|
+
below it, which is why a tab row put beside a `RouteTravel` follows the thumb
|
|
310
|
+
with no wiring at all: the bar is named, so the browser recognises it from one
|
|
311
|
+
page to the next, and any transition moves it on the same clock as everything
|
|
312
|
+
else in that transition.
|
|
313
|
+
|
|
314
|
+
### Asking for a page while one is on its way
|
|
315
|
+
|
|
316
|
+
A travel is not a queue: a tab pressed while another page is arriving does not
|
|
317
|
+
wait its turn, and it does not start a second travel on top of the first either
|
|
318
|
+
— there is one pair of pictures, and a second transition would drop them
|
|
319
|
+
mid-slide. The travel in flight is simply aimed somewhere else, and where it is
|
|
320
|
+
aimed decides what that costs:
|
|
321
|
+
|
|
322
|
+
- **back where it set off from** — that is not another travel, it is this one
|
|
323
|
+
undone: the same pictures, run backwards. Nothing has to be asked of the
|
|
324
|
+
router (the press already put the page back), and that is exactly what makes
|
|
325
|
+
this the delicate one — three things at once:
|
|
326
|
+
- the picture being brought in is LIVE, and the page it shows is now the
|
|
327
|
+
page one is going back TO: both sides show the same thing and the way back
|
|
328
|
+
is invisible — one presses, and one is simply there. So the PAGES are held
|
|
329
|
+
where they are (`freezeRouteRender`) until the pictures are back at their
|
|
330
|
+
start; only the pages, because nothing is being photographed here and
|
|
331
|
+
everything else may keep rendering. The page being left stays on screen
|
|
332
|
+
while it is brought back — the rule this file states about reverts,
|
|
333
|
+
applied one step earlier;
|
|
334
|
+
- the way back is paid for in DISTANCE, not in time: the way in is eased, so
|
|
335
|
+
at half of its time a travel has covered ~80% of its distance, and rewound
|
|
336
|
+
at `-1` the visible way home collapses into the steep end of the curve — a
|
|
337
|
+
snap, not a return. The pictures walk home over how far they visibly are
|
|
338
|
+
from home, at the travel's own pace (`revertWalkTime`);
|
|
339
|
+
- both of the above run straight into the compositor traps: the distance is
|
|
340
|
+
computed from the clock through the easing curve (the pseudo-elements'
|
|
341
|
+
animated position cannot be read), and the rate is handed over with
|
|
342
|
+
`updatePlaybackRate`, never the `playbackRate` setter. The traps
|
|
343
|
+
themselves — including why no JS reading and no ordinary screenshot will
|
|
344
|
+
ever show this class of bug — are in the animations skill, "The main
|
|
345
|
+
thread lies about a running transition";
|
|
346
|
+
- **further the same way** — the picture being brought in is LIVE, so pointing
|
|
347
|
+
the router at another page is all it takes for it to show that one instead.
|
|
348
|
+
Nothing moves, and it costs nothing. Two tabs along or five makes no
|
|
349
|
+
difference: a travel goes from where it left to where it is going, never
|
|
350
|
+
through what lies between — which is just as well here, since the pages in
|
|
351
|
+
between are not mounted and there is nothing to show of them;
|
|
352
|
+
- **the other way** — the pictures have to change places, so the pair starts
|
|
353
|
+
again from the beginning. A travel barely begun turns around unnoticed; one
|
|
354
|
+
nearly arrived snaps back first. That is the price of changing one's mind
|
|
355
|
+
late, and there is no picture that could have covered it.
|
|
356
|
+
|
|
357
|
+
### One gesture that bar cannot follow
|
|
358
|
+
|
|
359
|
+
A travel that turns around mid-gesture (out of the start of the box it was
|
|
360
|
+
holding, into the page the other way) keeps its pages by pointing the router
|
|
361
|
+
elsewhere while the same transition plays — that is what makes it seamless. But
|
|
362
|
+
everything ELSE the transition carries was photographed when it began: where it
|
|
363
|
+
stood, and where it was going to stand. That second place is now one nobody is
|
|
364
|
+
going to, and the thing itself has already moved on in the live page — so the
|
|
365
|
+
picture and the thing are in two places at once, and one sees two bars.
|
|
366
|
+
|
|
367
|
+
So on that one gesture the pictures of everything that is not the pages are
|
|
368
|
+
dropped, and those things are left where they are, live: the bar jumps to the
|
|
369
|
+
tab one is heading for instead of sliding to a tab one is not. A slide would be
|
|
370
|
+
nicer, and it is not available — the browser measured both of its ends before
|
|
371
|
+
the hand changed its mind, and neither can be asked for again.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@jsenv/dom": "0.17.
|
|
32
|
+
"@jsenv/dom": "0.17.7",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.2"
|
|
35
35
|
},
|