@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.
- package/dist/dev/jsenv_navi.js +532 -77
- package/dist/dev/jsenv_navi.js.map +10 -9
- package/dist/jsenv_navi.js +532 -77
- package/dist/jsenv_navi.js.map +10 -9
- package/docs/drag_to_travel.md +15 -1
- package/docs/mobile_touch.md +64 -5
- package/docs/navigation.md +54 -3
- package/package.json +1 -1
package/docs/drag_to_travel.md
CHANGED
|
@@ -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.
|
|
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
|
package/docs/mobile_touch.md
CHANGED
|
@@ -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
|
|
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.
|
|
308
|
-
|
|
309
|
-
|
|
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
|
package/docs/navigation.md
CHANGED
|
@@ -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
|
|
594
|
-
the row has come to rest, and a tab already built
|
|
595
|
-
holds nothing, and `null` is all it ever reads
|
|
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
|