@jsenv/navi 0.29.415 → 0.29.417

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.
@@ -79,7 +79,10 @@ to start when unsure which export solves a problem.
79
79
  map; a pair of routes is animated by `RouteTravel` or by a transition, never
80
80
  both; a test waits for the page arriving, never for its address; the page
81
81
  being left stays mounted, hidden, until the movement is over (so what it
82
- registers globally must let the last arrival win); what a transition costs.
82
+ registers globally must let the last arrival win, and an action it reads
83
+ goes through `useAsyncData`, which leaves a page no longer shown alone —
84
+ `.value` on the action's signals re-renders it hidden); what a transition
85
+ costs.
83
86
  Read before animating a navigation, before writing a custom movement, and
84
87
  before believing a symptom that only appears once a pair is animated.
85
88
  - `view_transitions.md` — what the browser does to any transition an app
@@ -221,9 +224,10 @@ to start when unsure which export solves a problem.
221
224
  - `mobile_touch.md` — what Chrome and Safari do with a finger a gesture also
222
225
  reads, measured: they commit it to their own pan ~8px in (read a finger's
223
226
  intent before that), `touch-action` restarts at every scroller, when the
224
- click survives, the tap Chrome drops after a touch drag, and how to reproduce
225
- all of it without a device (Chrome's mobile touch emulator, Safari in the iOS
226
- simulator). Read before reading a finger yourself, and before concluding a
227
+ click survives, the tap Chrome drops after a touch drag, the capture every
228
+ finger lands with (which `hasPointerCapture` misses on a field), and how to
229
+ reproduce all of it without a device (Chrome's mobile touch emulator, Safari
230
+ in the iOS simulator). Read before reading a finger yourself, and before concluding a
227
231
  gesture works because it works with a mouse.
228
232
 
229
233
  ### Layout, CSS and text
@@ -19,6 +19,7 @@ changes, measure again before trusting a number here.
19
19
  - [Only the start is contested](#only-the-start-is-contested)
20
20
  - [The click is the browser's call as much as ours](#the-click-is-the-browsers-call-as-much-as-ours)
21
21
  - [A tap dropped after a touch drag](#a-tap-dropped-after-a-touch-drag)
22
+ - [A finger is captured to what it touched, inside shadow trees too](#a-finger-is-captured-to-what-it-touched-inside-shadow-trees-too)
22
23
  - [A finger never makes a wheel event](#a-finger-never-makes-a-wheel-event)
23
24
  - [Verifying without a device](#verifying-without-a-device)
24
25
 
@@ -160,6 +161,40 @@ Chromium's `gesture_provider.cc` (`ignore_single_tap_`, reset on the next down)
160
161
  and `tap_suppression_controller.cc` (the tap that stops a fling, 180ms) for the
161
162
  two mechanisms that do NOT explain it.
162
163
 
164
+ ## A finger is captured to what it touched, inside shadow trees too
165
+
166
+ A touch pointer is captured the moment it lands, by the browser, to the element
167
+ it landed on — nobody calls `setPointerCapture`. A mouse and a pen are not.
168
+ That capture is announced like any other, with a `gotpointercapture` just
169
+ before the first pointer event that follows, and a finger resting still sends
170
+ one soon: its contact changes, so `pointermove`s with unchanged coordinates
171
+ come within the first 100ms or so. Nearly every touch press therefore sees a
172
+ `gotpointercapture` it never asked for. Code that reads it as "another gesture
173
+ took this press" gives up on almost every finger.
174
+
175
+ Recognising that capture is where Chrome misleads: **it goes to the deepest
176
+ element under the finger, shadow trees included.** On an `<input>` or a
177
+ `<textarea>` that is the inner editor, inside the field's own shadow tree; in a
178
+ web component, whatever its shadow root holds there. `hasPointerCapture` on
179
+ what the page sees — the field, the host — answers `false` at `pointerdown`,
180
+ after `gotpointercapture`, and for the whole press. Yet the `gotpointercapture`
181
+ reaches the page retargeted to that same element. An open shadow root shows
182
+ both halves: the host answers `false`, the element inside it `true`.
183
+
184
+ So the browser's capture is recognised by where it lands, never asked for: on
185
+ a touch press, a capture announced on the press's own target is the browser's,
186
+ whenever it arrives. Compare both events from the same tree: each is
187
+ retargeted to the shadow host from outside. A gesture that takes the press onto
188
+ that same element cannot be told apart from it, so it has to say so another way.
189
+
190
+ Measured in Chromium 153 through `Input.dispatchTouchEvent`. The capture does
191
+ not depend on the slop, so the desktop path shows it; see
192
+ [Verifying without a device](#verifying-without-a-device). The same `false` on
193
+ a field shows on an Android phone (Chrome 153). Not measured in Safari.
194
+
195
+ Reference: `implicitCaptureHolder` and `takePress` in `@jsenv/dom`'s
196
+ `press_held.js`.
197
+
163
198
  ## A finger never makes a wheel event
164
199
 
165
200
  No mobile browser turns a touch into `wheel` events: those come from a mouse
package/docs/resource.md CHANGED
@@ -292,9 +292,26 @@ Keep their **ids**, not the objects: an object is a snapshot, and it stops
292
292
  following its row the moment anything writes to it. `RESOURCE.useAllByIds(ids)`
293
293
  reads those rows as they are now — a kept id turned back into something that
294
294
  stays fresh, and the ids the store no longer holds simply drop out.
295
- `RESOURCE.useById(id)` is the singular, `RESOURCE.useArray()` the whole store;
296
- all three subscribe the render that calls them, and none is a hook (a `.map()`
297
- over them is fine).
295
+ `RESOURCE.useById(id)` is the singular, `RESOURCE.useArray()` the whole store.
296
+ None is a hook (a `.map()` over them is fine), and each subscribes the render
297
+ that calls it to what it returns: `useById` and `useAllByIds` to the rows they
298
+ hand back — a card reading its own row stays still while other rows of the
299
+ store are written, and re-renders when its row is — `useArray` to the whole
300
+ store. An entry of `useAllByIds` given by a unique key rather than by id can
301
+ only be found by reading the whole store, and that read follows it too.
302
+
303
+ A row counts as written only when something in it changed:
304
+
305
+ - **An answer** — what a callback returns — is compared to the row held in
306
+ depth. Parsed JSON makes every object nested in a row a new one; a
307
+ revalidation returning the same rows keeps every one of them, and their
308
+ readers stay still.
309
+ - **A relation** changes its row when the row points at other children. What
310
+ happens to the children themselves is followed through the relation:
311
+ `game.place.label` read in a render re-renders it when the place is written,
312
+ and the game row stays the one it was.
313
+ - **A value written with `store.upsert()`** is compared by reference: a new
314
+ object is a change, whatever it holds.
298
315
 
299
316
  ## Relations: pick one of the four methods
300
317
 
@@ -552,9 +552,11 @@ Measured on that page, first frame of the movement: 63 → 45 ms at full speed,
552
552
  What that means for a page and for the application around it:
553
553
 
554
554
  - **It is still mounted for the length of the movement.** Its effects keep
555
- running, and it still reads the address: whatever re-renders when the URL
556
- changes re-renders in it too, hidden (see
557
- [What a transition costs](#what-a-transition-costs)).
555
+ running, and it still reads the address: a component reading the URL, a
556
+ route or a signal directly re-renders in it too, hidden (see
557
+ [What a transition costs](#what-a-transition-costs)). What it reads through
558
+ `useAsyncData` does not: navi leaves a page alone as soon as its container
559
+ would no longer show it.
558
560
  - **It is taken down after the page arriving is up.** A cleanup that puts back
559
561
  what it found — a document title, a class on `<body>`, a value in a shared
560
562
  store — puts a stale value back over the new page's. Such a registration is
@@ -643,10 +645,30 @@ elements):
643
645
  at a route (`<Link route>`, `<Button route>`) reads one flag — whether it is
644
646
  current — and stays still unless that flips: a profile opening leaves the
645
647
  links to a hundred other profiles exactly as they were (on the list of
646
- thirty cards: 90 re-renders gone, first frame 58 → 41 ms). What else reacts
647
- is worth keeping to what has to — measured: every card of the page being
648
- left re-rendering when the page arriving loads its own games into the store
649
- the cards read theirs from.
648
+ thirty cards: 90 re-renders gone, first frame 58 → 41 ms). A row read with
649
+ `RESOURCE.useById` follows that row only, so the page arriving loading rows
650
+ of its own into the same store leaves the cards of the page being left as
651
+ they were. What moves last is the page's own reading of its route's action
652
+ — its params, its data and its state all change as the route stops
653
+ matching. `useAsyncData` gives no render to a page its container no longer
654
+ shows: the page is about to be taken down, or hidden until the movement is
655
+ over (on the list of thirty cards: about 450 component renders gone from the
656
+ callback, first frame 10–20 ms sooner at 4× CPU, nothing measurable at full
657
+ speed). A component reading the action's signals itself
658
+ (`action.dataSignal.value`) does not get that, and re-renders in the hidden
659
+ page.
660
+ - **Restyling the page arriving more than once.** Built inside the update
661
+ callback, it is styled as it lands; anything written afterwards that its
662
+ elements inherit, or that re-selects them, restyles all of them again. On the
663
+ list of thirty cards (~7,000 elements) three did, 16–19 ms each at full
664
+ speed: the height of a list group's sticky label, written on the group once
665
+ measured (navi keeps it from being inherited, which took the movement 17 ms
666
+ sooner); the room a fixed bar takes (`--navi-fixed-bar-space-*`, feeding the
667
+ inherited `--navi-safe-area-inset-*`), which changes on the root when a bar
668
+ arrives or leaves with the page; and an app's `:has()` rule on the list
669
+ container over its rows (`.navi_list_container:has(.card) …`), evaluated
670
+ again as the rows land. An attribute the app sets on the container says the
671
+ same thing as that `:has()`, for nothing.
650
672
  - **Re-rendering what did not change, on the press itself.** A component that
651
673
  only wants a yes or a no reads a computed flag, not the signal it is derived
652
674
  from: a list whose rows each read the count of runs in flight re-rendered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.415",
3
+ "version": "0.29.417",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "preact": "^11.0.0-rc.2"
35
35
  },
36
36
  "dependencies": {
37
- "@jsenv/dom": "0.17.83",
37
+ "@jsenv/dom": "0.17.84",
38
38
  "@jsenv/humanize": "1.8.6",
39
39
  "@jsenv/validity": "0.4.5"
40
40
  },