@jsenv/navi 0.29.47 → 0.29.49

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.
@@ -7,6 +7,7 @@ What opens a `Dialog` or a `Popover`, and who owns the fact that it is open.
7
7
  - [Something else opens it: `triggerNaviCommand`](#something-else-opens-it-triggernavicommand)
8
8
  - [Which element receives the command](#which-element-receives-the-command)
9
9
  - [The anchor](#the-anchor)
10
+ - [Opening it ON something](#opening-it-on-something)
10
11
  - [Reacting to open and close](#reacting-to-open-and-close)
11
12
  - [Escape cancels, the other gestures keep](#escape-cancels-the-other-gestures-keep)
12
13
  - [When `open` is the right answer, and what it costs](#when-open-is-the-right-answer-and-what-it-costs)
@@ -124,24 +125,93 @@ The `anchor` prop always wins over whatever the command carried.
124
125
  event's anchor entirely, for a popover that must never be anchored to whatever
125
126
  opened it.
126
127
 
127
- ## Reacting to open and close
128
+ ## Opening it ON something
128
129
 
129
- `onClose` is called on every real close. There is no `onOpen`: an uncontrolled
130
- popup rewrites its own open handler, so a passed one would never run. Listen on
131
- the ref instead or, when the JS that opens is yours, do the work right where
132
- you trigger the command.
130
+ A popup that edits is never only open or closed: it is open **on** something. A
131
+ dialog that is "new radar" from the top of a list and "edit this radar" from a
132
+ row is one dialog with two modes, and the press is the only thing that knows
133
+ which one — so the press says it, with its own value:
134
+
135
+ ```jsx
136
+ <Button command="--navi-open" commandfor="radar-dialog">Nouveau radar</Button>
137
+ <Button value={radar.id} command="--navi-open" commandfor="radar-dialog" />
138
+
139
+ <Dialog
140
+ id="radar-dialog"
141
+ unmountWhenClosed
142
+ onOpen={(e) => {
143
+ editedRadarIdSignal.value = e.detail.value; // undefined = création
144
+ }}
145
+ >
146
+ ```
147
+
148
+ That subject travels as the command's **value**, not as an argument after a
149
+ colon (`--navi-open:radar-42`). The two places say different things and the
150
+ distinction holds across every command: an argument says WHAT the command does —
151
+ `--navi-go-to-slide:edit` needs one, "go" without a destination is not an
152
+ instruction — and `value` says what it is about. "Open" is already a complete
153
+ instruction; the radar is what it is about. A button therefore says it the way it
154
+ says it everywhere else, with `value`, and nothing has to be parsed.
155
+
156
+ A JS decision says the same thing through the same door:
133
157
 
134
158
  ```js
135
- useLayoutEffect(() => {
136
- const dialog = dialogRef.current;
137
- const onOpen = () => {
138
- /* … */
139
- };
140
- dialog.addEventListener("navi_request_open", onOpen);
141
- return () => dialog.removeEventListener("navi_request_open", onOpen);
142
- }, []);
159
+ triggerNaviCommand(dialogRef.current, "--navi-open", event, {
160
+ value: radar.id,
161
+ });
162
+ ```
163
+
164
+ `--navi-toggle` carries it too, on the half that opens.
165
+
166
+ ### `onOpen` runs before the popup has built anything
167
+
168
+ The order is the whole point, and it is a guarantee, not a coincidence:
169
+
170
+ ```
171
+ onOpen(openEvent) ← the subject is decided here
172
+ children mounted ← unmountWhenClosed rebuilds them from scratch, on that subject
173
+ positioned, shown
174
+ ```
175
+
176
+ So a dialog whose content is seeded once — an uncontrolled field on a
177
+ `defaultValue`, a form keyed on what it edits — reads the right thing on its very
178
+ first render. Learning it afterwards would mean mounting on the previous subject
179
+ and correcting it, which is a flicker at best and stale fields at worst.
180
+
181
+ The two other places one could listen are not that moment, and it is worth
182
+ knowing why:
183
+
184
+ | where | when it runs | chained to the caller |
185
+ | ----------------------------- | ------------------------------ | --------------------- |
186
+ | `onOpen` | before the content is built | yes — this prop |
187
+ | `onnavi_command` on the popup | after the command has been run | yes |
188
+ | `navi_request_open` listener | before the popup's own handler | on the element only |
189
+
190
+ `onnavi_command` receives the whole command string and its value, but it runs
191
+ **after** the opening: whatever it writes lands on a popup that is already open.
192
+ That works only as long as nothing has read the state yet — which
193
+ `unmountWhenClosed` makes a real race rather than a theoretical one.
194
+
195
+ A `navi_request_open` listener added on the element is the request itself, ahead
196
+ of the popup acting on it — but it is ordered against the popup's own handler by
197
+ registration, and it has to be attached in an effect on a ref. `onOpen` is that
198
+ moment, said as a prop.
199
+
200
+ ## Reacting to open and close
201
+
202
+ `onOpen` is called on every open, before the popup builds anything (see
203
+ [above](#opening-it-on-something)); `onClose` is called on every real close, and
204
+ carries `detail.isCancel` when the close meant "revert".
205
+
206
+ ```jsx
207
+ <Dialog onOpen={(e) => {}} onClose={(e) => {}}>
143
208
  ```
144
209
 
210
+ Neither can veto: `onClose` is the close happening, not a request to close.
211
+ Refusing a close is `onRequestClose`, which belongs to whoever owns an
212
+ `openController` (see `open_controller.js`) — an uncontrolled popup already
213
+ refuses the one close that matters, the one over a control mid-action.
214
+
145
215
  ### Closing when a button also runs an action
146
216
 
147
217
  Closing from inside the `action` does not close. While the action runs, the
@@ -232,6 +302,32 @@ value the app passes it. Escape on that first pass goes back to empty, not to
232
302
  what was on screen when the popup opened. From the second open onwards it puts
233
303
  back what was really there.
234
304
 
305
+ ### A picker that holds nothing and shows nothing
306
+
307
+ The gestures that KEEP (a click outside, a close cross) let a picker send what
308
+ it is showing: a picker sitting on a `defaultValue` holds nothing, so closing on
309
+ it untouched IS the answer ("yes, 1h30"), and the action runs.
310
+
311
+ A picker used as a **menu of gestures** — no `value`, no `defaultValue`, no
312
+ signal, each row a command — shows nothing, so there is nothing to confirm.
313
+ Closing it without choosing runs no action; only an explicit choice sends.
314
+
315
+ ```jsx
316
+ // Clicking outside closes this and sends nothing.
317
+ <Picker id="pause" mode="popover" variant="icon" action={pauseAction}>
318
+ <List selectable command="--navi-send">
319
+ <List.Item selectable id="24h" value="24h">
320
+
321
+ </List.Item>
322
+ </List>
323
+ </Picker>
324
+ ```
325
+
326
+ Note this is about a picker holding NOTHING. Passing `value={undefined}` is not
327
+ that: a `value` prop is held whatever is in it, and navi puts it back after each
328
+ click — the rows then appear to do nothing. Drop the prop instead of passing it
329
+ empty.
330
+
235
331
  ### `escapeEffect="close"`, and why it is a last resort
236
332
 
237
333
  `escapeEffect="close"` makes Escape say what a click outside says. It exists,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.47",
3
+ "version": "0.29.49",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {