@jsenv/navi 0.29.417 → 0.29.418

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.
@@ -1,247 +1,101 @@
1
1
  # Travelling by drag, and who owns the gesture
2
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.
3
+ A travel is a pointer or a wheel pushing a whole screen aside to bring in the
4
+ next one: slides inside a box (`SlideContainer`), pages that are URLs
5
+ (`RouteTravel`), a docked `Dialog` or a `SidePanel` pushed back the way it came
6
+ to close it, a row swiped aside with `interactions`. One module answers what
7
+ such a gesture IS — `drag_to_travel.js` in @jsenv/dom — and all of them read
8
+ it, so a hand never has to learn two sets of numbers.
9
+
10
+ What an application has to know is who gets a press that lands inside such a
11
+ box. It is decided at the press, from the DOM, before anything moves — so read
12
+ [Who owns a gesture](#who-owns-a-gesture) before putting anything that reads
13
+ the pointer inside a box that travels.
15
14
 
16
15
  - [What the rules are](#what-the-rules-are)
17
- - [The axis is read once, and read with a bias](#the-axis-is-read-once-and-read-with-a-bias)
18
- - [Two inputs, one travel](#two-inputs-one-travel)
19
16
  - [Who owns a gesture](#who-owns-a-gesture)
20
17
  - [Boxes inside boxes](#boxes-inside-boxes)
21
18
  - [Something being carried inside a box](#something-being-carried-inside-a-box)
22
- - [A popup pushed back the way it came](#a-popup-pushed-back-the-way-it-came)
23
19
  - [A surface in the top layer](#a-surface-in-the-top-layer)
24
- - [The browser also wants to answer the gesture](#the-browser-also-wants-to-answer-the-gesture)
25
- - [What is contained, and what still leaks](#what-is-contained-and-what-still-leaks)
26
- - [A hand reaching for something still moving is reaching for THAT thing](#a-hand-reaching-for-something-still-moving-is-reaching-for-that-thing)
27
- - [A hand that does not stop at the end of a page is asking for the next one](#a-hand-that-does-not-stop-at-the-end-of-a-page-is-asking-for-the-next-one)
28
- - [Two ways of holding a render still, and why only one is global](#two-ways-of-holding-a-render-still-and-why-only-one-is-global)
29
- - [On a touchscreen, the browser takes the gesture unless it is refused](#on-a-touchscreen-the-browser-takes-the-gesture-unless-it-is-refused)
30
20
  - [A navi component that reads the pointer marks ITSELF](#a-navi-component-that-reads-the-pointer-marks-itself)
21
+ - [What is contained, and what still leaks](#what-is-contained-and-what-still-leaks)
31
22
  - [The two consumers that travel between screens](#the-two-consumers-that-travel-between-screens)
32
- - [Asking for a page while one is on its way](#asking-for-a-page-while-one-is-on-its-way)
33
23
  - [One gesture that bar cannot follow](#one-gesture-that-bar-cannot-follow)
34
- - [Verifying a gesture](#verifying-a-gesture)
35
24
 
36
25
  ## What the rules are
37
26
 
38
27
  - A press is not a gesture until it has wandered 10px with a mouse, 6px with a
39
- finger or a pen, and the axis it leans on then is the axis it walks, for good
40
- — how that axis is read, why the reading is biased, and why a finger's number
41
- is a deadline rather than a feel, has [a section of its own
42
- below](#the-axis-is-read-once-and-read-with-a-bias). **Except on something
43
- already moving**: its axis is already decided, and a diagonal would ask for
44
- two travels at once when only one screen can arrive. What the hand wants
45
- there — the next screen, or that one — is read from its first steps, [see
46
- below](#a-hand-reaching-for-something-still-moving-is-reaching-for-that-thing).
47
- - **Of the way covered when the gesture forms, only the threshold is withheld**
48
- — never the whole first report. Movement reports are coalesced to frames, so
49
- the faster the gesture the bigger its first report is: charged whole, a flick
50
- delivered in one report sets off with nothing left of itself to have pulled —
51
- it moves nothing on screen and is refused at the release for it, and what the
52
- hand feels is a swipe that did strictly nothing.
28
+ finger or a pen, and the axis it leans on then is the axis it walks, for good:
29
+ a diagonal would ask for two travels at once when only one screen can arrive.
30
+ The reading leans towards the box's own axis — a thumb swiping sideways moves
31
+ along an arc, and its first pixels lean off-axis far more than the swipe does
32
+ — so the cross axis takes the press only when it clearly dominates, and a
33
+ scroll, near-pure on its axis from the first pixel, still leaves whole. A
34
+ finger's 6px is a deadline rather than a feel: the browser commits a touch to
35
+ its own pan about 8px in, and whatever is read after that is read about a
36
+ pointer it has already cancelled (see
37
+ [mobile_touch.md](./mobile_touch.md#the-browser-decides-about-8px-in-and-does-not-wait)).
38
+ Its price is the click of a tap that shook more than 6px.
53
39
  - **A hand still moving when it lets go says where this goes, and it says it
54
- both ways.** Towards what it was bringing in, the travel carries on — however
55
- slowly and whatever the distance: an intention still being acted on at the
56
- lift. Away from it, fast, everything goes back whatever the distance already
57
- covered — otherwise a screen caught in flight and thrown back still arrives,
58
- because the picture alone decided. Only a release at rest, or a slow retreat,
59
- is judged by the picture — about a third of a box — because there the
60
- position is the only witness left.
61
- - **A travel caught in flight and thrown on the way it was going asks for the
62
- one after.** Two swipes the same way are two screens, however close
63
- together: the second one is not a vote for the screen already arriving. See
64
- [below](#a-hand-reaching-for-something-still-moving-is-reaching-for-that-thing).
65
- - **The bars those verdicts compare against sit well below the speed the
66
- fingertip actually has, and they must.** Velocity is averaged over a trailing
67
- window (pointer events arrive irregularly, and the last one before a release
68
- often repeats its coordinates — measured on that pair alone, every throw ends
69
- at zero), and the release itself adds one more sample at the same place a
70
- moment later. Both pull the measure down from the hand's peak: a threshold
71
- sized against the hand refuses the hand.
40
+ both ways.** Towards what it was bringing in, the travel carries on, however
41
+ slowly, once it has pulled a few pixels. Away from it, fast, everything goes
42
+ back whatever the distance already covered — otherwise a screen caught in
43
+ flight and thrown back still arrives. Only a release at rest is judged by the
44
+ picture: about a third of the box.
72
45
  - Pulling towards nothing follows the finger at a fraction of its distance and
73
46
  comes back: a wall one can lean on, never walk through.
74
47
  - **A direction that will be refused is one of those walls, and it must be a
75
- wall from the first pixel.** Whatever holds the user where they are — a slide
76
- waiting for its answer, a step reachable only by finishing the one before —
77
- the gesture has to read it where it is ARMED, not only when it is let go of.
78
- Read at the release alone, the hold does half its job: it forbids the arrival
79
- and allows the whole journey, so the screen one may not reach is walked to,
80
- read on the way, and then taken back. A wall that shows what is behind it and
81
- pushes you back reads as a bug even when it is a rule. What the consumer owes
82
- the gesture is therefore not "is there a screen that way" but "may I go
83
- there" — the same question its release gate asks, asked earlier. Said `false`
84
- there, the direction is simply a dead end like any other, and everything else
85
- follows for free: the rubber band, the screen kept off stage, and a release
86
- with nothing left to refuse.
87
- - A hand can go further than one box, and those extra pixels are not owed back:
88
- once the end is reached the gesture is measured from where the finger IS, so
89
- turning around moves the picture at once. Measured from the origin instead, a
90
- hand that came in fast would push against a screen that does not answer for as
91
- many pixels as it went too far.
92
-
93
- ## The axis is read once, and read with a bias
94
-
95
- The axis is decided on the first movement report after the threshold and never
96
- revisited. For a thumb that report is a few pixels of movement, and those pixels
97
- misstate the gesture: a thumb swiping sideways moves along an ARC, and the
98
- start of the arc leans off-axis far more than the swipe does. Read even
99
- (whichever axis covered more), the lean hands the whole press to an axis
100
- nobody meant — and a press given up is given up WHOLE: the hand's remaining
101
- hundred pixels are read by no one, and what it feels is a swipe that did
102
- nothing at all.
103
-
104
- So on a box that travels one axis, the reading is biased towards that axis:
105
- the cross axis takes the press only when it clearly dominates the first
106
- report (2×, `AXIS_CROSS_DOMINANCE`). A gesture that really belongs to the
107
- other axis — a scroll — is near-pure on its axis from the first pixel (4-6×),
108
- so it still leaves whole, at once, in the same tick. A box that travels both
109
- axes keeps the even reading: every answer is a travel there, and there is
110
- nothing to protect.
111
-
112
- Why the decision cannot simply wait for more evidence — this boundary is
113
- physics, not caution:
114
-
115
- - the browser is racing for the same press, on the axis `touch-action` leaves
116
- it — and on every axis over a scroller inside the box, since a scroller
117
- resets `touch-action` for what it holds;
118
- - and it does not wait for us: **about 8px in, it commits the touch to its own
119
- pan**, reading the direction evenly (Chrome at the first `touchmove` it
120
- sends, Safari at the one near 9px). From then on refusing a `touchmove`
121
- changes nothing: the pointer is cancelled, and the swipe does nothing — or
122
- the travel that had started goes back;
123
- - refusing a touchmove is **irreversible for that touch** too, the other way
124
- round: one refused before that moment keeps the whole touch from scrolling,
125
- and un-refusing later resurrects nothing;
126
- - so the reading has to be done before the browser's, and every frame spent
127
- gathering evidence past it is not evidence, it is a lost gesture. A decision
128
- deferred until the arc has proven itself would make every genuinely vertical
129
- swipe over the box a dead gesture — the exact bug, mirrored onto the page.
130
-
131
- Hence the finger's threshold, 6px of distance (`DRAG_START_THRESHOLD_TOUCH`):
132
- under the browser's 8px with a margin, because Safari does not wait for the
133
- answer to a report that jumps over its decision. A distance and not a per-axis
134
- reach, because the browser measures its own slop as one. Its price is the
135
- click of a tap that shook more than 6px. The engines' side of all this —
136
- measured, per engine, with how to reproduce it — is
137
- [mobile_touch.md](./mobile_touch.md).
138
-
139
- The bias moves the frame-one boundary to where the two hands actually
140
- separate; nothing can remove the boundary. It only works because it is read
141
- first: read after the browser's own reading, it is never consulted, the
142
- pointer already being cancelled. A first report steeper than the bias (a start
143
- more than twice off-axis) is genuinely ambiguous with a scroll and goes to the
144
- page — which at least answers it visibly.
145
-
146
- ## Two inputs, one travel
147
-
148
- A thumb dragging the page and a wheel pushing it sideways both ask to travel,
149
- and both consumers take both. But they do not ask for the same thing, and this
150
- is the one place where the two really part:
151
-
152
- - **a hand HOLDS a screen** and says where to put it. It is owed every pixel, it
153
- may change its mind halfway, and letting go is a question with an answer
154
- (`onStart`/`onPull`/`onEnd`);
155
- - **a wheel POINTS at the next screen** and says "that one". What travels is a
156
- row of slides, not a long strip one stops in the middle of, so one push moves
157
- one slide — `onStep`, and the travel that follows plays at its own pace,
158
- exactly as it would from a tab pressed or an arrow key.
159
-
160
- A wheel gesture also has **no press and no release**: it is a stream that begins
161
- with its first event and ends in silence — a gap long enough to mean the hand is
162
- gone, and long enough to survive the busiest frames of a travel (a navigation, a
163
- render, a picture being taken).
164
-
165
- Cutting that stream into pushes is the whole difficulty, because momentum keeps
166
- arriving with the fingers gone, and counted it turns one flick into five slides.
167
- Two things say "the hand asked again", and a stream only ever has one of them:
168
-
169
- - **a gap.** A mouse spends tens of milliseconds between two notches; a
170
- trackpad, which sends whether or not the fingers are still there, never does.
171
- So a notch is a step, however fast the wheel is spun;
172
- - **a number that grows after having shrunk.** Momentum only ever weakens, so a
173
- trackpad picking up again is a hand pushing again. One flick — rise, peak,
174
- decay — is therefore one step, and a second flick over the tail of the first
175
- is heard as its own.
176
-
177
- Both are read against a stream that never announces itself: the browser sees
178
- one unbroken burst (the tail of the first push never went silent), so "the
179
- hand asked again" is reconstructed, never received. Two consequences are easy
180
- to get wrong from there:
181
-
182
- - **a second push is answered like a first event — a screen, now** — never with
183
- credit towards one. The price a screen after the first costs (an accumulated
184
- delta, deliberately steep) exists to keep momentum from walking slides on its
185
- own; charged to a hand that pushed again over the tail, it reads as "my
186
- swipes are ignored until I wait for silence". The stream itself says which is
187
- which: once it has faded into a recognized tail, growing **twice in a row**
188
- is a hand (`WHEEL_REGROW_RUN`) — decay jitter bumps up in isolated events and
189
- never twice consecutively.
190
- - **cross-axis events are swallowed absolutely, and they renew nothing.** A
191
- hand is never perfectly straight, so events leaning the other way are the
192
- gesture's own wobble and are eaten with the rest — and there the swallowing
193
- has to be total, because no per-event reading tells a scroll's onset from
194
- that wobble. It was tried, and every filter leaks: end-of-fade crumbs are a
195
- pixel or two landing on either axis (a `(0,1)` event is "clearly the other
196
- axis" by any dominance test), a slightly diagonal swipe's tail carries
197
- hand-sized deltas on both axes, and one crumb let through scrolls the
198
- slide's own content under the travel — a header creeping off the edge of the
199
- box, one pixel per leak. Do not re-attempt a "hand it back when it is really
200
- a scroll" heuristic; the honest boundary is elsewhere: a swallowed
201
- cross-axis event does NOT renew the claim. Silence is the only end a wheel
202
- gesture has, and it is silence ON ITS AXIS — renewed by what it eats, the
203
- gesture would outlive its own stream, and a hand starting a scroll over the
204
- tail would extend, event by event, the very deadness it was waiting out.
205
- Left to lapse, the claim dies shortly after the travel axis goes quiet (on a
206
- system where touching the surface kills the old momentum, that is moments
207
- after the new scroll begins), and the browser answers the rest of the scroll
208
- itself.
209
-
210
- Taking it is also the only way to stop the browser from answering it: on a
211
- laptop a horizontal two-finger swipe IS the back-navigation gesture, and a
212
- region that neither takes it nor lets it go is the worst of the three — the page
213
- rocks and nothing happens.
214
-
215
- Not the same thing as **carrying an object** across the page, although both read
216
- the pointer through the same loop
217
- ([drag_gesture.js](../../dom/src/interaction/drag/drag_gesture.js)): a carry has
218
- it lay a backdrop over the document, make everything else `inert`, take the
219
- focus and block the scroll keys. Here nothing is picked up and the page must keep
220
- its focus and its scrolling while a screen slides, so the travel asks it for none
221
- of that (`documentInteractions: "manual"`). Same loop, other gesture.
48
+ wall from the first pixel.** Whatever holds the user where they are — a
49
+ `<Slide required>` waiting for its answer, a `preventNavNext` — is read where
50
+ the gesture is ARMED, not only when it is let go of. Read at the release
51
+ alone, the screen one may not reach is walked to, read on the way, and then
52
+ taken back: a wall that shows what is behind it and pushes you back reads as a
53
+ bug even when it is a rule. What a lock answers is therefore not "is there a
54
+ screen that way" but "may I go there".
55
+ - **A hand that walks a whole box and keeps going is asking for the next one**,
56
+ and the gesture walks on into it without being let go of.
57
+ - **A wheel push moves one screen**, the way an arrow key does — never a
58
+ distance. What keeps arriving after the fingers are gone is momentum and is
59
+ not counted: a second screen takes a second push, or one that insists.
60
+ - **A press on a travel still moving is the next swipe first.** A step the way
61
+ it is going asks for the screen after, and the travel goes on there without
62
+ ever having stopped; only a step against it, or a finger saying nothing for
63
+ about 100ms, catches it where it stands. Let go before either — a tap — and
64
+ the travel was never touched.
65
+
66
+ How to drive a gesture without a device, and why the obvious way hides the race
67
+ that loses swipes on a phone, is
68
+ [mobile_touch.md](./mobile_touch.md#verifying-without-a-device).
222
69
 
223
70
  ## Who owns a gesture
224
71
 
225
72
  Five things can claim a pointer that landed on a travelling box, and all five
226
- are read before the box moves:
227
-
228
- 1. **What says so itself.** A field, a `contenteditable`, a dedicated drag
229
- handle (`data-drag-handle`), or anything carrying `data-no-drag-travel`.
73
+ are read at the press, before the box moves:
74
+
75
+ 1. **What says so itself.** What the browser already answers the pointer on — a
76
+ field, a `contenteditable`, a `select`, a range; a dedicated drag handle
77
+ (`data-drag-handle`); anything carrying `data-no-drag-travel`; an element
78
+ whose `selfInteractions` names `drag` or `"*"` (see
79
+ [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions));
80
+ and a popover or a `<dialog>` inside the box, which is a layer over it. A
81
+ button, a link or a checkbox is not among them on purpose: a drag from one
82
+ travels, and the click it would have made is swallowed. A field that only
83
+ reads the press — a picker's façade is an `<input>` that opens a popup — says
84
+ so with `data-press-only`, and a travel starts there like anywhere else.
230
85
  2. **A scroller in between with room left that way.** It keeps the gesture until
231
86
  it has no room left, and only then hands the travel over — so a row that
232
87
  scrolls sideways inside a page still scrolls sideways.
233
- 3. **Another travelling box in between.** The innermost one takes the axes it
234
- walks, and leaves the ones it does not to whoever is above it.
235
- 4. **Something in between that is picked up and carried.** A row taken out of a
236
- list, a card carried across a board: it takes the axes it is dragged on and
237
- leaves the others — see [Something being carried inside a
238
- box](#something-being-carried-inside-a-box).
239
- 5. **A surface in the top layer in between.** Nothing above it gets the gesture
240
- at all — see [A surface in the top layer](#a-surface-in-the-top-layer).
241
-
242
- And one thing narrows it from the other end: a popup that names a **grip** reads
243
- the press only there — see [A popup pushed back the way it
244
- came](#a-popup-pushed-back-the-way-it-came).
88
+ 3. **Another travelling box in between** — see
89
+ [Boxes inside boxes](#boxes-inside-boxes).
90
+ 4. **Something in between that is picked up and carried** — see
91
+ [Something being carried inside a box](#something-being-carried-inside-a-box).
92
+ 5. **A surface in the top layer in between** — see
93
+ [A surface in the top layer](#a-surface-in-the-top-layer).
94
+
95
+ And one thing narrows it from the other end: a docked `Dialog` reads the press
96
+ only on its grip — its header, plus anything carrying `data-swipe-grip` — so a
97
+ board, a map or a list it holds keeps its own gestures, and a sheet with neither
98
+ is not pushed back at all. A `SidePanel` is pushed from its whole surface.
245
99
 
246
100
  ### Boxes inside boxes
247
101
 
@@ -256,54 +110,32 @@ to be declared for this: each box says which axes it travels in the DOM
256
110
  read.
257
111
 
258
112
  Decided at the press, once and for all: from the first pixel the gesture belongs
259
- to whoever asked the browser for the pointer last, which is the outermost box —
260
- so the arbitration has to happen before anyone asks, and the box that does not
261
- own the gesture never does. The consequence is that an inner box sitting on its
262
- last slide does not hand the gesture over mid-drag: it leans on its wall, the way
263
- it does when it is alone. Travelling the box around it means starting the gesture
264
- outside it.
113
+ to whoever asked the browser for the pointer last, which is the outermost box.
114
+ So an inner box sitting on its last slide does not hand the gesture over
115
+ mid-drag: it leans on its wall, the way it does when it is alone. Travelling the
116
+ box around it means starting the gesture outside it.
265
117
 
266
118
  ### Something being carried inside a box
267
119
 
268
- A drag reads the same press a travel does and holds the pointer from it, so the
269
- two share a finger exactly as two travelling boxes do: what is picked up says
270
- which axes it walks (`data-drag-source`, written from `data-drag-axis` by
271
- `interactions={{ move, reorder, land, toss, leave }}` — see
272
- [drag_interactions.md](./drag_interactions.md)), and
273
- the box above keeps what is left. A list reordered along its own line inside a
274
- row of slides swiped sideways: both gestures live, and neither had to be told
275
- about the other.
120
+ A drag (`interactions={{ move, reorder, land, toss, leave }}`, see
121
+ [drag_interactions.md](./drag_interactions.md)) reads the same press a travel
122
+ does and holds the pointer from it, so the two share a finger exactly as two
123
+ travelling boxes do: what is picked up says which axes it walks, and the box
124
+ above keeps what is left. A list reordered along its own line inside a row of
125
+ slides swiped sideways: both gestures live, and neither had to be told about the
126
+ other.
276
127
 
277
128
  When the two want the same axes — a piece carried both ways inside a sheet
278
129
  pushed down to close it — nothing is left and the press is the piece's, whole.
279
130
  That is the right way round: the box above is a surface, and the thing in it is
280
131
  what the hand came for.
281
132
 
282
- Something that is NOT free to be carried takes nothing: `move: "refuse"` — a row
283
- pinned by whoever owns the list, a court whose place on a plan is settled — walks
284
- no axis, so a swipe that starts on it is the swipe of the box it stands in. It
285
- keeps the press itself, to say no where the grab would have been felt; a surface
286
- that pans is the one thing that takes even that (see
287
- [drag_interactions.md](./drag_interactions.md#the-hand-pulls-and-nothing-follows-refuse)
288
- and [pan_zoom.md](./pan_zoom.md)).
289
-
290
- The exception is a **dedicated handle** (`data-drag-handle`), which has no axis:
291
- it is a place whose only purpose is to be taken hold of, from the first pixel,
292
- so it takes the press outright.
293
-
294
- ### A popup pushed back the way it came
295
-
296
- A `Dialog` docked to an edge (`dockedOnSmallTouchScreen`) and a `SidePanel`
297
- close by being pushed back the way they came in — a third consumer
298
- of this same travel, `swipe_to_close.js`. A popup that names a **grip** reads the
299
- press only there: for a `Dialog` that is its header, plus anything carrying
300
- `data-swipe-grip`. Everything else it holds is content the finger came to operate
301
- — a board a piece is dragged across, a map, a list — and a press there never
302
- reaches the travel at all, whatever it is made of.
303
-
304
- So a sheet with no header and nothing marked is not pushed back; it closes by
305
- its own controls, the backdrop and Escape. A `SidePanel` names no grip and is
306
- pushed from its whole surface, which suits a panel made of nothing else.
133
+ Something that is NOT free to be carried takes nothing: `move: "refuse"` walks
134
+ no axis, so a swipe that starts on it is the swipe of the box it stands in (see
135
+ [drag_interactions.md](./drag_interactions.md#the-hand-pulls-and-nothing-follows-refuse)).
136
+ A dedicated handle (`data-drag-handle`) is the opposite case: it has no axis, it
137
+ is a place whose only purpose is to be taken hold of, so it takes the press
138
+ outright.
307
139
 
308
140
  ### A surface in the top layer
309
141
 
@@ -320,521 +152,92 @@ surface, and behind is not under the finger. `showModal()` does not do this on
320
152
  its own: the rest of the document is made inert, but the press still bubbles out
321
153
  of the (not inert) dialog to a listener that sits above it.
322
154
 
323
- ### The browser also wants to answer the gesture
324
-
325
- A gesture that is already answered — something is being dragged — must not be
326
- answered a second time by the browser. Two of its answers show up as "the whole
327
- page moved a little, and it looked wrong":
328
-
329
- - **the leftovers of a scroll**, handed up the chain until something moves: a
330
- list inside the box reaches its end and the page scrolls behind the travel.
331
- `overscroll-behavior-<axis>: contain !important`, **written once and for all,
332
- never while the finger is down**: a browser decides what a gesture may do when
333
- the gesture BEGINS (at the touchstart, at the first wheel event), so a
334
- property written after that decision arrives too late for the gesture it was
335
- meant for. That is what "usually it does not move, sometimes it does" is made
336
- of — and it is why none of this can be done in JS at the moment it is needed.
337
- On the travelling axis only — the other one is the content's own scrolling,
338
- and containing does not stop scrolling anyway, it stops spilling. WHERE that
339
- property is written is a question of its own, and the engines do not answer it
340
- the same way: see [What is contained, and what still
341
- leaks](#what-is-contained-and-what-still-leaks);
342
- - **the elastic bounce** at the end of a page, and the swipe that goes back in
343
- history with it: `overscroll-behavior: none` on the document while a finger is
344
- down. Same lateness applies, so this is a last resort behind the rule above
345
- rather than the thing that does the work;
346
- - **the selection** a drag paints across the text it crosses: `user-select:
347
- none`, but only once the press has become a travel — a press on text IS how
348
- one selects it, and nothing has said otherwise yet. What its first pixels
349
- selected is collapsed at that same moment: it was the beginning of the
350
- travel, not a selection.
351
-
352
- Both are written by the gesture itself (`data-drag-travel-gesture` and
353
- `data-drag-travel-walking` on `:root`), so a page that bounces the rest of the
354
- time goes on bouncing. `preventDefault()` on each move says the same thing to
355
- the browser for what those two properties do not cover.
356
-
357
- ### What is contained, and what still leaks
358
-
359
- Containing is only ever read on a **scroll container** — an element whose
360
- `overflow` is `hidden`, `auto` or `scroll`, whether or not it has anything to
361
- scroll (`clip` cuts without making one). Three places
362
- could carry it: the travelling box, everything inside it, or the scrollers
363
- themselves. Which of the three works is an engine question, and the answer
364
- splits in two:
365
-
366
- - **Blink** asks every scroll container between the pointer and the page whether
367
- the gesture may go past it, _even one with nothing to scroll_. Containing the
368
- travelling box is therefore the whole answer — and saying it to everything
369
- inside is actively harmful: anything that happens to clip (a line with an
370
- ellipsis, a rounded card, the invisible checkbox that covers a selectable row)
371
- becomes a **dead zone under the wheel**, a container that stops the gesture
372
- and has nothing to move with it. This is the bug that made a list refuse the
373
- wheel inside a popup and only answer its scrollbar.
374
- - **Gecko and WebKit** ask only the containers that actually scroll. The
375
- travelling box is walked past — it travels, it does not scroll — so the
376
- scroller itself has to be told, and saying it to everything is harmless there.
377
-
378
- So `drag_to_travel.js` writes it on the box for everyone, and on everything
379
- inside only outside Blink (`@supports not (-webkit-app-region: none)`, one of
380
- the few properties that names an engine rather than a user agent string).
381
-
382
- It also names the scrollers a browser makes on its own — `textarea`,
383
- `select[multiple]`, `select[size]` — wherever they are inside the box. Nobody
384
- declared those, so nothing can find them by looking; being native is exactly
385
- what makes them nameable. `input` is deliberately not among them: it has
386
- nothing to scroll on the axis anything travels on, and containing it is how a
387
- row-wide invisible checkbox becomes a hole under the wheel. The cost of naming
388
- them is that an empty `textarea` is contained too — a browser cannot be asked
389
- "only if it scrolls" — so on Blink a wheel over one moves nothing rather than
390
- the list around it.
391
-
392
- On Blink this leaves the boxes that are **not scroll containers**, since those
393
- are never asked. Of navi's own: a `SidePanel` is a `Dialog`, which scrolls
394
- (`overflow: auto`), and is asked. A `SlideContainer` cuts with `overflow: clip`,
395
- which is not a scroll container (`hidden` would be one, and would let the slides
396
- off stage be scrolled to), so it is not asked. A `RouteTravel` box does not clip
397
- at all and must not be made a scroll container: one there would become the
398
- nearest one for every `position: sticky` inside the pages it holds. A row marked
399
- swipeable by `interactions` is not one either.
400
-
401
- Which is why navi contains what it KNOWS scrolls, in `box.jsx`:
402
-
403
- ```css
404
- [data-drag-travel*="y"] [data-scrollable] {
405
- overscroll-behavior-y: contain !important;
406
- }
407
- ```
408
-
409
- `[data-scrollable]` is worn by a `Box` that ASKED for `overflow: auto|scroll`,
410
- never by one that merely clips — so this cannot make a dead zone the way `*`
411
- does, and it puts the containment exactly where every engine reads it. A list, a
412
- dialog body, a scrolling panel inside a travelling page are covered on Blink
413
- again.
414
-
415
- What still leaks, and only on Blink, and only under a box that is not a scroll
416
- container: a
417
- scroller **nobody declared and no tag names** — a bare
418
- `<div style="overflow: auto">`, a widget from elsewhere. Its leftovers reach the
419
- page, which is the old symptom in a much smaller corner. Two ways out, per case:
420
- give the scroller a `Box` with an `overflow` (it is then declared), or contain it
421
- by hand. The general fix belongs to Blink.
422
-
423
- A note for whoever tests this: **Firefox cannot be measured with a synthetic
424
- wheel**. Playwright dispatches one outside APZ, where Gecko enforces
425
- `overscroll-behavior`, so containment never shows up there — inline, from a
426
- stylesheet, headless or headed alike. The non-Blink branch is therefore left
427
- exactly as it always was rather than tuned against a measurement that does not
428
- exist.
429
-
430
- ### A hand reaching for something still moving is reaching for THAT thing
431
-
432
- A gesture arriving while a travel is playing takes **that travel** over — it does
433
- not ask for a new one, and it is not refused. Refusing it is what makes a page
434
- rock: a gesture given back to the browser is answered by the browser, over a
435
- travel that is already moving.
436
-
437
- But a hand landing on a travel is two different hands, and the likelier one is
438
- not reaching for it at all: **it is the next swipe**, thrown while the last one
439
- is still playing. Stopping the travel at the press answers the rarer hand —
440
- the one catching — and makes the likelier one feel a stall: the screen stops
441
- dead under a swipe that starts from rest, then crawls at the finger's speed
442
- before it goes anywhere. So what the hand is doing is listened to first
443
- (`inFlight`), with the touch refused from the first pixel meanwhile (the
444
- browser's deadline does not wait for us — see
445
- [mobile_touch.md](./mobile_touch.md)):
446
-
447
- - **a step the travel's way** (the start threshold) is the next swipe: one
448
- screen further (`onPushOn`), and the travel goes on there from where it is —
449
- as a second arrow press sends it — never having stopped. The hand holds
450
- nothing; it pushed;
451
- - **a step against it, or a hand saying nothing** for `DRAG_CATCH_HOLD_MS`
452
- (100ms) is a catch: the travel stops where it stands at that moment, and the
453
- hand holds it from there;
454
- - **let go before either** — a tap — and the travel was never touched.
455
-
456
- The price is paid by the rarer hand: a catch lands up to 100ms after the finger,
457
- so the screen goes on that much under it. A slow hand stepping the travel's way
458
- before stopping reads as the next swipe too. Both are the right side to be
459
- wrong on.
460
-
461
- Both consumers answer the next swipe without stopping anything, each in its own
462
- way:
463
-
464
- - a `SlideContainer` sets off a new travel from where the track is, to the slide
465
- after — the one a second arrow press asks for — over the distance left, so it
466
- speeds up;
467
- - a `RouteTravel` keeps the transition it is playing and points the router one
468
- page further — what a tab pressed while it plays does (see [Asking for a page
469
- while one is on its
470
- way](#asking-for-a-page-while-one-is-on-its-way)) — so the pace does not
471
- change and the page arriving is simply the next one. What the transition
472
- carries besides the pages cannot follow that, see [One gesture that bar
473
- cannot follow](#one-gesture-that-bar-cannot-follow).
474
-
475
- Position alone does not say either is working. What gives it away is SPEED:
476
- measure the position frame by frame across the press (for a `RouteTravel`, the
477
- `currentTime` and `playState` of the transition's animations). A next swipe
478
- must not change the pace at all until the travel takes the new target, and
479
- then only keep it or speed up; a catch must stop on the frame the hold is
480
- decided, not later.
481
-
482
- Taking over means the pictures stop where they are and answer the finger again,
483
- from where they stand (`slack`) rather than from zero. Only one box is in hand,
484
- and walking out of either of its ends is a travel of its own — see below.
485
-
486
- What letting go of a travel caught this way means is read from the hand, three
487
- ways:
488
-
489
- - **merely touched** — a press that moved less than a gesture's worth: nobody
490
- asked the travel to stop, it was asked to wait, and it carries on to where it
491
- was going;
492
- - **thrown back** — against the way it was going, fast: everything goes back;
493
- - **thrown on** — the way it was already going, fast, having moved by more than
494
- a tremor: that is a second push, and it asks for the screen AFTER the one
495
- arriving (`thrownOn`) — the same answer as a step the travel's way, for a
496
- hand that caught it first. Read as a verdict on the box in hand, it only
497
- confirms what was already arriving, and the hand feels its second swipe
498
- swallowed. So the gesture
499
- asks for the next box at the release (`onEdge` with `thrown`), with the
500
- picture still short of the end of the one in hand, and nothing on screen
501
- moves for it: the travel goes on from where the hand left it, to one screen
502
- further. It is the wheel's rule (a second push over the tail is a screen, not
503
- credit towards one) said for a hand.
504
-
505
- A slow hand going the same way and stopping before it lets go is not thrown:
506
- there the picture decides, and the screen arriving arrives.
507
-
508
- ### A hand that does not stop at the end of a page is asking for the next one
509
-
510
- One travel brings in one neighbour, but a gesture is not over because a travel
511
- is: reaching an end and carrying on says "and the one after that", and being
512
- made to let go and press again to say it is a wall in the middle of a movement.
513
- So the gesture asks for another box at the end it reached (`onEdge`), and the
514
- pixels past that end are the new box's first ones — nothing is spent twice.
515
-
516
- The two ends cost differently, and it is worth knowing which one is being felt:
517
-
518
- - **out the far end** — the page arrived, and the page it was leaving is gone:
519
- there is no pair left to travel with, so the next travel wants pictures of its
520
- own. A navigation, a render, a snapshot — and over those frames nothing
521
- follows the finger before catching up with it. At the start of a gesture that
522
- gap is invisible, the hand has barely moved; here the hand is at full speed;
523
- - **back out of the start** — the pair in hand is already the right one: the
524
- still it starts from is the same page, and what is being brought in is LIVE,
525
- so pointing the router at the other neighbour is enough for it to show that
526
- one instead. The travel turns around where it stands, on the same transition,
527
- and there is no gap at all.
528
-
529
- That cost is a `RouteTravel` one: a `SlideContainer` has every slide mounted,
530
- so handing over is only re-staging — the slide reached becomes the one dragged,
531
- the one left stays beside it, the slide past it is put on the other side, and
532
- nothing on screen moves. A throw (`thrown`, above) is answered differently for
533
- the same reason: a `SlideContainer` hands over by re-staging, while a
534
- `RouteTravel` would have to jump its pictures the rest of the way before
535
- handing over, a page seen leaping — so it aims the travel in hand one page
536
- further instead, hands nothing over, and lets it go: it arrives there.
537
-
538
- What the browser will not turn around with it is everything ELSE the
539
- transition carries — see [One gesture that bar cannot
540
- follow](#one-gesture-that-bar-cannot-follow).
541
-
542
- What a travel in hand must never lose:
543
-
544
- - **a travel being undone is not up for grabs.** Its end is already decided;
545
- held again mid-revert, its animations never finish, the wait for them never
546
- resolves — and the pictures stand where they are, over a page that cannot be
547
- touched anymore;
548
- - **a held travel is let go of before anything else animates.** A hold is
549
- written in CSS against whatever transition is running (see the animations
550
- skill), so a transition starting while a finger holds ours would be born
551
- paused with nobody holding it — it never finishes, and the page freezes under
552
- its pictures. Every transition navi starts passes through one funnel, which
553
- releases the hold first;
554
- - **the hold belongs to a travel, not to the page.** Only the travel that took
555
- it may give it back — and it must give it back even when it ends after
556
- something else has replaced it, or the hold survives its owner;
557
- - **a capture that goes while the pointer is still down was taken, not given.**
558
- The ends a gesture has are the pointer going up and the pointer being
559
- cancelled; a `lostpointercapture` before either means someone else asked for
560
- the pointer (or the element it was held on left the document). What was being
561
- carried goes back rather than landing wherever the hand happened to be, and a
562
- travel comes home rather than committing. Home is where the travel was going
563
- BEFORE the press: one this gesture began goes back, one it caught in flight
564
- carries on to the end that was decided before the finger landed — the
565
- gesture said nothing, and nothing is what it changes. On a touchscreen this
566
- is the common case, not a corner: a finger reaching for a travel lands on the
567
- document root (the box is captured, unpointable), where neither the box's
568
- `touch-action` nor its touchmove listener is on the touch's path — so while
569
- a travel plays, `RouteTravel` keeps the touch refusable from the root, and
570
- the gesture that catches it (started from the grab) refuses every touchmove
571
- of that press. A scroll begun over a page still sliding is that page's, not
572
- the document's;
573
- - **a gesture must hear its own end wherever it is delivered.** A pointer can be
574
- cancelled somewhere the box is not on the path (the document root, during a
575
- transition): missed, the gesture never ends, and whatever it was holding stays
576
- held. The end is listened for on the window too, filtered by pointer id;
577
- - **a box's own navigations are not somebody changing the route.** Routing is
578
- asynchronous: a travel's navigation lands well after the travel decided
579
- anything about it — sometimes after it was undone. Read back as "the route
580
- changed", it starts a second travel nobody asked for, over pictures already
581
- showing something else. So the box remembers what it asked for and recognises
582
- its own answer when it arrives;
583
- - **a travel ENDS, whatever happened on the way.** Whoever set the hold lifts it,
584
- and the "a travel is playing" state is cleared in a `finally`. A travel left in
585
- flight is not a small leak: it freezes the page under its own pictures, and
586
- every gesture after it finds the box busy.
587
-
588
- One browser fact makes this harder than it reads: **an element captured in a
589
- view transition cannot be pointed at.** It is not painted where it stands
590
- anymore, so nothing hit-tests to it — the press falls through to the nearest
591
- ancestor still being painted, whatever the pseudo-elements are told about
592
- `pointer-events`. Both readings answer it the same way: the event is caught at
593
- the document and handed to the box when it fell inside its rectangle, which is
594
- where the hand thinks it is.
595
-
596
- ### Two ways of holding a render still, and why only one is global
597
-
598
- Nothing may reach the DOM between the moment a transition is asked for and the
599
- moment the browser photographs the page — the photograph is taken a frame later,
600
- and Preact renders sooner than that. So a navigation holds rendering from its
601
- very first write until the transition's own callback: **all of it**, because a
602
- view transition photographs the whole document. Hold only the routes and the tab
603
- row updates first — the bar is then photographed already under the tab one is
604
- going to, and it has nowhere to slide from. It lasts one frame.
605
-
606
- The other hold is the revert above, and it can last a whole travel. There
607
- nothing is being photographed: the pictures exist, and the only thing that must
608
- not change is what the LIVE one shows. So only the pages are held
609
- (`freezeRouteRender`), by the containers themselves — a route container keeps
610
- returning the branch it returned last time — and the rest of the document goes
611
- on rendering.
612
-
613
- The same browser fact decides something bigger: **`RouteTravel` opts the page
614
- OUT of the transition** (`view-transition-name: none` on the root, against the browser's
615
- default). Captured, the whole page would be unpointable for the length of every
616
- travel — a tab row beside the box stops highlighting, the cursor goes back to an
617
- arrow, and a press on the tab one has just changed one's mind about goes
618
- nowhere. Left live it answers as it always did, and nothing shows through where
619
- the pages are: the box itself IS captured, so it paints nothing, and the two
620
- pictures cover its rectangle between them at every moment of the travel.
621
-
622
- It costs more for a wheel than for a press, because a press is one event and a
623
- wheel gesture is a stream: heard on the box alone, a wheel that sets a travel
624
- off loses every event after the first. What one sees then is a page nudged a
625
- few pixels, going quiet, being put back — and scrolling behind the travel with
626
- everything that was not taken.
627
-
628
- ### On a touchscreen, the browser takes the gesture unless it is refused
629
-
630
- A `pointermove` is a report; a **`touchmove` is the decision**. Left alone, the
631
- browser consumes the touch to scroll with — it latches on the first move, and a
632
- touch it has taken is a pointer stream it CANCELS. The gesture then dies at its
633
- second pixel: the finger is still down, nothing reads it anymore, and whatever
634
- travel had started finishes without anyone. It is invisible with a mouse, which
635
- is why it survives a whole session of desktop testing.
636
-
637
- So a travel that has become ours refuses the `touchmove` (`preventDefault`), and
638
- only then — a finger that means to scroll must still scroll. "Only then" has a
639
- deadline: the browser takes the touch about 8px in if nothing has refused it by
640
- then, which is why a finger becomes a travel at 6px (see [the axis
641
- section](#the-axis-is-read-once-and-read-with-a-bias)). `touch-action` on the
642
- box does not cover for a late answer over content that scrolls: a scroller
643
- inside the box resets it for what it holds, and Chrome then pans even the axis
644
- the box travels on, with nothing to scroll that way (see
645
- [mobile_touch.md](./mobile_touch.md#touch-action-stops-at-the-first-scroller)).
646
- Three details make the refusal hold:
647
-
648
- - the listener sits on the element the touch LANDED on as well as on the window: a
649
- touch keeps being dispatched at the node it started on, and a travel may
650
- replace the DOM under the finger (a page that travels navigates), after which
651
- that node no longer passes through the window on its way anywhere;
652
- - the pointer is captured on the BOX rather than on what the finger landed on,
653
- for the same reason: what the caller does may take that target away, and a
654
- capture whose element leaves the document is a capture the browser drops;
655
- - and it is captured only once the travel has been ACCEPTED — not when the press
656
- crossed its threshold. There is one capture per pointer for the whole document,
657
- so taking it is taking it from whoever had it, who is then told the very thing
658
- it is told when its own gesture ends. A travel that gives itself up one event
659
- later (an axis this box does not walk, an `onStart` that refuses) would have
660
- killed a gesture already carrying something. Until the capture is claimed the
661
- moves are read from the window, filtered by pointer id, so nothing is missed
662
- for not owning the pointer.
663
-
664
- And the refusal has to be _listenable_ **before the finger lands**, even though
665
- it only refuses later: whether a touchmove can be refused at all is decided
666
- when the touch begins, from the non-passive listeners present at that moment.
667
- Registered at the pointerdown it is already too late — the events arrive
668
- `cancelable: false`, the listener runs, calls `preventDefault`, and nothing
669
- happens. What that costs is not the start of the gesture but its middle: the
670
- travel takes the press fine (the axis the browser was left never moved), and
671
- then the thumb's arc bends towards the axis `touch-action` leaves to the page —
672
- the browser starts the page's own scroll and CANCELS the pointer stream, and
673
- the screen the hand was carrying snaps back with the finger still down.
674
-
675
- So every box that travels says it from its own render, next to the attribute
676
- naming its axes: `onTouchMove={keepTouchRefusable}` — a no-op listener whose
677
- being registered is the whole of it (the same function everything picked up
678
- and carried registers, see `keepTouchRefusable` in drag_after_intent.js). A
679
- JSX prop is enough because the passive-by-default intervention only covers
680
- `window`, `document` and `document.body`: on any other element a plain
681
- listener is non-passive already — which matters, because a framework's event
682
- prop cannot say `passive: false`. The one place the explicit option is
683
- load-bearing is a listener that goes down on the document root (see
684
- drag_to.js). And a surface whose swipe starts only from a grip carrying
685
- `touch-action: none` needs none of this: the browser was never offered
686
- anything there to take back.
687
-
688
155
  ### A navi component that reads the pointer marks ITSELF
689
156
 
690
157
  `data-no-drag-travel` is written by the component that takes the pointer, never
691
- by whoever puts it in a page:
158
+ by whoever puts it in a page. The caller cannot know — a `Table` whose columns
159
+ can be dragged, a `Wheel` spun with a thumb, a canvas one draws on all look like
160
+ ordinary content from outside — and will not find out until they watch a page
161
+ leave under their finger. The component knows, so the component says it.
692
162
 
693
- ```jsx
694
- <div className="navi_wheel_viewport" data-no-drag-travel="">
695
- ```
163
+ The rule is the application's too: a component of yours that reads the pointer
164
+ by hand, rather than through `interactions`, marks its own element
165
+ `data-no-drag-travel`.
696
166
 
697
- The caller cannot know — a `Table` whose columns can be dragged, a `Wheel` spun
698
- with a thumb, a canvas one draws on all look like ordinary content from
699
- outside — and will not find out until they watch a page leave under their
700
- finger. The component knows, so the component says it.
167
+ ### What is contained, and what still leaks
701
168
 
702
- _Currently marked: the wheel viewport, the table resize handles, the cells of a
703
- table whose columns can be dragged, a surface declaring `pan` or `zoom`._
169
+ A travel must not take the page with it: a list inside the box that reaches its
170
+ end must not scroll the page behind the travel, and the page must not bounce, or
171
+ go back in history, under a screen being dragged. navi says so on everything it
172
+ knows scrolls inside a travelling box — the box itself, a `Box` that asked for
173
+ an `overflow`, and the scrollers a browser makes on its own (a `textarea`, a
174
+ `select` showing a list; an empty one too, so on Blink a wheel over it moves
175
+ nothing rather than the list around it). It is written once and for all rather
176
+ than while a finger is down, because a browser decides what a gesture may do
177
+ when the gesture begins; the bounce is refused only while a gesture runs, so a
178
+ page that bounces the rest of the time goes on bouncing.
179
+
180
+ What still leaks, only on Blink (Chrome, Edge), and only under a box that is not
181
+ a scroll container — a `SlideContainer` (it cuts with `overflow: clip`), a
182
+ `RouteTravel`, a swiped row: a scroller **nobody declared and no tag names** — a
183
+ bare `<div style="overflow: auto">`, a widget from elsewhere. Its leftovers
184
+ reach the page. Two ways out, per case: give the scroller a `Box` with an
185
+ `overflow` (it is then declared), or contain it by hand
186
+ (`overscroll-behavior-x: contain` under a box travelling sideways). The general
187
+ fix belongs to Blink.
704
188
 
705
189
  ## The two consumers that travel between screens
706
190
 
707
- A popup being pushed back towards its edge is a travel too, and it is the simple
708
- one: one box, one direction, no neighbour to bring in (see [A popup pushed back
709
- the way it came](#a-popup-pushed-back-the-way-it-came)). The two below carry
710
- screens, and everything the rest of this file weighs is about them.
191
+ A popup pushed back towards its edge and a swiped row are travels too, the
192
+ simple kind: one box, one direction, no neighbour to bring in. The two below
193
+ carry screens.
711
194
 
712
- | | `SlideContainer` | `RouteTravel` |
713
- | ----------------------- | ---------------------------------- | ---------------------------------- |
714
- | what the screens are | `<Slide>`s in one box, all mounted | routes — one mounted, ever |
715
- | what says which is here | `current` / a command | the URL |
716
- | what the finger moves | a translated track | the pictures of a view transition |
717
- | letting go too early | the track comes back | the transition is played backwards |
718
- | what says the order | the layout map | the `<Route>` children, in order |
719
- | what one wheel push is | `move(±1)`, as an arrow key | one travel, as a tab pressed |
195
+ | | `SlideContainer` | `RouteTravel` |
196
+ | ----------------------- | ----------------------------------- | ---------------------------------- |
197
+ | what the screens are | `<Slide>`s in one box, all mounted | routes — one mounted, ever |
198
+ | what says which is here | a `signal`, `current` / a command | the URL |
199
+ | what the finger moves | a translated track | the pictures of a view transition |
200
+ | letting go too early | the track comes back | the transition is played backwards |
201
+ | what says the order | the layout map | the `<Route>` children, in order |
202
+ | what one wheel push is | `move(±1)`, as an arrow key | one travel, as a tab pressed |
720
203
 
721
204
  Both expose how far the travel has come, and the way to read it differs because
722
- what draws an indicator differs: `SlideContainer` writes
723
- `--slide-travel-progress` on its box (a number, declared with `@property`, so it
724
- interpolates) for anything drawn inside it; `RouteTravel` leaves it to the
725
- browser — give the indicator a `view-transition-name` of its own and it is
726
- animated from where it was to where it is, even from outside the box.
727
-
728
- A `<Nav>` does that for its own bar without being told anything about the box
729
- below it, which is why a tab row put beside a `RouteTravel` follows the thumb
730
- with no wiring at all: the bar is named, so the browser recognises it from one
731
- page to the next, and any transition moves it on the same clock as everything
732
- else in that transition.
733
-
734
- ### Asking for a page while one is on its way
735
-
736
- A travel is not a queue: a tab pressed while another page is arriving does not
737
- wait its turn, and it does not start a second travel on top of the first either
738
- — there is one pair of pictures, and a second transition would drop them
739
- mid-slide. The travel in flight is simply aimed somewhere else, and where it is
740
- aimed decides what that costs:
741
-
742
- - **back where it set off from** — that is not another travel, it is this one
743
- undone: the same pictures, run backwards. Nothing has to be asked of the
744
- router (the press already put the page back), and that is exactly what makes
745
- this the delicate one — three things at once:
746
- - the picture being brought in is LIVE, and the page it shows is now the
747
- page one is going back TO: both sides show the same thing and the way back
748
- is invisible — one presses, and one is simply there. So the PAGES are held
749
- where they are (`freezeRouteRender`) until the pictures are back at their
750
- start; only the pages, because nothing is being photographed here and
751
- everything else may keep rendering. The page being left stays on screen
752
- while it is brought back — the rule this file states about reverts,
753
- applied one step earlier;
754
- - the way back is paid for in DISTANCE, not in time: the way in is eased, so
755
- at half of its time a travel has covered ~80% of its distance, and rewound
756
- at `-1` the visible way home collapses into the steep end of the curve — a
757
- snap, not a return. The pictures walk home over how far they visibly are
758
- from home, at the travel's own pace (`walkPicturesHome` in
759
- `src/transition/view_transition_revert.js`);
760
- - both of the above run straight into the compositor traps: the distance is
761
- computed from the clock through the easing curve (the pseudo-elements'
762
- animated position cannot be read), and the rate is handed over with
763
- `updatePlaybackRate`, never the `playbackRate` setter. The traps
764
- themselves — including why no JS reading and no ordinary screenshot will
765
- ever show this class of bug — are in the animations skill, "The main
766
- thread lies about a running transition";
767
- - **further the same way** — the picture being brought in is LIVE, so pointing
768
- the router at another page is all it takes for it to show that one instead.
769
- Nothing moves, and it costs nothing. Two tabs along or five makes no
770
- difference: a travel goes from where it left to where it is going, never
771
- through what lies between — which is just as well here, since the pages in
772
- between are not mounted and there is nothing to show of them;
773
- - **the other way** — the pictures have to change places, so the pair starts
774
- again from the beginning. A travel barely begun turns around unnoticed; one
775
- nearly arrived snaps back first. That is the price of changing one's mind
776
- late, and there is no picture that could have covered it.
205
+ what draws an indicator differs. `SlideContainer` writes
206
+ `--slide-travel-progress` — where the picture stands against the current slide,
207
+ in boxes: 0 on it, +1 a whole box before it, -1 one after — on its box, for
208
+ anything drawn inside it, and on every element marked
209
+ `data-slide-container-follows={id}` outside it (a tab bar above the box). It is
210
+ declared with `@property`, so it interpolates. `RouteTravel` leaves it to the
211
+ browser: give the indicator a `view-transition-name` of its own and it is
212
+ animated from where it was to where it is, even from outside the box (see
213
+ [view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)).
214
+
215
+ A travel is not a queue: a tab pressed while a page is arriving does not wait
216
+ its turn, and does not start a second travel on top of the first either. The
217
+ travel in flight is aimed there instead. Back where it set off from, it is
218
+ undone — the same pictures, run backwards; further the same way, it costs
219
+ nothing; the other way, the pictures start again from the beginning, and a page
220
+ nearly arrived snaps back first.
221
+
222
+ A hand that walks a `RouteTravel` page whole and keeps going gets the next page,
223
+ but out of the far end there is a short gap: the page it was leaving is gone,
224
+ the next travel needs pictures of its own (a navigation, a render, a snapshot),
225
+ and over those frames nothing follows the finger before catching up with it.
226
+ Walked back out of the start there is none — the pair in hand is already the
227
+ right one. A `SlideContainer` has every slide mounted and hands over without a
228
+ gap either way.
777
229
 
778
230
  ### One gesture that bar cannot follow
779
231
 
780
- A travel that turns around mid-gesture (out of the start of the box it was
781
- holding, into the page the other way) keeps its pages by pointing the router
782
- elsewhere while the same transition plays — that is what makes it seamless. But
783
- everything ELSE the transition carries was photographed when it began: where it
784
- stood, and where it was going to stand. That second place is now one nobody is
785
- going to, and the thing itself has already moved on in the live page — so the
786
- picture and the thing are in two places at once, and one sees two bars.
787
-
788
- The same holds whenever a travel is aimed further the same way while it plays
789
- — a tab pressed two pages along, the next swipe landing on a page still
790
- arriving: the bar was photographed going to a tab nobody is going to anymore.
232
+ A `RouteTravel` travel that turns around mid-gesture, or is aimed further the
233
+ same way while it plays — a tab pressed two pages along, the next swipe landing
234
+ on a page still arriving — keeps its pages by pointing the router elsewhere
235
+ under the same transition. But everything ELSE the transition carries was
236
+ photographed when it began, on its way to a tab nobody is going to anymore, and
237
+ the thing itself has already moved on in the live page: one would see two bars.
791
238
 
792
239
  So on those gestures the pictures of everything that is not the pages are
793
240
  dropped, and those things are left where they are, live: the bar jumps to the
794
241
  tab one is heading for instead of sliding to a tab one is not. A slide would be
795
242
  nicer, and it is not available — the browser measured both of its ends before
796
243
  the hand changed its mind, and neither can be asked for again.
797
-
798
- ## Verifying a gesture
799
-
800
- A gesture is verified the way movement is (see the animations skill,
801
- "Verifying") — driven synthetically, read as numbers — but what has to be
802
- synthesized is the HAND'S imperfection, because that is where gesture bugs
803
- live. A gesture simulated as clean, evenly-spaced points along one axis passes
804
- forever and proves nothing:
805
-
806
- - **coalesce**: deliver a whole flick in one or two moves — that is what a
807
- fast thumb looks like to the main thread;
808
- - **arc**: lean the first points off-axis the way a thumb does, and only then
809
- commit to the axis;
810
- - **interleave**: start the second gesture while the first one's travel — or
811
- its momentum tail — is still playing, and sweep the delay between the two;
812
- - **for a wheel**: a swipe is a ramp, a peak and a decaying tail at frame
813
- rate, and the interesting cases live in the tail — a second swipe over it, a
814
- cross-axis scroll into it, decay jitter that must not step.
815
-
816
- The reading side has traps of its own:
817
-
818
- - coordinates measured off an element that is mid-travel are stale by dispatch
819
- time — aim at where the CONTAINER is, never at where the content was;
820
- - a scripted gesture's wall-clock is diluted by driver round-trips, so
821
- measured velocities come out below the numbers written in the script — read
822
- the measured ones, not the intended ones;
823
- - what the real browser does with an unclaimed touch — take it for a pan and
824
- cancel the stream, withhold the click — is reproducible without a device, but
825
- only through two paths: Chrome's mobile touch emulator (headed, driven with
826
- mouse events) and Safari in the iOS simulator driven by `safaridriver`. The
827
- obvious one, Playwright's `touchscreen` / CDP `Input.dispatchTouchEvent`,
828
- runs Chrome's desktop slop (15px), where a travel deciding at 10px always
829
- wins and the race that loses swipes on a phone never shows. The recipe is in
830
- [mobile_touch.md](./mobile_touch.md#verifying-without-a-device). What stays a
831
- device question: a real finger's lift, a real device's event timing, a
832
- momentum tail killed by a landing finger (and the Firefox wheel limitation
833
- above is the same kind of fact).
834
-
835
- What to read while it runs: the `navi_drag_*` events (grab, start, release)
836
- say which stage a press reached and with what velocity; the box's state
837
- attribute (`data-slide-current`, the URL) says what came of it; the track's
838
- measured position, sampled across frames, says what the screen did in
839
- between — including the mini-movements and stalls no end-state check ever
840
- sees.