@jsenv/navi 0.29.387 → 0.29.389
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 +4 -4
- package/dist/dev/jsenv_navi.js +556 -392
- package/dist/dev/jsenv_navi.js.map +81 -81
- package/dist/dev/jsenv_navi_side_effects.js +87 -78
- package/dist/dev/jsenv_navi_side_effects.js.map +4 -4
- package/dist/jsenv_navi.js +551 -387
- package/dist/jsenv_navi.js.map +81 -81
- package/dist/jsenv_navi_side_effects.js +87 -78
- package/dist/jsenv_navi_side_effects.js.map +4 -4
- package/docs/AI_INSTRUCTIONS.md +39 -23
- package/docs/actions.md +6 -4
- package/docs/autofocus.md +2 -1
- package/docs/badge_list.md +5 -3
- package/docs/control_group.md +4 -3
- package/docs/control_object.md +1 -1
- package/docs/control_value.md +8 -9
- package/docs/css_architecture.md +16 -14
- package/docs/dialog_shape.md +15 -10
- package/docs/drag_interactions.md +49 -41
- package/docs/drag_to_travel.md +32 -24
- package/docs/dynamic_import.md +7 -5
- package/docs/error_handling.md +31 -30
- package/docs/field_validation.md +35 -6
- package/docs/i18n.md +16 -7
- package/docs/interactions.md +28 -23
- package/docs/list_action.md +8 -5
- package/docs/list_refresh.md +8 -1
- package/docs/mobile_layout_pitfalls.md +6 -5
- package/docs/navigation.md +7 -5
- package/docs/network_policy.md +22 -7
- package/docs/pan_zoom.md +16 -9
- package/docs/popup_backdrop.md +11 -5
- package/docs/popup_lift.md +5 -3
- package/docs/popup_open.md +39 -28
- package/docs/resource.md +31 -20
- package/docs/route_transitions.md +13 -10
- package/docs/safe_area.md +59 -39
- package/docs/scroll.md +11 -9
- package/docs/testid.md +10 -1
- package/docs/typography.md +2 -3
- package/docs/view_transitions.md +9 -9
- package/docs/z_index.md +6 -5
- package/package.json +2 -2
package/docs/error_handling.md
CHANGED
|
@@ -184,12 +184,16 @@ report — window `error`, the jsenv overlay in dev — and that is the right an
|
|
|
184
184
|
for it: nothing had to guess whether someone was waiting.
|
|
185
185
|
|
|
186
186
|
**Some runs have nobody to reject at**, and they say so where they start: one
|
|
187
|
-
started from a signal effect because its params changed, one
|
|
188
|
-
|
|
187
|
+
started from a signal effect because its params changed, one started by the
|
|
188
|
+
render that reads it (`{ run: true }`), one whose failure the control that
|
|
189
|
+
started it already draws, a routing whose result every caller drops.
|
|
189
190
|
They pass through [`runUnwatched()`](../src/action/run_unwatched.js), which takes
|
|
190
|
-
the rejection without hiding the error — it stays in `errorSignal`. That is
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
the rejection without hiding the error — it stays in `errorSignal`. That is
|
|
192
|
+
every place navi swallows one but the prefetch on intent, dropped without a
|
|
193
|
+
word since nothing on screen asked for it and the arrival runs the action again
|
|
194
|
+
([dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)). Taking
|
|
195
|
+
it is what obliges that place to ask the next question: did anything display
|
|
196
|
+
this error? (below).
|
|
193
197
|
|
|
194
198
|
Two things must never become a rejection, and do not. **A navigation**: the
|
|
195
199
|
routing's promise is what the browser calls the navigation itself, and rejecting
|
|
@@ -212,24 +216,19 @@ Whatever displays the error sets `error.__handled_by__` (via
|
|
|
212
216
|
one thing and only one: **this error is on screen somewhere.**
|
|
213
217
|
|
|
214
218
|
It is the same mark the jsenv supervisor reads to keep its dev overlay out of
|
|
215
|
-
the way
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// when an error was handled by an ErrorBoundary we will nonetheless emit an error
|
|
221
|
-
// event on the window object. This is to make up for react compatibility in dev mode
|
|
222
|
-
// and thus make the Next.js dev overlay work.
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Without the mark, an app calmly displaying "you are offline" gets a crash
|
|
226
|
-
overlay thrown over it.
|
|
219
|
+
the way: a window `error` or `unhandledrejection` carrying it is not shown. An
|
|
220
|
+
error on screen can still reach window another way — a run started by hand and
|
|
221
|
+
never awaited — and without the mark an app calmly displaying "you are offline"
|
|
222
|
+
gets a crash overlay thrown over it. navi's own report reads the same mark
|
|
223
|
+
(below).
|
|
227
224
|
|
|
228
225
|
Everything navi ships already marks: `<ErrorBoundary>` when it renders its
|
|
229
|
-
fallback, `<ActionRenderer>`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
fallback, `<ActionRenderer>` when it renders its error branch,
|
|
227
|
+
`useAsyncData({ error: true })` when it hands the error to the component, and a
|
|
228
|
+
run given an `onError` — asking for the error is taking it. Every control with
|
|
229
|
+
an `action` runs it that way, so the control claims the failure whatever it
|
|
230
|
+
then shows: a callout, a boundary (`actionErrorEffect="throw"`), or nothing
|
|
231
|
+
(`"none"`, a mapping returning nothing).
|
|
233
232
|
|
|
234
233
|
## Taken by nobody: the report
|
|
235
234
|
|
|
@@ -240,10 +239,9 @@ code that produced it.
|
|
|
240
239
|
**Being read is enough to call the report off**, displayed or not. Once a render
|
|
241
240
|
has the error, everything that can happen next is already covered without this
|
|
242
241
|
module: it is displayed (and marked), or it is thrown — and a thrown error either
|
|
243
|
-
finds a boundary that displays it, or reaches window on its own, since
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this module cannot see which of those happened.
|
|
242
|
+
finds a boundary that displays it, or reaches window on its own, since preact
|
|
243
|
+
re-throws what no boundary handled. Reporting it here too would be a second
|
|
244
|
+
voice, and the wrong one: this module cannot see which of those happened.
|
|
247
245
|
|
|
248
246
|
**And a render stopped in the middle read nothing after it.** `useAsyncData`
|
|
249
247
|
delegates a failure by throwing it out of the render — that is how the error
|
|
@@ -259,9 +257,11 @@ one, is not covered: nothing tells it apart from an action nobody reads.
|
|
|
259
257
|
|
|
260
258
|
One error is never reported whoever looks at it: a **`NetworkPolicyError`**.
|
|
261
259
|
The app declared the state that produced it and the request never left, so there
|
|
262
|
-
is no bug to point at — it is data a screen shows.
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
is no bug to point at — it is data a screen shows. When one reaches window
|
|
261
|
+
anyway, navi cancels the event, which keeps the browser console and the jsenv
|
|
262
|
+
overlay out of it. A boundary displaying one still leaves a `console.error` line
|
|
263
|
+
in dev: `preact/debug` logs every error a boundary catches, and reads nothing
|
|
264
|
+
that would tell it the error is handled
|
|
265
265
|
([network_policy.md](./network_policy.md)).
|
|
266
266
|
|
|
267
267
|
So what reaches the report is an error **nothing looked at** — an action nobody
|
|
@@ -287,8 +287,9 @@ easy to miss when writing another one:
|
|
|
287
287
|
|
|
288
288
|
- **Mark only what you actually render.** Marking on catch, before knowing
|
|
289
289
|
whether anything is displayed, turns a boundary into a bug swallower: a
|
|
290
|
-
`TypeError`
|
|
291
|
-
|
|
290
|
+
`TypeError` it then re-throws reaches window marked as on screen, so the
|
|
291
|
+
overlay that would have pointed at it stays out of the way — a blank page AND
|
|
292
|
+
a silent one. navi's boundary marks
|
|
292
293
|
just before rendering its fallback, and re-throws untouched when it has no
|
|
293
294
|
fallback — nothing to display means nothing handled.
|
|
294
295
|
- **Reset on the document URL, not only on the rerun.** Re-running the failed
|
package/docs/field_validation.md
CHANGED
|
@@ -25,7 +25,8 @@ a validity rule, and the constraint is only its browser-side caller.
|
|
|
25
25
|
|
|
26
26
|
## Constraints
|
|
27
27
|
|
|
28
|
-
A constraint is `{ name, check(field) }
|
|
28
|
+
A constraint is `{ name, check(field) }` — a bare `check` function is accepted
|
|
29
|
+
too. `check` returns `null` when the value
|
|
29
30
|
passes, or the message to show — a string, or `{ message, target }` when the
|
|
30
31
|
callout belongs on another element than the control itself.
|
|
31
32
|
|
|
@@ -44,7 +45,7 @@ plain HTML.
|
|
|
44
45
|
| ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
45
46
|
| `required`, `pattern` | same | what the platform's attributes mean |
|
|
46
47
|
| `minLength`, `maxLength` | same | a string too short or too long |
|
|
47
|
-
| `min`, `max`, `step` | same | a number, date, time or duration out of range
|
|
48
|
+
| `min`, `max`, `step` | same | a number, date, time or duration out of range, or off its step |
|
|
48
49
|
| `singleSpace` | `data-single-space` | a leading or trailing space, two in a row |
|
|
49
50
|
| `displayable` | `data-displayable` | zalgo, a value showing nothing, blank lines in series, a joiner joining nothing |
|
|
50
51
|
| `maxStackedMarks` | `data-max-stacked-marks` | (parameter of `displayable`) |
|
|
@@ -64,9 +65,34 @@ A boolean switch (`displayable`, `singleSpace`, `noEmoji`) passed as `false` is
|
|
|
64
65
|
off, so `noEmoji={settings.strictNames}` says what it looks like it says. The
|
|
65
66
|
others carry a value — a count, a selector — and are off by being absent.
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
**`singleSpace="autoFix"` corrects the value instead of refusing it.** A text
|
|
69
|
+
field ends with a space more often than anyone means it to — a pause after a
|
|
70
|
+
word, a mobile keyboard after a suggestion, a paste — and a refusal then asks
|
|
71
|
+
the person to find and delete a character they cannot see. With `"autoFix"`,
|
|
72
|
+
the leading and trailing spaces are removed and a run of spaces becomes one:
|
|
73
|
+
|
|
74
|
+
```jsx
|
|
75
|
+
<Input name="title" singleSpace="autoFix" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **At commit, never while typing.** The correction runs when the field is
|
|
79
|
+
left, and before an action reads the value. Correcting on each keystroke would
|
|
80
|
+
eat the space the person is about to follow with a word, so an action run
|
|
81
|
+
from typing (a search, debounced or not) reads the value as typed.
|
|
82
|
+
- **Nothing to say.** The correction is validity's own `SINGLE_SPACE_RULE`, so
|
|
83
|
+
the value always passes the rule: no callout, and a server re-checking with
|
|
84
|
+
`singleSpace: true` accepts what it receives.
|
|
85
|
+
- **It lands everywhere the value lives**: the field, its ui state, the bound
|
|
86
|
+
`signal`. A `disabled` or `readOnly` field is left alone, since its value is
|
|
87
|
+
the app's and not the person's.
|
|
88
|
+
- **`singleSpace` is the only constraint that offers it.** A correction is worth
|
|
89
|
+
it only where it cannot change what the person meant; a `maxLength` silently
|
|
90
|
+
cutting a text would.
|
|
91
|
+
|
|
92
|
+
Most constraints take a `<name>Message` prop to replace their sentence for one
|
|
93
|
+
field (`requiredMessage`, `singleSpaceMessage`, `oneOfMessage`…; `step` and the
|
|
94
|
+
time span constraints have none). To change it everywhere, override the i18n
|
|
95
|
+
key instead — see below.
|
|
70
96
|
|
|
71
97
|
Two props sit beside them and belong to the browser alone, because they act
|
|
72
98
|
before there is a value to validate: `charGuard` blocks a keystroke that is not
|
|
@@ -78,7 +104,10 @@ silently swallowing it.
|
|
|
78
104
|
field can refuse the keystroke with the same knowledge the value is checked
|
|
79
105
|
against: `charGuard="tel"`, `charGuard="slug"`, `charGuard="noEmoji"`. Refusing
|
|
80
106
|
the keystroke and refusing the value are different jobs — the pair
|
|
81
|
-
`maxLengthGuard`/`maxLength` is the same split — and a
|
|
107
|
+
`maxLengthGuard`/`maxLength` is the same split — and a guard brings the value
|
|
108
|
+
check along: `maxLengthGuard` is checked at submit as `maxLength`, `charGuard`
|
|
109
|
+
sets an `Input`'s `pattern` from its class unless one is given. `maxLength`
|
|
110
|
+
alone is the submit-only half.
|
|
82
111
|
|
|
83
112
|
**A guard answers for the gesture, never for what the field already holds.** A
|
|
84
113
|
value can arrive already outside the class or already too long — a
|
package/docs/i18n.md
CHANGED
|
@@ -117,10 +117,17 @@ resolved at registration time — so register `"fr"` before `"fr-CA"`.
|
|
|
117
117
|
### What comes out
|
|
118
118
|
|
|
119
119
|
`i18n(key, values, { lang })` returns the translation with placeholders
|
|
120
|
-
replaced.
|
|
121
|
-
|
|
122
|
-
`fallbackLang` at creation
|
|
123
|
-
|
|
120
|
+
replaced. The language is resolved per key: the preferred languages in order
|
|
121
|
+
(`lang`, or the whole `languagesSignal` list by default), then the
|
|
122
|
+
`fallbackLang` given at creation, and the first one translating the key wins. A
|
|
123
|
+
half-translated language therefore shows its own words where it has them and the
|
|
124
|
+
next language's everywhere else.
|
|
125
|
+
|
|
126
|
+
When none of them translates the key, it comes back as is, so a missing
|
|
127
|
+
translation degrades to readable text rather than blank space or a crash. With
|
|
128
|
+
opaque keys that text is the key's name, which is why an instance with opaque
|
|
129
|
+
keys wants a `fallbackLang` holding every key — `naviI18n` falls back to
|
|
130
|
+
English.
|
|
124
131
|
|
|
125
132
|
`i18n.has(key)` is the way to tell "no translation" apart from "translation that
|
|
126
133
|
happens to equal the key".
|
|
@@ -139,9 +146,11 @@ setPreferredLanguage("fr"); // what this user picked, from a language picker
|
|
|
139
146
|
```
|
|
140
147
|
|
|
141
148
|
`setSupportedLanguages` matters more than it looks: without it, a browser
|
|
142
|
-
preferring German
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
preferring German reads German wherever German exists — navi's time wording,
|
|
150
|
+
every date and number — and the next language it lists, or English, for the
|
|
151
|
+
rest: navi's buttons and messages, the app's own texts. One page, two
|
|
152
|
+
languages. With it, that browser lands on the app's best available language for
|
|
153
|
+
all of them.
|
|
145
154
|
|
|
146
155
|
It decides more than the wording: `<Time>` and the format helpers
|
|
147
156
|
(`formatNumber`, `formatTime`, …) read the same signal, so **a document that never calls it follows the browser,
|
package/docs/interactions.md
CHANGED
|
@@ -104,20 +104,21 @@ which the carrying interactions answer with).
|
|
|
104
104
|
Two holds on one press — a `longpress` declared on something inside an
|
|
105
105
|
element that declares one too — are answered by the nearer one, the way a
|
|
106
106
|
click is the innermost target's: the inner hold fires, the outer wait is given
|
|
107
|
-
up.
|
|
108
|
-
first.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
up. That is with equal delays; an outer hold made shorter than the inner one
|
|
108
|
+
fires first. navi says so in dev, once per element, the first time it happens:
|
|
109
|
+
the outer hold is a declaration that can never fire, and a picker opened by
|
|
110
|
+
`openOn="longpress"` around a card that already holds one is exactly where it
|
|
111
|
+
happens. One hold has one meaning — two things on one card want two gestures
|
|
112
|
+
(a hold and a click), not two holds. A press that lands on something carried
|
|
113
|
+
inside the element (`move`, `moving`, `reorder`…) is that thing's the same
|
|
114
|
+
way: the outer hold does not wait on it, whether the source drags by holding
|
|
115
|
+
or by distance — except a source that says `"refuse"` for this press, which
|
|
116
|
+
carries nothing.
|
|
112
117
|
|
|
113
118
|
There is no `dblclick` in that table, on purpose: the browser fires it for a
|
|
114
119
|
mouse and never for a finger, so an element declaring it would answer half the
|
|
115
120
|
hands that reach it. `double_click` is the same gesture counted from the
|
|
116
|
-
pointer — see [Twice, whichever hand it is](#twice-whichever-hand-it-is).
|
|
117
|
-
outer hold is a declaration that can never fire, and a picker opened by
|
|
118
|
-
`openOn="longpress"` around a card that already holds one is exactly where it
|
|
119
|
-
happens. One hold has one meaning — two things on one card want two gestures
|
|
120
|
-
(a hold and a click), not two holds.
|
|
121
|
+
pointer — see [Twice, whichever hand it is](#twice-whichever-hand-it-is).
|
|
121
122
|
|
|
122
123
|
A name nothing knows how to detect produces a dev warning naming the detectors
|
|
123
124
|
that exist. The carrying family and the two surface streams each have a file of
|
|
@@ -148,8 +149,9 @@ ancestor can also listen for one, and `preventDefault()` on it means "not this
|
|
|
148
149
|
time".
|
|
149
150
|
|
|
150
151
|
The lower-level event the interaction was read from is reachable too:
|
|
151
|
-
`interactionEvent.detail.event` is the `pointerdown` a
|
|
152
|
-
of
|
|
152
|
+
`interactionEvent.detail.event` is the `pointerdown` a hold or a drag was made
|
|
153
|
+
of (a drag's release included), and the `pointerup` that ended a swipe or a tap
|
|
154
|
+
— which is how a menu is opened at the point the press happened.
|
|
153
155
|
|
|
154
156
|
## Reaching the control
|
|
155
157
|
|
|
@@ -157,8 +159,8 @@ Everything goes through the interaction gate of the **nearest control** — itse
|
|
|
157
159
|
an ancestor, or the one control it wraps, in that order. So a disabled, read-only
|
|
158
160
|
or busy control answers a swipe the way it answers a click: it says why, where the
|
|
159
161
|
interaction happened, and nothing runs. A `Box` with no control anywhere near it
|
|
160
|
-
still answers a callback; only `"request_action"
|
|
161
|
-
in dev.
|
|
162
|
+
still answers a callback; only the two requests (`"request_action"`,
|
|
163
|
+
`"request_ui_action"`) have nothing to ask, and say so in dev.
|
|
162
164
|
|
|
163
165
|
A `Box` that lays out **several** controls — a row of badges, a toolbar — belongs
|
|
164
166
|
to none of them: its interactions are its own, answered with no gate, exactly as
|
|
@@ -544,10 +546,11 @@ returns how to undo whatever it did. Listeners, attributes, anything: it is a
|
|
|
544
546
|
plain setup and teardown, so a detector counts what it needs in its own closure
|
|
545
547
|
and nothing has to hold state on its behalf.
|
|
546
548
|
|
|
547
|
-
`claims`
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
which of them were
|
|
549
|
+
`claims` is asked one name at a time, and a detector claims a **set** of them
|
|
550
|
+
rather than one, because interactions sharing an input have to be arbitrated
|
|
551
|
+
together — a swipe, a hold and a click dispute the same press, and read apart
|
|
552
|
+
they walk over each other. `types` (third argument) is which of them were
|
|
553
|
+
actually declared here.
|
|
551
554
|
|
|
552
555
|
`trigger(type, originalEvent, detail)` says the interaction happened. Called with
|
|
553
556
|
a single event — `trigger(event)` — the type is the detector's own, which only
|
|
@@ -577,17 +580,18 @@ container above it does not take the gesture:
|
|
|
577
580
|
under the same finger reads it before answering: a list reordered vertically
|
|
578
581
|
inside a row of slides swiped sideways leaves the sideways gesture alone, and a
|
|
579
582
|
piece carried both ways inside a bottom sheet takes the press whole. Nothing to
|
|
580
|
-
wire — see `docs/drag_to_travel.md`. A `Dialog` docked to
|
|
583
|
+
wire — see `docs/drag_to_travel.md`. A `Dialog` docked to an edge goes
|
|
581
584
|
further and reads the press only on its header (plus anything carrying
|
|
582
585
|
`data-swipe-grip`), so its body is free whatever is in it.
|
|
583
586
|
- **A hold does not take the context menu.** Declaring `longpress` says what a
|
|
584
587
|
held finger does; a right click comes from the other button and keeps opening
|
|
585
588
|
the browser's menu. Declare `contextmenu` beside it to make the right click do
|
|
586
589
|
the same thing. (A held _finger_ is the system's own context-menu gesture, and
|
|
587
|
-
that one is refused
|
|
590
|
+
that one is refused for the length of the press, not just of the wait.)
|
|
588
591
|
- **Where the press already means something, text is not selected.** An element
|
|
589
|
-
declaring `longpress
|
|
590
|
-
`
|
|
592
|
+
declaring `longpress`, a swipe or a counted tap (`double_click`,
|
|
593
|
+
`single_click`), and a drag source standing in a `data-drag-on-contact`
|
|
594
|
+
place, keep their text unselectable: the browser answers
|
|
591
595
|
that same press with a selection of its own — the word under the thumb, blue,
|
|
592
596
|
with handles — and nothing takes it back once the press is over. For every
|
|
593
597
|
pointer, mouse included, which cannot finish a selection begun where the press
|
|
@@ -621,6 +625,7 @@ container above it does not take the gesture:
|
|
|
621
625
|
- `src/control/interaction/interaction_press.js` — swipes, holds and the two
|
|
622
626
|
taps, and what a swipe writes on the element.
|
|
623
627
|
- `src/control/interaction/interaction_keyboard.js`,
|
|
624
|
-
`interaction_native.js` — the
|
|
628
|
+
`interaction_native.js` — shortcuts, and the browser's own events. The
|
|
629
|
+
carrying and surface detectors are referenced from their own docs.
|
|
625
630
|
- `src/control/demos/38_interactions_demo.html` — every case above, plus a
|
|
626
631
|
mailbox, a board, a surface, and a custom gesture registered from the page.
|
package/docs/list_action.md
CHANGED
|
@@ -29,7 +29,8 @@ 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 ("
|
|
32
|
+
own words ("le choix est en cours d'enregistrement", "la sélection…" in a
|
|
33
|
+
`multiple` list) rather than in a row's.
|
|
33
34
|
|
|
34
35
|
## On a row's control: that row waits alone
|
|
35
36
|
|
|
@@ -38,8 +39,9 @@ part of it: it holds no action, nothing about it is busy, and the other rows
|
|
|
38
39
|
stay live. Two rows can be in flight at once.
|
|
39
40
|
|
|
40
41
|
```jsx
|
|
41
|
-
<List selectable multiple>
|
|
42
|
+
<List id="the_list" selectable multiple>
|
|
42
43
|
<List.Item
|
|
44
|
+
id={rowId}
|
|
43
45
|
selectable
|
|
44
46
|
value={person}
|
|
45
47
|
readOnly={pending}
|
|
@@ -60,7 +62,7 @@ stay live. Two rows can be in flight at once.
|
|
|
60
62
|
|
|
61
63
|
The command is what marks the row, and it runs **only if the action succeeded**
|
|
62
64
|
— a button given both runs its action first and lets its command follow (see
|
|
63
|
-
[actions.md](./actions.md)). So a refused invitation leaves the row unmarked
|
|
65
|
+
[actions.md](./actions.md#a-press-that-opens-something-and-waits-for-the-answer)). So a refused invitation leaves the row unmarked
|
|
64
66
|
without anything to undo.
|
|
65
67
|
|
|
66
68
|
`selectableArea="manual"` is about the row's own surface, not about the button:
|
|
@@ -149,8 +151,9 @@ nothing about a row's own action stops someone from starting one on every row it
|
|
|
149
151
|
draws — a dozen requests in flight because the list happened to be long.
|
|
150
152
|
|
|
151
153
|
So a list allows **four runs at once by default**. While that many are out,
|
|
152
|
-
every
|
|
153
|
-
the next press is possible again the moment one comes
|
|
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.
|
|
154
157
|
|
|
155
158
|
```jsx
|
|
156
159
|
<List parallelGuard={2}> // stricter
|
package/docs/list_refresh.md
CHANGED
|
@@ -184,7 +184,8 @@ sends nothing and changes no state, so the network is silent and
|
|
|
184
184
|
`onRequestStateChange` — which reports what a request is doing — has no request
|
|
185
185
|
to report. A run that declined and a run that was never mounted look identical.
|
|
186
186
|
`debugScroll` is where that difference is visible; it carries the render window
|
|
187
|
-
and the run's asking, which are one subject.
|
|
187
|
+
and the run's asking, which are one subject. It logs in navi's development build
|
|
188
|
+
only.
|
|
188
189
|
|
|
189
190
|
```jsx
|
|
190
191
|
window.askLog = [];
|
|
@@ -251,6 +252,12 @@ Defaults are `{ GET: false, GET_MANY: ["POST"], GET_RANGE: ["POST", "DELETE"] }`
|
|
|
251
252
|
| `GET_MANY: ["POST"]` | Whether a new item belongs in this list depends on filters, pagination, sort — the backend knows, the client does not. `DELETE` is excluded because the store already removes the item from every list. |
|
|
252
253
|
| `GET_RANGE: ["POST", "DELETE"]` | A slice is a range of places: a row leaving the collection shifts every place after it, which the store cannot do. Add the verb that moves an item in or out of the collection — a `PATCH` that archives, one that changes an item's tab. |
|
|
253
254
|
|
|
255
|
+
`rerunOn` is read key by key: a key it names replaces that default, a key it
|
|
256
|
+
leaves out keeps it. `rerunOn: { GET_RANGE: ["POST", "DELETE", "PATCH"] }`
|
|
257
|
+
changes what re-reads the slices and nothing else — `GET_MANY` still reruns after
|
|
258
|
+
`POST`. `false` turns a key off. A `withParams()` scope or a relation reads its
|
|
259
|
+
own `rerunOn` the same way, against the resource it is declared on.
|
|
260
|
+
|
|
254
261
|
Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
|
|
255
262
|
request and a `loading` pass to obtain something the response already contained.
|
|
256
263
|
When a `PATCH` "doesn't show", the fix is almost always server-side — return the
|
|
@@ -91,9 +91,9 @@ far from the wrapper:
|
|
|
91
91
|
other axis to `auto`.
|
|
92
92
|
|
|
93
93
|
Note: `overflow: hidden` on `<html>` would additionally create a new containing block,
|
|
94
|
-
which could break `position: fixed` in edge cases.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
which could break `position: fixed` in edge cases. A dialog in the top layer is out of
|
|
95
|
+
reach of that: the top layer takes it out of every ancestor's containing block, which is
|
|
96
|
+
why navi's `Dialog` stays where it is written, in its opener's subtree.
|
|
97
97
|
|
|
98
98
|
### The wrapper is a net, not a fix: find what overflows
|
|
99
99
|
|
|
@@ -132,8 +132,9 @@ scrolls down to it when it opens.
|
|
|
132
132
|
Fix: **place `<dialog>` as the first child of `<body>`**, before any scrollable content.
|
|
133
133
|
`scrollIntoView` on an element already at the top of the document has no effect.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
This is advice for a `<dialog>` written by hand. navi's `Dialog` is not moved: it
|
|
136
|
+
renders in its opener's subtree (see
|
|
137
|
+
[css_architecture.md](./css_architecture.md#counting-children-what-navi-puts-in-your-tree)).
|
|
137
138
|
|
|
138
139
|
### Summary
|
|
139
140
|
|
package/docs/navigation.md
CHANGED
|
@@ -713,12 +713,14 @@ too early and put back:
|
|
|
713
713
|
hands out.
|
|
714
714
|
|
|
715
715
|
**Only where the row owns the document**: nothing between the travelling box and
|
|
716
|
-
the viewport may scroll
|
|
717
|
-
|
|
716
|
+
the viewport may scroll. A row inside a scroller of its own — a frame in an
|
|
717
|
+
article, a panel beside other content — shares nothing with the document, and
|
|
718
718
|
the offset there belongs to the page around it, which the reader never left; the
|
|
719
|
-
travel leaves it alone. An `overflow: hidden`
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
travel leaves it alone. An `overflow: hidden` on any ancestor is enough to put
|
|
720
|
+
the row outside the document's scrollport — it holds an offset of its own even
|
|
721
|
+
without a scrollbar, whereas `overflow: clip` holds none and does not count — so
|
|
722
|
+
a row that should give positions back and does not is worth looking at from
|
|
723
|
+
that angle first.
|
|
722
724
|
|
|
723
725
|
Pages that scroll inside themselves rather than scrolling the document are not
|
|
724
726
|
concerned either way: each one brings its own scrollport, which goes away with
|
package/docs/network_policy.md
CHANGED
|
@@ -29,8 +29,11 @@ cache the app never has to write:
|
|
|
29
29
|
- **A route action left behind is aborted, not reset.** Its value survives; only
|
|
30
30
|
a run in flight is called off.
|
|
31
31
|
- **Running a completed action is a no-op.** Coming back to `/games/abc` from
|
|
32
|
-
the list sends nothing — `run()` on a
|
|
33
|
-
Only `rerun()` goes back to the
|
|
32
|
+
the list sends nothing while the instance that read it is alive — `run()` on a
|
|
33
|
+
`COMPLETED` action already has its data. Only `rerun()` goes back to the
|
|
34
|
+
network (see [actions.md](./actions.md)). An instance nothing references any
|
|
35
|
+
more is asked again, with the store row drawn meanwhile; under the policy
|
|
36
|
+
that ask is answered from the store (below).
|
|
34
37
|
- **A failed rerun keeps the previous value.** Only `errorSignal` and the
|
|
35
38
|
running state move; what was on screen stays on screen.
|
|
36
39
|
|
|
@@ -97,10 +100,18 @@ was asked:
|
|
|
97
100
|
the reason.
|
|
98
101
|
|
|
99
102
|
It is an error only in the way it travels. Nothing treats it as a bug: navi
|
|
100
|
-
never reports it as unhandled, and
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
never reports it as unhandled, and when one reaches `window` anyway — a run
|
|
104
|
+
called by hand whose failure nobody catches, a render throw no boundary took
|
|
105
|
+
— navi cancels the event, so neither the browser console nor the jsenv
|
|
106
|
+
overlay says anything. The person reads the screen; nobody else has to hear
|
|
107
|
+
about it.
|
|
108
|
+
|
|
109
|
+
One line is left, in dev only. A boundary displaying it — `<ErrorBoundary>`,
|
|
110
|
+
a control with `actionErrorEffect="throw"` — makes `preact/debug` log it with
|
|
111
|
+
`console.error`, as it logs every error a boundary catches, 404s and failed
|
|
112
|
+
fetches included. `preact/debug` reads no option or flag that would say the
|
|
113
|
+
error is handled, so that line stays. The jsenv overlay stays quiet, and a
|
|
114
|
+
build does not load `preact/debug`.
|
|
104
115
|
|
|
105
116
|
- **A write is refused before the press.** A control bound to a `POST`, `PUT`,
|
|
106
117
|
`PATCH` or `DELETE` action — or any control inside a `<Form>` bound to one —
|
|
@@ -109,7 +120,11 @@ was asked:
|
|
|
109
120
|
|
|
110
121
|
Which actions the policy sees: those declaring a verb (`meta.verb`) — every
|
|
111
122
|
action a `resource()` makes. A plain `createAction` may not touch the network
|
|
112
|
-
at all, so it is left alone
|
|
123
|
+
at all, so it is left alone. `createAction(callback, { meta: { verb } })` opts
|
|
124
|
+
it into what the action layer holds — a control bound to a write verb is
|
|
125
|
+
read-only, a completed `"GET"` is not rerun — but its callback is still called
|
|
126
|
+
when it runs: answering from the store and settling with a `NetworkPolicyError`
|
|
127
|
+
are done by the callbacks a `resource()` wraps.
|
|
113
128
|
|
|
114
129
|
## Holding writes while reads go out
|
|
115
130
|
|
package/docs/pan_zoom.md
CHANGED
|
@@ -47,7 +47,9 @@ the release leaves behind. A pointer pans only once it has travelled a few px
|
|
|
47
47
|
`pan` still gets its hold, and a `double_click` still gets its two presses.
|
|
48
48
|
|
|
49
49
|
Declared alone, `zoom` takes two fingers and the wheel and leaves one pointer to
|
|
50
|
-
whatever else reads it
|
|
50
|
+
whatever else reads it: nothing captures it, waits on it or swallows its click
|
|
51
|
+
until a second finger lands, so a `click` or a `longpress` declared beside `zoom`
|
|
52
|
+
answers it as it would anywhere. `pan` alone leaves the wheel to the page.
|
|
51
53
|
|
|
52
54
|
### A surface that scrolls past: `data-pan-after-hold`
|
|
53
55
|
|
|
@@ -73,8 +75,12 @@ Opt-in, and for the same reason its opposite is (see
|
|
|
73
75
|
[`data-drag-on-contact`](./drag_interactions.md#a-finger-that-does-not-have-to-wait-data-drag-on-contact)):
|
|
74
76
|
navi cannot see whether anything behind the surface scrolls. You know;
|
|
75
77
|
say so. It spends the hold, though: a `longpress` declared beside a `pan` that
|
|
76
|
-
waits asks one finger to answer two waits
|
|
77
|
-
|
|
78
|
+
waits asks one finger to answer two waits, and the surface's — the shorter one
|
|
79
|
+
— is the one answered.
|
|
80
|
+
|
|
81
|
+
On a surface that only zooms there is no pan to wait for: one finger is the
|
|
82
|
+
page's to scroll, two are the surface's, and `"kept"` (below) has nothing to
|
|
83
|
+
keep.
|
|
78
84
|
|
|
79
85
|
The wait is then asked before every pan, which is right while each finger
|
|
80
86
|
landing there is genuinely ambiguous. Once the user has settled into the plan it
|
|
@@ -152,11 +158,11 @@ place `data-pan-after-hold` is.
|
|
|
152
158
|
|
|
153
159
|
### When the surface has the hand: `grab`, `release`, `[data-grabbed]`
|
|
154
160
|
|
|
155
|
-
A surface that is asked for — by a hold,
|
|
156
|
-
has an instant where it becomes the hand's, and nothing on
|
|
157
|
-
hand then moves too early and scrolls the page instead, or
|
|
158
|
-
moment out of doubt. So the surface says it, the same two
|
|
159
|
-
says (`grab`, `release`) and the same attribute:
|
|
161
|
+
A surface that is asked for — by a hold, by the first few pixels of travel, or
|
|
162
|
+
by a second finger — has an instant where it becomes the hand's, and nothing on
|
|
163
|
+
screen says it. The hand then moves too early and scrolls the page instead, or
|
|
164
|
+
waits long past the moment out of doubt. So the surface says it, the same two
|
|
165
|
+
words a carried element says (`grab`, `release`) and the same attribute:
|
|
160
166
|
|
|
161
167
|
```jsx
|
|
162
168
|
<Box
|
|
@@ -180,7 +186,8 @@ says (`grab`, `release`) and the same attribute:
|
|
|
180
186
|
}
|
|
181
187
|
```
|
|
182
188
|
|
|
183
|
-
`[data-grabbed]` is on the element for as long as a hand is on the surface
|
|
189
|
+
`[data-grabbed]` is on the element for as long as a hand is on the surface (on
|
|
190
|
+
one that only zooms, from the second finger landing to the last one lifting), so
|
|
184
191
|
a contour, a veil or a raised shadow needs no listener — which is what this is
|
|
185
192
|
usually for. (`[data-hand-kept]` is the other one, and it outlasts the finger:
|
|
186
193
|
see [`data-pan-after-hold`](#a-surface-that-scrolls-past-data-pan-after-hold).) `grab` and `release` are for the rest: a vibration, a state kept
|
package/docs/popup_backdrop.md
CHANGED
|
@@ -62,9 +62,10 @@ dismissing click must not also press what is under it — without dimming the
|
|
|
62
62
|
page for it. `backdrop={false}` is for a popup that must not absorb at all.
|
|
63
63
|
|
|
64
64
|
**It is what decides whether a popup is modal.** `Popover` is never modal,
|
|
65
|
-
either layer
|
|
66
|
-
|
|
67
|
-
genuinely inert; without one it goes to the
|
|
65
|
+
either layer, and neither is a `layer="local"` dialog. A top-layer `Dialog` is
|
|
66
|
+
modal exactly when it has a wall: with one it is `showModal()`'d and the
|
|
67
|
+
browser makes everything behind genuinely inert; without one it goes to the
|
|
68
|
+
same top layer through the Popover
|
|
68
69
|
API (`popover="manual"`), placed against the screen just the same, over a page
|
|
69
70
|
that stays live. `Popup` forwards `backdrop` in both modes, so which of popover
|
|
70
71
|
or dialog the small-screen resolution picks says nothing about whether one
|
|
@@ -179,7 +180,12 @@ panel that closes on that press then reopens on the click that follows is a
|
|
|
179
180
|
panel meant to stay open, blinking.
|
|
180
181
|
|
|
181
182
|
```jsx
|
|
182
|
-
<SidePanel
|
|
183
|
+
<SidePanel
|
|
184
|
+
id="error_panel"
|
|
185
|
+
signal={openCardIdSignal}
|
|
186
|
+
backdrop={false}
|
|
187
|
+
closeByPressOutside
|
|
188
|
+
>
|
|
183
189
|
…
|
|
184
190
|
</SidePanel>
|
|
185
191
|
|
|
@@ -198,7 +204,7 @@ adds a whole thing to the popup's ground — the card, its count, its buttons
|
|
|
198
204
|
and nothing in it is a dismissal.
|
|
199
205
|
|
|
200
206
|
It reads a press the page hears, so it belongs with a popup that has no wall
|
|
201
|
-
(`backdrop={false}
|
|
207
|
+
(`backdrop={false}`): behind a wall, no card is pressable.
|
|
202
208
|
|
|
203
209
|
### `pointer-events: none` and `inert` are not this
|
|
204
210
|
|
package/docs/popup_lift.md
CHANGED
|
@@ -132,8 +132,9 @@ corners itself, or on the card inside it, reads the same.
|
|
|
132
132
|
|
|
133
133
|
## Same width, or a wider box
|
|
134
134
|
|
|
135
|
-
`dialogSizeFromAnchor` makes the trigger's box a floor for the
|
|
136
|
-
`dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too
|
|
135
|
+
On a `Picker`, `dialogSizeFromAnchor` makes the trigger's box a floor for the
|
|
136
|
+
dialog; `dialogMaxWidth="var(--anchor-width)"` makes it the ceiling too (on a
|
|
137
|
+
`Dialog` the same props are `sizeFromAnchor` and `maxWidth`). Together they
|
|
137
138
|
say "the same card, brought forward": the box travels and may grow in height,
|
|
138
139
|
never in width. Left out, `dialogExpandX` lets the sheet take the room it has,
|
|
139
140
|
and the width change is animated — the card keeps its scale inside a box
|
|
@@ -334,7 +335,8 @@ into the picture of the state it closes into.
|
|
|
334
335
|
|
|
335
336
|
## What it costs, and where the time goes
|
|
336
337
|
|
|
337
|
-
Measured at CPU ×6 on the demo bench
|
|
338
|
+
Measured at CPU ×6 on the demo bench
|
|
339
|
+
(`src/control/demos/picker/12_picker_card_demo.html#lift-bench`),
|
|
338
340
|
a card of a real edit sheet's weight (≈215 elements, 13 fields, 62 picker
|
|
339
341
|
nodes):
|
|
340
342
|
|