@jsenv/navi 0.29.425 → 0.29.426

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.
@@ -116,8 +116,11 @@ start when unsure which export solves a problem.
116
116
  keeps the shape of the question. Before wiring a value with `value` +
117
117
  `uiAction`.
118
118
  - `control_object.md` — one value made of several controls: `ControlGroup` versus
119
- `Form`, naming, `<Picker type="object">`, a settings sheet. Before putting
120
- anything in a picker popup.
119
+ `Form`, naming, `<Picker type="object">`, a settings sheet, controls that are
120
+ not there (lazy slides, popups, deferred boxes, items outside the render
121
+ window: the group keeps their keys, shared state is written through it).
122
+ Before putting anything in a picker popup, and before lazy-mounting anything
123
+ that holds named controls.
121
124
  - `group.md` — `<Group>`: several controls reading as one framed object. Before
122
125
  negative margins or `border-radius: 0` by hand.
123
126
  - `form_changed.md` — a form sends nothing when nothing changed; what "changed"
@@ -179,9 +182,10 @@ start when unsure which export solves a problem.
179
182
  - `scroll.md` — where scrolling happens: `header`/`body`/`footer`, a `List`'s
180
183
  `scroller`, where a list opens (a position handed back whole, `visibleCount`
181
184
  included), a popup that scrolls, many rows (`<List.Items>`, the render window
182
- and its first picture, `findText`), hover while scrolling. Before writing CSS
183
- to make something scroll, and before rendering a collection as `<List.Item>`
184
- children.
185
+ and its first picture, `findText`), many sections (`<Box mount="after-paint">`
186
+ and what it leaves out of reach), hover while scrolling. Before writing CSS to
187
+ make something scroll, before rendering a collection as `<List.Item>`
188
+ children, and before deferring part of a page.
185
189
  - `z_index.md` — DOM order first; a `z-index` without `isolation: isolate`
186
190
  competes with the page; navi's bands. Before writing a `z-index`.
187
191
  - `typography.md` — text is a component; `maxLines` is the one truncation; one
@@ -12,6 +12,7 @@ whose value is an object needs in its popup.
12
12
  - [Naming, and what a nameless group does](#naming-and-what-a-nameless-group-does)
13
13
  - [A picker whose value is an object](#a-picker-whose-value-is-an-object)
14
14
  - [A group holds what it was given](#a-group-holds-what-it-was-given)
15
+ - [A control that is not there](#a-control-that-is-not-there)
15
16
  - [One line, one key](#one-line-one-key)
16
17
  - [A settings sheet](#a-settings-sheet)
17
18
  - [`Group` is not `ControlGroup`](#group-is-not-controlgroup)
@@ -142,7 +143,9 @@ not while the children are not all there yet:
142
143
 
143
144
  - a group **told** a value holds it whole, before any child has registered to
144
145
  show it: items still loading, a popup built at open, a row scrolled out of a
145
- virtualized list do not make the value smaller;
146
+ virtualized list do not make the value smaller, nor does a child acting while
147
+ others are away (see
148
+ [A control that is not there](#a-control-that-is-not-there));
146
149
  - **a child mounting or unmounting is not somebody answering.** While children
147
150
  arrive, their aggregate is a partial reading; the group takes it for its value
148
151
  only once it derived that value itself (nobody handed it one), or once a child
@@ -209,6 +212,76 @@ const moveTo = (from, to) => {
209
212
  };
210
213
  ```
211
214
 
215
+ ## A control that is not there
216
+
217
+ The answer lives in the group — and in the signal bound to it, or to the
218
+ picker around it — and the controls are views of it. A view can be missing
219
+ while the answer still matters: a slide not built yet
220
+ (`SlideContainer mount="near"`), a popup or an `Expandable` not opened yet, a
221
+ `<Box mount="after-paint">` before its first frame, an item outside a list's
222
+ render window, a page parked while a route action reruns. None of that makes
223
+ the answer smaller:
224
+
225
+ - **a key stays until a control of that name, there, says otherwise.** The
226
+ group keeps what it holds for a control that is not there, the way a
227
+ selectable list keeps the selection of items it does not draw. A control that
228
+ arrives is placed from it; one that leaves takes nothing with it;
229
+ - **a value put ON the group reaches every key**, whether its control is there
230
+ or not: the group's `signal` or `value`, a picker filling its popup, a cancel
231
+ putting back what the picker held at open, a `--navi-update` aimed at the
232
+ group. It replaces the whole object, which is also how a key is dropped on
233
+ purpose;
234
+ - **a control's own `signal` reaches that control only.** Written while the
235
+ control is not there, nobody is listening: the group never hears of it, and a
236
+ cancel has nothing to take back. The answer is ONE signal, on the group or on
237
+ the picker holding it — never one per control, assembled by hand;
238
+ - **only a control that is there checks itself.** `required`, a pattern, a range
239
+ are verified by the control, so a field not built yet is not validated;
240
+ - **a field hidden on purpose keeps its key too**: the group cannot tell
241
+ "removed" from "not built yet". Empty it before hiding it, or put the value
242
+ without it on the group.
243
+
244
+ So something shared by controls that are not all there — seven days of which
245
+ one is on screen, a copy of one day onto the others — is written as one value,
246
+ through the group:
247
+
248
+ ```jsx
249
+ // the week, keyed by day: what the tabs, the summary and the save read
250
+ const weekSignal = useSignal({ 1: [], 2: [18, 19], 3: [], … });
251
+
252
+ <Picker type="object" signal={weekSignal} action={saveWeek} ui={<WeekSummary />}>
253
+ <ControlGroup id="week">
254
+ <SlideContainer mount="near" signal={daySignal}>
255
+ {DAYS.map((day) => (
256
+ <Slide key={day} area={day}>
257
+ <List selectable multiple name={day}>…</List>
258
+ </Slide>
259
+ ))}
260
+ </SlideContainer>
261
+ {/* every day, the ones not built included — and a cancel takes it back */}
262
+ <Button
263
+ command="--navi-update"
264
+ commandFor="week"
265
+ value={sameEveryDay(weekSignal.value[daySignal.value])}
266
+ >
267
+ Same hours every day
268
+ </Button>
269
+ </ControlGroup>
270
+ </Picker>
271
+ ```
272
+
273
+ ```jsx
274
+ // ✗ one signal per day, written by hand: the days not built never hear it,
275
+ // and Escape leaves them changed
276
+ for (const day of DAYS) {
277
+ daySignals[day].value = [...shownHours];
278
+ }
279
+ ```
280
+
281
+ The picker's signal follows the popup gesture by gesture, so the button reads
282
+ the day shown from it; the dots on the tabs read it too, for days whose slide
283
+ was never built.
284
+
212
285
  ## One line, one key
213
286
 
214
287
  A row that opens a popup is one control, so what it answers arrives under its
@@ -274,7 +347,8 @@ one being tried.
274
347
 
275
348
  **Every field answers**, including those on another tab: they stay mounted and
276
349
  come back in the object, and whoever receives it reads `origin` to know which
277
- count. Aggregating the sheet to a single value of its own
350
+ count. Under `mount="near"` a tab not built yet answers with what the group
351
+ holds for it ([A control that is not there](#a-control-that-is-not-there)). Aggregating the sheet to a single value of its own
278
352
  (`aggregateChildStates`) throws away what makes the answer readable, starting
279
353
  with the tab. **A button inside is not a field**: the one asking for a position
280
354
  acts on the press, and what it answers goes into a control that IS one (a
@@ -565,7 +565,10 @@ of the document's scrollport — it holds an offset even without a scrollbar, wh
565
565
  `overflow: clip` holds none — so it is the first thing to look for when a row
566
566
  does not give positions back. And a page whose height depends on something still
567
567
  loading is not tall enough when its position is put back: only the page knows
568
- when it is whole.
568
+ when it is whole. What navi itself builds after the first frame
569
+ (`<Box mount="after-paint">`, see
570
+ [scroll.md](./scroll.md#many-sections-box-mountafter-paint)) is not such a
571
+ page: the part the position shows is built before it is written.
569
572
 
570
573
  ## Creating something, then editing it
571
574
 
@@ -895,9 +895,12 @@ opening — so the `mount` prop answers both at once:
895
895
  | `"while-opened"` | not mounted | not mounted |
896
896
 
897
897
  `"always"` is for content something depends on before any opening: a value read
898
- off it (the default of a picker told no value whose value is read), fields a surrounding form
899
- submits, a size measured from outside. `"idle"` is `"always"` minus the cost on
900
- the render that draws the page.
898
+ off it (the default of a picker told no value whose value is read), fields a
899
+ surrounding form must validate or read their own `defaultValue` from, a size
900
+ measured from outside. What such a form was handed it keeps for fields not built
901
+ yet, and hands them once they are
902
+ ([control_object.md](./control_object.md#a-control-that-is-not-there)).
903
+ `"idle"` is `"always"` minus the cost on the render that draws the page.
901
904
 
902
905
  Whatever the value but `"while-opened"`, intent on what opens the popup builds
903
906
  the content ahead of the click, `onOpen` or not: a pointer over its anchor, or
package/docs/scroll.md CHANGED
@@ -307,6 +307,11 @@ A run whose rows all fit the window is just rows: nothing is virtualized, and
307
307
  it costs what the same rows would as children. There is no reason to hold back
308
308
  from it for a list that might grow.
309
309
 
310
+ What an item outside the window holds is not lost with its row: a selection
311
+ keeps the items it does not draw (`--navi-select` reaches them by id), and a
312
+ group keeps the keys of named controls that are not there
313
+ ([control_object.md](./control_object.md#a-control-that-is-not-there)).
314
+
310
315
  ### What the list knows, and what it guesses
311
316
 
312
317
  A virtualized list has one aim: draw as few items as it can, and never show a
@@ -433,6 +438,54 @@ on the run when the collection changes as a whole (never to refresh it, see
433
438
  searched), `src/control/demos/integration/1_list_loaded_by_scroll_demo.html` (a
434
439
  run reading a slice at a time).
435
440
 
441
+ ## Many sections: `<Box mount="after-paint">`
442
+
443
+ The render window's twin for a page that is not a list — a profile, a settings
444
+ screen, sections stacked in a column. The browser paints nothing until the
445
+ render that builds the page has ended, and the sections below the first screen
446
+ weigh in it as much as those on it.
447
+
448
+ ```jsx
449
+ <Box flex="y" spacing="l">
450
+ <ProfileHeader user={user} />
451
+ <UpcomingGames user={user} />
452
+ <Box flex="y" spacing="l" mount="after-paint">
453
+ <Searches user={user} />
454
+ <Badges user={user} />
455
+ <LinkedAccounts />
456
+ </Box>
457
+ </Box>
458
+ ```
459
+
460
+ The box is empty in the render that mounts it, and built right after the first
461
+ frame that shows the page (the first frame of its route transition, when there
462
+ is one), then kept: a later render of the page renders it as usual.
463
+
464
+ Which sections lie below the screen is not left to the app's guess, which a
465
+ taller phone or an empty first section breaks. navi looks at each box before
466
+ that frame is painted, and one the frame would show — on the screen, or above
467
+ it — is built before the paint all the same: marking too much costs a second
468
+ render in that frame, never a section popping in. A page put back where it was
469
+ left (see
470
+ [navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)) has the
471
+ boxes that offset shows built before the offset is written, so the offset is not
472
+ clamped by a document still missing its bottom.
473
+
474
+ What navi cannot do is make the content exist before it is built. So the box is
475
+ only for content nothing reaches during that first frame:
476
+
477
+ - **Named controls** inside are not there until built: what a group holds for
478
+ them waits, a value written into their own `signal` reaches nothing, and
479
+ nothing inside checks itself (see
480
+ [control_object.md](./control_object.md#a-control-that-is-not-there)).
481
+ - **Ids**: a `commandFor`, a popup `anchor` or a `triggerNaviCommand` aimed
482
+ inside finds nothing, and neither does a focus put back there. A `#fragment`
483
+ aimed inside lands a frame late, once the element is there; a popup opened
484
+ from the address opens once built, since it reads its signal then.
485
+ - **Size**: the box has no height until it is built (a `minHeight` reserves
486
+ one). Anything measuring it in that frame — a sticky offset, a size read from
487
+ outside — sees it empty.
488
+
436
489
  ## Hover while scrolling
437
490
 
438
491
  A scroll moves the content under a pointer that does not move, and the browser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.425",
3
+ "version": "0.29.426",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {