@jsenv/navi 0.29.459 → 0.29.461
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 +5618 -5051
- package/dist/dev/jsenv_navi.js.map +80 -79
- package/dist/jsenv_navi.js +5626 -5059
- package/dist/jsenv_navi.js.map +80 -79
- package/docs/AI_INSTRUCTIONS.md +2 -1
- package/docs/actions.md +18 -2
- package/docs/dynamic_import.md +4 -0
- package/docs/mobile_touch.md +56 -0
- package/docs/navigation.md +48 -4
- package/docs/resource.md +21 -0
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -58,7 +58,8 @@ start when unsure which export solves a problem.
|
|
|
58
58
|
|
|
59
59
|
- `navigation.md` — the position of the user belongs in the URL by default:
|
|
60
60
|
routes and which branch renders, search params, redirections, the back arrow
|
|
61
|
-
(`navBack`), where a navigation lands (a `#id` for an element
|
|
61
|
+
(`navBack`), where a navigation lands (a `#id` for an element; a page not
|
|
62
|
+
whole yet holds its position with `useHoldScrollRestoration`), a
|
|
62
63
|
`SlideContainer` read from the URL, what a layer drawn over the screen may say
|
|
63
64
|
in its address. Before any routing code.
|
|
64
65
|
- `route_transitions.md` — a transition states a relation between two pages;
|
package/docs/actions.md
CHANGED
|
@@ -202,8 +202,9 @@ this one cannot start before the component that draws it exists — a render lat
|
|
|
202
202
|
at best, a gesture late in a popup. It is for a parameter chosen inside the
|
|
203
203
|
component and dying with it. Outside a component,
|
|
204
204
|
`actionRunEffect(action, () => …)` is that same run declared once — it runs on
|
|
205
|
-
the first truthy params, reruns when they change
|
|
206
|
-
and the
|
|
205
|
+
the first truthy params, reruns when they change (unless the action declared
|
|
206
|
+
`keep` and still holds the answer for the new ones), aborts when they go
|
|
207
|
+
false — and the machinery `routeAction` is built on.
|
|
207
208
|
|
|
208
209
|
**Which of the two it is, is decided by the parameter, not by what draws it.**
|
|
209
210
|
If the parameter is in the address — a path param, a search param bound to a
|
|
@@ -222,6 +223,21 @@ decided to write, whatever its parameter, and `{ run: true }` says it — a toke
|
|
|
222
223
|
a share button must already hold when pressed, for instance, since the OS share
|
|
223
224
|
sheet only opens inside the gesture, never after an `await`.
|
|
224
225
|
|
|
226
|
+
**A head start is `{ prerun: true }`.** When the params are known before the
|
|
227
|
+
component owning the read exists — a card already says which chapter a book
|
|
228
|
+
has, the book page draws its pages once the book is there — a component
|
|
229
|
+
drawing nothing can send the read ahead, on the instance the owner will read:
|
|
230
|
+
|
|
231
|
+
```jsx
|
|
232
|
+
// draws nothing: the pages leave with the book instead of after it
|
|
233
|
+
useAsyncData(chapterPagesAction, { prerun: true });
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
It needs no `<Loading>`, and its failure reaches no boundary: nothing on screen
|
|
237
|
+
asked for a prerun, so the owner's `{ run: true }` asks again, once, when it
|
|
238
|
+
reads it. A failure of the owner's own run is shown, and asked again by a
|
|
239
|
+
`rerun()`, never by a render. A head start still running is the owner's wait.
|
|
240
|
+
|
|
225
241
|
`{ onLoad }` is what the screen does with the data **once, when it becomes
|
|
226
242
|
known** — seed the fields someone is about to edit, focus something, remember
|
|
227
243
|
where a list was:
|
package/docs/dynamic_import.md
CHANGED
|
@@ -68,6 +68,10 @@ Everything the data layer decides then holds for the code:
|
|
|
68
68
|
`activeRouteReadsSignal`, which leaves the code out: rerun, it would take a
|
|
69
69
|
page reading it delegated off the screen for a frame.
|
|
70
70
|
- **Fetched ahead**, on intent ([below](#ahead-of-the-render-intent)).
|
|
71
|
+
- **A reload lands where the reader was.** The scroll position waits for the
|
|
72
|
+
code and the render that draws the page, rather than being written into the
|
|
73
|
+
pending screen and clamped to its height
|
|
74
|
+
([navigation.md](./navigation.md#a-page-that-is-not-whole-yet)).
|
|
71
75
|
|
|
72
76
|
## A component inside a page that stays: who asks for its code
|
|
73
77
|
|
package/docs/mobile_touch.md
CHANGED
|
@@ -17,6 +17,7 @@ changes, measure again before trusting a number here.
|
|
|
17
17
|
- [The browser decides about 8px in, and does not wait](#the-browser-decides-about-8px-in-and-does-not-wait)
|
|
18
18
|
- [touch-action stops at the first scroller](#touch-action-stops-at-the-first-scroller)
|
|
19
19
|
- [Only the start is contested](#only-the-start-is-contested)
|
|
20
|
+
- [A rounded clip makes the scroll wait for the main thread](#a-rounded-clip-makes-the-scroll-wait-for-the-main-thread)
|
|
20
21
|
- [The click is the browser's call as much as ours](#the-click-is-the-browsers-call-as-much-as-ours)
|
|
21
22
|
- [A tap dropped after a touch drag](#a-tap-dropped-after-a-touch-drag)
|
|
22
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)
|
|
@@ -122,6 +123,48 @@ it back:
|
|
|
122
123
|
A gesture that has won the start keeps the touch. What loses gestures is the
|
|
123
124
|
first ~8px.
|
|
124
125
|
|
|
126
|
+
## A rounded clip makes the scroll wait for the main thread
|
|
127
|
+
|
|
128
|
+
Before it scrolls under a finger, Chrome's compositor finds the scroller under
|
|
129
|
+
it. Where it cannot tell on its own, it asks the main thread, and the scroll
|
|
130
|
+
starts when the main thread answers — after whatever task is running there.
|
|
131
|
+
Meanwhile the finger keeps moving: the content holds still, then catches up in
|
|
132
|
+
one step. A small scroll is mostly its start, so that is where it is felt. The
|
|
133
|
+
wheel asks the same way on a desktop, where the main thread is rarely busy at
|
|
134
|
+
that moment.
|
|
135
|
+
|
|
136
|
+
Two things make Chrome ask (Chrome 155, mobile emulation; not measured in
|
|
137
|
+
Safari):
|
|
138
|
+
|
|
139
|
+
- **A rounded clip on the scroller or around it**: `overflow` other than
|
|
140
|
+
`visible` with a `border-radius`, or a rounded `clip-path`. Every scroll
|
|
141
|
+
start inside it is asked, wherever the finger lands. A popup that scrolls is
|
|
142
|
+
one: a rounded `overflow: auto` box.
|
|
143
|
+
- **Anything painted over the scroller**, even with `pointer-events: none` and
|
|
144
|
+
square corners (an overlay drawing an edge above the items, say), but only
|
|
145
|
+
where it covers it: a 6px band along the top leaves the rest decided on the
|
|
146
|
+
compositor.
|
|
147
|
+
|
|
148
|
+
The wait costs whatever runs at the touch: 0.1–2ms when nothing does; with a
|
|
149
|
+
render built on the touch at 6× CPU, 7.5ms median and up to 27ms, and a scroll
|
|
150
|
+
started a frame late one time in three. So navi keeps both away from what
|
|
151
|
+
scrolls. A framed `List` does not clip to its rounded corners: its edge stays
|
|
152
|
+
under the items, and one with a background covers the inside of the curve. A
|
|
153
|
+
picker popup whose list scrolls itself does not clip either. And a finger on
|
|
154
|
+
something that opens builds nothing before it has been held ~80ms. Wrapping a
|
|
155
|
+
`List` in a rounded box of your own with `overflow: hidden` brings the wait
|
|
156
|
+
back.
|
|
157
|
+
|
|
158
|
+
A box that stops clipping needs `min-height: 0` if it is a flex or grid item
|
|
159
|
+
holding the scroller: without a clip its minimum height is its content's, so a
|
|
160
|
+
box given a height grows to all its items and the page scrolls instead. Nothing
|
|
161
|
+
waits for the main thread then, because the wrong thing scrolls — check that
|
|
162
|
+
the scroller's own `scrollTop` moved.
|
|
163
|
+
|
|
164
|
+
Reference: `.navi_list_container` in `src/control/list/list.jsx`, the
|
|
165
|
+
`[data-picker-popup]` rules in `src/control/picker/picker_custom.jsx`,
|
|
166
|
+
`TOUCH_WARM_HOLD_MS` in `src/layout/popup_content_mount.js`.
|
|
167
|
+
|
|
125
168
|
## The click is the browser's call as much as ours
|
|
126
169
|
|
|
127
170
|
- **Chrome** clicks after a touch that moved up to 8px and never beyond, whether
|
|
@@ -221,6 +264,19 @@ configuration: "mobile" })`, then `Input.dispatchMouseEvent` with
|
|
|
221
264
|
through the desktop configuration instead: a 15px slop. Anything deciding at
|
|
222
265
|
10px wins there, and the race that loses swipes on a phone is invisible.
|
|
223
266
|
|
|
267
|
+
Where a scroll's start is decided shows only in a trace, never in the page's
|
|
268
|
+
events: `Tracing.start` with `input`, `cc` and `disabled-by-default-cc.debug`.
|
|
269
|
+
After the last `InputHandlerProxy::HandleTouchStart`, a
|
|
270
|
+
`WidgetInputHandlerManager::FindScrollTargetOnMainThread` is the scroller looked
|
|
271
|
+
up on the main thread, the gap between the two
|
|
272
|
+
`InputHandlerProxy::HandleGestureScrollBegin` is how long that took, and a
|
|
273
|
+
`delta_y` larger than one move on the first
|
|
274
|
+
`InputHandler::ScrollLatchedScroller` says the first step merged two moves.
|
|
275
|
+
Not under Playwright: its injected script puts a non-passive `touchstart`
|
|
276
|
+
listener on `window`, which makes every touch blocking and changes the path
|
|
277
|
+
being measured. Start Chrome by hand (`--headless=new
|
|
278
|
+
--remote-debugging-port`) and drive the page's websocket directly.
|
|
279
|
+
|
|
224
280
|
**Safari**: the iOS simulator, driven by `safaridriver`, which turns W3C touch
|
|
225
281
|
actions into real UIKit touches — Safari's own pan recognizers compete for
|
|
226
282
|
them exactly as on a phone:
|
package/docs/navigation.md
CHANGED
|
@@ -16,6 +16,7 @@ linking to them, and turning them into tabs.
|
|
|
16
16
|
- [The back arrow: `navBack`](#the-back-arrow-navback)
|
|
17
17
|
- [Tabs that travel: `RouteTravel`](#tabs-that-travel-routetravel)
|
|
18
18
|
- [Where a navigation lands: the scroll](#where-a-navigation-lands-the-scroll)
|
|
19
|
+
- [A page that is not whole yet](#a-page-that-is-not-whole-yet)
|
|
19
20
|
- [Landing on an element: the fragment](#landing-on-an-element-the-fragment)
|
|
20
21
|
- [Creating something, then editing it](#creating-something-then-editing-it)
|
|
21
22
|
- [Tabs that are not routes](#tabs-that-are-not-routes)
|
|
@@ -481,7 +482,8 @@ Five cases, and they are not a policy to configure but five different facts:
|
|
|
481
482
|
- **Replacing the entry** (`<Link replace>`, `route.redirectTo()`, a param
|
|
482
483
|
settling) moves nothing: the same place said differently — except in a row of
|
|
483
484
|
tabs, below.
|
|
484
|
-
- **A reload** lands where one was, as it would have without navi
|
|
485
|
+
- **A reload** lands where one was, as it would have without navi, once the
|
|
486
|
+
page is there ([below](#a-page-that-is-not-whole-yet)).
|
|
485
487
|
|
|
486
488
|
Where the browser exposes its stack (the Navigation API), **a `<Link>` whose
|
|
487
489
|
destination is the entry right next to the current one becomes a real
|
|
@@ -490,6 +492,47 @@ A, B, A, B… growth, and the scroll comes back — and one step forward too. On
|
|
|
490
492
|
towards entries of this document, and never when the push carries explicit
|
|
491
493
|
state. An arrow that must ALWAYS behave as a back is `navBack()`.
|
|
492
494
|
|
|
495
|
+
### A page that is not whole yet
|
|
496
|
+
|
|
497
|
+
A position is put back into whatever the page shows at that moment. When that is
|
|
498
|
+
a stand-in — a pending screen, a `<Loading>` fallback, a list of three skeletons
|
|
499
|
+
— the document is shorter than the page the position was taken on, the offset is
|
|
500
|
+
clamped to it, and the reader lands at the top of a page that arrives a moment
|
|
501
|
+
later.
|
|
502
|
+
|
|
503
|
+
**After a reload, navi waits for the route actions** that the first render had
|
|
504
|
+
nothing to draw for: the page's code arriving on its own
|
|
505
|
+
([dynamic_import.md](./dynamic_import.md)), a read the previous document did not
|
|
506
|
+
keep ([`keepPageOnScreen`](./resource.md#keeppageonscreen-the-page-on-screen-drawn-again-by-the-next-document) draws a kept one at once, and that one
|
|
507
|
+
is not waited for). The position is put back once they have answered and the
|
|
508
|
+
page has rendered. When one of them fails, nothing is written: the screen saying
|
|
509
|
+
so is not the page the position was taken on, and the position stays for the
|
|
510
|
+
reload that brings it back. What it costs: a slow read that every page asks for
|
|
511
|
+
delays the position of every page, since nothing tells navi which reads set the
|
|
512
|
+
height.
|
|
513
|
+
|
|
514
|
+
**What navi cannot see, the page says**: a read it starts itself
|
|
515
|
+
(`useAsyncData(…, { run: true })`), content whose height arrives late. While
|
|
516
|
+
`useHoldScrollRestoration(held)` is given `true`, the position of a reload or of
|
|
517
|
+
a back/forward waits; it is put back after the render that turns it `false`:
|
|
518
|
+
|
|
519
|
+
```jsx
|
|
520
|
+
const [games] = useAsyncData(GAMES, { loading: true, run: true });
|
|
521
|
+
useHoldScrollRestoration(games === undefined);
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
A read that fails leaves it `true`, and nothing is written, for the same reason
|
|
525
|
+
as above. A row of tabs is not held: it gives a tab its position as it travels,
|
|
526
|
+
and measures right after (see [below](#a-row-of-tabs-where-a-replace-is-an-arrival)).
|
|
527
|
+
|
|
528
|
+
**The reader taking over cancels it.** A finger, a click, a wheel or a key
|
|
529
|
+
before the position is written, and it is not written: moved afterwards, they
|
|
530
|
+
would lose whatever they had started on. Going elsewhere cancels it too.
|
|
531
|
+
|
|
532
|
+
A `<List>` that opens on an item is the opposite case, and needs nothing: it
|
|
533
|
+
places the document itself, by measuring the item, and the url's offset is not
|
|
534
|
+
put back over it ([scroll.md](./scroll.md#where-the-list-opens-and-where-it-is)).
|
|
535
|
+
|
|
493
536
|
### Landing on an element: the fragment
|
|
494
537
|
|
|
495
538
|
A link meant to bring one element under the reader's eyes — a notification
|
|
@@ -564,9 +607,10 @@ the viewport may scroll, and a row inside a scroller of its own leaves that
|
|
|
564
607
|
offset alone. An `overflow: hidden` on any ancestor is enough to take the row out
|
|
565
608
|
of the document's scrollport — it holds an offset even without a scrollbar, where
|
|
566
609
|
`overflow: clip` holds none — so it is the first thing to look for when a row
|
|
567
|
-
does not give positions back. And a
|
|
568
|
-
loading is not tall enough when its position is put back:
|
|
569
|
-
|
|
610
|
+
does not give positions back. And a tab whose height depends on something still
|
|
611
|
+
loading is not tall enough when its position is put back: the row does not wait
|
|
612
|
+
for it ([A page that is not whole yet](#a-page-that-is-not-whole-yet) is for a
|
|
613
|
+
reload and a back). What navi itself builds after the first frame
|
|
570
614
|
(`<Box mount="after-paint">`, see
|
|
571
615
|
[scroll.md](./scroll.md#many-sections-box-mountafter-paint)) is not such a
|
|
572
616
|
page: the part the position shows is built before it is written.
|
package/docs/resource.md
CHANGED
|
@@ -613,6 +613,27 @@ is inferred — a relation that really is a sub-route of the parent is modelled
|
|
|
613
613
|
with the relationship methods ([above](#relations-and-autorerun)), never
|
|
614
614
|
declared as a dependency.
|
|
615
615
|
|
|
616
|
+
When only some writes change what the reads show — a count of videos moves
|
|
617
|
+
when one is created or deleted, not when a chapter is rated — name them with
|
|
618
|
+
`on`:
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
const SUMMARY = resource("summary", {
|
|
622
|
+
dependencies: [
|
|
623
|
+
BOOK, // every write
|
|
624
|
+
{ resource: VIDEO, on: ["PATCH_MANY", "DELETE"] },
|
|
625
|
+
{ resource: VIDEO_FILE, on: ["POST", "DELETE"] },
|
|
626
|
+
],
|
|
627
|
+
GET: …,
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
`on` names callbacks, not verbs: `PATCH` does not cover `PATCH_MANY` the way it
|
|
632
|
+
does in `rerunOn`, so each write keeps its own answer. A name that is not a
|
|
633
|
+
write, or that the resource does not define, throws at declaration — a
|
|
634
|
+
dependency that never fires leaves a count on its old value with nothing to say
|
|
635
|
+
so.
|
|
636
|
+
|
|
616
637
|
## A function calling the verb, or the instance
|
|
617
638
|
|
|
618
639
|
Both forms are real, and what separates them is narrower than it looks:
|