@jsenv/navi 0.29.398 → 0.29.400
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.
- package/dist/dev/jsenv_navi.js +1105 -273
- package/dist/dev/jsenv_navi.js.map +27 -26
- package/dist/jsenv_navi.js +1105 -273
- package/dist/jsenv_navi.js.map +27 -26
- package/docs/AI_INSTRUCTIONS.md +10 -4
- package/docs/control_value.md +52 -6
- package/docs/drag_to_travel.md +32 -0
- package/docs/dynamic_import.md +3 -2
- package/docs/navigation.md +81 -1
- package/docs/popup_open.md +44 -9
- package/docs/scroll.md +3 -3
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -71,9 +71,10 @@ to start when unsure which export solves a problem.
|
|
|
71
71
|
|
|
72
72
|
- `navigation.md` — the position of the user belongs in the URL by default,
|
|
73
73
|
and that decision is not retrofittable: routes, params, search params,
|
|
74
|
-
`<Route>`, tab rows, `navBack`, `RouteTravel`, where a navigation lands
|
|
75
|
-
`
|
|
76
|
-
say in its address. Read before
|
|
74
|
+
`<Route>`, tab rows, `navBack`, `RouteTravel`, where a navigation lands (a
|
|
75
|
+
`#id` landing on the element it names), a `SlideContainer` read from the URL,
|
|
76
|
+
and what a layer drawn over the screen may say in its address. Read before
|
|
77
|
+
writing any routing code, and before scrolling to an element on arrival.
|
|
77
78
|
- `route_transitions.md` — a transition states a relation the user reads as a
|
|
78
79
|
map; a pair of routes is animated by `RouteTravel` or by a transition, never
|
|
79
80
|
both; a test waits for the page arriving, never for its address. Read before
|
|
@@ -137,7 +138,9 @@ to start when unsure which export solves a problem.
|
|
|
137
138
|
`action`, `command` + `commandFor`). Read before writing any handler whose
|
|
138
139
|
body only assigns state.
|
|
139
140
|
- `control_value.md` — who holds a control's value; `--navi-update` for a
|
|
140
|
-
button proposing a value;
|
|
141
|
+
button proposing a value; a `defaultValue` follows the record it was read
|
|
142
|
+
from (no `key` to remount); drawing what a picker holds (`ui={Component}`,
|
|
143
|
+
not a signal of your own); empty keeps the shape of the question; what a
|
|
141
144
|
`stateSignal` brings; a time of day typed (`TimeSpin`) or turned
|
|
142
145
|
(`TimeWheel`). Read before wiring a value with `value` + `uiAction`.
|
|
143
146
|
- `control_object.md` — one value made of several controls: `ControlGroup`
|
|
@@ -259,6 +262,9 @@ to start when unsure which export solves a problem.
|
|
|
259
262
|
- **Routing is signal-based**: URL state (including search params) two-way
|
|
260
263
|
syncs with signals. Don't build parallel state for what a route/query
|
|
261
264
|
signal already tracks.
|
|
265
|
+
- **A link lands on an element by its fragment**: give the element an `id` and
|
|
266
|
+
link to `#id`. Never a search param plus `scrollIntoView` in an effect (see
|
|
267
|
+
`navigation.md`).
|
|
262
268
|
- **Actions** model async operations with a lifecycle (idle, running,
|
|
263
269
|
completed, failed, aborted). Components read an action's state via
|
|
264
270
|
`useAsyncData`, not by manually tracking loading/error booleans. Reading does
|
package/docs/control_value.md
CHANGED
|
@@ -10,6 +10,8 @@ somewhere else in the app.
|
|
|
10
10
|
- [A button that proposes a value is `--navi-update`](#a-button-that-proposes-a-value-is---navi-update)
|
|
11
11
|
- [`--navi-update:smooth`: the control is seen answering](#--navi-updatesmooth-the-control-is-seen-answering)
|
|
12
12
|
- [`signal` + `defaultValue`: the answer and where it starts](#signal--defaultvalue-the-answer-and-where-it-starts)
|
|
13
|
+
- [A `defaultValue` follows what it was read from](#a-defaultvalue-follows-what-it-was-read-from)
|
|
14
|
+
- [Drawing what a control holds](#drawing-what-a-control-holds)
|
|
13
15
|
- [What a signal holds, control by control](#what-a-signal-holds-control-by-control)
|
|
14
16
|
- [Empty keeps the shape of the question](#empty-keeps-the-shape-of-the-question)
|
|
15
17
|
- [Which controls take a `signal`](#which-controls-take-a-signal)
|
|
@@ -22,12 +24,12 @@ somewhere else in the app.
|
|
|
22
24
|
|
|
23
25
|
## The three answers
|
|
24
26
|
|
|
25
|
-
| what you pass | who holds the value | when the user acts
|
|
26
|
-
| ---------------------- | ------------------- |
|
|
27
|
-
| nothing | the control | it keeps it; `uiAction` tells you
|
|
28
|
-
| `defaultValue` | the control | same — the default is
|
|
29
|
-
| `signal` | the signal | the control writes it back, both ways
|
|
30
|
-
| `value` (or `checked`) | you | nothing moves until you hand a new one down
|
|
27
|
+
| what you pass | who holds the value | when the user acts |
|
|
28
|
+
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------- |
|
|
29
|
+
| nothing | the control | it keeps it; `uiAction` tells you |
|
|
30
|
+
| `defaultValue` | the control | same — the default is where it starts, and a new one is taken while nothing is edited |
|
|
31
|
+
| `signal` | the signal | the control writes it back, both ways |
|
|
32
|
+
| `value` (or `checked`) | you | nothing moves until you hand a new one down |
|
|
31
33
|
|
|
32
34
|
A control given `value` and nothing to listen to it (`uiAction`, `action`, a
|
|
33
35
|
`signal`, a surrounding form) is read-only, and says so in dev: it is showing
|
|
@@ -228,6 +230,50 @@ default rather than leaving it blank — which is what makes "nothing decided
|
|
|
228
230
|
here, use the usual answer" expressible at all. Without a `defaultValue`, an
|
|
229
231
|
emptied signal empties the control.
|
|
230
232
|
|
|
233
|
+
## A `defaultValue` follows what it was read from
|
|
234
|
+
|
|
235
|
+
A field editing a record starts on that record, `defaultValue={user.first_name}`,
|
|
236
|
+
and the record moves after mount: the save comes back normalized (a trailing
|
|
237
|
+
space trimmed), another screen or device changed it, a refresh arrived. The
|
|
238
|
+
control takes the new default **while it holds no edit** — nothing typed, picked
|
|
239
|
+
or toggled since the value the outside last accepted. An edit is never undone:
|
|
240
|
+
the default waits, and lands once that edit is accepted (the action succeeded)
|
|
241
|
+
or taken back (the action failed, the popup was cancelled).
|
|
242
|
+
|
|
243
|
+
This holds for a control, for a group given a `defaultValue` (a selectable list,
|
|
244
|
+
a `ControlGroup`), and for everything inside a picker's popup, which stays
|
|
245
|
+
mounted while closed. So the record goes in as `defaultValue` and nothing
|
|
246
|
+
else: no `key` to remount the popup when it changes. A key is worse than
|
|
247
|
+
unnecessary here, since it also destroys whatever is being typed when the
|
|
248
|
+
answer to ANOTHER field arrives.
|
|
249
|
+
|
|
250
|
+
A picker's cancel follows the same line. It puts back what the picker held at
|
|
251
|
+
open, which undoes the person's edits, but what the outside moved during the
|
|
252
|
+
opening stays moved.
|
|
253
|
+
|
|
254
|
+
## Drawing what a control holds
|
|
255
|
+
|
|
256
|
+
A picker's trigger usually shows the choice (a label for a code, a summary of
|
|
257
|
+
an object). Read it from the picker rather than keeping a copy:
|
|
258
|
+
|
|
259
|
+
```jsx
|
|
260
|
+
const VisibilityLabel = ({ value }) => <Text>{labelOf(value)}</Text>;
|
|
261
|
+
|
|
262
|
+
<Picker name="visibility" defaultValue={user.visibility} ui={VisibilityLabel}>
|
|
263
|
+
…
|
|
264
|
+
</Picker>;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`ui={Component}` is handed `value`, `loading` and `interactive`. A `ui` given as
|
|
268
|
+
an element reads the same three with `usePickerState()`.
|
|
269
|
+
|
|
270
|
+
What not to write: `const s = useSignal(defaultValue)` bound as `signal={s}`,
|
|
271
|
+
just to have something to read. `useSignal` reads its argument once, so the
|
|
272
|
+
signal never hears the record move, and a bound signal takes precedence over the
|
|
273
|
+
control's own `defaultValue`. The field is stuck on its first value. A signal is
|
|
274
|
+
for a value the app owns and writes itself (a url param, state shared between
|
|
275
|
+
screens), not a mirror kept for drawing.
|
|
276
|
+
|
|
231
277
|
## What a signal holds, control by control
|
|
232
278
|
|
|
233
279
|
The signal holds what the control is ABOUT, which is not always its `value`
|
package/docs/drag_to_travel.md
CHANGED
|
@@ -59,6 +59,10 @@ and are referenced from here rather than restated.
|
|
|
59
59
|
because the picture alone decided. Only a release at rest, or a slow retreat,
|
|
60
60
|
is judged by the picture — about a third of a box — because there the
|
|
61
61
|
position is the only witness left.
|
|
62
|
+
- **A travel caught in flight and thrown on the way it was going asks for the
|
|
63
|
+
one after.** Two swipes the same way are two screens, however close
|
|
64
|
+
together: the second one is not a vote for the screen already arriving. See
|
|
65
|
+
[below](#a-hand-reaching-for-something-still-moving-is-reaching-for-that-thing).
|
|
62
66
|
- **The bars those verdicts compare against sit well below the speed the
|
|
63
67
|
fingertip actually has, and they must.** Velocity is averaged over a trailing
|
|
64
68
|
window (pointer events arrive irregularly, and the last one before a release
|
|
@@ -449,6 +453,26 @@ Taking over means the pictures stop where they are and answer the finger again,
|
|
|
449
453
|
from where they stand (`slack`) rather than from zero. Only one box is in hand,
|
|
450
454
|
and walking out of either of its ends is a travel of its own — see below.
|
|
451
455
|
|
|
456
|
+
What letting go of it again means is read from the hand, three ways:
|
|
457
|
+
|
|
458
|
+
- **merely touched** — a press that moved less than a gesture's worth: nobody
|
|
459
|
+
asked the travel to stop, it was asked to wait, and it carries on to where it
|
|
460
|
+
was going;
|
|
461
|
+
- **thrown back** — against the way it was going, fast: everything goes back;
|
|
462
|
+
- **thrown on** — the way it was already going, fast, having moved by more than
|
|
463
|
+
a tremor: that is a second push, and it asks for the screen AFTER the one
|
|
464
|
+
arriving (`thrownOn`). Read as a verdict on the box in hand, it only confirms
|
|
465
|
+
what was already arriving — the travel stops at the press and goes on to the
|
|
466
|
+
same place, and the hand feels its second swipe swallowed. So the gesture
|
|
467
|
+
asks for the next box at the release (`onEdge` with `thrown`), with the
|
|
468
|
+
picture still short of the end of the one in hand, and nothing on screen
|
|
469
|
+
moves for it: the travel goes on from where the hand left it, to one screen
|
|
470
|
+
further. It is the wheel's rule (a second push over the tail is a screen, not
|
|
471
|
+
credit towards one) said for a hand.
|
|
472
|
+
|
|
473
|
+
A slow hand going the same way and stopping before it lets go is not thrown:
|
|
474
|
+
there the picture decides, and the screen arriving arrives.
|
|
475
|
+
|
|
452
476
|
### A hand that does not stop at the end of a page is asking for the next one
|
|
453
477
|
|
|
454
478
|
One travel brings in one neighbour, but a gesture is not over because a travel
|
|
@@ -470,6 +494,14 @@ The two ends cost differently, and it is worth knowing which one is being felt:
|
|
|
470
494
|
one instead. The travel turns around where it stands, on the same transition,
|
|
471
495
|
and there is no gap at all.
|
|
472
496
|
|
|
497
|
+
That cost is a `RouteTravel` one: a `SlideContainer` has every slide mounted,
|
|
498
|
+
so handing over is only re-staging — the slide reached becomes the one dragged,
|
|
499
|
+
the one left stays beside it, the slide past it is put on the other side, and
|
|
500
|
+
nothing on screen moves. Which is also why only a `SlideContainer` answers a
|
|
501
|
+
throw (`thrown`, above): a `RouteTravel` would have to jump its pictures the
|
|
502
|
+
rest of the way before handing over, a page seen leaping, so it declines and
|
|
503
|
+
the page caught simply arrives.
|
|
504
|
+
|
|
473
505
|
What the browser will not turn around with it is everything ELSE the
|
|
474
506
|
transition carries — see [One gesture that bar cannot
|
|
475
507
|
follow](#one-gesture-that-bar-cannot-follow).
|
package/docs/dynamic_import.md
CHANGED
|
@@ -146,8 +146,9 @@ by the code arriving.
|
|
|
146
146
|
`prefetch={false}` opts one out. The url is matched against the routes on
|
|
147
147
|
screen, a section and the page inside it alike, and what is prerun is **every
|
|
148
148
|
route action that asks nothing of the address** — a page's code, a read
|
|
149
|
-
without params. An action whose params come from the address is
|
|
150
|
-
prefetched: a prefetch has no address, the id may still be
|
|
149
|
+
without params. An action whose params come from the address is not
|
|
150
|
+
prefetched by a link: a link's prefetch has no address, the id may still be
|
|
151
|
+
chosen.
|
|
151
152
|
`routeAction(route, action, undefined, { prefetch: false })` keeps a
|
|
152
153
|
param-less read that is not worth a hover out of it.
|
|
153
154
|
|
package/docs/navigation.md
CHANGED
|
@@ -16,6 +16,7 @@ linking to them, and turning them into tabs.
|
|
|
16
16
|
- [The back arrow: `navBack`](#the-back-arrow-navback)
|
|
17
17
|
- [Tabs that travel: `RouteTravel`](#tabs-that-travel-routetravel)
|
|
18
18
|
- [Where a navigation lands: the scroll](#where-a-navigation-lands-the-scroll)
|
|
19
|
+
- [Landing on an element: the fragment](#landing-on-an-element-the-fragment)
|
|
19
20
|
- [Creating something, then editing it](#creating-something-then-editing-it)
|
|
20
21
|
- [Tabs that are not routes](#tabs-that-are-not-routes)
|
|
21
22
|
- [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
@@ -694,7 +695,7 @@ the two, never both: see [route_transitions.md](./route_transitions.md).
|
|
|
694
695
|
|
|
695
696
|
## Where a navigation lands: the scroll
|
|
696
697
|
|
|
697
|
-
|
|
698
|
+
Five cases, and they are not a policy to configure but five different facts:
|
|
698
699
|
|
|
699
700
|
- **Going somewhere new** (a `<Link>`, anything that pushes to another path)
|
|
700
701
|
lands at the top. It is an arrival: the offset one had elsewhere means nothing
|
|
@@ -704,6 +705,10 @@ Four cases, and they are not a policy to configure but four different facts:
|
|
|
704
705
|
reader is scrolled in — a [layer](#a-layer-over-the-screen-what-its-address-may-say)
|
|
705
706
|
opening, a [state whose values are places](#a-state-whose-values-are-places-history-push)
|
|
706
707
|
written — and moves nothing, like a replace.
|
|
708
|
+
- **Going somewhere new, to an element** (`/places/le-set#tournaments`) lands
|
|
709
|
+
on the element whose `id` is the fragment, once it is rendered — even when it
|
|
710
|
+
arrives with the data a second later. See
|
|
711
|
+
[Landing on an element](#landing-on-an-element-the-fragment).
|
|
707
712
|
- **Going back or forward** (the browser's buttons, `navBack()`,
|
|
708
713
|
`history.back()`) lands where that page was left. navi keeps the position and
|
|
709
714
|
puts it back once the page is really rendered, which is what the browser
|
|
@@ -732,6 +737,81 @@ in a browser with no Navigation API — is `navBack()`. Where there may be
|
|
|
732
737
|
nothing to go back to (a shared link opened cold), decide what the arrow does
|
|
733
738
|
from the history, not from the link.
|
|
734
739
|
|
|
740
|
+
### Landing on an element: the fragment
|
|
741
|
+
|
|
742
|
+
A link meant to bring one element under the reader's eyes — a notification
|
|
743
|
+
pointing at a section several screens down, a shared link to a comment, a row
|
|
744
|
+
in a list — is an `id` on the element and a `#id` in the link. Nothing else:
|
|
745
|
+
|
|
746
|
+
```jsx
|
|
747
|
+
<Box id="tournaments">…</Box>
|
|
748
|
+
|
|
749
|
+
// anywhere else
|
|
750
|
+
<Link href={`${PLACE_ROUTE.buildUrl({ slug })}#tournaments`}>…</Link>
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
`<Link route>` builds no fragment, so the link spells its `href`.
|
|
754
|
+
|
|
755
|
+
The browser alone is not enough in an app. It answers a fragment when the
|
|
756
|
+
document finishes loading and on a fragment navigation within the page — both
|
|
757
|
+
before the data has drawn the element — and a navigation navi routes is not a
|
|
758
|
+
fragment navigation at all. So navi answers every URL carrying a hash itself:
|
|
759
|
+
|
|
760
|
+
- **It waits for the element, and for the page.** The element is reached when
|
|
761
|
+
it exists and shows something (`checkVisibility`): one still loading, or
|
|
762
|
+
rendered inside a closed tab or a folded `<details>`, has not arrived yet.
|
|
763
|
+
navi opens nothing to reach it, and reaches it as soon as it shows — the
|
|
764
|
+
reader opening the `<details>`, switching to the tab — while the wait lasts.
|
|
765
|
+
And it is reached once no route or action is loading: until then, what sits
|
|
766
|
+
above it may still be drawn and push it down — and a page reusing the node of
|
|
767
|
+
the page being left (`/places/a#tournaments` → `/places/b#tournaments`) holds
|
|
768
|
+
it before its own data has come.
|
|
769
|
+
- **It gives up.** Once the document has had no route or action loading for
|
|
770
|
+
`graceAfterIdle` (1 s). A link to an element that is gone lands where a link
|
|
771
|
+
without a fragment would — at the top of a page one arrives at — with no
|
|
772
|
+
mark. A document that never stops working is answered after `maxWait`
|
|
773
|
+
(10 s), with the element as it stands.
|
|
774
|
+
- **A return is not an arrival.** Back, forward and a reload land where the
|
|
775
|
+
page was left, as they do without a fragment; the fragment then only puts
|
|
776
|
+
`:target` on its element — no scroll, no ring, no focus.
|
|
777
|
+
- **It answers once per arrival.** What is watched is the path and the hash: a
|
|
778
|
+
search param written while the reader is there (a filter, a page) does not
|
|
779
|
+
throw them back to the element. Pressing the very link one is on answers
|
|
780
|
+
again.
|
|
781
|
+
- **What the reader gets**: the element against the top edge, instantly (what
|
|
782
|
+
the browser does with a fragment); the keyboard focus on it when it is itself
|
|
783
|
+
focusable — an `id` on the link or the button, not on a box around one; and a
|
|
784
|
+
fading ring, the element carrying `data-url-target` for `markDuration` (2 s).
|
|
785
|
+
The ring is coloured by `--navi-url-target-color` and lives in `@layer navi`:
|
|
786
|
+
an unlayered `[data-url-target] { … }` replaces it, `animation: none` removes
|
|
787
|
+
it. Its length is `markDuration`, which also publishes
|
|
788
|
+
`--navi-url-target-duration` to CSS — setting only the variable makes the ring
|
|
789
|
+
and the attribute disagree.
|
|
790
|
+
- **Adjusting it**: `setUrlTargetOptions()`, once, at the app's start — the
|
|
791
|
+
alignment (`block`), `behavior`, `markDuration`, `graceAfterIdle`, `maxWait`.
|
|
792
|
+
|
|
793
|
+
**`:target` is the lasting "this one"**, and `data-url-target` the moment it
|
|
794
|
+
arrived. navi sets `:target` on the element as it lands on it — the browser
|
|
795
|
+
alone never would after a navigation navi routes, nor for an element that
|
|
796
|
+
arrived after the load — and takes it off once the URL stops naming the
|
|
797
|
+
element, which the browser alone would not either: a search param written on
|
|
798
|
+
the page rebuilds the address without its fragment, a back returns to the page
|
|
799
|
+
without one. It stays on the element navi landed on; a node rendered in its
|
|
800
|
+
place later is another one. In JS, `useUrlTargetId()` is the id the hash
|
|
801
|
+
designates, `""` when there is none, re-rendering when it changes.
|
|
802
|
+
|
|
803
|
+
#### A fragment, or a search param
|
|
804
|
+
|
|
805
|
+
A fragment says where to look; a search param says what to show. When the page
|
|
806
|
+
draws something differently for the value — a filter, a selected row, an open
|
|
807
|
+
panel — it is a [search param](#search-params). When the page is the same and
|
|
808
|
+
only the reader's eyes should go somewhere, it is a fragment, down to a single
|
|
809
|
+
row (`#tournament_42`).
|
|
810
|
+
|
|
811
|
+
A search param plus a `scrollIntoView` in an effect rebuilds this, less well:
|
|
812
|
+
the effect fires at mount, before what sits above the element has laid out,
|
|
813
|
+
and nothing ever tells it to stop waiting.
|
|
814
|
+
|
|
735
815
|
### A row of tabs, where a replace IS an arrival
|
|
736
816
|
|
|
737
817
|
The tabs of a `<RouteTravel>` navigate by replacing, and yet each one is another
|
package/docs/popup_open.md
CHANGED
|
@@ -1008,7 +1008,8 @@ slots".
|
|
|
1008
1008
|
Being open is where the user is, and what a popup draws belongs to the screen
|
|
1009
1009
|
exactly the way a page's own data does. So a popup that loads something binds
|
|
1010
1010
|
its open state (above) and asks for its data with a `routeAction` whose params
|
|
1011
|
-
are `false` while it is closed
|
|
1011
|
+
are `false` while it is closed — and its content reads the question itself,
|
|
1012
|
+
without the part that says whether it is open:
|
|
1012
1013
|
|
|
1013
1014
|
```js
|
|
1014
1015
|
const groupSheetOpenSignal = stateSignal(false, {
|
|
@@ -1016,12 +1017,17 @@ const groupSheetOpenSignal = stateSignal(false, {
|
|
|
1016
1017
|
type: "boolean",
|
|
1017
1018
|
});
|
|
1018
1019
|
|
|
1020
|
+
// asked while the sheet is open
|
|
1019
1021
|
export const GROUP_MEMBERS = routeAction(GAME_ROUTE, USER.GET_MANY, () => {
|
|
1020
1022
|
if (!groupSheetOpenSignal.value) {
|
|
1021
1023
|
return false;
|
|
1022
1024
|
}
|
|
1023
1025
|
return { group: groupSignal.value };
|
|
1024
1026
|
});
|
|
1027
|
+
// read by the sheet, open or closed
|
|
1028
|
+
export const GROUP_MEMBERS_SHOWN = USER.GET_MANY.bindParams({
|
|
1029
|
+
group: groupSignal,
|
|
1030
|
+
});
|
|
1025
1031
|
```
|
|
1026
1032
|
|
|
1027
1033
|
```jsx
|
|
@@ -1030,15 +1036,16 @@ export const GROUP_MEMBERS = routeAction(GAME_ROUTE, USER.GET_MANY, () => {
|
|
|
1030
1036
|
</Dialog>;
|
|
1031
1037
|
|
|
1032
1038
|
const GroupMembers = () => {
|
|
1033
|
-
const [members] = useAsyncData(
|
|
1039
|
+
const [members] = useAsyncData(GROUP_MEMBERS_SHOWN); // reads, never runs
|
|
1034
1040
|
…
|
|
1035
1041
|
};
|
|
1036
1042
|
```
|
|
1037
1043
|
|
|
1038
1044
|
The same shape when the popup is one of many — a sheet on every card, opened
|
|
1039
1045
|
from wherever the card is read. Its open state then says which one, and the
|
|
1040
|
-
route action reads the id straight from it
|
|
1041
|
-
card is not a
|
|
1046
|
+
route action reads the id straight from it, while each card's content reads the
|
|
1047
|
+
question of its own card. Declared on the root route, since a card is not a
|
|
1048
|
+
page:
|
|
1042
1049
|
|
|
1043
1050
|
```js
|
|
1044
1051
|
const seatSheetSignal = stateSignal(undefined, {
|
|
@@ -1048,18 +1055,39 @@ const seatSheetSignal = stateSignal(undefined, {
|
|
|
1048
1055
|
});
|
|
1049
1056
|
const ANY_PAGE = route("/", { searchParams: { seat: seatSheetSignal } });
|
|
1050
1057
|
|
|
1058
|
+
export const seatableUsersOf = (gameId) => ({ game: gameId });
|
|
1051
1059
|
export const SEATABLE_USERS = routeAction(ANY_PAGE, USER.GET_MANY, () => {
|
|
1052
1060
|
const gameId = seatSheetSignal.value;
|
|
1053
|
-
return gameId ?
|
|
1061
|
+
return gameId ? seatableUsersOf(gameId) : false;
|
|
1054
1062
|
});
|
|
1055
1063
|
```
|
|
1056
1064
|
|
|
1057
1065
|
```jsx
|
|
1058
1066
|
<Dialog signal={seatSheetSignal} value={game.id}>
|
|
1059
|
-
<SeatableUsers />
|
|
1060
|
-
</Dialog
|
|
1067
|
+
<SeatableUsers gameId={game.id} />
|
|
1068
|
+
</Dialog>;
|
|
1069
|
+
|
|
1070
|
+
const SeatableUsers = ({ gameId }) => {
|
|
1071
|
+
const [users] = useAsyncData(
|
|
1072
|
+
USER.GET_MANY.bindParams(seatableUsersOf(gameId)),
|
|
1073
|
+
);
|
|
1074
|
+
…
|
|
1075
|
+
};
|
|
1061
1076
|
```
|
|
1062
1077
|
|
|
1078
|
+
**The content reads its question, never the route action.** The route action's
|
|
1079
|
+
params are `false` while the popup is closed, and for the route action that is
|
|
1080
|
+
right: it says _when_ to ask. Read by the content, the same `false` says there
|
|
1081
|
+
is nothing to show — `useAsyncData` hands back `undefined` the moment the popup
|
|
1082
|
+
closes, the list empties while hidden, and the next opening builds it again from
|
|
1083
|
+
the top: the scroll position is lost, and every row is rendered once more for an
|
|
1084
|
+
answer that never left the store. When the popup is one of many, it is worse: the
|
|
1085
|
+
route action follows whichever popup is open, so a card's sheet kept mounted
|
|
1086
|
+
since an earlier opening redraws with another card's rows each time that one
|
|
1087
|
+
opens. Closing a popup does not change what it is about, so what it reads leaves
|
|
1088
|
+
the open state out. Equal params share one instance — the content reads the very
|
|
1089
|
+
one the route action runs, so nothing is asked twice and nothing is copied.
|
|
1090
|
+
|
|
1063
1091
|
**A popup that waits holds its own `<Loading>`**, like every other part of a
|
|
1064
1092
|
screen that can wait — it is not built into `Dialog`/`Popover`, because only the
|
|
1065
1093
|
code inside knows whether anything in there loads at all:
|
|
@@ -1116,7 +1144,8 @@ become a popup's private request — asked for late, and asked for alone.
|
|
|
1116
1144
|
|
|
1117
1145
|
A closed popup builds nothing: `children` are mounted on the first open, and
|
|
1118
1146
|
stay mounted afterwards — a reopened popup finds its scroll position and its
|
|
1119
|
-
half-typed form where it left them
|
|
1147
|
+
half-typed form where it left them, as long as the data it draws does not leave
|
|
1148
|
+
with the close (see [A popup that loads data](#a-popup-that-loads-data)).
|
|
1120
1149
|
|
|
1121
1150
|
"Closed" is two states, not one — never opened yet, and closed again after an
|
|
1122
1151
|
opening — so the `mount` prop answers both at once:
|
|
@@ -1134,7 +1163,13 @@ off it, fields a surrounding form submits, a size measured from outside.
|
|
|
1134
1163
|
|
|
1135
1164
|
Whatever the value but `"while-opened"`, intent on the anchor — a pointer
|
|
1136
1165
|
entering it, focus landing in it — builds the content ahead of the click that
|
|
1137
|
-
will open it.
|
|
1166
|
+
will open it. A `"while-opened"` popup is warmed only by a press on what opens
|
|
1167
|
+
it — a `--navi-open` button, a picker's trigger when a press opens it, an
|
|
1168
|
+
expandable's UI part — and a press that ends without opening it (a scroll, a
|
|
1169
|
+
drag, a release elsewhere) throws the content away: there is still one content
|
|
1170
|
+
at a time. A popup with an `onOpen` is never warmed, so the content is always
|
|
1171
|
+
built after it (see
|
|
1172
|
+
[`onOpen` runs before the popup has built anything](#onopen-runs-before-the-popup-has-built-anything)).
|
|
1138
1173
|
|
|
1139
1174
|
`"while-opened"` is what an uncontrolled field seeded from a `defaultValue`
|
|
1140
1175
|
needs: without it, a popup reopened after the underlying value changed still
|
package/docs/scroll.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
Where scrolling happens in a navi app, and how the pieces that live inside a
|
|
4
4
|
scrolling area (`Box header/body/footer`, `List`, a popup) are told about it.
|
|
5
5
|
|
|
6
|
-
Where a navigation LANDS is another subject — a push at the top, a
|
|
7
|
-
|
|
8
|
-
it was read at: see
|
|
6
|
+
Where a navigation LANDS is another subject — a push at the top, a push with a
|
|
7
|
+
`#id` on the element it names, a back or forward where the page was left, a row
|
|
8
|
+
of tabs giving each tab back the offset it was read at: see
|
|
9
9
|
[navigation.md](./navigation.md#where-a-navigation-lands-the-scroll).
|
|
10
10
|
|
|
11
11
|
- [What makes header/body/footer work: the overflow](#what-makes-headerbodyfooter-work-the-overflow)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.400",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"preact": "^11.0.0-rc.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/dom": "0.17.
|
|
37
|
+
"@jsenv/dom": "0.17.76",
|
|
38
38
|
"@jsenv/humanize": "1.8.5",
|
|
39
39
|
"@jsenv/validity": "0.4.5"
|
|
40
40
|
},
|