@jsenv/navi 0.29.104 → 0.29.106

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.
@@ -220,9 +220,9 @@ consistency across the app, not from any single call site.
220
220
  It also holds `ownTarget`, for an affordance an application draws inside a
221
221
  zone that belongs to another control — a chip's cross, an eye, a diskette:
222
222
  the three modes and the question that picks one (does it write to the control
223
- it sits in?), the `data-own-target` attribute for an element you draw
224
- yourself, and what `ownTarget` does NOT stop (a plain `onClick` on an
225
- ancestor).
223
+ it sits in?), the `Box` form that claims the press and nothing more (so an
224
+ affordance keeps its own drawing instead of becoming a control), and what
225
+ `ownTarget` does NOT stop (a plain `onClick` on an ancestor).
226
226
  Read it before reading the pointer by hand — who owns a press between nested
227
227
  boxes, and what a touch may do, are decided before the first pixel moves and
228
228
  cannot be got right from outside navi — and before stopping the propagation of
package/docs/actions.md CHANGED
@@ -168,6 +168,39 @@ To merely REMEMBER the value rather than send it, neither is the answer: bind a
168
168
  signal and drop the callback entirely — see
169
169
  [control_value.md](./control_value.md).
170
170
 
171
+ ## A press that opens something and waits for the answer
172
+
173
+ A press that runs work is an `action`; one that reports a value is a `uiAction`;
174
+ one that asks something of a control near it is a `command` (a value proposed is
175
+ `--navi-update`, see
176
+ [control_value.md](./control_value.md#a-button-that-proposes-a-value-is---navi-update)).
177
+ Reaching for a plain `onClick` usually means one of those was missed.
178
+
179
+ The press that looks like a fourth case is the one that opens something and then
180
+ does something with what came of it — "save this guest", pressed on a row,
181
+ replacing the guest once the profile exists. It is not a fourth case and it is
182
+ not a dialog plus a way home: it is a `Picker`, whose whole shape is a trigger,
183
+ a popup, and an `action` that runs on what the popup settled.
184
+
185
+ ```jsx
186
+ <Picker variant="icon" rightSlotIcon={<DisketteSvg />} action={onCreated}>
187
+ <GuestSavePrompt kind="player" name={guest.name} />
188
+ </Picker>
189
+ ```
190
+
191
+ Written this way, what the popup needs to know travels as props rather than
192
+ through the press, and the popup is built the first time it opens rather than
193
+ once per row. See
194
+ [popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it).
195
+
196
+ What is left for an `onClick` is what no value can express — imperative work
197
+ with nothing to open and nothing to send. And one nuance worth knowing: on an
198
+ `ownTarget`, a caller's `onClick` runs inside that control's own interaction
199
+ gate rather than firing from the DOM (see
200
+ [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-owntarget)),
201
+ so the usual objection — an `onClick` fires on a read-only control — does not
202
+ apply there. Everywhere else it does.
203
+
171
204
  ## `uiAction` mirrors the state, it does not report a gesture
172
205
 
173
206
  `uiAction` fires whenever the control's state changes, whoever changed it. The
@@ -10,8 +10,9 @@ for, and nothing is set up for a case that cannot happen:
10
10
  - **`shrinkWrap`** — a hidden clone of the list is laid out to find the widest
11
11
  wrapped row, and the list is narrowed to it so the last row isn't ragged.
12
12
  Opt-in outside a `Picker` (a picker draws a border around the list, so the
13
- ragged edge shows; elsewhere the work would often go unseen), and skipped
14
- whenever `maxLines` is in play.
13
+ ragged edge shows; elsewhere the work would often go unseen). With `maxLines`
14
+ the rows are read first, at the full width, and the list is narrowed once the
15
+ surplus is gone — in place, no clone.
15
16
  - **`maxLines`** — every badge is laid out once, hidden; where the rows fell is
16
17
  read; the list is rendered again with the badges that fit and a `+N more`
17
18
  badge for the rest. Both renders land in the same frame. A width change of
@@ -563,18 +563,27 @@ about a value the affordance does not write, so answering "read-only" to a
563
563
  gesture that was never going to write anything says nothing true. Use it only
564
564
  when that is really the case.
565
565
 
566
- #### On an element you draw yourself
566
+ #### On something you draw yourself
567
567
 
568
- The claim is one attribute, and nothing is asked of the element carrying it —
569
- `ownTarget` is only the prop that writes it on a navi control:
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:
570
571
 
571
572
  ```jsx
572
- <button class="court_side" data-own-target="always" onClick={explain}>
573
+ <Box as="button" ownTarget className="court_side" onClick={explain}>
573
574
  ```
574
575
 
575
- That is what the controls above read, and what the gesture readers read
576
- (`data-drag-handle`, `data-drag-ignore` and friends are the same vocabulary). An
577
- application keeps its own drawing and gets the press ownership all the same.
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
578
+ 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.
582
+
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.
578
587
 
579
588
  #### navi steps back; a plain `onClick` does not
580
589
 
@@ -8,6 +8,7 @@ What opens a `Dialog` or a `Popover`, and who owns the fact that it is open.
8
8
  - [Which element receives the command](#which-element-receives-the-command)
9
9
  - [The anchor](#the-anchor)
10
10
  - [Opening it ON something](#opening-it-on-something)
11
+ - [A press that opens a popup and acts on it](#a-press-that-opens-a-popup-and-acts-on-it)
11
12
  - [Reacting to open and close](#reacting-to-open-and-close)
12
13
  - [Escape cancels, the other gestures keep](#escape-cancels-the-other-gestures-keep)
13
14
  - [When `open` is the right answer, and what it costs](#when-open-is-the-right-answer-and-what-it-costs)
@@ -202,6 +203,107 @@ of the popup acting on it — but it is ordered against the popup's own handler
202
203
  registration, and it has to be attached in an effect on a ref. `onOpen` is that
203
204
  moment, said as a prop.
204
205
 
206
+ ## A press that opens a popup and acts on it
207
+
208
+ A press that opens something and then does something with what came of it — a
209
+ "save this guest" prompt on a row, which replaces the guest once the profile
210
+ exists — is not a dialog plus a way home. It is a `Picker`: a trigger, a popup,
211
+ and an `action` that runs on what the popup settled.
212
+
213
+ ```jsx
214
+ // one per row: the trigger IS the thing that receives the answer
215
+ <Picker
216
+ variant="icon"
217
+ rightSlotIcon={<DisketteSvg />}
218
+ action={async (created) => {
219
+ await USERS.GET_MANY.rerun();
220
+ replaceGuest(guest, created);
221
+ }}
222
+ >
223
+ <GuestSavePrompt kind="player" name={guest.name} />
224
+ </Picker>
225
+ ```
226
+
227
+ Two things fall out of writing it this way, and both are the reason to prefer it
228
+ over a shared dialog opened by `--navi-open`:
229
+
230
+ - **what the popup needs to know travels as props**, because the popup is
231
+ written where the press is. No value to carry through the command, nothing to
232
+ read back out of an event;
233
+ - **the popup is built the first time it opens**, not once per row on the render
234
+ that draws the list (see [what a popup holds while it is
235
+ closed](#what-the-popup-holds-while-it-is-closed)). A hundred rows is a
236
+ hundred triggers, not a hundred dialogs.
237
+
238
+ The same component can of course be written once and used in every picker —
239
+ `<GuestSavePrompt>` above is one — so "the prompt exists once" is a question
240
+ about components, not about the DOM.
241
+
242
+ ### Composing a value, or doing work
243
+
244
+ A picker mirrors **one** control in its popup — the first one that is not a
245
+ button, a link or a control saying it is not the answer (`allowNameless`). That
246
+ mirror is what makes `<Picker><List selectable/></Picker>` work with nothing
247
+ wired: the picker's value IS the list's, both ways, and the picker's `action`
248
+ runs on it when the popup closes.
249
+
250
+ That is the shape for a popup that **composes a value**. A popup that **does
251
+ work** — creates a profile, uploads a file — is the other shape, and it does not
252
+ need the picker's `action` at all: the work is written where the press is, so
253
+ its callback already has everything around it.
254
+
255
+ ```jsx
256
+ <Picker variant="icon" rightSlotIcon={<DisketteSvg />}>
257
+ <Form
258
+ action={async (fields) => {
259
+ const created = await USERS.POST(fields);
260
+ replaceGuest(guest, created); // the row is right here
261
+ }}
262
+ >
263
+
264
+ </Form>
265
+ </Picker>
266
+ ```
267
+
268
+ Nothing travels back, because nothing left. This is the difference a shared
269
+ dialog hides: a popup written once, far from every press that opens it, has to
270
+ be told what it is about and has to answer somebody — and neither question
271
+ exists once the popup is written where it is used.
272
+
273
+ ### A trigger that is only an icon
274
+
275
+ `variant="icon"` draws no value, and therefore no slot beside one either: the
276
+ whole trigger is its `ui`.
277
+
278
+ ```jsx
279
+ <Picker variant="icon" ui={<DisketteSvg />} />
280
+ ```
281
+
282
+ Left out, that `ui` is the icon the slot would have shown — the chevron, or the
283
+ one the picker's type carries (a pencil for `type="text"`, a calendar for
284
+ `type="date"`), so `<Picker type="date" variant="icon" />` is a calendar and
285
+ nothing else. `rightSlotIcon`/`rightSlot` belong to the shapes that DO draw a
286
+ value and want something beside it; under `variant="icon"` the first is only the
287
+ default for `ui`, and the second has nowhere to go — the clear cross included.
288
+
289
+ ### When a shared popup is still the right answer
290
+
291
+ Two cases, and only two:
292
+
293
+ - **the press can come from anywhere** — a keyboard shortcut, a menu, a button,
294
+ all opening the same thing. Written per press it would exist several times
295
+ over, each with its own open state;
296
+ - **the popup has to outlive its trigger** — a row that leaves while its dialog
297
+ is open (a list refreshing under it) takes a popup written inside it with it.
298
+
299
+ Neither is "one popup per row of a list", which is what a picker is for.
300
+
301
+ Do not mix the two. A `<Form>` at the root of a picker's popup IS the mirrored
302
+ control, so its value is the picker's value: handing the picker something else
303
+ (a created profile, say) pushes it back down into the form's named fields and
304
+ comes back as the form's aggregate. When the popup does work, let the work keep
305
+ its result.
306
+
205
307
  ## Reacting to open and close
206
308
 
207
309
  `onOpen` is called on every open, before the popup builds anything (see
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.104",
3
+ "version": "0.29.106",
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.27",
32
+ "@jsenv/dom": "0.17.28",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.2"
35
35
  },