@jsenv/navi 0.29.417 → 0.29.419
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/README.md +1 -1
- package/dist/dev/jsenv_navi.js +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +291 -454
- package/docs/drag_to_travel.md +152 -748
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +170 -326
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/popup_open.md
CHANGED
|
@@ -26,63 +26,36 @@ Where the focus goes once it is open is its own subject — see
|
|
|
26
26
|
A `Dialog`/`Popover` with no `open` prop keeps its own open state and listens
|
|
27
27
|
for requests to change it. That is the default way to use one, and it buys
|
|
28
28
|
something a `useState` in the parent cannot give back: **a popup refuses to
|
|
29
|
-
close while a control inside it is mid-action**.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
A form that is sending holds an answer that is neither committed nor given up.
|
|
41
|
-
Escape, the backdrop, a close button — all of them ask, and the busy control
|
|
42
|
-
answers, the same way it would answer anyone else.
|
|
43
|
-
|
|
44
|
-
That is the right answer for a run bounded by a request that comes back. A run
|
|
45
|
-
deliberately left going is the other case — activating a service worker update,
|
|
46
|
-
which lands only once the browser switches over and can take as long as the
|
|
47
|
-
page's own in-flight work takes. Nothing is held there: the app stays usable
|
|
48
|
-
while it settles, and a panel that waits for it is a panel that may never close
|
|
49
|
-
again. The control says so, and no popup around it is told:
|
|
50
|
-
|
|
51
|
-
```jsx
|
|
52
|
-
<Button action={() => activateUpdate()} actionStandalone>
|
|
53
|
-
Activate
|
|
54
|
-
</Button>
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
It is still busy for itself — the spinner, the second press refused, the error
|
|
58
|
-
callout — which is the whole point of keeping `action` (see
|
|
29
|
+
close while a control inside it is mid-action**. A form that is sending holds an
|
|
30
|
+
answer that is neither committed nor given up. Escape, the backdrop, a close
|
|
31
|
+
button — all of them ask, and the busy control answers, the same way it would
|
|
32
|
+
answer anyone else.
|
|
33
|
+
|
|
34
|
+
A run deliberately left going — activating a service worker update, which lands
|
|
35
|
+
only once the browser switches over — must not hold a panel that may then never
|
|
36
|
+
close. It says `actionStandalone`: still busy for itself, and no popup around it
|
|
37
|
+
is told (see
|
|
59
38
|
[interactions.md](./interactions.md#the-fourth-question-whose-wait-is-it)).
|
|
60
39
|
|
|
61
|
-
Between the two sits a run that IS being waited on and may never end
|
|
62
|
-
over a network that stopped answering settles neither way, and the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
it:
|
|
40
|
+
Between the two sits a run that IS being waited on and may never end: a request
|
|
41
|
+
over a network that stopped answering settles neither way, and the popup no
|
|
42
|
+
longer closes until the page is reloaded. `actionAbortable` gives that hold a
|
|
43
|
+
release — the person waiting decides the answer is not coming, and closing is
|
|
44
|
+
how they say it:
|
|
67
45
|
|
|
68
46
|
```jsx
|
|
69
47
|
<Form action={saveScore} actionAbortable>
|
|
70
48
|
```
|
|
71
49
|
|
|
72
|
-
Every close request then goes through and calls the run off on the way out
|
|
73
|
-
Escape, the backdrop, the cross, the phone's back gesture
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
What it costs is written down: aborting frees the client and nothing more, so
|
|
80
|
-
the write may have landed anyway (see
|
|
81
|
-
[actions.md](./actions.md#aborting-saves-resources-it-does-not-undo)). Say it
|
|
50
|
+
Every close request then goes through and calls the run off on the way out —
|
|
51
|
+
Escape, the backdrop, the cross, the phone's back gesture alike: the cancel/keep
|
|
52
|
+
distinction below is about the VALUE a popup holds, and once the answer is on
|
|
53
|
+
the wire each of them means "I want out of here". Aborting frees the client and
|
|
54
|
+
nothing more, so the write may have landed anyway (see
|
|
55
|
+
[actions.md](./actions.md#aborting-saves-resources-it-does-not-undo)): say it
|
|
82
56
|
where the screen can be re-opened on what is actually there, never where the
|
|
83
|
-
popup is the only place the outcome could be read.
|
|
84
|
-
|
|
85
|
-
given up on and one that may not still refuses, since calling off half of them
|
|
57
|
+
popup is the only place the outcome could be read. A popup holding one run that
|
|
58
|
+
may be given up on and one that may not still refuses: calling off half of them
|
|
86
59
|
would cost an answer and change nothing on screen.
|
|
87
60
|
|
|
88
61
|
So the question is never "should this popup be controlled?" but "what triggers
|
|
@@ -106,13 +79,12 @@ where that state lives:
|
|
|
106
79
|
```
|
|
107
80
|
|
|
108
81
|
The available commands: `--navi-open`, `--navi-close`, `--navi-toggle`,
|
|
109
|
-
`--navi-cancel` (closes, telling the popup the close means "revert"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
press:
|
|
82
|
+
`--navi-cancel` (closes, telling the popup the close means "revert" — what Escape
|
|
83
|
+
says, see [below](#escape-cancels-the-other-gestures-keep)), `--navi-confirm`
|
|
84
|
+
(says yes, then closes). `--navi-close:all` closes every popup above the button,
|
|
85
|
+
nearest first; a popup that refuses to close keeps the ones above it open too. A
|
|
86
|
+
link that leaves is the usual case — a badge shown over a sheet, both left in
|
|
87
|
+
one press:
|
|
116
88
|
|
|
117
89
|
```jsx
|
|
118
90
|
<Link href={PLAYER_ROUTE.buildUrl({ playerId })} command="--navi-close:all">
|
|
@@ -122,48 +94,42 @@ press:
|
|
|
122
94
|
|
|
123
95
|
## Something else opens it: `triggerNaviCommand`
|
|
124
96
|
|
|
125
|
-
The attributes
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
drag, a double-click, a keyboard shortcut, a server answer, an
|
|
133
|
-
`IntersectionObserver`:
|
|
97
|
+
The attributes cover more than "this button opens that dialog": `commandFor`
|
|
98
|
+
says to whom, `value` says what it is about, `--navi-x:argument` says how. Check
|
|
99
|
+
none of those is the answer before writing JS: `triggerNaviCommand` is the last
|
|
100
|
+
resort, for a decision rather than a press on the element carrying the
|
|
101
|
+
attributes — a long press, the end of a drag, a double-click, a keyboard
|
|
102
|
+
shortcut, a server answer, an `IntersectionObserver`. A plain `Box` has no
|
|
103
|
+
command wiring of its own, so its click is one too:
|
|
134
104
|
|
|
135
105
|
```jsx
|
|
136
106
|
import { triggerNaviCommand } from "@jsenv/navi";
|
|
137
107
|
|
|
138
108
|
const dialogRef = useRef(null);
|
|
139
|
-
const open = (event) => {
|
|
140
|
-
if (draggedRef.current) {
|
|
141
|
-
// the click that ends a throw is not a request to open
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
triggerNaviCommand(dialogRef.current, "--navi-open", event);
|
|
145
|
-
};
|
|
146
109
|
|
|
147
|
-
<Box
|
|
110
|
+
<Box
|
|
111
|
+
interactions={{
|
|
112
|
+
click: (event) =>
|
|
113
|
+
triggerNaviCommand(dialogRef.current, "--navi-open", event),
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
…
|
|
117
|
+
</Box>
|
|
148
118
|
<Dialog ref={dialogRef}>…</Dialog>
|
|
149
119
|
```
|
|
150
120
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
121
|
+
No guard against a drag: beside a `move` on the same box, the click a drag
|
|
122
|
+
leaves behind is already suppressed. It is the entry point the attributes go
|
|
123
|
+
through — same target resolution, same events — so the popup stays uncontrolled,
|
|
124
|
+
and keeps its say over closing.
|
|
154
125
|
|
|
155
126
|
### The event is forwarded, not invented
|
|
156
127
|
|
|
157
128
|
`event` is what caused the decision, and it is mandatory — triggering a command
|
|
158
|
-
without one throws.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
So the event to pass is **the one that is already there**, threaded down through
|
|
164
|
-
every function between the handler and the call. A `CustomEvent` built on the
|
|
165
|
-
spot satisfies the signature and defeats its purpose: the origin is a name
|
|
166
|
-
instead of a gesture, and everything read off the real event is gone.
|
|
129
|
+
without one throws. The popup reads its chain to give the focus back and to know
|
|
130
|
+
whether a mousedown's click must be swallowed. So the event to pass is **the one
|
|
131
|
+
that is already there**, threaded down through every function between the
|
|
132
|
+
handler and the call:
|
|
167
133
|
|
|
168
134
|
```js
|
|
169
135
|
// ✗ the handler drops the event, the command is told a story instead
|
|
@@ -178,34 +144,19 @@ const openMenu = (event) => {
|
|
|
178
144
|
};
|
|
179
145
|
```
|
|
180
146
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
sequence.
|
|
186
|
-
|
|
187
|
-
The exception is the sequence that genuinely started on its own — a timer
|
|
188
|
-
firing, an action settling, a signal changing. Then say what happened with a
|
|
189
|
-
`CustomEvent` named after it, rather than leaving the origin unsaid:
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
import { chainEvent, triggerNaviCommand } from "@jsenv/navi";
|
|
193
|
-
|
|
194
|
-
const expiredEvent = new CustomEvent("session_expired");
|
|
195
|
-
chainEvent(expiredEvent, causeEvent); // when something did precede it
|
|
196
|
-
triggerNaviCommand(dialogRef.current, "--navi-open", expiredEvent);
|
|
197
|
-
```
|
|
147
|
+
Only a sequence that genuinely started on its own — a timer firing, an action
|
|
148
|
+
settling, a signal changing — builds one: a `CustomEvent` named after what
|
|
149
|
+
happened, chained (`chainEvent`) to whatever preceded it, as the
|
|
150
|
+
`triggerNaviCommand` JSDoc shows.
|
|
198
151
|
|
|
199
152
|
### Opening while the finger is still down
|
|
200
153
|
|
|
201
|
-
A menu opened by a `longpress` appears **during** the press that asked for it
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
came down). Nothing to do, and nothing to give up: the very next press outside
|
|
208
|
-
closes the popup as usual.
|
|
154
|
+
A menu opened by a `longpress` appears **during** the press that asked for it,
|
|
155
|
+
and the release of that press does not dismiss it, whatever the browser makes of
|
|
156
|
+
it afterwards (a tap ends with a synthesized `mousedown`, `mouseup` and `click`
|
|
157
|
+
at the place the finger left — on a backdrop that did not exist when the finger
|
|
158
|
+
came down). Nothing to do: the very next press outside closes the popup as
|
|
159
|
+
usual.
|
|
209
160
|
|
|
210
161
|
```jsx
|
|
211
162
|
<Row
|
|
@@ -217,55 +168,36 @@ closes the popup as usual.
|
|
|
217
168
|
/>
|
|
218
169
|
```
|
|
219
170
|
|
|
220
|
-
It is one more reason to forward the real event: what the popup reads to tell its
|
|
221
|
-
own opening press from somebody else's comes from that gesture.
|
|
222
|
-
|
|
223
171
|
## Which element receives the command
|
|
224
172
|
|
|
225
173
|
The first argument is the command's **source** — the element it is triggered
|
|
226
174
|
_from_. The target is resolved from it, in this order:
|
|
227
175
|
|
|
228
|
-
1. `commandFor="someId"` on the source (
|
|
229
|
-
`commandfor` attribute, which is what is read),
|
|
176
|
+
1. `commandFor="someId"` on the source (the HTML `commandfor` attribute),
|
|
230
177
|
2. `navi-command-target="parent-control" | "child-control"`,
|
|
231
178
|
3. the command's own fallback — for the popup commands, `closest("[aria-expanded]")`.
|
|
232
179
|
|
|
233
|
-
A popup carries `aria-expanded` from its
|
|
234
|
-
element itself as the source resolves to that
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
To trigger from another element instead, give that element a `commandFor`
|
|
239
|
-
pointing at the popup's `id` — attribute-driven target resolution, JS-driven
|
|
240
|
-
timing.
|
|
180
|
+
A popup carries `aria-expanded` from its first render, and `closest()` starts at
|
|
181
|
+
the element itself, so the popup element passed as the source resolves to that
|
|
182
|
+
popup — the short form above. To trigger from another element, give it a
|
|
183
|
+
`commandFor` pointing at the popup's `id`.
|
|
241
184
|
|
|
242
185
|
## The anchor
|
|
243
186
|
|
|
244
187
|
A popup opens on the place the open names, on the `anchor` prop when the open
|
|
245
188
|
names none, and on whoever asked when nothing else says. In that order:
|
|
246
189
|
|
|
247
|
-
1. **`detail.anchor`** —
|
|
248
|
-
|
|
249
|
-
the press
|
|
190
|
+
1. **`detail.anchor`** — `triggerNaviCommand`'s `anchor` option: a statement
|
|
191
|
+
about that one opening. A menu belongs at the point the press happened, and
|
|
192
|
+
the press is the only thing that knows that point.
|
|
250
193
|
2. **the `anchor` prop** — where this popup opens when nobody says.
|
|
251
194
|
3. **`detail.source`** — who asked. A button therefore opens the popup on
|
|
252
195
|
itself, and a popup passed as its own source is its own anchor.
|
|
253
196
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
`
|
|
257
|
-
|
|
258
|
-
relative to its anchor, say what the anchor is:
|
|
259
|
-
|
|
260
|
-
```jsx
|
|
261
|
-
<Popover ref={popoverRef} anchor={rowRef}>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
`anchorCustomEventDetail="ignore"` drops the open's side of that order
|
|
265
|
-
entirely, leaving the `anchor` prop alone — for a popover that must never be
|
|
266
|
-
anchored to whatever opened it (`SidePanel` does this), and for a dialog that
|
|
267
|
-
must never be sized from it (`sizeFromAnchor`) or lifted out of it
|
|
268
|
-
(`animation="lifting"`).
|
|
197
|
+
A `Popover` is positioned against it; a `Dialog` only reads it for
|
|
198
|
+
`sizeFromAnchor` and `animation="lifting"` (see [popup_lift.md](./popup_lift.md)).
|
|
199
|
+
`anchorCustomEventDetail="ignore"` drops the open's side of that order — for a
|
|
200
|
+
popup that must never be anchored to whatever opened it (`SidePanel` does this).
|
|
269
201
|
|
|
270
202
|
## Opening it ON something
|
|
271
203
|
|
|
@@ -288,14 +220,11 @@ which one — so the press says it, with its own value:
|
|
|
288
220
|
```
|
|
289
221
|
|
|
290
222
|
That subject travels as the command's **value**, not as an argument after a
|
|
291
|
-
colon (`--navi-open:radar-42`). The
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
says it everywhere else, with `value`, and nothing has to be parsed.
|
|
297
|
-
|
|
298
|
-
A JS decision says the same thing through the same door:
|
|
223
|
+
colon (`--navi-open:radar-42`). The distinction holds across every command: an
|
|
224
|
+
argument says WHAT the command does — `--navi-go-to-slide:edit` needs one, "go"
|
|
225
|
+
without a destination is not an instruction — and `value` says what it is about.
|
|
226
|
+
"Open" is already a complete instruction. A JS decision says it through the same
|
|
227
|
+
door, and `--navi-toggle` carries it too, on the half that opens:
|
|
299
228
|
|
|
300
229
|
```js
|
|
301
230
|
triggerNaviCommand(dialogRef.current, "--navi-open", event, {
|
|
@@ -303,11 +232,9 @@ triggerNaviCommand(dialogRef.current, "--navi-open", event, {
|
|
|
303
232
|
});
|
|
304
233
|
```
|
|
305
234
|
|
|
306
|
-
`--navi-toggle` carries it too, on the half that opens.
|
|
307
|
-
|
|
308
235
|
### `onOpen` runs before the popup has built anything
|
|
309
236
|
|
|
310
|
-
|
|
237
|
+
On a `Dialog`/`Popover` the order is a guarantee, not a coincidence:
|
|
311
238
|
|
|
312
239
|
```
|
|
313
240
|
onOpen(openEvent) ← the subject is decided here
|
|
@@ -316,79 +243,29 @@ positioned, shown
|
|
|
316
243
|
```
|
|
317
244
|
|
|
318
245
|
So a dialog whose content is seeded once — an uncontrolled field on a
|
|
319
|
-
`defaultValue`, a form keyed on what it edits — reads the right thing on its
|
|
320
|
-
first render
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
`onnavi_command` receives the whole command string and its value, but it runs
|
|
333
|
-
**after** the opening: whatever it writes lands on a popup that is already open.
|
|
334
|
-
That works only as long as nothing has read the state yet — which
|
|
335
|
-
`mount="while-opened"` makes a real race rather than a theoretical one.
|
|
336
|
-
|
|
337
|
-
A `navi_request_open` listener added on the element is the request itself, ahead
|
|
338
|
-
of the popup acting on it — but it is ordered against the popup's own handler by
|
|
339
|
-
registration, and it has to be attached in an effect on a ref. `onOpen` is that
|
|
340
|
-
moment, said as a prop.
|
|
246
|
+
`defaultValue`, a form keyed on what it edits — reads the right thing on its
|
|
247
|
+
very first render, instead of mounting on the previous subject and being
|
|
248
|
+
corrected (a flicker at best, stale fields at worst). Only
|
|
249
|
+
`mount="while-opened"` makes it true every time: content kept from an earlier
|
|
250
|
+
opening, or warmed by a hover, is already built when `onOpen` runs (see
|
|
251
|
+
[what the popup holds while it is closed](#what-the-popup-holds-while-it-is-closed)).
|
|
252
|
+
A `Picker`'s `onOpen` is not this moment: it runs once its popup is built and
|
|
253
|
+
shown.
|
|
254
|
+
|
|
255
|
+
The other places one could listen are not that moment: `onnavi_command` on the
|
|
256
|
+
popup runs **after** the opening, so what it writes lands on a popup already
|
|
257
|
+
open — a real race under `mount="while-opened"`; a `navi_request_open` listener
|
|
258
|
+
is ordered against the popup's own handler by registration.
|
|
341
259
|
|
|
342
260
|
## A press that opens a popup and acts on it
|
|
343
261
|
|
|
344
262
|
A press that opens something and then does something with what came of it — a
|
|
345
263
|
"save this guest" prompt on a row, which replaces the guest once the profile
|
|
346
|
-
exists — is not a dialog plus a way home. It is a `Picker`: a trigger
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
```jsx
|
|
350
|
-
// one per row: the trigger IS the thing that receives the answer
|
|
351
|
-
<Picker
|
|
352
|
-
variant="icon"
|
|
353
|
-
rightSlotIcon={<DisketteSvg />}
|
|
354
|
-
action={async (created) => {
|
|
355
|
-
await USERS.GET_MANY.rerun();
|
|
356
|
-
replaceGuest(guest, created);
|
|
357
|
-
}}
|
|
358
|
-
>
|
|
359
|
-
<GuestSavePrompt kind="player" name={guest.name} />
|
|
360
|
-
</Picker>
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Two things fall out of writing it this way, and both are the reason to prefer it
|
|
364
|
-
over a shared dialog opened by `--navi-open`:
|
|
365
|
-
|
|
366
|
-
- **what the popup needs to know travels as props**, because the popup is
|
|
367
|
-
written where the press is. No value to carry through the command, nothing to
|
|
368
|
-
read back out of an event;
|
|
369
|
-
- **the popup is built the first time it opens**, not once per row on the render
|
|
370
|
-
that draws the list (see [what a popup holds while it is
|
|
371
|
-
closed](#what-the-popup-holds-while-it-is-closed)). A hundred rows is a
|
|
372
|
-
hundred triggers, not a hundred dialogs.
|
|
373
|
-
|
|
374
|
-
The same component can of course be written once and used in every picker —
|
|
375
|
-
`<GuestSavePrompt>` above is one — so "the prompt exists once" is a question
|
|
376
|
-
about components, not about the DOM.
|
|
377
|
-
|
|
378
|
-
### Composing a value, or doing work
|
|
379
|
-
|
|
380
|
-
A picker mirrors **one** control in its popup — the first one that is not a
|
|
381
|
-
button, a link or a control that answers for itself (`standalone`). That
|
|
382
|
-
mirror is what makes `<Picker><List selectable/></Picker>` work with nothing
|
|
383
|
-
wired: the picker's value IS the list's, both ways, and the picker's `action`
|
|
384
|
-
runs on it when the popup closes.
|
|
385
|
-
|
|
386
|
-
That is the shape for a popup that **composes a value**. A popup that **does
|
|
387
|
-
work** — creates a profile, uploads a file — is the other shape, and it does not
|
|
388
|
-
need the picker's `action` at all: the work is written where the press is, so
|
|
389
|
-
its callback already has everything around it.
|
|
264
|
+
exists — is not a dialog plus a way home. It is a `Picker`: a trigger and a
|
|
265
|
+
popup, written where the press is.
|
|
390
266
|
|
|
391
267
|
```jsx
|
|
268
|
+
// one per row: the popup is written where the press is
|
|
392
269
|
<Picker variant="icon" rightSlotIcon={<DisketteSvg />}>
|
|
393
270
|
<Form
|
|
394
271
|
action={async (fields) => {
|
|
@@ -401,22 +278,50 @@ its callback already has everything around it.
|
|
|
401
278
|
</Picker>
|
|
402
279
|
```
|
|
403
280
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
281
|
+
What the popup needs to know travels as props, and what it does has the row in
|
|
282
|
+
scope: no value carried through the command, nothing read back out of an event,
|
|
283
|
+
nobody to answer. That is the difference a shared dialog hides — written once,
|
|
284
|
+
far from every press that opens it, it has to be told what it is about and has
|
|
285
|
+
to answer somebody. The content can still be one component used in every picker:
|
|
286
|
+
"the prompt exists once" is a question about components, not about the DOM.
|
|
287
|
+
|
|
288
|
+
What each row costs depends on what the picker is told. A picker told no value —
|
|
289
|
+
no `value`, `defaultValue` or `signal` — builds its popup at render, closed: it
|
|
290
|
+
reads its value off the control in there, which has to exist before anything
|
|
291
|
+
opens. A hundred rows is then a hundred closed popups. One heavy enough to
|
|
292
|
+
matter can say `mount="from-first-open"`, and the picker then knows nothing of
|
|
293
|
+
what that control holds until the first opening: its trigger cannot draw it, and
|
|
294
|
+
nothing reading the picker sees it. A picker told a value, a `type="confirm"`
|
|
295
|
+
and a `mode="callout"` build on the first open already.
|
|
296
|
+
|
|
297
|
+
### Composing a value, or doing work
|
|
298
|
+
|
|
299
|
+
A picker mirrors **one** control in its popup — the first one that is not a
|
|
300
|
+
button, a link or a control that answers for itself (`standalone`). That mirror
|
|
301
|
+
is what makes `<Picker><List selectable/></Picker>` work with nothing wired: the
|
|
302
|
+
picker's value IS the list's, both ways, and the picker's `action` runs on it
|
|
303
|
+
when the popup closes. That is the shape for a popup that **composes a value**.
|
|
304
|
+
A popup that **does work** — creates a profile, uploads a file — is the one
|
|
305
|
+
above: the work is written where the press is, its callback already has
|
|
306
|
+
everything around it, and the picker needs no `action`. Nothing travels back,
|
|
307
|
+
because nothing left.
|
|
308
|
+
|
|
309
|
+
**Do not mix the two.** A `<Form>` at the root of a picker's popup IS the
|
|
310
|
+
mirrored control, so its value is the picker's value: handing the picker
|
|
311
|
+
something else (a created profile, say) pushes it back down into the form's
|
|
312
|
+
named fields and comes back as the form's aggregate. When the popup does work,
|
|
313
|
+
let the work keep its result.
|
|
408
314
|
|
|
409
315
|
### The trigger wears the wait
|
|
410
316
|
|
|
411
|
-
The picker's `action` runs on the trigger, not in the popup: it is dispatched
|
|
412
|
-
|
|
317
|
+
The picker's `action` runs on the trigger, not in the popup: it is dispatched on
|
|
318
|
+
the close that keeps, and the popup goes while it runs. So what is still on
|
|
413
319
|
screen answers for the write — `aria-busy` and the loading outline on the
|
|
414
|
-
trigger while the request is out, the error callout on it if the server
|
|
415
|
-
|
|
416
|
-
for a picker
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
as a card and is refused as a card, wherever in the tree the sheet was written.
|
|
320
|
+
trigger while the request is out, the error callout on it if the server refuses,
|
|
321
|
+
and the value rolled back to the last accepted one (`resetOnError`, on by
|
|
322
|
+
default for a picker). Nobody is left mid-edit behind a closed popup, and a card
|
|
323
|
+
that IS the trigger (`variant="bare"`, the card as `ui`) waits as a card and is
|
|
324
|
+
refused as a card, wherever in the tree the sheet was written.
|
|
420
325
|
|
|
421
326
|
```jsx
|
|
422
327
|
<Picker
|
|
@@ -435,50 +340,32 @@ as a card and is refused as a card, wherever in the tree the sheet was written.
|
|
|
435
340
|
</Picker>
|
|
436
341
|
```
|
|
437
342
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`loading` is the wait: navi's loading outline is a two-pixel run around the
|
|
447
|
-
box, which a card-sized trigger moves clear of its frame with
|
|
448
|
-
`loadingOutlineInset`, or draws its own waiting state for, with
|
|
449
|
-
`loadingOutline="custom"` so the two do not add up.
|
|
343
|
+
A `ui` reading what the picker holds — `ui={MatchCard}` is handed `value`,
|
|
344
|
+
`loading` and `interactive`, an element reads them with `usePickerState()` —
|
|
345
|
+
shows the answer the moment the sheet leaves and takes it back on a refusal (see
|
|
346
|
+
[control_value.md](./control_value.md#drawing-what-a-control-holds)). Drawn from
|
|
347
|
+
the caller's own state, as [a settings sheet](./control_object.md#a-settings-sheet)
|
|
348
|
+
is, the trigger keeps showing the saved answer until that state moves: which to
|
|
349
|
+
draw from is whether the trigger should show the pending answer or the saved
|
|
350
|
+
one.
|
|
450
351
|
|
|
451
352
|
What the picker measures as "changed since open" is what its mirrored group
|
|
452
|
-
holds. A piece of the answer living outside the controls — a seating
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
`openWhileReadOnly={false}`: a busy picker otherwise still opens, to be read,
|
|
469
|
-
and a sheet of fields to fill in is not that. One wait, one outline as well:
|
|
470
|
-
the inner picker says `loadingOutline="custom"`, and the card's is drawn by
|
|
471
|
-
the picker whose box the card is. A hold declared inside the card answers before the
|
|
472
|
-
card's own: the nearer hold takes the press. `animation="lifting"` with
|
|
473
|
-
`dialogSizeFromAnchor` (as wide as the card, floor and ceiling),
|
|
474
|
-
`popupBackgroundColor="transparent"` and `popupBoxShadow="none"` lifts the
|
|
475
|
-
card out of its place and puts it back (`data-lift` on the card inside the
|
|
476
|
-
sheet) — what to respect for that movement to be right and quick is
|
|
477
|
-
`popup_lift.md`. `12_picker_card_demo.html` shows all of it, against a
|
|
478
|
-
backend that answers when told to.
|
|
479
|
-
|
|
480
|
-
A gesture inside the sheet that is not a field — cancel the game, delete it —
|
|
481
|
-
is still an answer the sheet gives. A named button says which:
|
|
353
|
+
holds. A piece of the answer living outside the controls — a seating rearranged
|
|
354
|
+
by drag, kept in component state — has to be held by a control in the group too
|
|
355
|
+
(a named control bound to that state), or a close over it reads as nothing
|
|
356
|
+
changed and nothing runs.
|
|
357
|
+
|
|
358
|
+
A card in a list opens on `openOn="longpress"`, so a tap on it stays a tap. Two
|
|
359
|
+
pickers on one card — the score sheet inside the edit card — are one wait: the
|
|
360
|
+
inner one names the card as its `anchor` and says `standalone`, each wears the
|
|
361
|
+
other's run through `loading` (from its `onActionStart`/`onActionEnd`), the
|
|
362
|
+
score picker says `openWhileReadOnly={false}` (a busy picker otherwise still
|
|
363
|
+
opens, to be read), and the inner one's `loadingOutline="custom"` leaves one
|
|
364
|
+
outline. `12_picker_card_demo.html` shows all of it, lifting included
|
|
365
|
+
([popup_lift.md](./popup_lift.md)).
|
|
366
|
+
|
|
367
|
+
A gesture inside the sheet that is not a field — cancel the game, delete it — is
|
|
368
|
+
still an answer the sheet gives. A named button says which:
|
|
482
369
|
|
|
483
370
|
```jsx
|
|
484
371
|
<Button name="op" value="cancel" command="--navi-send">
|
|
@@ -486,18 +373,15 @@ is still an answer the sheet gives. A named button says which:
|
|
|
486
373
|
</Button>
|
|
487
374
|
```
|
|
488
375
|
|
|
489
|
-
Its name and value travel with the fields
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
request is out. No button inside the popup needs an action of its own: a run
|
|
495
|
-
started in there would belong to a control the close takes away.
|
|
376
|
+
Its name and value travel with the fields, the sheet leaves on the send, and the
|
|
377
|
+
picker's `action` receives `{ …fields, op: "cancel" }` — worn by the trigger
|
|
378
|
+
like any other write, and a drawing reading `usePickerState()` can already show
|
|
379
|
+
the "cancelled" stamp. No button inside the popup needs an action of its own: a
|
|
380
|
+
run started in there would belong to a control the close takes away.
|
|
496
381
|
|
|
497
|
-
This is not `optimistic
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
the store and says nothing until the answer lands.
|
|
382
|
+
This is not `optimistic`, which is "draw no wait at all": here the wait is drawn,
|
|
383
|
+
on the trigger. An optimistic picker is for a write not worth showing — the card
|
|
384
|
+
then reads the store and says nothing until the answer lands.
|
|
501
385
|
|
|
502
386
|
The same shape with nothing to write is a **door**: a drawing that grows to be
|
|
503
387
|
looked at — a weather scene, a plan — and comes back. `picksNothing`, no
|
|
@@ -519,73 +403,21 @@ looked at — a weather scene, a plan — and comes back. `picksNothing`, no
|
|
|
519
403
|
```
|
|
520
404
|
|
|
521
405
|
A `Button` opening a `Dialog` with an `anchor` does the same and is not wrong;
|
|
522
|
-
what the picker removes is the id plumbing (the trigger is the anchor, the
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
shared](#when-a-shared-popup-is-still-the-right-answer).
|
|
527
|
-
|
|
528
|
-
### A trigger that is only an icon
|
|
529
|
-
|
|
530
|
-
`variant="icon"` draws no value, and therefore no slot beside one either: the
|
|
531
|
-
whole trigger is its `ui`.
|
|
532
|
-
|
|
533
|
-
```jsx
|
|
534
|
-
<Picker variant="icon" ui={<DisketteSvg />} />
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Left out, that `ui` is the icon the slot would have shown — the chevron, or the
|
|
538
|
-
one the picker's type carries (a pencil for `type="text"`, a calendar for
|
|
539
|
-
`type="date"`), so `<Picker type="date" variant="icon" />` is a calendar and
|
|
540
|
-
nothing else. `rightSlotIcon`/`rightSlot` belong to the shapes that DO draw a
|
|
541
|
-
value and want something beside it; under `variant="icon"` the first is only the
|
|
542
|
-
default for `ui`, and the second has nowhere to go — the clear cross included.
|
|
406
|
+
what the picker removes is the id plumbing (the trigger is the anchor, the popup
|
|
407
|
+
is its own), and what it keeps is the day the drawing becomes editable — it is
|
|
408
|
+
already the thing that wears the wait. The button + shared dialog stays the
|
|
409
|
+
answer for
|
|
410
|
+
[the cases where a popup must be shared](#when-a-shared-popup-is-still-the-right-answer).
|
|
543
411
|
|
|
544
412
|
### A trigger that draws nothing, pressed from elsewhere
|
|
545
413
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
id={court.id}
|
|
554
|
-
interactions={{
|
|
555
|
-
move: (event) => placeCourt(court, event.detail),
|
|
556
|
-
click: (event) =>
|
|
557
|
-
triggerNaviCommand(pickerRef.current, "--navi-open", event, {
|
|
558
|
-
anchor: event.currentTarget,
|
|
559
|
-
value: court.settings,
|
|
560
|
-
}),
|
|
561
|
-
}}
|
|
562
|
-
/>
|
|
563
|
-
|
|
564
|
-
// out of the map's flow: what draws nothing still takes a box (below)
|
|
565
|
-
<div style={{ position: "relative" }}>
|
|
566
|
-
<Picker
|
|
567
|
-
ref={pickerRef}
|
|
568
|
-
variant="headless"
|
|
569
|
-
value={court.settings}
|
|
570
|
-
uiAction={(settings) => showCourt(court, settings)}
|
|
571
|
-
action={(settings) => saveCourt(court, settings)}
|
|
572
|
-
>
|
|
573
|
-
<CourtSettings />
|
|
574
|
-
</Picker>
|
|
575
|
-
</div>
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
Everything a picker promises still holds — the value at open, "changed since
|
|
579
|
-
open", the revert on Escape, the action on a close that keeps — and the popup
|
|
580
|
-
lands on the object because the open named it (see [the
|
|
581
|
-
anchor](#the-anchor)).
|
|
582
|
-
|
|
583
|
-
One thing to know about `variant="headless"`: it is not the `ui`'s box but its
|
|
584
|
-
parent's. It stretches to the nearest positioned element around it, so that a
|
|
585
|
-
picker put INSIDE what opens it — a button, a row — hangs its popup off that
|
|
586
|
-
whole element. A picker opened from something it does not sit in must therefore
|
|
587
|
-
be given an element of its own to stretch into; dropped straight into the map
|
|
588
|
-
frame it would cover it, and every press on the empty plan would open it.
|
|
414
|
+
An object placed on a map, dragged by its own `move`, owns its press and cannot
|
|
415
|
+
be a picker's façade: it opens the picker itself from its `click`
|
|
416
|
+
(`triggerNaviCommand(pickerRef.current, "--navi-open", event, { anchor, value })`).
|
|
417
|
+
That picker is `variant="headless"`, which stretches to the nearest positioned
|
|
418
|
+
element around it: opened from something it does not sit in, it needs an element
|
|
419
|
+
of its own to stretch into — dropped straight into the map frame it would cover
|
|
420
|
+
it, and every press on the empty plan would open it.
|
|
589
421
|
|
|
590
422
|
### When a shared popup is still the right answer
|
|
591
423
|
|
|
@@ -603,31 +435,20 @@ Three cases, and only three:
|
|
|
603
435
|
|
|
604
436
|
None of them is "one popup per row of a list", which is what a picker is for.
|
|
605
437
|
|
|
606
|
-
Do not mix the two. A `<Form>` at the root of a picker's popup IS the mirrored
|
|
607
|
-
control, so its value is the picker's value: handing the picker something else
|
|
608
|
-
(a created profile, say) pushes it back down into the form's named fields and
|
|
609
|
-
comes back as the form's aggregate. When the popup does work, let the work keep
|
|
610
|
-
its result.
|
|
611
|
-
|
|
612
438
|
## Reacting to open and close
|
|
613
439
|
|
|
614
|
-
`onOpen` is called on every open
|
|
615
|
-
[above](#
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
Neither can veto: `onClose` is the close happening, not a request to close.
|
|
623
|
-
Refusing a close is `onRequestClose`, which belongs to whoever owns an
|
|
624
|
-
`openController` (see `open_controller.js`) — an uncontrolled popup already
|
|
625
|
-
refuses the one close that matters, the one over a control mid-action.
|
|
440
|
+
`onOpen` is called on every open (for when, see
|
|
441
|
+
[above](#onopen-runs-before-the-popup-has-built-anything)), and `onClose` on
|
|
442
|
+
every real close, with `detail.isCancel` when the close meant "revert". Neither
|
|
443
|
+
can veto: `onClose` is the close happening, not a request to close. The refusals
|
|
444
|
+
are navi's: a close over a control mid-action, and a `Picker`'s close over a
|
|
445
|
+
value that does not validate.
|
|
626
446
|
|
|
627
447
|
### Closing when a button also runs an action
|
|
628
448
|
|
|
629
449
|
A button that carries both runs them in that order: the action first, the
|
|
630
|
-
command once it succeeded
|
|
450
|
+
command once it succeeded (see
|
|
451
|
+
[actions.md](./actions.md#a-press-that-opens-something-and-waits-for-the-answer)).
|
|
631
452
|
|
|
632
453
|
```jsx
|
|
633
454
|
// Stays open while save() runs, closes when it resolves, stays open if it
|
|
@@ -637,67 +458,48 @@ command once it succeeded.
|
|
|
637
458
|
</Button>
|
|
638
459
|
```
|
|
639
460
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
`
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
the
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
popup stays open, and the first Escape afterwards dismisses that callout rather
|
|
660
|
-
than the popup, which reads as a popup that no longer closes at all. Nothing has
|
|
661
|
-
to be hand-written to work around it: `command` next to `action` is that
|
|
662
|
-
workaround, done at the one moment where the action has settled and the button
|
|
663
|
-
is no longer busy.
|
|
664
|
-
|
|
665
|
-
When what opened the popup is still on screen and should answer for the write
|
|
666
|
-
— its wait, its refusal — the popup was a picker's all along: see [the trigger
|
|
667
|
-
wears the wait](#the-trigger-wears-the-wait). What follows is for the other
|
|
668
|
-
case, a popup nobody stands in for. To close on the press — the answer taken as
|
|
669
|
-
soon as it is given, the save running on its own behind a closed popup — say so
|
|
670
|
-
on the control:
|
|
461
|
+
Closing first would take the form off the screen over a request that can still
|
|
462
|
+
fail, and put its error callout on a button nobody can see; an error or an abort
|
|
463
|
+
— a `confirm` answered "no" is an abort — leaves the popup where it is. The
|
|
464
|
+
action may replace that button while it runs: what the command aims at is read
|
|
465
|
+
at the press, so the popup pressed in is the popup that closes, even when the
|
|
466
|
+
press is what emptied it.
|
|
467
|
+
|
|
468
|
+
The wait is why closing **from inside** the action does not close: while it
|
|
469
|
+
runs, the button that started it is busy, and a busy control is exactly what a
|
|
470
|
+
popup refuses to close over. The busy control raises a callout, the popup stays
|
|
471
|
+
open, and the first Escape afterwards dismisses that callout rather than the
|
|
472
|
+
popup — which reads as a popup that no longer closes at all. `command` next to
|
|
473
|
+
`action` is the way: it runs at the one moment the action has settled.
|
|
474
|
+
|
|
475
|
+
When what opened the popup is still on screen and should answer for the write,
|
|
476
|
+
the popup was a picker's all along (see
|
|
477
|
+
[the trigger wears the wait](#the-trigger-wears-the-wait)). For a popup nobody
|
|
478
|
+
stands in for, closing on the press — the save running on its own behind a
|
|
479
|
+
closed popup — is said on the control:
|
|
671
480
|
|
|
672
481
|
```jsx
|
|
673
482
|
<Form command="--navi-close" action={saveScore} optimistic resetOnError>
|
|
674
483
|
```
|
|
675
484
|
|
|
676
|
-
`optimistic`
|
|
677
|
-
|
|
678
|
-
the
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
which keeps the refusal next to what it is about. What the page shows meanwhile — a card reading
|
|
683
|
-
the store — is the app's to draw and to take back. For a write not worth being
|
|
684
|
-
waited for; never for one whose refusal changes what the person does next.
|
|
685
|
-
|
|
686
|
-
Nobody wanting to close, but everybody wanting to be ABLE to, is a different
|
|
687
|
-
need and has its own answer: `actionStandalone` (see the top of this page) frees
|
|
688
|
-
the popup without closing it, and the error callout stays where the press was.
|
|
485
|
+
Know what `optimistic` costs: **a save that fails does so behind a closed
|
|
486
|
+
popup**. `resetOnError` puts the control back, and the error callout is drawn on
|
|
487
|
+
what surrounds the closed popup — the card it opened from, typically; what the
|
|
488
|
+
page shows meanwhile is the app's to draw and to take back. For a write not
|
|
489
|
+
worth being waited for; never for one whose refusal changes what the person
|
|
490
|
+
does next.
|
|
689
491
|
|
|
690
492
|
## Escape cancels, the other gestures keep
|
|
691
493
|
|
|
692
494
|
The gestures that close a popup do not all mean the same thing, and that is on
|
|
693
495
|
purpose:
|
|
694
496
|
|
|
695
|
-
| gesture |
|
|
696
|
-
| ------------------------------ |
|
|
697
|
-
| Escape | cancel
|
|
698
|
-
| a press outside | close, keep
|
|
699
|
-
| a close cross (`--navi-close`) | close, keep
|
|
700
|
-
| `--navi-cancel` on a button | cancel
|
|
497
|
+
| gesture | means | who decides |
|
|
498
|
+
| ------------------------------ | ----------- | -------------------------------------------------------------------------------- |
|
|
499
|
+
| Escape | cancel | always on a `Dialog`/`Popover`; a `Picker`'s `escapeEffect="cancel"` (default) |
|
|
500
|
+
| a press outside | close, keep | `pressOutside="close"`, default of `Dialog` and `Picker` (`Popover`: `"ignore"`) |
|
|
501
|
+
| a close cross (`--navi-close`) | close, keep | [the cross](#the-close-cross) |
|
|
502
|
+
| `--navi-cancel` on a button | cancel | the button |
|
|
701
503
|
|
|
702
504
|
Escape says "forget it". It is the one gesture that has meant that everywhere,
|
|
703
505
|
for as long as there have been dialogs, and navi keeps it that way. **A popup
|
|
@@ -710,12 +512,13 @@ something else.
|
|
|
710
512
|
Cancelling is not itself an undo: it marks the close, and whoever holds a value
|
|
711
513
|
decides what to do with the mark.
|
|
712
514
|
|
|
713
|
-
- `Dialog` and `Popover` hold nothing, so they undo nothing
|
|
714
|
-
|
|
715
|
-
caller's own business.
|
|
515
|
+
- `Dialog` and `Popover` hold nothing, so they undo nothing: `onClose` receives
|
|
516
|
+
`detail.isCancel`, and reverting is the caller's own business.
|
|
716
517
|
- `Picker` holds a value, so it puts back **the value it held when it opened**.
|
|
717
518
|
That is what makes a picker a picker: opening one is trying something on, and
|
|
718
|
-
Escape is putting it back.
|
|
519
|
+
Escape is putting it back. What the outside moved while it was open stays
|
|
520
|
+
moved (see
|
|
521
|
+
[control_value.md](./control_value.md#a-defaultvalue-follows-what-it-was-read-from)).
|
|
719
522
|
|
|
720
523
|
```jsx
|
|
721
524
|
// Escape here puts back the level the picker held at open, and the list's
|
|
@@ -725,22 +528,14 @@ decides what to do with the mark.
|
|
|
725
528
|
</Picker>
|
|
726
529
|
```
|
|
727
530
|
|
|
728
|
-
The trap is the FIRST open. A picker holds what its popup told it, and before
|
|
729
|
-
the popup has ever been open it has been told nothing — so "the value at open"
|
|
730
|
-
is nothing, even when the control inside starts on a `defaultValue` or on a
|
|
731
|
-
value the app passes it. Escape on that first pass goes back to empty, not to
|
|
732
|
-
what was on screen when the popup opened. From the second open onwards it puts
|
|
733
|
-
back what was really there.
|
|
734
|
-
|
|
735
531
|
### A picker that holds nothing and shows nothing
|
|
736
532
|
|
|
737
533
|
The gestures that KEEP (a click outside, a close cross) let a picker send what
|
|
738
534
|
it is showing: a picker sitting on a `defaultValue` holds nothing, so closing on
|
|
739
|
-
it untouched IS the answer ("yes, 1h30"), and the action runs.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
Closing it without choosing runs no action; only an explicit choice sends.
|
|
535
|
+
it untouched IS the answer ("yes, 1h30"), and the action runs. A picker used as
|
|
536
|
+
a **menu of gestures** — no `value`, no `defaultValue`, no signal, each row a
|
|
537
|
+
command — shows nothing, so there is nothing to confirm: closing it without
|
|
538
|
+
choosing runs no action, and only an explicit choice sends.
|
|
744
539
|
|
|
745
540
|
```jsx
|
|
746
541
|
// Clicking outside closes this and sends nothing.
|
|
@@ -753,28 +548,30 @@ Closing it without choosing runs no action; only an explicit choice sends.
|
|
|
753
548
|
</Picker>
|
|
754
549
|
```
|
|
755
550
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
empty.
|
|
551
|
+
`value={undefined}` is not holding nothing: a `value` prop is held whatever is in
|
|
552
|
+
it, and navi puts it back after each click — the rows then appear to do nothing.
|
|
553
|
+
Drop the prop instead of passing it empty.
|
|
760
554
|
|
|
761
555
|
### `escapeEffect="close"`, and why it is a last resort
|
|
762
556
|
|
|
763
557
|
`escapeEffect="close"` — a `Picker` prop; a `Dialog`/`Popover` has no such
|
|
764
|
-
switch — makes Escape say what a click outside says. It
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
cross first.
|
|
558
|
+
switch — makes Escape say what a click outside says. It takes away the only key
|
|
559
|
+
that undoes, and a popup with no way back is one people stop opening: reach for
|
|
560
|
+
a close cross first.
|
|
768
561
|
|
|
769
|
-
A
|
|
770
|
-
|
|
771
|
-
|
|
562
|
+
A cancel can undo the address too: under a pushed history entry — a dialog
|
|
563
|
+
picker with an `id`, a `navState={{ type: "push" }}` — it goes back in history,
|
|
564
|
+
taking with it anything written to the url while the popup was open (a route
|
|
565
|
+
`stateSignal`, a search param). One more reason Escape and the click outside are
|
|
566
|
+
not interchangeable.
|
|
772
567
|
|
|
773
568
|
## The close cross
|
|
774
569
|
|
|
775
570
|
The cross is navi's own: `<Dialog.Close />`, `<Popover.Close />`,
|
|
776
|
-
`<Popup.Close />` — one component under three names
|
|
777
|
-
|
|
571
|
+
`<Popup.Close />` — one component under three names. The caller places it, and
|
|
572
|
+
nothing else; it brings the `aria-label` in the active language (`label`
|
|
573
|
+
overrides it) and the padding that turns a three-millimetre glyph into a target
|
|
574
|
+
a thumb can hit:
|
|
778
575
|
|
|
779
576
|
```jsx
|
|
780
577
|
<Dialog id="duration">
|
|
@@ -786,27 +583,18 @@ popup is being written. The caller places it, and nothing else:
|
|
|
786
583
|
</Dialog>
|
|
787
584
|
```
|
|
788
585
|
|
|
789
|
-
It comes with the things a hand-written cross has to remember: the `aria-label`
|
|
790
|
-
in the active language (`label` overrides it), and the padding that turns a
|
|
791
|
-
three-millimetre glyph into a target a thumb can hit.
|
|
792
|
-
|
|
793
586
|
### It is the way out, so the state around it does not reach it
|
|
794
587
|
|
|
795
588
|
**A close button written by hand refuses the press inside a read-only control.**
|
|
796
|
-
A read-only `Picker` still opens
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
way out
|
|
804
|
-
|
|
805
|
-
The same holds for a disabled zone, and for a form busy sending: the way out of
|
|
806
|
-
a popup is never held by the state of what the popup belongs to.
|
|
807
|
-
|
|
808
|
-
If a cross really has to be hand-written — it is drawn into a bigger affordance
|
|
809
|
-
of the app's own, say — it carries the claim itself:
|
|
589
|
+
A read-only `Picker` still opens, to be read, and hands its popup that same
|
|
590
|
+
read-only. Closing writes nothing to the picker, and `<Dialog.Close />` says so
|
|
591
|
+
(`whenSelfInteractionsBlocked="ignore"`, see
|
|
592
|
+
[interactions.md](./interactions.md#where-the-zone-blocks-does-it-write-to-the-control-it-sits-in));
|
|
593
|
+
a `<Button command="--navi-close" />` does not, wears the read-only it was
|
|
594
|
+
handed, and answers a press aimed at the way out with "this action is not
|
|
595
|
+
available right now". The same holds for a disabled zone and for a form busy
|
|
596
|
+
sending: the way out of a popup is never held by the state of what the popup
|
|
597
|
+
belongs to. A cross that really has to be hand-written carries the claim itself:
|
|
810
598
|
|
|
811
599
|
```jsx
|
|
812
600
|
<Button command="--navi-close" icon whenSelfInteractionsBlocked="ignore">
|
|
@@ -817,30 +605,20 @@ of the app's own, say — it carries the claim itself:
|
|
|
817
605
|
### It asks, it does not force
|
|
818
606
|
|
|
819
607
|
Being exempt from the state around it is not being exempt from the popup's
|
|
820
|
-
answer. The cross sends `--navi-close`,
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
says why ([above](#the-popup-owns-its-open-state));
|
|
826
|
-
- `onRequestClose` calls `preventDefault()` → the popup stays open;
|
|
827
|
-
- a `Picker` takes the close as an answer, so it validates what its popup holds
|
|
828
|
-
and runs its action on the way out — and an invalid value keeps it open.
|
|
829
|
-
|
|
830
|
-
The cross therefore lands exactly where a click outside lands: same request,
|
|
831
|
-
same refusals, same "close, keep". A popup that must not be dismissed at a given
|
|
832
|
-
moment says so in its own `onRequestClose`, where Escape and the backdrop are
|
|
833
|
-
refused too — not by hiding or disabling the cross, which only removes the one
|
|
834
|
-
way out that is visible.
|
|
608
|
+
answer. The cross sends `--navi-close`, the same close REQUEST as a click
|
|
609
|
+
outside — same refusals, same "close, keep": a control inside mid-action refuses
|
|
610
|
+
it and says why, and a `Picker` validates what its popup holds and runs its
|
|
611
|
+
action on the way out, an invalid value keeping it open. Hiding or disabling the
|
|
612
|
+
cross to hold a popup open only removes the one way out that is visible.
|
|
835
613
|
|
|
836
614
|
## When the app holds the open state
|
|
837
615
|
|
|
838
616
|
A popup whose being-open is a fact about the application rather than about the
|
|
839
617
|
user's last gesture — a sheet an address can be reloaded into, an error the app
|
|
840
618
|
decides to show — needs somewhere to keep that fact. Three props say it, and
|
|
841
|
-
what separates them is where the state lives
|
|
842
|
-
|
|
843
|
-
|
|
619
|
+
what separates them is where the state lives: all three go through the same
|
|
620
|
+
close request, so a busy control inside still refuses to be left mid-action.
|
|
621
|
+
What changes is whether anyone hears the refusal.
|
|
844
622
|
|
|
845
623
|
### `signal` — the app holds it, both ways
|
|
846
624
|
|
|
@@ -849,49 +627,33 @@ refuses to be left mid-action. What changes is whether anyone hears the refusal.
|
|
|
849
627
|
```
|
|
850
628
|
|
|
851
629
|
The popup opens and closes to match the signal, and writes into it whenever it
|
|
852
|
-
opens or closes on its own
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
on them (`SidePanel`) all take it. A signal already `true` at mount means the
|
|
858
|
-
popup was already open when the page appeared: no entrance plays.
|
|
630
|
+
opens or closes on its own — Escape, the backdrop, a `--navi-close` — and after
|
|
631
|
+
a close a busy control denied, when the signal says "open" again, because that
|
|
632
|
+
is what is true (see [state_binding.md](./state_binding.md)). A signal already
|
|
633
|
+
`true` at mount means the popup was already open when the page appeared: no
|
|
634
|
+
entrance plays.
|
|
859
635
|
|
|
860
|
-
`value` makes the signal say WHICH popup is open, for several sharing it —
|
|
861
|
-
|
|
636
|
+
`value` makes the signal say WHICH popup is open, for several sharing it — one
|
|
637
|
+
sheet per card in a feed, with a single `?seat=<gameId>` for all of them:
|
|
862
638
|
|
|
863
639
|
```jsx
|
|
864
640
|
<Dialog signal={seatSheetSignal} value={game.id} />
|
|
865
641
|
```
|
|
866
642
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
same writes, the same history rules — only what "open" is worth in the signal
|
|
871
|
-
changes. A popup is open on exactly one value: what varies while it is open (a
|
|
872
|
-
tab inside it) is a param of its own, see
|
|
643
|
+
Opening writes the value, closing writes `undefined`, which a state signal reads
|
|
644
|
+
as its default and takes out of the url. A popup is open on exactly one value:
|
|
645
|
+
what varies while it is open (a tab inside it) is a param of its own, see
|
|
873
646
|
[navigation.md](./navigation.md#places-inside-the-layer).
|
|
874
647
|
|
|
875
648
|
The other way round — ONE sheet showing whichever card the address names — is
|
|
876
|
-
the same signal with no `value`:
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
open, and a signal that already reads open is left as it is — the address holds
|
|
885
|
-
the card, never `true`. With the route's search-param `stateSignal` as the walk
|
|
886
|
-
signal, that is how [a row of
|
|
887
|
-
cards](./popup_lift.md#a-row-of-cards-one-popup-that-walks) survives leaving
|
|
888
|
-
the page: the address names the card, and the sheet reopens on it.
|
|
889
|
-
|
|
890
|
-
```jsx
|
|
891
|
-
<SidePanel signal={errorOpenSignal} side="right">
|
|
892
|
-
<ErrorPanel /> {/* reads errorOpenSignal.value to know which card */}
|
|
893
|
-
</SidePanel>
|
|
894
|
-
```
|
|
649
|
+
the same signal with no `value`: anything but `false`, `null` and `undefined`
|
|
650
|
+
reads as open, opening writes `true` only into a signal that reads closed, and
|
|
651
|
+
closing writes `undefined` (`false` where it held `true`). The content reads the
|
|
652
|
+
id from the signal and a card's press writes it — in `onOpen` when the popup is
|
|
653
|
+
opened ON the card, which runs before the popup writes its own open. With the
|
|
654
|
+
route's search-param `stateSignal` as the signal, that is how
|
|
655
|
+
[a row of cards](./popup_lift.md#a-row-of-cards-one-popup-that-walks) survives
|
|
656
|
+
leaving the page: the address names the card, and the sheet reopens on it.
|
|
895
657
|
|
|
896
658
|
### `navState` — the history entry holds it
|
|
897
659
|
|
|
@@ -900,35 +662,32 @@ the page: the address names the card, and the sheet reopens on it.
|
|
|
900
662
|
```
|
|
901
663
|
|
|
902
664
|
The open state goes into the history entry, so a screen left and come back to
|
|
903
|
-
finds the popup as it was, and so does a reload. `
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
read it — navi warns when a mount finds such a key. A picker whose popup leads
|
|
913
|
-
somewhere, a link inside it, has an `id`.
|
|
665
|
+
finds the popup as it was, and so does a reload. `{ type: "push" }` makes the
|
|
666
|
+
opening an entry of its own: the back button closes the popup rather than
|
|
667
|
+
leaving the screen, and a cancel goes back with the entry
|
|
668
|
+
([above](#escapeeffectclose-and-why-it-is-a-last-resort)). A `Picker` needs none
|
|
669
|
+
of this, only an `id`: its open state is nav state under that id, pushed in
|
|
670
|
+
dialog mode. Without an `id` the key names one mount — the page left with the
|
|
671
|
+
picker open and come back to is a new mount, and the state stays in the entry
|
|
672
|
+
with nothing to read it (navi warns). A picker whose popup leads somewhere, a
|
|
673
|
+
link inside it, has an `id`.
|
|
914
674
|
|
|
915
675
|
The two meet when the signal IS a route's: a search-param `stateSignal` given to
|
|
916
676
|
`signal` puts the open state in the address itself, where a link can point at
|
|
917
|
-
it
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
|
|
677
|
+
it — the shape of a popup that is a LAYER over the screen, whose address is
|
|
678
|
+
[navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say)'s
|
|
679
|
+
decision.
|
|
921
680
|
|
|
922
681
|
### `open`, and what it costs
|
|
923
682
|
|
|
924
683
|
`open` is the one-way half of `signal` — the parent re-renders with a boolean
|
|
925
|
-
and the popup follows — and the refusal is what it costs. `open={false}`
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
684
|
+
and the popup follows — and the refusal is what it costs. `open={false}` asks
|
|
685
|
+
for the close as a cancel, and when a busy control denies it, the parent's state
|
|
686
|
+
says closed while the popup stayed open. The two disagree from then on: only a
|
|
687
|
+
_change_ of `open` is read, so setting it back to `true` matches the popup's
|
|
688
|
+
real state and does nothing, and nothing closes it until the prop goes to
|
|
689
|
+
`false` again or the popup closes on its own. A `signal` has no such gap, since
|
|
690
|
+
the popup writes back what really happened.
|
|
932
691
|
|
|
933
692
|
`defaultOpen` is the middle ground: mount-only, and the popup owns everything
|
|
934
693
|
afterwards. `defaultOpen="interaction"` means the mount _is_ the opening (the
|
|
@@ -939,9 +698,10 @@ the page appeared (no entrance).
|
|
|
939
698
|
|
|
940
699
|
A board with a detail pane wants one panel that stays open as long as something
|
|
941
700
|
asks to be shown in it, and whose content changes from one card to the next
|
|
942
|
-
without
|
|
943
|
-
|
|
944
|
-
the
|
|
701
|
+
without closing and reopening. That is a slot: `createSlot(Renderer)` keeps the
|
|
702
|
+
renderer mounted whether or not anything fills it, and the panel is the
|
|
703
|
+
renderer. `<PanelSlot />` sits once at the board level, and so does the one
|
|
704
|
+
`<PanelSlotFill>`, rendered by the board with the named card's content:
|
|
945
705
|
|
|
946
706
|
```jsx
|
|
947
707
|
const Panel = ({ children }) => (
|
|
@@ -950,13 +710,7 @@ const Panel = ({ children }) => (
|
|
|
950
710
|
</SidePanel>
|
|
951
711
|
);
|
|
952
712
|
const [PanelSlot, PanelSlotFill] = createSlot(Panel);
|
|
953
|
-
```
|
|
954
713
|
|
|
955
|
-
`<PanelSlot />` sits once at the board level, and so does the one
|
|
956
|
-
`<PanelSlotFill>`, rendered by the board with the named card's content as its
|
|
957
|
-
children:
|
|
958
|
-
|
|
959
|
-
```jsx
|
|
960
714
|
const openCardId = openCardIdSignal.value;
|
|
961
715
|
return (
|
|
962
716
|
<Box>
|
|
@@ -973,35 +727,20 @@ return (
|
|
|
973
727
|
);
|
|
974
728
|
```
|
|
975
729
|
|
|
976
|
-
Two rules hold this shape together
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
of the two directions. The slot cannot tell that gap from a real emptying, and
|
|
986
|
-
does not try to: a fix there would have to guess the scheduler. Rendered once,
|
|
987
|
-
by the component that holds the choice, the fill never leaves; only its
|
|
988
|
-
children change, and there is no gap to fall into.
|
|
989
|
-
|
|
990
|
-
**The panel is bound to the screen's state, not to `isFilled`.** `isFilled` is
|
|
991
|
-
a fact about the render — whether a fill is mounted right now — and the signal
|
|
992
|
-
is a fact about the screen — which card the address names. `open={isFilled}`
|
|
993
|
-
turns any empty frame into a close, and its `onClose` then clears the very
|
|
994
|
-
state the next fill was waiting for. Bound to the signal, the panel is open
|
|
995
|
-
exactly while a card is named, its own ways out (the close cross, Escape, an
|
|
996
|
-
outside press) clear the signal themselves, and the fill only has to say what
|
|
997
|
-
the panel shows. `isFilled` stays the right `open` for a renderer that has no
|
|
998
|
-
such state — a toolbar or a status shown while anything fills it — fed by the
|
|
999
|
-
one fill above.
|
|
730
|
+
Two rules hold this shape together. **One `SlotFill`, rendered where the choice
|
|
731
|
+
is made** — not one in each card, rendered by the card that is open: two cards
|
|
732
|
+
crossing are two renders, and the slot is empty between them. **The panel is
|
|
733
|
+
bound to the screen's state, not to `isFilled`**: `open={isFilled}` turns that
|
|
734
|
+
empty frame into a close, whose `onClose` then clears the state the next fill was
|
|
735
|
+
waiting for. Bound to the signal, the panel is open exactly while a card is
|
|
736
|
+
named, and its own ways out clear the signal themselves. `isFilled` stays the
|
|
737
|
+
right `open` for a renderer with no such state — a toolbar shown while anything
|
|
738
|
+
fills it.
|
|
1000
739
|
|
|
1001
740
|
A press on another card is not an outside press once the card names the panel
|
|
1002
|
-
with `data-navi-popup-inside` (see
|
|
1003
|
-
|
|
1004
|
-
slots".
|
|
741
|
+
with `data-navi-popup-inside` (see
|
|
742
|
+
[popup_backdrop.md](./popup_backdrop.md#a-box-of-the-page-that-is-not-outside)).
|
|
743
|
+
The full example is `src/layout/demos/7_slot_demo.html`, "Two side-panel slots".
|
|
1005
744
|
|
|
1006
745
|
## A popup that loads data
|
|
1007
746
|
|
|
@@ -1016,6 +755,9 @@ const groupSheetOpenSignal = stateSignal(false, {
|
|
|
1016
755
|
id: "group_sheet",
|
|
1017
756
|
type: "boolean",
|
|
1018
757
|
});
|
|
758
|
+
export const GAME_ROUTE = route("/games/:gameId", {
|
|
759
|
+
searchParams: { group_sheet: groupSheetOpenSignal },
|
|
760
|
+
});
|
|
1019
761
|
|
|
1020
762
|
// asked while the sheet is open
|
|
1021
763
|
export const GROUP_MEMBERS = routeAction(GAME_ROUTE, USER.GET_MANY, () => {
|
|
@@ -1042,10 +784,9 @@ const GroupMembers = () => {
|
|
|
1042
784
|
```
|
|
1043
785
|
|
|
1044
786
|
The same shape when the popup is one of many — a sheet on every card, opened
|
|
1045
|
-
from wherever the card is read
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
page:
|
|
787
|
+
from wherever the card is read: its open state says which one, the route action
|
|
788
|
+
reads the id straight from it, and each card's content reads the question of
|
|
789
|
+
its own card. Declared on the root route, since a card is not a page:
|
|
1049
790
|
|
|
1050
791
|
```js
|
|
1051
792
|
const seatSheetSignal = stateSignal(undefined, {
|
|
@@ -1076,17 +817,15 @@ const SeatableUsers = ({ gameId }) => {
|
|
|
1076
817
|
```
|
|
1077
818
|
|
|
1078
819
|
**The content reads its question, never the route action.** The route action's
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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.
|
|
820
|
+
`false` says _when_ to ask. Read by the content, the same `false` says there is
|
|
821
|
+
nothing to show: the list empties the moment the popup closes, and the next
|
|
822
|
+
opening builds it again from the top — the scroll position lost, every row
|
|
823
|
+
rendered again for an answer that never left the store. When the popup is one
|
|
824
|
+
of many it is worse: the route action follows whichever popup is open, so a
|
|
825
|
+
card's sheet kept mounted since an earlier opening redraws with another card's
|
|
826
|
+
rows each time that one opens. Closing a popup does not change what it is about.
|
|
827
|
+
Equal params share one instance, so the content reads the very one the route
|
|
828
|
+
action runs, and nothing is asked twice.
|
|
1090
829
|
|
|
1091
830
|
**A popup that waits holds its own `<Loading>`**, like every other part of a
|
|
1092
831
|
screen that can wait — it is not built into `Dialog`/`Popover`, because only the
|
|
@@ -1100,52 +839,45 @@ code inside knows whether anything in there loads at all:
|
|
|
1100
839
|
</Dialog>
|
|
1101
840
|
```
|
|
1102
841
|
|
|
1103
|
-
Without it the nearest boundary is one that holds the popup itself, and
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
[actions.md](./actions.md#reading-an-action));
|
|
1124
|
-
- the action layer's rerun rules, its dependencies
|
|
1125
|
-
([resource.md](./resource.md#dependencies-rerun-after-another-resource-writes))
|
|
1126
|
-
and the
|
|
1127
|
-
aborted-not-reset treatment a route action gets when the screen is left
|
|
1128
|
-
([network_policy.md](./network_policy.md));
|
|
1129
|
-
- a reload keeps the open state, so it keeps the request too;
|
|
1130
|
-
- and the routes keep saying what an address needs, which is why they declare
|
|
1131
|
-
it.
|
|
1132
|
-
|
|
1133
|
-
The fallback — the popup owning its request, `useAsyncData(action, { run: true })`
|
|
1134
|
-
— is for the parameter chosen inside the popup and dying with it: a filter the
|
|
1135
|
-
sheet itself holds and nothing else remembers. It cannot do better than the
|
|
1136
|
-
gesture that opened the popup, since it starts with the component that draws it;
|
|
1137
|
-
a route action left with the screen. Hand-written (`bindParams` in the component,
|
|
1138
|
-
`run()` from a `useEffect`, `data === undefined` as the loading flag) it looks
|
|
1139
|
-
harmless, and that is the problem: nothing at the call site says which of the two
|
|
1140
|
-
it is, so the day the parameter comes from the address, screen data has quietly
|
|
1141
|
-
become a popup's private request — asked for late, and asked for alone.
|
|
842
|
+
Without it the nearest boundary is one that holds the popup itself, and the
|
|
843
|
+
opening is lost: the popup does not open at all, and dev says why. The other
|
|
844
|
+
option is the component drawing its own wait
|
|
845
|
+
(`useAsyncData(action, { loading: true })`); what is not an option is neither.
|
|
846
|
+
Both are on `src/layout/demos/13_popup_loading_demo.html`.
|
|
847
|
+
|
|
848
|
+
What the popup gains is everything a page has: the request leaves **with the
|
|
849
|
+
screen**, in parallel with the rest of what the address needs, instead of behind
|
|
850
|
+
the gesture that opens the popup; `useAsyncData` reads it (see
|
|
851
|
+
[actions.md](./actions.md#reading-an-action)); the rerun rules, dependencies
|
|
852
|
+
([resource.md](./resource.md#dependencies-rerun-after-another-resource-writes))
|
|
853
|
+
and aborts of a screen left ([network_policy.md](./network_policy.md)) apply; a
|
|
854
|
+
reload keeps the open state, so it keeps the request too.
|
|
855
|
+
|
|
856
|
+
The popup owning its request — `useAsyncData(action, { run: true })` — is the
|
|
857
|
+
fallback, for a parameter chosen inside the popup and dying with it: a filter
|
|
858
|
+
the sheet itself holds and nothing else remembers. Hand-written, with `run()`
|
|
859
|
+
from a `useEffect`, nothing at the call site says which of the two it is, and
|
|
860
|
+
screen data quietly becomes a popup's private request — asked for late, and
|
|
861
|
+
alone.
|
|
1142
862
|
|
|
1143
863
|
## What the popup holds while it is closed
|
|
1144
864
|
|
|
1145
|
-
A closed
|
|
1146
|
-
stay mounted afterwards — a reopened popup finds its scroll position
|
|
1147
|
-
half-typed form where it left them, as long as the data it draws does
|
|
1148
|
-
with the close
|
|
865
|
+
A closed `Dialog`/`Popover` builds nothing: `children` are mounted on the first
|
|
866
|
+
open, and stay mounted afterwards — a reopened popup finds its scroll position
|
|
867
|
+
and its half-typed form where it left them, as long as the data it draws does
|
|
868
|
+
not leave with the close. A `Picker` told no value is the exception: it builds
|
|
869
|
+
its popup at render (see
|
|
870
|
+
[above](#a-press-that-opens-a-popup-and-acts-on-it)).
|
|
871
|
+
|
|
872
|
+
A `Dialog` nobody has asked for yet is not even built. It stands in as its bare
|
|
873
|
+
`<dialog>` — the id a command finds, the element a press announces itself on —
|
|
874
|
+
and the dialog proper is built at the first request: an open, a press on what
|
|
875
|
+
opens it, its `signal` or `open` saying open. From then on it stays built. So a
|
|
876
|
+
list whose rows each hold a few dialogs (a sheet per seat, a share sheet per
|
|
877
|
+
card) pays one element per closed dialog at every mount, not a dialog: a row
|
|
878
|
+
may carry the dialogs of its own actions. It is built at once when something
|
|
879
|
+
needs it from the start — open at mount, a `navState`, an `anchor` (a hover
|
|
880
|
+
builds the content ahead of the click), or a `mount` other than the default.
|
|
1149
881
|
|
|
1150
882
|
"Closed" is two states, not one — never opened yet, and closed again after an
|
|
1151
883
|
opening — so the `mount` prop answers both at once:
|
|
@@ -1158,28 +890,25 @@ opening — so the `mount` prop answers both at once:
|
|
|
1158
890
|
| `"while-opened"` | not mounted | not mounted |
|
|
1159
891
|
|
|
1160
892
|
`"always"` is for content something depends on before any opening: a value read
|
|
1161
|
-
off it
|
|
1162
|
-
`"idle"` is `"always"` minus the cost on
|
|
893
|
+
off it (the default of a picker told no value), fields a surrounding form
|
|
894
|
+
submits, a size measured from outside. `"idle"` is `"always"` minus the cost on
|
|
895
|
+
the render that draws the page.
|
|
1163
896
|
|
|
1164
897
|
Whatever the value but `"while-opened"`, intent on the anchor — a pointer
|
|
1165
|
-
entering it, focus landing in it — builds the content ahead of the click
|
|
1166
|
-
|
|
1167
|
-
it
|
|
1168
|
-
expandable's UI part
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
built after
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
<Dialog ref={dialogRef} mount="while-opened">
|
|
1180
|
-
<Textarea defaultValue={note.text} />
|
|
1181
|
-
</Dialog>
|
|
1182
|
-
```
|
|
898
|
+
entering it, focus landing in it — builds the content ahead of the click,
|
|
899
|
+
`onOpen` or not. A `"while-opened"` popup is warmed only by a press on what
|
|
900
|
+
opens it (a `--navi-open` button, a picker's trigger when a press opens it, an
|
|
901
|
+
expandable's UI part), throws that content away when the press ends without
|
|
902
|
+
opening it — one content at a time — and is never warmed on a
|
|
903
|
+
`Dialog`/`Popover` with an `onOpen`: the one value where the content is always
|
|
904
|
+
built after `onOpen`.
|
|
905
|
+
|
|
906
|
+
`"while-opened"` is for content whose fresh state is its initial state. An
|
|
907
|
+
untouched field does not need it to show a new `defaultValue`: it follows one
|
|
908
|
+
while it holds no edit (see
|
|
909
|
+
[control_value.md](./control_value.md#a-defaultvalue-follows-what-it-was-read-from)).
|
|
910
|
+
What kept content carries into the next opening is an unsent edit, and
|
|
911
|
+
`"while-opened"` is what throws that away.
|
|
1183
912
|
|
|
1184
913
|
The content is dropped only once the exit transition is over, so the popup never
|
|
1185
914
|
plays it on a blank surface; a popup reopened while it was leaving keeps the
|