@jsenv/navi 0.30.5 → 0.30.7

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.
@@ -48,9 +48,22 @@ web does, and these are its rules, not ours:
48
48
 
49
49
  What stays ours:
50
50
 
51
- - **A travel asked for** (a tab, a key, a command, a state) plays in `duration`,
52
- one box long whatever the distance, in `SlideContainer`. In `RouteTravel` it is
53
- the browser's smooth scroll.
51
+ - **A travel asked for** (a tab, a key, a command, a state) is one box long whatever
52
+ the distance: the pages in between are not crossed. In `SlideContainer` it plays in
53
+ `duration`. In `RouteTravel` it is the browser's smooth scroll, which has no
54
+ duration to ask for. A finger landing on it finishes it first.
55
+ - **A press beats a row still moving.** It sets off at once from what is on screen,
56
+ and the row never lands where the hand was sending it. In `SlideContainer`
57
+ `onCurrentChange` hears about the slide pressed and not about that one, and on iOS a
58
+ swipe during that travel is ignored, and the next one works.
59
+ - **What says where one is follows the screen.** While a hand holds the row, one is on
60
+ the page held. Once it has let go, what reads the row reads where it is going:
61
+ `useSlideContainer` (`current`, `can`, `held`), a `Nav`'s tabs, a `StepList`'s steps
62
+ and the chevrons in `SlideContainer`, so Next pressed during a snap counts from there;
63
+ a link's current state and `routeTravelSettledPageSignal` in `RouteTravel`. A trackpad
64
+ has no lift, so the nearest page answers. What the state holds waits for rest: the
65
+ bound signal and `onCurrentChange`, and in `RouteTravel` the address, except where it
66
+ runs ahead to build a page.
54
67
  - **A direction that is refused is the end of the row.** Whatever holds the user
55
68
  where they are (a `<Slide required>` waiting for its answer, a `preventNavNext`)
56
69
  takes what lies beyond off the row, so nothing can be scrolled to. There is no
@@ -231,7 +244,7 @@ A row of screens is a scroll container, and contains itself.
231
244
  | what says which is here | a `signal`, `current` / a command | the URL |
232
245
  | what a screen not on it shows | itself, built or `mount="near"` | itself once visited; before that, see below |
233
246
  | what says the order | the layout map | the `<Route>` children, or `routes` |
234
- | a travel asked for | plays in `duration`, one box long | the browser's smooth scroll |
247
+ | a travel asked for | plays in `duration`, one box long | the browser's smooth scroll, one box long |
235
248
 
236
249
  A `RouteTravel` page not visited yet can only be built by the address moving, since
237
250
  a route's actions ask only while it matches. So the address moves once the hand has
@@ -9,8 +9,8 @@ fixed, they differ between engines, and **none of them shows with a mouse**. A
9
9
  gesture can pass a whole session of desktop testing and still fail one swipe
10
10
  in three on a phone.
11
11
 
12
- What follows is measured, not remembered: Chromium 153 through its mobile touch
13
- emulator, and Safari on the iOS 26.5 simulator driven with real touches (see
12
+ What follows is measured, not remembered: Chromium 153 and Chrome 156 through the
13
+ mobile touch emulator, and Safari on the iOS 26.5 simulator driven with real touches (see
14
14
  [Verifying without a device](#verifying-without-a-device)). When an engine
15
15
  changes, measure again before trusting a number here.
16
16
 
@@ -22,6 +22,7 @@ changes, measure again before trusting a number here.
22
22
  - [A tap dropped after a touch drag](#a-tap-dropped-after-a-touch-drag)
23
23
  - [A finger is captured to what it touched, inside shadow trees too](#a-finger-is-captured-to-what-it-touched-inside-shadow-trees-too)
24
24
  - [A finger never makes a wheel event](#a-finger-never-makes-a-wheel-event)
25
+ - [A snap keeps running after the finger](#a-snap-keeps-running-after-the-finger)
25
26
  - [Verifying without a device](#verifying-without-a-device)
26
27
 
27
28
  ## The browser decides about 8px in, and does not wait
@@ -229,6 +230,11 @@ where it lands, never asked for: on a touch press, a capture announced on the
229
230
  press's own target (compared from the same tree) is the browser's, and a gesture
230
231
  taking the press onto that same element has to say so another way.
231
232
 
233
+ The rest of the touch goes there too, **even once that element has left the document**:
234
+ a page re-rendering under the finger takes it out, and its `touchend` is dispatched on
235
+ the detached node and reaches no ancestor. Nothing on the page around it hears the
236
+ lift. What has to hear it listens on `event.target` from the `touchstart`.
237
+
232
238
  Measured in Chromium 153 (`Input.dispatchTouchEvent`: the capture does not
233
239
  depend on the slop) and on an Android phone (Chrome 153); not in Safari.
234
240
  Reference: `implicitCaptureHolder` and `takePress` in `@jsenv/dom`'s
@@ -243,6 +249,51 @@ paths differ in timing: a wheel gesture is claimed and refused on its very first
243
249
  event, so the browser never gets a moment to decide, while a finger has to
244
250
  travel a few pixels before anything can be read.
245
251
 
252
+ ## A snap keeps running after the finger
253
+
254
+ A row with `scroll-snap-type: mandatory` goes on moving once the finger has lifted:
255
+ about 600ms in Chrome, about 700ms in Safari, slowing all the way, so the last 300ms
256
+ cover barely 40px of a 390px box. A press on something outside the row arrives during
257
+ that time and gets its click, in Safari too. What it does to the row depends on the
258
+ engine, because neither plays that movement on the main thread:
259
+
260
+ | Asked of a row mid-snap | Chrome | Safari on iOS |
261
+ | --------------------------------- | --------------------------------------------------------------------- | --------------------------- |
262
+ | Instant `scrollTo` | Ignored: the snap goes on and overwrites it | Stops it |
263
+ | Smooth `scrollTo` | Replaces it until it ends: a short trip gets the snap back, and loses | Not measured |
264
+ | `scroll-snap-type: none` | Ignored | Jumps to the target at once |
265
+ | `overflow: hidden`, held | Moves nothing, but the snap resumes when it is taken back | Stops it |
266
+ | `overflow: clip`, held two frames | Stops it: there is no scroller left to play it on | Stops it |
267
+ | `display` changed for one layout | Stops it (see below) | Stops it (see below) |
268
+
269
+ A change undone in the same task reaches nothing in Chrome, whatever the property,
270
+ since the compositor never hears of it: only a `display` change acts that fast. It
271
+ rebuilds the box, and it costs: `display: none` starts every CSS animation and
272
+ transition inside over, `display: block` brings a scroller inside back to its top
273
+ unless what the row holds keeps its size for that layout, and WebKit (desktop, under a
274
+ synthetic wheel) then ignores the next write to the position and loses a scroller's
275
+ own position inside.
276
+
277
+ On Safari the last three rows were measured with a write following them, and a write
278
+ alone is enough, so they say nothing of their own there.
279
+
280
+ `scrollend` says little here: in Chrome a write gets one at once, with the row still
281
+ moving. A row moved by other means with its snap left running ends worse than late:
282
+ with a track animated over it, tens of pixels off any slide in Chrome, half a box off
283
+ in Safari, and it stays there. Anything that moves a row on a press has to end the snap
284
+ first.
285
+
286
+ A box given `overflow: clip` is no scroller until it is given back, and that has its
287
+ own cost: **a touch landing meanwhile starts no scroll in Safari**, even when the page
288
+ gives the scroller back on `pointerdown`, since the scroll view does not exist yet when
289
+ the finger lands. Chrome takes that swipe. Given back, the row stands where it stood
290
+ with nothing written, but for the rest of that task WebKit still starts from 0: a smooth
291
+ scroll to 0 sees nothing to do, and the row stays where it is, off any page. Ask a frame
292
+ later.
293
+
294
+ Firefox is not in this table: Playwright's synthetic wheel never leaves its row
295
+ mid-snap, so it takes real input, events posted through the OS (`CGEvent` on macOS).
296
+
246
297
  ## Verifying without a device
247
298
 
248
299
  Both engines can show the browser's own answers — the pan, the
@@ -262,6 +313,10 @@ configuration: "mobile" })`, then `Input.dispatchMouseEvent` with
262
313
  - those calls are **never acknowledged**: send them without awaiting, with a
263
314
  sleep between moves (~16ms is a frame).
264
315
 
316
+ A tap through this path waits about 300ms for a possible double tap, under
317
+ `isMobile` too, so a press meant for a moment in a movement lands 300ms after it, at
318
+ the end of a snap. `touch-action: manipulation` on what is pressed removes the wait.
319
+
265
320
  `Input.dispatchTouchEvent` — what Playwright's `touchscreen` uses — goes
266
321
  through the desktop configuration instead: a 15px slop. Anything deciding at
267
322
  10px wins there, and the race that loses swipes on a phone is invisible.
@@ -304,9 +359,13 @@ its momentum included (over 3 s: flings closer than that add up). The window
304
359
  has to be on screen in the current Space: when it is not, activating the
305
360
  Simulator does not bring it, and the events land in whatever window is under
306
361
  the pointer — check which window is topmost at the points of the drag before
307
- sending any, and wait for the Space to switch after activating. What the page
308
- records is read back through a request it sends itself (the page runs on a
309
- local server), since nothing can query it.
362
+ sending any, and wait for the Space to switch after activating. On screen is not
363
+ on top: behind another app's window, activating does not raise it either, System
364
+ Events does (`set frontmost to true`, then `AXRaise`), and another app can come back
365
+ over it within seconds, so raise it and check right before each gesture, and again
366
+ after any pause inside one. A tap works the same way, and reaches other elements
367
+ during momentum. What the page records is read back through a request it sends itself
368
+ (the page runs on a local server), since nothing can query it.
310
369
 
311
370
  What to synthesize is the hand's imperfection — a gesture simulated as clean,
312
371
  evenly spaced points passes forever and proves nothing: a slow start landing a
@@ -613,9 +613,10 @@ const shownId = settledPage ? settledPage.params.gameId : currentId;
613
613
 
614
614
  It holds the `{ route, params }` the hand set off from, and is `null` from the moment
615
615
  a lifted finger leaves the row moving, the URL being the truth again: the row is only
616
- playing out what the browser decided. A trackpad has no lift, so there it lasts until
617
- the row has come to rest, and a tab already built waits for that too. A tab pressed
618
- holds nothing, and `null` is all it ever reads. One signal for the document: on a
616
+ playing out what the browser decided. A trackpad has no lift, so there it holds the
617
+ page nearest the row until the row has come to rest, and the URL of a tab already built
618
+ waits for that too. A tab pressed holds nothing, and `null` is all it ever reads, even
619
+ over a row a hand has left moving: the press wins at once. One signal for the document: on a
619
620
  page with several boxes, `route` says which row it is.
620
621
 
621
622
  `RouteTravel` is for pages that form a ROW the finger can push; pages related
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.30.5",
3
+ "version": "0.30.7",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {