@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.
- package/README.md +1 -1
- package/dist/dev/jsenv_navi.js +584 -196
- package/dist/dev/jsenv_navi.js.map +35 -35
- package/dist/jsenv_navi.js +563 -187
- package/dist/jsenv_navi.js.map +35 -35
- package/docs/AI_INSTRUCTIONS.md +177 -261
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +169 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +39 -43
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +115 -174
- package/docs/drag_to_travel.md +10 -9
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +66 -81
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +6 -4
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -482
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -335
- package/docs/state_binding.md +2 -1
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/list_action.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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 (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 —
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
package/docs/list_refresh.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
`data === undefined
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
`renderItem` gets `{ refreshing }` —
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
`
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
[route_transitions.md](./route_transitions.md))
|
|
161
|
-
rendering hold for the one frame the browser needs to photograph it
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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`
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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`) |
|
|
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
|
|
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)
|
package/docs/mobile_touch.md
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
`
|
|
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
|