@jsenv/navi 0.29.418 → 0.29.420

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +584 -196
  3. package/dist/dev/jsenv_navi.js.map +35 -35
  4. package/dist/jsenv_navi.js +563 -187
  5. package/dist/jsenv_navi.js.map +35 -35
  6. package/docs/AI_INSTRUCTIONS.md +177 -261
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +169 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +39 -43
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +115 -174
  16. package/docs/drag_to_travel.md +10 -9
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +66 -81
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +6 -4
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -482
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -335
  37. package/docs/state_binding.md +2 -1
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -29,8 +29,7 @@ refuses in the meantime, which is what you want when the rows are alternatives.
29
29
  ```
30
30
 
31
31
  A press on another row while it runs is refused and says so, in the selection's
32
- own words ("le choix est en cours d'enregistrement", "la sélection…" in a
33
- `multiple` list) rather than in a row's.
32
+ own words rather than in a row's.
34
33
 
35
34
  ## On a row's control: that row waits alone
36
35
 
@@ -61,22 +60,20 @@ stay live. Two rows can be in flight at once.
61
60
  ```
62
61
 
63
62
  The command is what marks the row, and it runs **only if the action succeeded**
64
- — a button given both runs its action first and lets its command follow (see
65
- [actions.md](./actions.md#a-press-that-opens-something-and-waits-for-the-answer)). So a refused invitation leaves the row unmarked
66
- without anything to undo.
67
-
68
- `selectableArea="manual"` is about the row's own surface, not about the button:
69
- a control inside a row answers its own press whichever area the row claims.
70
- What it removes is the row selecting under a press that lands beside the button
71
- — here what marks the row is the command, once the invitation went through.
63
+ (see [actions.md](./actions.md#a-press-that-opens-something-and-waits-for-the-answer)),
64
+ so a refused invitation leaves the row unmarked without anything to undo.
65
+ `selectableArea="manual"` keeps the row from selecting under a press that lands
66
+ beside the button — here what marks the row is the command, once the invitation
67
+ went through; a control inside a row answers its own press whichever area the
68
+ row claims.
72
69
 
73
70
  ## Who draws the wait, and who answers a press
74
71
 
75
72
  **Whatever control is on the row carries the wait and the refusal.** A button,
76
73
  a checkbox, a picker — it has a place to draw a spinner and an anchor to hang a
77
- callout on, and it is what the user pressed. The row itself is the fallback,
78
- for a row that holds nothing but text: it draws a loading outline, swallows
79
- presses (its buttons included) and opens the sentence itself.
74
+ callout on, and it is what the user pressed. The row itself is the fallback, for
75
+ a row that holds nothing but text: it draws a loading outline, swallows presses
76
+ (its buttons included) and opens the sentence itself.
80
77
 
81
78
  That is why a selectable row does not answer a press twice. It swallows the
82
79
  press so nothing inside it acts, then asks the control it carries to explain —
@@ -95,13 +92,8 @@ belongs on the button; pressed on the row's own surface, on the row:
95
92
  | the button of a row held back by something else | the button |
96
93
 
97
94
  The last one needs nothing special: that row is not blocked, so the press
98
- reaches the button and the button's own gate answers it.
99
-
100
- A row that would rather always take it says so once:
101
-
102
- ```jsx
103
- <List.Item data-callout-anchor="item" />
104
- ```
95
+ reaches the button and the button's own gate answers it. A row that would rather
96
+ always take it says so once: `<List.Item data-callout-anchor="item" />`.
105
97
 
106
98
  ## Read-only, not loading
107
99
 
@@ -113,10 +105,10 @@ use, and what says that is `readOnly`:
113
105
  ```
114
106
 
115
107
  `loading` on a row means the ROW is waiting on something — being added to the
116
- list, removed from it, saved where it is (see the `loading` values on
117
- `List.Item`). Putting it there for a control's run draws a second wait next to
118
- the one the button is already drawing, and says the row is being changed when it
119
- is only being used.
108
+ list, removed from it, saved where it is (the `loading` values of `List.Item`).
109
+ Putting it there for a control's run draws a second wait next to the one the
110
+ button is already drawing, and says the row is being changed when it is only
111
+ being used.
120
112
 
121
113
  Give the row a `readOnlyMessage` when it is held for a reason that will pass:
122
114
  the default sentence is about availability ("this option is not available"),
@@ -125,45 +117,38 @@ which is the wrong thing to say about a wait.
125
117
  ## Hold the rows, not the list
126
118
 
127
119
  It is tempting to write `<List readOnly={pending}>` to keep a second row from
128
- being pressed while one is in flight. It works, and it works by luck: the
129
- command the button fires on success (`--navi-select`) is aimed at the list, and
130
- a list still read-only at that moment refuses it — the call goes through and the
131
- row is never marked, silently.
120
+ being pressed while one is in flight, and it works by luck: the command the
121
+ button fires on success (`--navi-select`) is aimed at the list, and a list still
122
+ read-only at that moment refuses it — the call goes through and the row is never
123
+ marked, silently. Whether it lands comes down to one tick (the flag cleared in a
124
+ `finally`, or a moment later), and nothing on screen says which you wrote.
132
125
 
133
- Whether it lands comes down to one tick. Clearing the flag in a `finally` clears
134
- it before the command runs, so the selection arrives; clearing it a moment later
135
- — a transition, a debounce, one more `await` on the way out — loses it. Nothing
136
- on screen says which of the two you wrote.
137
-
138
- Hold the rows instead. `readOnly` on every row is what "one at a time" looks
139
- like, it says the same thing on screen, and it leaves the list free to take the
140
- answer whenever the answer comes:
126
+ Hold the rows instead: `readOnly` on every row says the same thing on screen and
127
+ leaves the list free to take the answer whenever the answer comes.
141
128
 
142
129
  ```jsx
143
130
  <List.Item readOnly={pending !== null} /> // all of them: one at a time
144
131
  <List.Item readOnly={pending.includes(name)} /> // this one: each on its own
145
132
  ```
146
133
 
147
- ## How many at once: `parallelGuard`
134
+ One at a time also needs no state of the app's: `<List parallelGuard={1}>`
135
+ holds every control that would start a run while one is out (below).
148
136
 
149
- A list is a place where the same gesture is available many times over, and
150
- nothing about a row's own action stops someone from starting one on every row it
151
- draws — a dozen requests in flight because the list happened to be long.
137
+ ## How many at once: `parallelGuard`
152
138
 
153
- So a list allows **four runs at once by default**. While that many are out,
154
- every control in it that would start another run goes read-only and says how
155
- many it is waiting on; the next press is possible again the moment one comes
156
- back.
139
+ Nothing about a row's own action stops someone from starting one on every row a
140
+ list draws — a dozen requests in flight because the list happened to be long. So
141
+ a list allows **four runs at once by default**. While that many are out, every
142
+ control in it that would start another run goes read-only and says how many it
143
+ is waiting on; the next press is possible again the moment one comes back.
157
144
 
158
145
  ```jsx
159
146
  <List parallelGuard={2}> // stricter
160
147
  <List parallelGuard={Infinity}> // lifted, without taking it out of the tree
161
148
  ```
162
149
 
163
- It counts runs, not values — which is the whole difference from `maxLengthGuard`
164
- next to it: that one says how many things the selection may HOLD, this one how
165
- many may be HAPPENING. A list can carry either, or both, and they refuse for
166
- unrelated reasons.
167
-
168
- Only a control that would start a run is held: a row with nothing to run, or a
169
- button that merely fires a command, never waits its turn.
150
+ It counts runs, not values — the whole difference from `maxLengthGuard`, which
151
+ says how many things the selection may HOLD, where this one says how many may be
152
+ HAPPENING. A list can carry either, or both, and they refuse for unrelated
153
+ reasons. Only a control that would start a run is held: a row with nothing to
154
+ run, or a button that merely fires a command, never waits its turn.
@@ -2,15 +2,15 @@
2
2
 
3
3
  When a write touches one item of a list, two questions decide what the user
4
4
  sees: **what goes back to the network**, and **what stays on screen meanwhile**.
5
- Getting either wrong turns "pause one row" into a full page reload.
6
-
7
- The same two questions decide what a list does when the user leaves it and
8
- comes back, which is the second half of this file.
5
+ Getting either wrong turns "pause one row" into a full page reload. The same two
6
+ questions decide what a list does when the user leaves it and comes back, which
7
+ is the second half of this file.
9
8
 
10
9
  The short answer:
11
10
 
12
- - A write that returns the modified item fixes every list containing it, with
13
- no request and no loading state at all.
11
+ - A write that returns the modified item fixes every list read through an action
12
+ that contains it, with no request and no loading state at all — and, in a
13
+ `<List.Items>` over `GET_RANGE`, every row reading its item with `useById`.
14
14
  - `useAsyncData(action, { loading: true })` keeps returning the previous data
15
15
  while the action re-runs — the list is never taken away unless the component
16
16
  throws it away.
@@ -20,24 +20,17 @@ The short answer:
20
20
  ## `loading: true` returns the previous value
21
21
 
22
22
  During a re-run, `useAsyncData(action, { loading: true })` hands back the
23
- **previous** data with `loading` up — never `undefined`, which is the first
24
- load only, for a `GET_MANY` as for anything else — so the emptiness test is
25
- `data === undefined`, and a list is never blanked for a checkbox ticked on one
26
- of its rows. Read `loading` as "what you are displaying is from before", not
27
- "there is nothing to display". The four combinations of `data` and `loading`,
28
- and what each one draws, are in [data_states.md](./data_states.md).
23
+ **previous** data with `loading` up. Read `loading` as "what you are displaying
24
+ is from before", never as "there is nothing to display": the emptiness test is
25
+ `data === undefined`
26
+ ([data_states.md](./data_states.md#data-and-loading-are-independent)), and a
27
+ refresh that failed comes back beside the rows, not in place of them
28
+ ([data_states.md](./data_states.md#an-error-is-a-message-on-the-screen-not-the-screen)).
29
29
 
30
30
  `<List loading>` is the first-load answer, not the refresh one: it replaces the
31
31
  rows with skeletons. Pass it while stale rows exist and they disappear — same
32
32
  mistake as `loading ? null :`, one level down.
33
33
 
34
- A refresh that failed does not unmake the rows either. With `error: true` the
35
- failure comes back **beside** them, so the list stays and the failure is said
36
- over it — a strip above the rows, a retry, `dismissError()` to close the strip
37
- without asking anything again. Taking the rows away is `<ErrorBoundary>`'s job,
38
- and only when the screen genuinely cannot be drawn — see
39
- [data_states.md](./data_states.md#an-error-is-a-message-on-the-screen-not-the-screen).
40
-
41
34
  ## What updates without a request
42
35
 
43
36
  An action's `data` is a computation over the resource store, not a snapshot of
@@ -50,22 +43,25 @@ The condition is the whole hinge of the system:
50
43
  > **the write's callback must return the item, with its key** — `{ id, … }`.
51
44
 
52
45
  Partial props are fine (`{ id, paused: true }` merges into the stored item); the
53
- key is what cannot be missing. The corollary matters just as much: a callback
54
- that returns nothing — a `204`, or a `fetch` whose result is dropped — writes
55
- nothing to the store, and the change never reaches the screen. There, the only
56
- way back is a re-read.
46
+ key is what cannot be missing. A callback that returns nothing — a `204`, or a
47
+ `fetch` whose result is dropped — is refused with a `TypeError` naming the verb
48
+ (`game.PATCH must return an object…`), and nothing reaches the store. For a
49
+ `204`, return what was written yourself: `{ id, ...props }`.
57
50
 
58
51
  `DELETE` is symmetric: returning the id drops the item from the store, and every
59
52
  list containing it drops it too.
60
53
 
61
- ## A paginated list stays on screen too
54
+ A `<List.Items>` over `GET_RANGE` keeps the objects it was handed, so its rows
55
+ follow a write only where they read their item from the store with `useById`
56
+ ([resource.md](./resource.md#get_range-feeding-a-list-that-loads-as-it-scrolls)).
62
57
 
63
- A `<List.Items>` reading through `GET_RANGE` draws places in a collection, not
64
- a list of ids, so nothing the store does can fix them: a row that changed tab,
65
- or one that was deleted, moves every row after it one rank up, and only the
66
- collection knows who fills the last place.
58
+ ## A paginated list stays on screen too
67
59
 
68
- It is told, and it re-reads by itself:
60
+ A `<List.Items>` reading through `GET_RANGE` draws places in a collection, not a
61
+ list of ids, so nothing the store does can fix them: a row that changed tab, or
62
+ one that was deleted, moves every row after it one rank up, and only the
63
+ collection knows who fills the last place. It is told, and it re-reads by
64
+ itself:
69
65
 
70
66
  ```jsx
71
67
  <List.Items
@@ -88,11 +84,11 @@ it meanwhile:
88
84
  | answer received | the new rows | — |
89
85
 
90
86
  The rows, the scroll position and the row being read all stay; the slices
91
- outside the window are forgotten only once the answer is in, and asked for
92
- again if the user goes back to them. A re-read that fails leaves the rows from
93
- before on screen. While it is in flight, the list carries `navi-refreshing` and
94
- `renderItem` gets `{ refreshing }` — read it as "what you see is from before",
95
- never as "there is nothing to see".
87
+ outside the window are forgotten only once the answer is in, and asked for again
88
+ if the user goes back to them. A re-read that fails leaves the rows from before
89
+ on screen. While it is in flight, the list carries `navi-refreshing` and
90
+ `renderItem` gets `{ refreshing }` — "what you see is from before", never "there
91
+ is nothing to see".
96
92
 
97
93
  An app that knows a row is on its way out (it is the one deleting it) says so
98
94
  itself: it is the one rendering the row, so it draws it loading, muted, or not
@@ -118,24 +114,19 @@ GAME.GET_RANGE { radar: "R-42" } → { count: 18, byIndex: … }
118
114
 
119
115
  Ids, never rows: the rows are in the store already, shared and live, and a row
120
116
  dropped from the store simply stops resolving — a composition cannot hold a
121
- stale copy of anything.
122
-
123
- A run that finds a composition takes the `refreshing` line of the table above
124
- rather than the loading one: the rows are on screen while it asks again for the
117
+ stale copy of anything. A run that finds a composition takes the `refreshing`
118
+ line of the table above: the rows are on screen while it asks again for the
125
119
  window it draws. So the two lists an app cannot tell apart from the outside —
126
120
  one reading `GET_MANY`, one reading `GET_RANGE` — look the same on the way back:
127
121
  neither blanks, neither shows a first load. What they do behind that is not the
128
- same, and the next section is about exactly that.
129
-
130
- What a composition is about is the **values** its params hold, not the reader
131
- instance: `GET_RANGE.bindParams({ scope: "thread" })` called from two places
132
- reads and writes the same one (and gives back the same reader, memoized the way
133
- an action's `bindParams` is).
122
+ same (next section).
134
123
 
135
- The rest follows the rules already stated: a verb in `rerunOn.GET_RANGE`, or
136
- `reader.invalidate()`, drops the compositions — they stand for an order that is
137
- gone — and `memoryBudget` (1000 ranks by default) trims the ranks far from any
138
- window, which are asked for again if the user goes back to them.
124
+ A composition is about the **values** its params hold, not the reader instance:
125
+ `GET_RANGE.bindParams({ scope: "thread" })` called from two places reads and
126
+ writes the same one. A verb in `rerunOn.GET_RANGE`, or `reader.invalidate()`,
127
+ drops the compositions — they stand for an order that is gone — and
128
+ `memoryBudget` (1000 ranks by default) trims the ranks far from any window,
129
+ which are asked for again if the user goes back to them.
139
130
 
140
131
  ### Who decides the re-read — and who does not
141
132
 
@@ -144,48 +135,36 @@ playing between the two pages: the **source** the list reads through is what
144
135
  answers, and it answers the same way whether the user arrived by a link, by
145
136
  `history.back()`, or under a route transition.
146
137
 
147
- | The list reads through | Coming back to it |
148
- | ------------------------------- | ----------------------------------------------------------------------------------------------------- |
149
- | `routeAction` over `GET_MANY` | **nothing goes out** — the action holds its response, and `.run()` on a `COMPLETED` action is a no-op |
150
- | `<List.Items>` over `GET_RANGE` | **one ask goes out** — the reader kept ranks, not rows, and revalidates the window it draws |
151
-
152
- Both are deliberate, and they are not in tension: an action that kept its answer
153
- has the answer, while a composition is a claim about an order that any write
154
- elsewhere may have made false. What a `GET_MANY` list wants on the way back it
155
- has to say itself — `.rerun()` when the route becomes current again, or a verb
156
- in `rerunOn` if a write is what makes it stale.
157
-
158
- Nothing above changes when `defineRouteTransition` is written for the pair the
159
- list is walked through. A transition states a relation between two pages (see
160
- [route_transitions.md](./route_transitions.md)); it takes the document's
161
- rendering hold for the one frame the browser needs to photograph it, and gives
162
- it back. It never decides what the page arriving is allowed to ask for. Held by
163
- `tests/route_transition_list_revisit/`, which mounts the same app twice — with
164
- and without a relation on the pair — and walks the way back ten times on each,
165
- counting what goes out at every revisit.
166
-
167
- When a list stops refreshing after a transition was added, two things account
168
- for it, in this order:
169
-
170
- 1. **The revisit did not happen.** A back taken before the page being opened was
171
- ever on screen returns to a list that never left, and a page that never left
172
- has nothing to come back from (see
173
- [route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)).
174
- This is what an automated walk does by default, and it is the answer far more
175
- often than the next one.
176
- 2. **The list is on the first row of the table.** A `GET_MANY` list never
177
- refreshed on its own; what changed is whatever else in the app was doing the
178
- re-read.
138
+ | The list reads through | Coming back to it |
139
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `routeAction` over `GET_MANY` | **nothing goes out** — the action holds its response, and `.run()` on a `COMPLETED` action is a no-op; only `.rerun()` goes back to the network |
141
+ | `<List.Items>` over `GET_RANGE` | **one ask goes out** — the reader kept ranks, not rows, and revalidates the window it draws |
142
+
143
+ Both are deliberate: an action that kept its answer has the answer, while a
144
+ composition is a claim about an order that any write elsewhere may have made
145
+ false. What a `GET_MANY` list wants on the way back it has to say itself —
146
+ `.rerun()` when the route becomes current again, or a verb in `rerunOn` if a
147
+ write is what makes it stale.
148
+
149
+ A `defineRouteTransition` written for the pair changes none of this: a
150
+ transition states a relation between two pages (see
151
+ [route_transitions.md](./route_transitions.md)) and takes the document's
152
+ rendering hold for the one frame the browser needs to photograph it; it never
153
+ decides what the page arriving may ask for. A list that stops refreshing after a
154
+ transition was added most often saw no revisit at all — a back taken before the
155
+ page being opened was ever on screen returns to a list that never left
156
+ ([route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page));
157
+ otherwise it is a `GET_MANY` list, which never refreshed on its own.
179
158
 
180
159
  ### Watching what the run asks for
181
160
 
182
161
  A run that decides **not** to ask is invisible from the application's side: it
183
162
  sends nothing and changes no state, so the network is silent and
184
- `onRequestStateChange` — which reports what a request is doing — has no request
185
- to report. A run that declined and a run that was never mounted look identical.
186
- `debugScroll` is where that difference is visible; it carries the render window
187
- and the run's asking, which are one subject. It logs in navi's development build
188
- only.
163
+ `onRequestStateChange` has no request to report — a run that declined and a run
164
+ that was never mounted look identical. `debugScroll` is where that difference is
165
+ visible, in navi's development build only. Record into an array rather than
166
+ printing: the sink is called during rendering, and a `console.log` in the way
167
+ can move a timing-sensitive symptom.
189
168
 
190
169
  ```jsx
191
170
  window.askLog = [];
@@ -194,7 +173,8 @@ window.askLog = [];
194
173
  </NaviDebug>;
195
174
  ```
196
175
 
197
- One line per pass of the run, whatever the outcome:
176
+ One line per pass of the run, whatever the outcome — so no line for a revisit
177
+ means the run never rendered:
198
178
 
199
179
  ```
200
180
  ask 0-49: sent (revalidating=true holdPending=false count=412)
@@ -211,26 +191,11 @@ ask 0-49: held on a row not reached yet (revalidating=true holdPending=true coun
211
191
  | `a request still covers this window` | what is in flight is still what the list would draw |
212
192
  | `this range was asked for already` | asking again could only produce the same answer |
213
193
 
214
- The state that decides is on the line rather than left to be inferred:
215
-
216
- - **`revalidating`** — the run knows what it holds is from before. A revisit
217
- showing `revalidating=false` never restored a composition, which is a
218
- different problem from one showing `revalidating=true` and no `sent`.
219
- - **`holdPending`** — the list is held somewhere it has not reached.
220
- - **`count`** — how many rows the run stands for, `undefined` before its first
221
- answer.
222
-
223
- **Record, do not print.** The sink is called during rendering: push into an
224
- array. Formatting an object in a console costs far more than what it measures,
225
- and a timing-sensitive symptom moves under one — a list that fails to refresh on
226
- the first revisit can start refreshing on the first two as soon as a
227
- `console.log` is in the way, which makes the log a report about the log.
228
-
229
- An absence in the trace is a fact too: no line for a revisit means the run never
230
- rendered, which is about the list being mounted, not about what it asked for.
231
- The usual cause is a revisit that never happened — a back taken before the page
232
- being opened was ever on screen, which returns to a list that never left (see
233
- [route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)).
194
+ The state that decides is on the line: `revalidating` — the run knows what it
195
+ holds is from before, so a revisit showing `revalidating=false` never restored a
196
+ composition, a different problem from `revalidating=true` and no `sent`;
197
+ `holdPending` — the list is held somewhere it has not reached; `count` — how
198
+ many rows the run stands for, `undefined` before its first answer.
234
199
 
235
200
  ## `rerunOn`, verb by verb
236
201
 
@@ -270,7 +235,7 @@ updated item — not client-side refreshing.
270
235
  | a field of one item | no — the write's response is enough |
271
236
  | membership of the list | yes (`POST`) — the backend decides who belongs |
272
237
  | the ORDER of the list | yes — the store stores, it does not sort (see below) |
273
- | nothing came back (`204`) | yes — there is nothing to put in the store |
238
+ | nothing came back (`204`) | no — the callback returns `{ id, ...props }` itself |
274
239
  | a place in a paginated list | yes — a `GET_RANGE` reads places, and places shift |
275
240
 
276
241
  ## The store stores, it does not sort
@@ -286,17 +251,10 @@ comes back in the old order on the next visit. After a reorder, re-read the list
286
251
  explicitly:
287
252
 
288
253
  ```js
289
- await REORDER_ACTION.run({ … });
254
+ await REORDER_ACTION({ … });
290
255
  GET_MANY_ACTION.rerun();
291
256
  ```
292
257
 
293
- ## `.rerun()`, not `.run()`, to refresh
294
-
295
- `.run()` on an action that already `COMPLETED` does nothing: it is a request to
296
- have the data, and the data is there. Wiring a "check now" button to `.run()`
297
- therefore checks nothing, silently. Use `.rerun()`, which resets the action and
298
- runs it again.
299
-
300
258
  ## See also
301
259
 
302
260
  - [route_transitions.md](./route_transitions.md#waiting-for-a-navigation-the-address-is-not-the-page)
@@ -45,26 +45,43 @@ The two engines reach that moment differently:
45
45
  them from any point before about 9px keeps the whole touch for the page, and
46
46
  so does refusing the first one alone: native scrolling never starts, even
47
47
  when the later ones are left alone. If the one at about 9px goes unrefused,
48
- Safari pans. It does not wait for the page's
49
- answer to a report that jumps over that distance in one go: after a small
50
- unrefused report, a report that jumps past 9px is already too late.
48
+ Safari pans. It does not wait for the page's answer to a report that jumps
49
+ over that distance in one go: after a small unrefused report, a report that
50
+ jumps past 9px is already too late.
51
51
 
52
52
  So **a gesture that shares a surface with the page's pan has to read its
53
53
  intent before about 8px**, and measure it as a distance (`Math.hypot`), not per
54
54
  axis: the browser measures its slop as a distance, and a per-axis threshold is
55
55
  up to 1.4 times later on a diagonal. Waiting for more evidence is not
56
56
  available, because whatever is read after the browser's moment is read about
57
- a pointer that has already been cancelled. The travel reads a finger's intent
58
- at 6px for exactly this reason (`DRAG_START_THRESHOLD_TOUCH` in
59
- `@jsenv/dom`'s `drag_to_travel.js`). Its bias toward the box's own axis only
60
- means something because it is read before the browser reads the same pixels
61
- evenly.
57
+ a pointer that has already been cancelled. It is why a travel reads a finger at
58
+ 6px, leaning towards its own axis before the browser reads the same pixels
59
+ evenly (see [drag_to_travel.md](./drag_to_travel.md#what-the-rules-are)).
62
60
 
63
61
  Whether a `touchmove` can be refused at all is settled even earlier, when the
64
- finger lands: a non-passive listener must already be on the touch's path (see
65
- `keepTouchRefusable`, and
66
- [drag_to_travel.md](./drag_to_travel.md#on-a-touchscreen-the-browser-takes-the-gesture-unless-it-is-refused)).
67
- Being refusable is decided at the touchstart; being refused, before 8px.
62
+ finger lands: the non-passive listener that will refuse must already be on the
63
+ touch's path. Registered at the `pointerdown`, it is handed events that are
64
+ `cancelable: false`, and its `preventDefault` does nothing, silently. A refusal
65
+ that only comes after a wait — a long press turning into a carry — also needs
66
+ an explicit `touch-action` other than `auto` on the region (`pan-y` still lets
67
+ the page scroll): left at `auto`, Chrome has taken the touch by the time the
68
+ wait is over. Being refusable is decided at the touchstart; being refused,
69
+ before 8px.
70
+
71
+ The passive-by-default intervention covers `touchmove` listeners on `window`,
72
+ `document` and `document.body`: one added there says `{ passive: false }`, while
73
+ on any other element a plain listener — a framework's event prop included — is
74
+ non-passive already (navi's travelling boxes render
75
+ `onTouchMove={keepTouchRefusable}`, from @jsenv/dom). And a touch keeps being
76
+ dispatched at the node it started on, even once that node has left the
77
+ document, where nothing above it hears it, the window included: a gesture that
78
+ may replace the DOM under the finger (a page that travels navigates) listens on
79
+ that node too.
80
+
81
+ Missed, the listener costs the middle of a gesture, not its start: the gesture
82
+ takes the press, then the thumb's arc bends towards the axis `touch-action`
83
+ leaves to the page, the browser starts its own scroll and cancels the pointer,
84
+ and what the hand was carrying snaps back under a finger still down.
68
85
 
69
86
  ## touch-action stops at the first scroller
70
87
 
@@ -112,6 +129,10 @@ first ~8px.
112
129
  - **Safari** does not click at all once a `touchmove` has been refused, even
113
130
  after 6px. A touch that was never refused still clicks: an unrefused sideways
114
131
  movement over `pan-y` clicks even after 20px.
132
+ - **Two taps** in the same place, 120ms apart, give two `pointerup`, ONE `click`
133
+ and no `dblclick` at all (measured on a phone): the browser keeps the double
134
+ tap for its own zoom. Whatever means "twice" under a finger counts presses —
135
+ see [interactions.md](./interactions.md#twice-whichever-hand-it-is).
115
136
 
116
137
  A gesture that reads its intent early therefore costs the click of a tap that
117
138
  shook more than its threshold. Safari drops that click on its own, and on
@@ -125,7 +146,7 @@ On Chrome for Android, the tap that follows a finger-driven drag does not fire
125
146
  its `click` when it comes quickly: `pointerdown` and `pointerup` arrive, the
126
147
  synthesized `click` (and the `mousedown`/`mouseup` compat events) does not. Wait
127
148
  about half a second and the same tap works. It is what makes a wheel feel
128
- broken — spin it, tap "Définir" at once, and the command never runs.
149
+ broken: a wheel spun, then a button tapped at once, and the command never runs.
129
150
 
130
151
  It is the browser's, not ours: a bare `div` moved by hand under `pointermove`,
131
152
  with no framework, reproduces it. An un-prevented touch drag feeds Chromium's
@@ -141,57 +162,30 @@ active, from a non-passive listener. `touchmove` rather than `touchstart`: the
141
162
  move is the event whose default the drag actually owns, and preventing
142
163
  `touchstart` also suppresses the surface's own focus and synthesized events.
143
164
 
144
- ```js
145
- const onTouchMove = (e) => {
146
- if (drag) {
147
- e.preventDefault();
148
- }
149
- };
150
- viewport.addEventListener("touchmove", onTouchMove, { passive: false });
151
- ```
152
-
153
- Two theories were tried first and disproven on the device, so they are not
154
- worth trying again: pointer capture on touch, and a momentum re-render moving
155
- the element under the tap.
165
+ Two theories were disproven on the device, not worth trying again: pointer
166
+ capture on touch, and a momentum re-render moving the element under the tap.
156
167
 
157
168
  Reference: `onTouchMove` in `src/control/wheel/wheel.jsx`; the reductions, one
158
169
  variable per row, in `src/control/demos/lab/` (`tap_after_drag_experiment.html`,
159
- `surface_css_matrix_experiment.html`, `preventdefault_matrix_experiment.html`);
160
- Chromium's `gesture_provider.cc` (`ignore_single_tap_`, reset on the next down)
161
- and `tap_suppression_controller.cc` (the tap that stops a fling, 180ms) for the
162
- two mechanisms that do NOT explain it.
170
+ `surface_css_matrix_experiment.html`, `preventdefault_matrix_experiment.html`).
163
171
 
164
172
  ## A finger is captured to what it touched, inside shadow trees too
165
173
 
166
174
  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
-
175
+ it landed on (a mouse and a pen are not), and a finger resting still announces
176
+ it within about 100ms: nearly every touch press sees a `gotpointercapture` it
177
+ never asked for. Code reading it as "another gesture took this press" gives up
178
+ on almost every finger. Chrome gives that capture to **the deepest element under
179
+ the finger, shadow trees included** — the inner editor of an `<input>`, whatever
180
+ a web component's shadow root holds there: `hasPointerCapture` on the field or
181
+ the host answers `false` for the whole press, while the `gotpointercapture`
182
+ reaches the page retargeted to it. So the browser's capture is recognised by
183
+ where it lands, never asked for: on a touch press, a capture announced on the
184
+ press's own target (compared from the same tree) is the browser's, and a gesture
185
+ taking the press onto that same element has to say so another way.
186
+
187
+ Measured in Chromium 153 (`Input.dispatchTouchEvent`: the capture does not
188
+ depend on the slop) and on an Android phone (Chrome 153); not in Safari.
195
189
  Reference: `implicitCaptureHolder` and `takePress` in `@jsenv/dom`'s
196
190
  `press_held.js`.
197
191
 
@@ -243,11 +237,11 @@ them exactly as on a phone:
243
237
  and a restarted `safaridriver`. The page's `localStorage` does not survive
244
238
  that, so whatever must be read is read before the swipe.
245
239
 
246
- What to synthesize is the hand's imperfection, as
247
- [drag_to_travel.md](./drag_to_travel.md#verifying-a-gesture) says: a slow start
248
- landing a report between 6 and 9px, steep first pixels, a report that jumps. What
249
- to read: the `touchmove`s with `cancelable` and `defaultPrevented`, the
250
- `pointercancel`, `scrollY`, and what the gesture itself says (for a travel,
240
+ What to synthesize is the hand's imperfection — a gesture simulated as clean,
241
+ evenly spaced points passes forever and proves nothing: a slow start landing a
242
+ report between 6 and 9px, steep first pixels, a report that jumps. What to read:
243
+ the `touchmove`s with `cancelable` and `defaultPrevented`, the `pointercancel`,
244
+ `scrollY`, and what the gesture itself says (for a travel,
251
245
  `data-drag-travel-walking` on `:root` and the slide that ends up current).
252
246
 
253
247
  What neither path shows: the lift of a real finger (the last reports before