@jsenv/navi 0.30.4 → 0.30.6

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.
@@ -51,6 +51,18 @@ What stays ours:
51
51
  - **A travel asked for** (a tab, a key, a command, a state) plays in `duration`,
52
52
  one box long whatever the distance, in `SlideContainer`. In `RouteTravel` it is
53
53
  the browser's smooth scroll.
54
+ - **A press beats a row still moving.** It sets off at once from what is on screen,
55
+ and the row never lands where the hand was sending it. In `SlideContainer`
56
+ `onCurrentChange` hears about the slide pressed and not about that one, and on iOS a
57
+ swipe during that travel is ignored, and the next one works.
58
+ - **What says where one is follows the screen.** While a hand holds the row, one is on
59
+ the page held. Once it has let go, what reads the row reads where it is going:
60
+ `useSlideContainer` (`current`, `can`, `held`), a `Nav`'s tabs, a `StepList`'s steps
61
+ and the chevrons in `SlideContainer`, so Next pressed during a snap counts from there;
62
+ a link's current state and `routeTravelSettledPageSignal` in `RouteTravel`. A trackpad
63
+ has no lift, so the nearest page answers. What the state holds waits for rest: the
64
+ bound signal and `onCurrentChange`, and in `RouteTravel` the address, except where it
65
+ runs ahead to build a page.
54
66
  - **A direction that is refused is the end of the row.** Whatever holds the user
55
67
  where they are (a `<Slide required>` waiting for its answer, a `preventNavNext`)
56
68
  takes what lies beyond off the row, so nothing can be scrolled to. There is no
@@ -235,7 +247,9 @@ A row of screens is a scroll container, and contains itself.
235
247
 
236
248
  A `RouteTravel` page not visited yet can only be built by the address moving, since
237
249
  a route's actions ask only while it matches. So the address moves once the hand has
238
- really pulled, 16px, and the page fills in under the finger. See
250
+ really pulled, 16px, and the page fills in under the finger. Before that its pane
251
+ is empty and the box's background shows, unless `renderNeighbour` says what stands
252
+ there. See
239
253
  [navigation.md](./navigation.md#tabs-that-travel-routetravel).
240
254
 
241
255
  Both tell what follows them how far the row has come. `SlideContainer` writes
@@ -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
@@ -569,6 +569,29 @@ given again around the page on screen as the reader moves through the list: it i
569
569
  taken once the row has come to rest, and nothing is seen moving for it. With fewer
570
570
  than two pages, the box is not a scroller at all.
571
571
 
572
+ **A neighbour not there yet is empty until the hand has pulled**, a page of a window
573
+ or a tab never visited, and it is the box's background one sees there.
574
+ `renderNeighbour` says what stands in its place: the page's background, its
575
+ skeleton, the name the list already holds.
576
+
577
+ ```jsx
578
+ <RouteTravel
579
+ routes={players.map(({ id, name }) => ({
580
+ route: PLAYER_ROUTE,
581
+ params: { id },
582
+ name,
583
+ }))}
584
+ renderNeighbour={(page) => <PlayerSkeleton name={page.name} />}
585
+ >
586
+ <PlayerPage />
587
+ </RouteTravel>
588
+ ```
589
+
590
+ It stands on each side of the page on screen, already painted when a finger lands,
591
+ since the browser moves the row whatever the main thread is doing. So it renders
592
+ with the page, and is worth keeping light. It is not the page: its route does not
593
+ match yet, so it asks for nothing.
594
+
572
595
  **The box has a height, or the document scrolls.** Pages may scroll themselves in a
573
596
  box the page gives a height to. If the document is what scrolls, it goes on doing
574
597
  so, and a page brought in stands where the reader is looking; what is lost is
@@ -590,9 +613,10 @@ const shownId = settledPage ? settledPage.params.gameId : currentId;
590
613
 
591
614
  It holds the `{ route, params }` the hand set off from, and is `null` from the moment
592
615
  a lifted finger leaves the row moving, the URL being the truth again: the row is only
593
- playing out what the browser decided. A trackpad has no lift, so there it lasts until
594
- the row has come to rest, and a tab already built waits for that too. A tab pressed
595
- 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
596
620
  page with several boxes, `route` says which row it is.
597
621
 
598
622
  `RouteTravel` is for pages that form a ROW the finger can push; pages related
@@ -1063,6 +1087,33 @@ is shown. A popup's `value` answers another one, WHICH of several popups is open
1063
1087
  (one sheet per card under a single `?seat=<gameId>`), not what varies inside one
1064
1088
  (see [popup_open.md](./popup_open.md#signal--the-app-holds-it-both-ways)).
1065
1089
 
1090
+ **What a tab reads is asked for while the layer is open, not while the tab is
1091
+ shown.** Under a hand the signal is only written once the row has landed, and a
1092
+ neighbour is on screen from the first pixel. So a read whose params depend on the
1093
+ signal asks after the travel, and its slide travels without it. `mount="near"` keeps
1094
+ the slide built, not what it reads. Ask from the opening, and let the slide read its
1095
+ question, as
1096
+ [a popup that loads data](./popup_open.md#a-popup-that-loads-data) does:
1097
+
1098
+ ```js
1099
+ // asked while the layer is open, whichever tab is on screen
1100
+ export const LINKED_ACCOUNTS = routeAction(ANY_PAGE, ACCOUNT.GET_MANY, () => {
1101
+ return settingsSignal.value;
1102
+ });
1103
+ // read by the slide, on screen, sliding away, or with the layer closing
1104
+ export const LINKED_ACCOUNTS_SHOWN = ACCOUNT.GET_MANY.bindParams(true);
1105
+ ```
1106
+
1107
+ Read through the route action, a slide loses its data as soon as there is
1108
+ [nothing to ask](./actions.md#nothing-to-ask-and-coming-back): while the layer
1109
+ closes, or as soon as the tab is left if the params follow it. The question standing,
1110
+ reopening finds the answer and sends nothing, unless the action's `keep` says that
1111
+ answer is too old.
1112
+
1113
+ The cost is one request per read at each first opening, for tabs that may never be
1114
+ looked at. navi does not tell an application which slides a hand can bring in, so a
1115
+ row too heavy to ask for everything has no finer answer than this.
1116
+
1066
1117
  ### A link inside the layer leads to a page
1067
1118
 
1068
1119
  The terms of use behind an About dialog, a profile behind a name in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.30.4",
3
+ "version": "0.30.6",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {