@jsenv/navi 0.29.148 → 0.29.150

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.
@@ -263,12 +263,14 @@ consistency across the app, not from any single call site.
263
263
  of navi's own.
264
264
  - `docs/interactions.md` — the `interactions` prop: making a component answer a
265
265
  swipe, a held press, a shortcut, and registering a gesture navi does not have.
266
- It also holds `ownTarget`, for an affordance an application draws inside a
267
- zone that belongs to another control — a chip's cross, an eye, a diskette:
268
- the three modes and the question that picks one (does it write to the control
269
- it sits in?), the `Box` form that claims the press and nothing more (so an
270
- affordance keeps its own drawing instead of becoming a control), and what
271
- `ownTarget` does NOT stop (a plain `onClick` on an ancestor).
266
+ It also holds `selfInteractions`, for an affordance an application draws
267
+ inside a zone that belongs to another control — a chip's cross, an eye, a
268
+ diskette: why the claim is a required LIST (a press is not a drag, and what
269
+ is not named stays the zone's), `whenSelfInteractionsBlocked` and the question
270
+ that picks one of its three modes (does it write to the control it sits in?),
271
+ the `Box` form that claims interactions and nothing more (so an affordance
272
+ keeps its own drawing instead of becoming a control), and what the claim does
273
+ NOT stop (a plain `onClick` on an ancestor).
272
274
  Read it before reading the pointer by hand — who owns a press between nested
273
275
  boxes, and what a touch may do, are decided before the first pixel moves and
274
276
  cannot be got right from outside navi — and before stopping the propagation of
package/docs/actions.md CHANGED
@@ -96,7 +96,9 @@ available while a rerun is in flight). Where a failure goes when no screen takes
96
96
  it — and why a run settles with its error rather than rejecting — is
97
97
  [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
98
98
  state at once — `{ idle, loading, completed, aborted, error, data, params }` —
99
- for a component that needs to look at it rather than render it.
99
+ for a component that needs to look at it rather than render it. It reads that
100
+ very instance: to know what moves when a control is handed the action, see
101
+ [the instance a control runs](#the-instance-a-control-runs).
100
102
 
101
103
  **Reading does not run.** `useAsyncData` waits for data someone else asked for
102
104
  — for a page, the route, through `routeAction`. A component reading an action
@@ -187,6 +189,38 @@ callback:
187
189
  </Button>
188
190
  ```
189
191
 
192
+ ## The instance a control runs
193
+
194
+ A control binds the action it is given to its own UI state and runs the result.
195
+ Which instance that is depends on whether the control carries a value:
196
+
197
+ - a control with **no value of its own** — a button — contributes no params, so
198
+ what it runs **is** the instance it was handed. `useActionStatus` on that
199
+ instance sees the click, the run, the data.
200
+ - a control that **carries a value** — an input, a form — runs the child
201
+ instance bound to that value. Those params are not the caller's, so neither
202
+ is the status: the instance the caller holds stays idle while the control's
203
+ run moves. Read the effect where it lands (the store, for a resource verb),
204
+ or listen to the run itself, below.
205
+
206
+ Every control with an `action` reports the run it performs, whichever instance
207
+ that is:
208
+
209
+ ```jsx
210
+ <Button
211
+ action={shareAction}
212
+ onActionStart={(e) => {}}
213
+ onActionEnd={(data, e) => {}}
214
+ onActionError={(error, e) => {}}
215
+ onActionAborted={(e) => {}}
216
+ />
217
+ ```
218
+
219
+ `onActionEnd` receives the data of a run that **completed** — a failure never
220
+ reaches it. A failure goes to `onActionError`, alongside wherever the error is
221
+ displayed (see [error_handling.md](./error_handling.md)); an abort to
222
+ `onActionAborted`.
223
+
190
224
  ## `action` or `uiAction`
191
225
 
192
226
  Both fire when a control's value changes, and they are not two ways of writing
@@ -257,10 +291,10 @@ once per row. See
257
291
  [popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it).
258
292
 
259
293
  What is left for an `onClick` is what no value can express — imperative work
260
- with nothing to open and nothing to send. And one nuance worth knowing: on an
261
- `ownTarget`, a caller's `onClick` runs inside that control's own interaction
262
- gate rather than firing from the DOM (see
263
- [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-owntarget)),
294
+ with nothing to open and nothing to send. And one nuance worth knowing: on a
295
+ control with `selfInteractions`, a caller's `onClick` runs inside that control's
296
+ own interaction gate rather than firing from the DOM (see
297
+ [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions)),
264
298
  so the usual objection — an `onClick` fires on a read-only control — does not
265
299
  apply there. Everywhere else it does.
266
300
 
@@ -479,7 +479,7 @@ holds; only its mechanisms change.
479
479
  | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
480
480
  | two routes, `/new` and `/:id/edit` | one dialog, two modes — hence the value it is opened ON ([popup_open.md](./popup_open.md#opening-it-on-something)) |
481
481
  | the draft lives in the `searchParams` | it lives in the dialog, and dies with it |
482
- | cancelling = leaving the screen, which is thrown away | `unmountWhenClosed` (+ a `key` on the form), same effect |
482
+ | cancelling = leaving the screen, which is thrown away | `mount="while-opened"` (+ a `key` on the form), same effect |
483
483
  | `RouteTravel` between the two screens | nothing: a dialog has no neighbour |
484
484
  | `onLoad` + `pristineKey`: the resource arrives after | it is already in hand — the dialog is opened FROM the list |
485
485
 
@@ -41,6 +41,11 @@ validation message on the **requester** (the submit button, not the form):
41
41
  else — `"none"` — means the control displays nothing because something else
42
42
  does. `<Details>` uses that: its `<ActionRenderer>` shows the error inside the
43
43
  open panel.
44
+ - `onActionError(error, event)` fires alongside whichever display was chosen —
45
+ it is for reacting to the failure, not for showing it (showing stays claimed
46
+ by the message or the boundary). Its siblings on every control with an
47
+ `action` — `onActionStart`, `onActionEnd`, `onActionAborted` — are in
48
+ [actions.md](./actions.md#the-instance-a-control-runs).
44
49
 
45
50
  ### A screen's data failed — a route action
46
51
 
@@ -531,64 +531,94 @@ sideways), while a `move` goes wherever it is put, a `land` wherever the board h
531
531
  places and a `toss` wherever it was thrown (`xy`). `data-drag-delay`,
532
532
  `data-drag-slop`, `data-drag-threshold` tune when the press becomes a grab.
533
533
 
534
- ### An affordance inside somebody else's box: `ownTarget`
534
+ ### An affordance inside somebody else's box: `selfInteractions`
535
535
 
536
536
  A chip's cross inside a carried piece, an eye on a row that travels, a diskette
537
- on a picker's façade. It is aimed AT, not merely inside, and the press belongs
538
- to it alone:
537
+ on a picker's façade. It is aimed AT, not merely inside — but aimed at for
538
+ WHAT, which is the whole of the prop:
539
539
 
540
540
  ```jsx
541
- <Badge.Button ownTarget onClick={() => remove(id)}>
541
+ <Badge.Button selfInteractions="click" onClick={() => remove(id)}>
542
542
  ×
543
543
  </Badge.Button>
544
544
  ```
545
545
 
546
- No gesture starts under it, no navi control above it answers the mousedown or
547
- the click, and its `onClick` waits for its own interaction gate instead of
548
- firing from the DOM.
546
+ The press is now this cross's alone: no navi control above it answers the
547
+ mousedown or the click, and its `onClick` waits for its own interaction gate
548
+ instead of firing from the DOM.
549
549
 
550
- #### Does it write to the control it sits in?
550
+ #### Why it is a list, and why it is required
551
551
 
552
- That question, and nothing else, picks the mode:
552
+ A press is not a drag. A drag announces itself — a few pixels of travel with a
553
+ mouse, a long hold with a finger — and a click is the absence of both, so the
554
+ two can be told apart without anyone guessing at pointerdown. An affordance
555
+ that claimed every gesture at once would be a HOLE in whatever it sits in: a
556
+ badge drawn against the edge of a card is a seventh of that card, and precisely
557
+ the edge one grabs to carry it.
553
558
 
554
- | what it does | mode | on a read-only / disabled / busy zone |
555
- | ------------------------------------------------------------ | -------------------- | ----------------------------------------- |
556
- | writes to it (a cross that removes, a stepper) | `ownTarget` | it goes |
557
- | writes to it, and its presence says there is something there | `ownTarget="refuse"` | it stays and refuses with a callout |
558
- | never touches it (a diskette saving into MY address book) | `ownTarget="always"` | nothing changes: still lit, still pressed |
559
+ So the claim names its interactions, and what it does not name stays the zone's:
560
+
561
+ | written | takes | leaves |
562
+ | ------------------------------ | ------------------------- | ----------------------------------- |
563
+ | `selfInteractions="click"` | the press | the grab — the card is still carried by it |
564
+ | `selfInteractions="click drag"`| both | — |
565
+ | `selfInteractions="*"` | every gesture, now and later | — |
566
+
567
+ `"*"` is there for the case where it is true, not as a shorthand: it is the one
568
+ value that will silently swallow a gesture navi has not shipped yet.
569
+
570
+ `data-drag-ignore` says a different thing, to the gesture alone and for all of
571
+ them at once: the press there is none of the gesture's business, and the element
572
+ keeps both its cursor and its text selection.
573
+
574
+ #### Where the zone blocks: does it write to the control it sits in?
575
+
576
+ That question, and nothing else, picks `whenSelfInteractionsBlocked` — what
577
+ becomes of the affordance where the zone around it is disabled or read-only.
578
+ The claimed interactions are its subject, and only them: the ones left to the
579
+ zone were never this element's to block.
580
+
581
+ | what it does | written | on a blocked zone |
582
+ | ------------------------------------------------------------ | -------------------------------------------- | ----------------------------------------- |
583
+ | writes to it (a cross that removes, a stepper) | nothing — `"hide"` is the default | it goes |
584
+ | writes to it, and its presence says there is something there | `whenSelfInteractionsBlocked="refuse"` | it stays and refuses with a callout |
585
+ | never touches it (a diskette saving into MY address book) | `whenSelfInteractionsBlocked="ignore"` | nothing changes: still lit, still pressed |
559
586
 
560
587
  A greyed cross that still removes is worse than no cross — hence the default.
561
- `"always"` is the other extreme and the caller owns it: the zone's read-only is
588
+ `"ignore"` is the other extreme and the caller owns it: the zone's read-only is
562
589
  about a value the affordance does not write, so answering "read-only" to a
563
590
  gesture that was never going to write anything says nothing true. Use it only
564
591
  when that is really the case.
565
592
 
593
+ Busy is not on the list because busy does not block: it is the read-only a
594
+ running action sets on its way that does.
595
+
566
596
  #### On something you draw yourself
567
597
 
568
- `ownTarget` is a `Box` prop too, so an affordance does not have to become a
569
- control to claim its press — a pastille positioned in a card's corner by its own
570
- class stays exactly what it was drawn as:
598
+ `selfInteractions` is a `Box` prop too, so an affordance does not have to become
599
+ a control to claim its interactions — a pastille positioned in a card's corner
600
+ by its own class stays exactly what it was drawn as:
571
601
 
572
602
  ```jsx
573
- <Box as="button" ownTarget className="court_side" onClick={explain}>
603
+ <Box as="button" selfInteractions="click" className="court_side" onClick={explain}>
574
604
  ```
575
605
 
576
- On a box the prop does exactly one thing: it writes `data-own-target`. That
577
- attribute is the claim — it is what the controls above read, and what the
606
+ On a box the prop does exactly one thing: it writes `data-self-interactions`.
607
+ That attribute is the claim — it is what the controls above read, and what the
578
608
  gesture readers read (`data-drag-handle`, `data-drag-ignore` and friends are the
579
- same vocabulary). Writing it by hand on an element navi does not render works
580
- and is the last resort: a typo there is silent, whereas the prop is spelled
581
- once.
609
+ same vocabulary), each picking its own word out of it. Writing it by hand on an
610
+ element navi does not render works and is the last resort: a typo there is
611
+ silent, whereas the prop is spelled once and checked.
582
612
 
583
- The modes above are the other half, and they belong to controls: they are about
584
- a gate, a callout and a control's own read-only, none of which a box has. A box
585
- claims the press and nothing more; put the affordance on a control when what it
586
- does about a held zone matters.
613
+ `whenSelfInteractionsBlocked` is the other half, and it belongs to controls: it
614
+ is about a gate, a callout and a control's own read-only, none of which a box
615
+ has. A box claims interactions and nothing more; put the affordance on a control
616
+ when what it does about a held zone matters.
587
617
 
588
618
  #### When the affordance should sit OUTSIDE instead
589
619
 
590
- `ownTarget` says a façade CAN yield a zone; it does not say it should. What
591
- decides is whether the affordance stays where the finger left it.
620
+ `selfInteractions` says a façade CAN yield a zone; it does not say it should.
621
+ What decides is whether the affordance stays where the finger left it.
592
622
 
593
623
  An icon that lives inside its control while that control is showing, and
594
624
  becomes a pill of its own once it is not, is a switch that moves when you flip
@@ -598,21 +628,36 @@ which is the whole gesture on a phone. `<ControlSwap>` is that row: two
598
628
  controls taking turns in the middle, a fixed cap at each end.
599
629
 
600
630
  So: an affordance that acts on what it sits in (a chip's cross, a stepper, an
601
- eye on a row) belongs inside, and takes its press back with `ownTarget`. One
602
- that swaps what is being shown belongs outside it, where it can stay put.
631
+ eye on a row) belongs inside, and takes its press back with `selfInteractions`.
632
+ One that swaps what is being shown belongs outside it, where it can stay put.
633
+
634
+ Whichever side of the frame it ends up on, the control must be told, because a
635
+ control draws affordances of its own and will otherwise draw a second one:
636
+
637
+ - **inside** — a field has slots for it, `Input.UI.LeftSlot`,
638
+ `Input.UI.RightSlot`, `Input.UI.IconSlot` (an icon sized on the line rather
639
+ than on a character) and `Input.UI.UnitSlot`. They label the field, so a
640
+ press lands on it rather than blurring it — except when the field is not
641
+ focused yet, where the slot may take the focus itself, which is what a clear
642
+ cross or a reveal-password eye needs.
643
+ - **outside** — the icon the type would have drawn has to go, or it sits two
644
+ centimetres from yours: `icon={null}`. It is the same prop that replaces it
645
+ (`icon={<MySvg />}`) and that leaves it alone (left out). A `search` field
646
+ still swaps that slot for its clear cross once it holds a value, whatever
647
+ `icon` says — the cross is about the value, not about the decoration.
648
+
649
+ ```jsx
650
+ <Input type="search" icon={null} /> // the row draws the magnifier
651
+ ```
603
652
 
604
653
  #### navi steps back; a plain `onClick` does not
605
654
 
606
- What `ownTarget` stops is navi answering: the controls above it, and the
607
- gestures. It does **not** stop the event — the propagation is left whole, so
655
+ What the claim stops is navi answering: the controls above it, and the gestures
656
+ it named. It does **not** stop the event — the propagation is left whole, so
608
657
  that everything which is not a navi interaction still sees the press it always
609
658
  saw. A raw `onClick` on an ancestor is one of those, and still fires; stop it
610
659
  there yourself if it must not.
611
660
 
612
- `data-drag-ignore` says a narrower thing, to the gesture alone: the press there
613
- is none of the gesture's business, and the element keeps both its cursor and its
614
- text selection.
615
-
616
661
  ### What says a thing can be picked up
617
662
 
618
663
  Almost nothing, on purpose. A **handle** (`data-drag-handle`) exists only to drag,
@@ -169,7 +169,7 @@ which one — so the press says it, with its own value:
169
169
 
170
170
  <Dialog
171
171
  id="radar-dialog"
172
- unmountWhenClosed
172
+ mount="while-opened"
173
173
  onOpen={(e) => {
174
174
  editedRadarIdSignal.value = e.detail.value; // undefined = création
175
175
  }}
@@ -200,7 +200,7 @@ The order is the whole point, and it is a guarantee, not a coincidence:
200
200
 
201
201
  ```
202
202
  onOpen(openEvent) ← the subject is decided here
203
- children mounted ← unmountWhenClosed rebuilds them from scratch, on that subject
203
+ children mounted ← mount="while-opened" rebuilds them from scratch, on that subject
204
204
  positioned, shown
205
205
  ```
206
206
 
@@ -221,7 +221,7 @@ knowing why:
221
221
  `onnavi_command` receives the whole command string and its value, but it runs
222
222
  **after** the opening: whatever it writes lands on a popup that is already open.
223
223
  That works only as long as nothing has read the state yet — which
224
- `unmountWhenClosed` makes a real race rather than a theoretical one.
224
+ `mount="while-opened"` makes a real race rather than a theoretical one.
225
225
 
226
226
  A `navi_request_open` listener added on the element is the request itself, ahead
227
227
  of the popup acting on it — but it is ordered against the popup's own handler by
@@ -609,23 +609,28 @@ A closed popup builds nothing: `children` are mounted on the first open, and
609
609
  stay mounted afterwards — a reopened popup finds its scroll position and its
610
610
  half-typed form where it left them.
611
611
 
612
- Two props move that line:
612
+ "Closed" is two states, not one — never opened yet, and closed again after an
613
+ opening — so the `mount` prop answers both at once:
613
614
 
614
- | prop | effect |
615
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
616
- | `mountWhenClosed` | build `children` right away — for content something depends on before any opening (a value read off it, fields a surrounding form submits, a size measured from outside) |
617
- | `unmountWhenClosed` | throw `children` away once the popup has finished closing — for content whose fresh state is its initial state |
615
+ | `mount` | before the first open | after a close |
616
+ | ----------------------------- | --------------------- | ------------- |
617
+ | `"always"` | mounted | mounted |
618
+ | `"from-first-open"` (default) | not mounted | mounted |
619
+ | `"while-opened"` | not mounted | not mounted |
618
620
 
619
- `unmountWhenClosed` is what an uncontrolled field seeded from a `defaultValue`
621
+ `"always"` is for content something depends on before any opening: a value read
622
+ off it, fields a surrounding form submits, a size measured from outside.
623
+
624
+ `"while-opened"` is what an uncontrolled field seeded from a `defaultValue`
620
625
  needs: without it, a popup reopened after the underlying value changed still
621
626
  shows what it showed at closing time.
622
627
 
623
628
  ```jsx
624
- <Dialog ref={dialogRef} unmountWhenClosed>
629
+ <Dialog ref={dialogRef} mount="while-opened">
625
630
  <Textarea defaultValue={note.text} />
626
631
  </Dialog>
627
632
  ```
628
633
 
629
634
  The content is dropped only once the exit transition is over, so the popup never
630
635
  plays it on a blank surface; a popup reopened while it was leaving keeps the
631
- content that opening just asked for. `mountWhenClosed` wins if both are set.
636
+ content that opening just asked for.
package/docs/resource.md CHANGED
@@ -138,6 +138,13 @@ One verb dispatching on a string gives up everything the store does for you:
138
138
  per-operation action state (loading/error per button), per-relation autorerun,
139
139
  and a child collection that other components can read.
140
140
 
141
+ The same smell wears a second coat: a verb on one relation carrying, in its
142
+ params, an id from a **different** relation — a `group_id` sent to the game ×
143
+ user shares, which the server expands into people. That is two relations
144
+ spelled as one, and the one the gesture is actually about (game × group) is the
145
+ one that ends up with no rows, no action state, and nothing for its row on
146
+ screen to read.
147
+
141
148
  | Situation | Use |
142
149
  | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
143
150
  | The child is a first-class entity with its own store, shared across parents (a user referenced by many games) | `.one()` / `.many()` |
@@ -149,6 +156,18 @@ and a child collection that other components can read.
149
156
  Singular vs plural is about the relation, not the verb: `.one`/`.scopedOne` for a
150
157
  single sub-object, `.many`/`.scopedMany` for a collection.
151
158
 
159
+ And the table has a negative worth naming, because the workaround looks fine in
160
+ a demo: **a write whose effect the response does not carry is a modelling bug,
161
+ not a case for the client to remember.** After the write, some row on screen
162
+ will claim "this happened" — and it must read that from the store, which only
163
+ holds what responses carried. When the relation actually written has no row
164
+ anywhere (the response answered with a different entity than the one the
165
+ gesture was about), whatever the screen memorizes instead lives on the client
166
+ and **dies with the session**: reload, and the row confidently claims the write
167
+ never happened. The fix is on the shape — give that relation its own
168
+ relationship method and a callback returning what was written, even when the
169
+ backend must grow a field for it — never a flag next to the button.
170
+
152
171
  ## Callback return contracts
153
172
 
154
173
  These are not guessable — each relationship method has its own shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.148",
3
+ "version": "0.29.150",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@jsenv/dom": "0.17.35",
32
+ "@jsenv/dom": "0.17.36",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.3"
35
35
  },
@@ -40,6 +40,7 @@
40
40
  "@jsenv/plugin-preact": "../../related/plugin-preact",
41
41
  "@jsenv/snapshot": "../../tooling/snapshot",
42
42
  "@jsenv/terminal-table": "../../tooling/terminal-table",
43
+ "@jsenv/test": "../../related/test",
43
44
  "@jsenv/urls": "../../tooling/urls",
44
45
  "@preact/signals": "2.11.1",
45
46
  "playwright": "1.62.1",