@jsenv/navi 0.29.387 → 0.29.388

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.
@@ -6,6 +6,8 @@ An element picked up and carried, and what letting go means. It is one gesture
6
6
  the gate they go through and how a control learns which of them asked are in
7
7
  [interactions.md](./interactions.md); who owns a press when the carried thing
8
8
  stands in a box that travels is in [drag_to_travel.md](./drag_to_travel.md).
9
+ Whatever the moment, `event.detail.event` is the `pointerdown` the carry began
10
+ with — for the release as well, unlike a swipe's.
9
11
 
10
12
  - [Carrying something: `move`, `reorder`, `land`, `toss`, `leave`](#carrying-something-move-reorder-land-toss-leave)
11
13
  - [While it is being moved: `moving`](#while-it-is-being-moved-moving)
@@ -29,11 +31,12 @@ differs is the release. One detector reads them all, because it is one press.
29
31
  `toss` and `leave` each **combine** with `reorder` and with `land`: dropped on
30
32
  another item the element changes places, thrown far and fast it is gotten rid of,
31
33
  let go of away from every place it leaves. `leave` combines with `move` as well.
32
- `toss` does **not** combine with `move`, and neither do `reorder` and `land` with
33
- each other — one release cannot mean two of those. Declared together, the
34
- copy-carrying one wins and `move` is never answered — the element itself never
35
- travels, and a release that is not the other outcome means nothing at all (a dev
36
- warning says so).
34
+ `move` does **not** combine with any of the three that carry a copy (`reorder`,
35
+ `land`, `toss`), and neither do `reorder` and `land` with each other — one
36
+ release cannot mean two of those. Declared together, the copy-carrying one wins
37
+ and `move` is never answered — the element itself never travels, and a release
38
+ that is not the other outcome means nothing at all; of `reorder` and `land`,
39
+ `land` wins. A dev warning says so in each case.
37
40
 
38
41
  `move` carries the element ITSELF and leaves it where it was put; the others
39
42
  carry a copy and put the original back. That is the same difference said in layout
@@ -50,9 +53,10 @@ place taken by the list.
50
53
  ```
51
54
 
52
55
  `data-drag-free` on the element or a container lets it leave; by default a `move`
53
- stays inside what one can SEE of its container — which requires that container to
54
- be a scroll container at all (`overflow` anything but `visible`), since there is
55
- nothing else for "inside" to mean. A `move` whose answer rejects travels back, because a
56
+ stays inside what one can SEE of its scroll container — the nearest ancestor whose
57
+ `overflow` is `auto` or `scroll`, the page's viewport when there is none — so a
58
+ board that only cuts its content (`hidden`, `clip`) does not hold what moves on
59
+ it. A `move` whose answer rejects travels back, because a
56
60
  place the application would not accept must not stay on screen as if it had.
57
61
 
58
62
  **Where the position is kept is the answer's.** An element drawn from state —
@@ -202,8 +206,9 @@ back, not a row deleted — a list declares `toss`, a surface declares `leave`.
202
206
  ```
203
207
 
204
208
  `leave` combines with every other outcome. Beside `land` or `reorder`, "outside" is
205
- away from every place. Beside `move` — the element itself travels, and nothing is a
206
- place — it is outside the surface the element stands in: the nearest
209
+ away from every place. Where nothing is a place — beside `move`, which carries the
210
+ element itself, beside `toss`, or alone — it is outside the surface the element
211
+ stands in: the nearest
207
212
  `data-droppable` ancestor, or, without one, what can be seen of the scroll
208
213
  container (a dev warning says which). Either way it is judged on the element's
209
214
  box no longer overlapping it, not on the pointer, which is still well inside the
@@ -220,8 +225,8 @@ frame when a small marker has just left it.
220
225
  </Plan>
221
226
  ```
222
227
 
223
- Its detail is `toss`'s — `{ id, x, y }`, the distance travelled being what an exit
224
- is animated with. Picked up and put straight back down stays a cancel: it has to
228
+ Its detail is `toss`'s without the speed — `{ id, x, y }`, the distance travelled
229
+ being what an exit is animated with. Picked up and put straight back down stays a cancel: it has to
225
230
  have gone somewhere to be away from anything.
226
231
 
227
232
  What the thing does while the answer is asked follows what was carried. A copy
@@ -283,8 +288,8 @@ place, swap the two, refuse.
283
288
  />
284
289
  ```
285
290
 
286
- `toId` is an element and **never null**: a copy over nothing is a release that
287
- meant nothing, and the interaction does not happen at all.
291
+ `toId` always names an element and is **never null**: a copy over nothing is a
292
+ release that meant nothing, and the interaction does not happen at all.
288
293
 
289
294
  **Which elements are places: those marked `data-droppable`, and only those.**
290
295
  Declaring `land` says an element can be CARRIED, which on a board is a different
@@ -487,9 +492,11 @@ vibration above, a state kept elsewhere, a counter.
487
492
 
488
493
  `grab` **reports, it does not ask**: what it returns is not waited on, and
489
494
  preventing its event does not call the gesture off. And it is not an interaction on
490
- its own — declared without one of the five above there is no gesture for it to be
491
- the beginning of, and a dev warning says so. Its detail carries `pointerType` and
492
- the `gestureInfo`.
495
+ its own — declared without one of the five above (or `moving`) there is no
496
+ gesture for it to be the beginning of, and a dev warning says so, unless the
497
+ element declares `pan` or `zoom`, whose moments they then are
498
+ ([pan_zoom.md](./pan_zoom.md)). Its detail carries `pointerType` and the
499
+ `gestureInfo`.
493
500
 
494
501
  A `longpress` needs none of this: it already happens at the moment the hold is
495
502
  acquired, not at the release.
@@ -574,7 +581,7 @@ prevented.
574
581
  One outcome refusing refuses the whole gesture — the five answer one carry, and
575
582
  something that must not be carried has none of them. Like `grab` and `release`,
576
583
  `refuse` **reports, it does not ask**, and it is not an interaction on its own.
577
- Its detail is `{ pointerType }` and nothing more: there is no `gestureInfo`
584
+ Its detail is `{ pointerType }` beside the press (`event`), and no `gestureInfo`
578
585
  because there was no gesture, and what feedback wants to know is which hand asked
579
586
  — a vibration for a finger, nothing extra for a mouse whose cursor has already
580
587
  said it.
@@ -587,11 +594,11 @@ usually gets its own from the list around it, which the copy has left. So the
587
594
  clone's look is the page's to declare, through the attributes the gesture puts on
588
595
  it:
589
596
 
590
- | Attribute | On |
591
- | ------------------------- | ------------------------------------------------------ |
592
- | `navi-drag-clone` | the copy being carried |
593
- | `navi-drag-clone-wrapper` | what positions it (already shadowed, in the top layer) |
594
- | `navi-drag-clone-source` | the original, still in place (already hidden) |
597
+ | Attribute | On |
598
+ | ------------------------- | ---------------------------------------------------- |
599
+ | `navi-drag-clone` | the copy being carried (already shadowed and scaled) |
600
+ | `navi-drag-clone-wrapper` | what positions it, in the top layer |
601
+ | `navi-drag-clone-source` | the original, still in place (already hidden) |
595
602
 
596
603
  ```css
597
604
  .task[navi-drag-clone] {
@@ -603,19 +610,19 @@ it:
603
610
  Reusing the item's own class is the point: the copy is that item, so it is styled
604
611
  as that item plus whatever being carried changes.
605
612
 
606
- **Which is also the trap, for anything positioned on a board.** The copy is the
607
- same element re-parented into a carrier box, so a geometry written in the style
608
- attribute follows it there — `width: calc(50% - 2 * var(--gap))` then means half
609
- of the copy instead of half of the board, and the piece is carried at the wrong
610
- size. Put what a piece LOOKS like in a class and leave only which place it is
611
- inline (two custom properties will do), then let it fill its carrier:
613
+ **Which is also the trap, for anything positioned on a board.** The copy is a
614
+ deep clone of the element put in a carrier box, so a geometry written in the style
615
+ attribute comes with it. navi takes back where it stands (`position`, `inset`,
616
+ `margin` and `translate` are reset on the copy) but not its size —
617
+ `width: calc(50% - 2 * var(--gap))` then means half of the carrier instead of
618
+ half of the board, and the piece is carried at the wrong size. Put what a piece
619
+ LOOKS like in a class and leave only which place it is inline (two custom
620
+ properties will do), then let it fill its carrier:
612
621
 
613
622
  ```css
614
623
  [navi-drag-clone-wrapper] .piece {
615
- position: static;
616
624
  width: 100%;
617
625
  height: 100%;
618
- translate: none;
619
626
  }
620
627
  ```
621
628
 
@@ -654,8 +661,8 @@ from for as long as the question is open — and if the answer refuses, the copy
654
661
  back to it.
655
662
 
656
663
  `data-drag-axis` says which axes the drag walks, and its default is not the same
657
- for every outcome: `reorder` alone walks the list (`y`, or `x` for a list that runs
658
- sideways), while a `move` goes wherever it is put, a `land` wherever the board has
664
+ for every outcome: `reorder` alone walks the list (`y`; a list that runs sideways
665
+ says `data-drag-axis="x"`), while a `move` goes wherever it is put, a `land` wherever the board has
659
666
  places, a `toss` wherever it was thrown and a `leave` out by whichever edge (`xy`).
660
667
  `data-drag-delay`,
661
668
  `data-drag-slop`, `data-drag-threshold` tune when the press becomes a grab.
@@ -728,6 +735,9 @@ leaves the paint alone:
728
735
  import { createDragGestureController, dragAfterIntent } from "@jsenv/navi";
729
736
 
730
737
  const onPointerDown = (pointerDownEvent) => {
738
+ // Read now: the callback below runs once the intent shows, after dispatch,
739
+ // when `currentTarget` is null.
740
+ const element = pointerDownEvent.currentTarget;
731
741
  dragAfterIntent(pointerDownEvent, () => {
732
742
  const controller = createDragGestureController({
733
743
  onDrag: (gestureInfo) => {
@@ -741,9 +751,7 @@ const onPointerDown = (pointerDownEvent) => {
741
751
  COURT.PUT.run({ angle: angleSignal.peek() });
742
752
  },
743
753
  });
744
- return controller.grabViaPointer(pointerDownEvent, {
745
- element: pointerDownEvent.currentTarget,
746
- });
754
+ return controller.grabViaPointer(pointerDownEvent, { element });
747
755
  });
748
756
  };
749
757
  ```
@@ -192,12 +192,13 @@ laptop a horizontal two-finger swipe IS the back-navigation gesture, and a
192
192
  region that neither takes it nor lets it go is the worst of the three — the page
193
193
  rocks and nothing happens.
194
194
 
195
- Not the same thing as the drag gesture it sits beside
196
- ([drag_gesture.js](../../dom/src/interaction/drag/drag_gesture.js)): that one is
197
- for **carrying an object** across the page — it lays a backdrop over the
198
- document, makes everything else `inert`, takes the focus and blocks the scroll
199
- keys. Here nothing is picked up and the page must keep its focus and its
200
- scrolling while a screen slides. Same word, other gesture.
195
+ Not the same thing as **carrying an object** across the page, although both read
196
+ the pointer through the same loop
197
+ ([drag_gesture.js](../../dom/src/interaction/drag/drag_gesture.js)): a carry has
198
+ it lay a backdrop over the document, make everything else `inert`, take the
199
+ focus and block the scroll keys. Here nothing is picked up and the page must keep
200
+ its focus and its scrolling while a screen slides, so the travel asks it for none
201
+ of that (`documentInteractions: "manual"`). Same loop, other gesture.
201
202
 
202
203
  ## Who owns a gesture
203
204
 
@@ -333,8 +334,9 @@ the browser for what those two properties do not cover.
333
334
 
334
335
  ### What is contained, and what still leaks
335
336
 
336
- Containing is only ever read on a **scroll container** — an element that clips,
337
- in the browser's sense, whether or not it has anything to scroll. Three places
337
+ Containing is only ever read on a **scroll container** — an element whose
338
+ `overflow` is `hidden`, `auto` or `scroll`, whether or not it has anything to
339
+ scroll (`clip` cuts without making one). Three places
338
340
  could carry it: the travelling box, everything inside it, or the scrollers
339
341
  themselves. Which of the three works is an engine question, and the answer
340
342
  splits in two:
@@ -365,12 +367,14 @@ them is that an empty `textarea` is contained too — a browser cannot be asked
365
367
  "only if it scrolls" — so on Blink a wheel over one moves nothing rather than
366
368
  the list around it.
367
369
 
368
- On Blink this leaves the boxes that **do not clip**, since those are never
369
- asked. Of navi's own: `SlideContainer` clips (`overflow: hidden`) and a
370
- `SidePanel` is a `Dialog`, which scrolls (`overflow: auto`) — both are asked. A
371
- `RouteTravel` box does not clip and must not be made to: a scroll container
372
- there would become the nearest one for every `position: sticky` inside the pages
373
- it holds. A row marked swipeable by `interactions` does not clip either.
370
+ On Blink this leaves the boxes that are **not scroll containers**, since those
371
+ are never asked. Of navi's own: a `SidePanel` is a `Dialog`, which scrolls
372
+ (`overflow: auto`), and is asked. A `SlideContainer` cuts with `overflow: clip`,
373
+ which is not a scroll container (`hidden` would be one, and would let the slides
374
+ off stage be scrolled to), so it is not asked. A `RouteTravel` box does not clip
375
+ at all and must not be made a scroll container: one there would become the
376
+ nearest one for every `position: sticky` inside the pages it holds. A row marked
377
+ swipeable by `interactions` is not one either.
374
378
 
375
379
  Which is why navi contains what it KNOWS scrolls, in `box.jsx`:
376
380
 
@@ -386,7 +390,8 @@ does, and it puts the containment exactly where every engine reads it. A list, a
386
390
  dialog body, a scrolling panel inside a travelling page are covered on Blink
387
391
  again.
388
392
 
389
- What still leaks, and only on Blink, and only under a box that does not clip: a
393
+ What still leaks, and only on Blink, and only under a box that is not a scroll
394
+ container: a
390
395
  scroller **nobody declared and no tag names** — a bare
391
396
  `<div style="overflow: auto">`, a widget from elsewhere. Its leftovers reach the
392
397
  page, which is the old symptom in a much smaller corner. Two ways out, per case:
@@ -447,10 +452,10 @@ The two ends cost differently, and it is worth knowing which one is being felt:
447
452
  and there is no gap at all.
448
453
 
449
454
  What the browser will not turn around with it is everything ELSE the
450
- transition carries — see "One gesture that bar cannot follow" at the end of
451
- this file.
455
+ transition carries — see [One gesture that bar cannot
456
+ follow](#one-gesture-that-bar-cannot-follow).
452
457
 
453
- Two things a travel in hand must never lose:
458
+ What a travel in hand must never lose:
454
459
 
455
460
  - **a travel being undone is not up for grabs.** Its end is already decided;
456
461
  held again mid-revert, its animations never finish, the wait for them never
@@ -546,13 +551,13 @@ travel had started finishes without anyone. It is invisible with a mouse, which
546
551
  is why it survives a whole session of desktop testing.
547
552
 
548
553
  So a travel that has become ours refuses the `touchmove` (`preventDefault`), and
549
- only then — a finger that means to scroll must still scroll. Two details make it
550
- hold:
554
+ only then — a finger that means to scroll must still scroll. Three details make
555
+ it hold:
551
556
 
552
- - the listener sits on the element the touch LANDED on as well as on the box: a
557
+ - the listener sits on the element the touch LANDED on as well as on the window: a
553
558
  touch keeps being dispatched at the node it started on, and a travel may
554
559
  replace the DOM under the finger (a page that travels navigates), after which
555
- that node no longer passes through the box on its way up;
560
+ that node no longer passes through the window on its way anywhere;
556
561
  - the pointer is captured on the BOX rather than on what the finger landed on,
557
562
  for the same reason: what the caller does may take that target away, and a
558
563
  capture whose element leaves the document is a capture the browser drops;
@@ -659,7 +664,8 @@ aimed decides what that costs:
659
664
  at half of its time a travel has covered ~80% of its distance, and rewound
660
665
  at `-1` the visible way home collapses into the steep end of the curve — a
661
666
  snap, not a return. The pictures walk home over how far they visibly are
662
- from home, at the travel's own pace (`revertWalkTime`);
667
+ from home, at the travel's own pace (`walkPicturesHome` in
668
+ `src/transition/view_transition_revert.js`);
663
669
  - both of the above run straight into the compositor traps: the distance is
664
670
  computed from the clock through the easing curve (the pseudo-elements'
665
671
  animated position cannot be read), and the rate is handed over with
@@ -126,9 +126,9 @@ what asks, not to the render.**
126
126
 
127
127
  It starts one render late, and per instance: a component fetched this way
128
128
  in three places asks three times (the browser fetches the module once), and
129
- coming back to it suspends for the microtask the import takes to answer
130
- from the module map. Reach for it when there is genuinely no state to read
131
- the request off — not to save declaring one.
129
+ coming back to it draws its wait again for the microtask the import takes to
130
+ answer from the module map. Reach for it when there is genuinely no state to
131
+ read the request off — not to save declaring one.
132
132
 
133
133
  Delegating instead (the hook without `loading: true`, under a `<Loading>`
134
134
  around the frame) follows the rule for data: **the `<Loading>` goes where the
@@ -210,8 +210,10 @@ The page or the component reading its code says it where it stands, as
210
210
  above. A failure left to a boundary — a page reading its code without
211
211
  `error: true` — reaches it like a failed read does; a boundary that shows only
212
212
  what came from the network and rethrows the rest as a bug recognises an import
213
- by its action — `error.action` is the route action the app declared for the
214
- code — rather than by the browser's message.
213
+ by its action rather than by the browser's message. `error.action` is the
214
+ instance that ran, not the route action itself (a route action hands out the
215
+ instance for its params), and every instance bound from one action carries its
216
+ `callback`: `error.action.callback === GAME_PAGE_CODE.callback`.
215
217
 
216
218
  ## Preact's `lazy()` is not this
217
219
 
@@ -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 whose failure the
188
- control that started it already draws, a routing whose result every caller drops.
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 the
191
- whole list of places navi swallows one, and taking it is what obliges that place
192
- to ask the next question: did anything display this error? (below).
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. It has to exist because `preact/debug` re-throws, in a `setTimeout`,
216
- **every error a boundary handled** — deliberately, so that React-style dev
217
- overlays keep working:
218
-
219
- ```js
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>` / `<Button action>` when they render their error
230
- branch, `useAsyncData({ error: true })`
231
- when it hands the error to the component, and a run given an `onError` — asking
232
- for the error is taking it.
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
- `preact/debug` re-throws what a boundary caught and an unbounded throw aborts the
245
- render loudly. Reporting it here too would be a second voice, and the wrong one:
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
@@ -287,8 +285,9 @@ easy to miss when writing another one:
287
285
 
288
286
  - **Mark only what you actually render.** Marking on catch, before knowing
289
287
  whether anything is displayed, turns a boundary into a bug swallower: a
290
- `TypeError` in a component becomes a blank page AND a silent one, since the
291
- mark muted the overlay that would have pointed at it. navi's boundary marks
288
+ `TypeError` it then re-throws reaches window marked as on screen, so the
289
+ overlay that would have pointed at it stays out of the way — a blank page AND
290
+ a silent one. navi's boundary marks
292
291
  just before rendering its fallback, and re-throws untouched when it has no
293
292
  fallback — nothing to display means nothing handled.
294
293
  - **Reset on the document URL, not only on the rerun.** Re-running the failed
@@ -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) }`. `check` returns `null` when the value
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
- Each constraint has a `<name>Message` prop (and a `data-<name>-message`
68
- attribute) to replace its sentence for one field. To change it everywhere,
69
- override the i18n key instead — see below.
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 field usually wants both.
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
@@ -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. Delays being equal; an outer hold made shorter than the inner one fires
108
- first. A press that lands on something carried inside the element (`move`,
109
- `moving`, `reorder`…) is that thing's the same way: the outer hold does not
110
- wait on it, whether the source drags by holding or by distance — except a
111
- source that says `"refuse"` for this press, which carries nothing.
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). navi says so in dev, once per element, the first time it happens: the
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 swipe or a hold was made
152
- of — which is how a menu is opened at the point the press happened.
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"` has nothing to ask, and says so
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` takes a **set** of names rather than one, because interactions sharing an
548
- input have to be arbitrated together — a swipe, a hold and a click dispute the
549
- same press, and read apart they walk over each other. `types` (third argument) is
550
- which of them were actually declared here.
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 the bottom edge goes
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 while the wait runs.)
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` or a swipe, and a drag source standing in a
590
- `data-drag-on-contact` place, keep their text unselectable: the browser answers
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 other two detectors.
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.
@@ -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 ("la sélection est en cours d'enregistrement") rather than in a row'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.
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 row that is not running goes read-only and says how many it is waiting on;
153
- the next press is possible again the moment one comes back.
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