@jsenv/navi 0.29.387 → 0.29.389
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 +4 -4
- package/dist/dev/jsenv_navi.js +556 -392
- package/dist/dev/jsenv_navi.js.map +81 -81
- package/dist/dev/jsenv_navi_side_effects.js +87 -78
- package/dist/dev/jsenv_navi_side_effects.js.map +4 -4
- package/dist/jsenv_navi.js +551 -387
- package/dist/jsenv_navi.js.map +81 -81
- package/dist/jsenv_navi_side_effects.js +87 -78
- package/dist/jsenv_navi_side_effects.js.map +4 -4
- package/docs/AI_INSTRUCTIONS.md +39 -23
- package/docs/actions.md +6 -4
- package/docs/autofocus.md +2 -1
- package/docs/badge_list.md +5 -3
- package/docs/control_group.md +4 -3
- package/docs/control_object.md +1 -1
- package/docs/control_value.md +8 -9
- package/docs/css_architecture.md +16 -14
- package/docs/dialog_shape.md +15 -10
- package/docs/drag_interactions.md +49 -41
- package/docs/drag_to_travel.md +32 -24
- package/docs/dynamic_import.md +7 -5
- package/docs/error_handling.md +31 -30
- package/docs/field_validation.md +35 -6
- package/docs/i18n.md +16 -7
- package/docs/interactions.md +28 -23
- package/docs/list_action.md +8 -5
- package/docs/list_refresh.md +8 -1
- package/docs/mobile_layout_pitfalls.md +6 -5
- package/docs/navigation.md +7 -5
- package/docs/network_policy.md +22 -7
- package/docs/pan_zoom.md +16 -9
- package/docs/popup_backdrop.md +11 -5
- package/docs/popup_lift.md +5 -3
- package/docs/popup_open.md +39 -28
- package/docs/resource.md +31 -20
- package/docs/route_transitions.md +13 -10
- package/docs/safe_area.md +59 -39
- package/docs/scroll.md +11 -9
- package/docs/testid.md +10 -1
- package/docs/typography.md +2 -3
- package/docs/view_transitions.md +9 -9
- package/docs/z_index.md +6 -5
- package/package.json +2 -2
package/docs/popup_open.md
CHANGED
|
@@ -91,7 +91,7 @@ where that state lives:
|
|
|
91
91
|
|
|
92
92
|
| what opens it | how |
|
|
93
93
|
| ---------------------------------- | ------------------------------------------------------ |
|
|
94
|
-
| a button | `command` / `
|
|
94
|
+
| a button | `command` / `commandFor` props, no `open` |
|
|
95
95
|
| a gesture, an event, a JS decision | `triggerNaviCommand(...)`, still no `open` |
|
|
96
96
|
| a state the app holds | `signal` — [below](#when-the-app-holds-the-open-state) |
|
|
97
97
|
| where the user is | `navState`, or a route `stateSignal` given to `signal` |
|
|
@@ -99,7 +99,7 @@ where that state lives:
|
|
|
99
99
|
## A button opens it: the attributes
|
|
100
100
|
|
|
101
101
|
```jsx
|
|
102
|
-
<Button command="--navi-open"
|
|
102
|
+
<Button command="--navi-open" commandFor="note-dialog">
|
|
103
103
|
Read the note
|
|
104
104
|
</Button>
|
|
105
105
|
<Dialog id="note-dialog">…</Dialog>
|
|
@@ -123,7 +123,7 @@ press:
|
|
|
123
123
|
## Something else opens it: `triggerNaviCommand`
|
|
124
124
|
|
|
125
125
|
The attributes fire on every click of the element that carries them, and they
|
|
126
|
-
cover more than "this button opens that dialog": `
|
|
126
|
+
cover more than "this button opens that dialog": `commandFor` says to whom,
|
|
127
127
|
`value` says what it is about, `--navi-x:argument` says how. Before writing any
|
|
128
128
|
JS here, check none of those is the answer — `triggerNaviCommand` is the last
|
|
129
129
|
resort, not the general way to run a command.
|
|
@@ -209,7 +209,7 @@ closes the popup as usual.
|
|
|
209
209
|
|
|
210
210
|
```jsx
|
|
211
211
|
<Row
|
|
212
|
-
|
|
212
|
+
commandFor="row_menu"
|
|
213
213
|
interactions={{
|
|
214
214
|
longpress: (event) =>
|
|
215
215
|
triggerNaviCommand(event.target, "--navi-open", event),
|
|
@@ -225,7 +225,8 @@ own opening press from somebody else's comes from that gesture.
|
|
|
225
225
|
The first argument is the command's **source** — the element it is triggered
|
|
226
226
|
_from_. The target is resolved from it, in this order:
|
|
227
227
|
|
|
228
|
-
1. `
|
|
228
|
+
1. `commandFor="someId"` on the source (written to the DOM as the HTML
|
|
229
|
+
`commandfor` attribute, which is what is read),
|
|
229
230
|
2. `navi-command-target="parent-control" | "child-control"`,
|
|
230
231
|
3. the command's own fallback — for the popup commands, `closest("[aria-expanded]")`.
|
|
231
232
|
|
|
@@ -234,7 +235,7 @@ element itself as the source resolves to that popup: `closest()` starts at the
|
|
|
234
235
|
element itself. That is the short form used above, and it is enough whenever the
|
|
235
236
|
JS that decides already holds the popup's ref.
|
|
236
237
|
|
|
237
|
-
To trigger from another element instead, give that element a `
|
|
238
|
+
To trigger from another element instead, give that element a `commandFor`
|
|
238
239
|
pointing at the popup's `id` — attribute-driven target resolution, JS-driven
|
|
239
240
|
timing.
|
|
240
241
|
|
|
@@ -250,10 +251,11 @@ names none, and on whoever asked when nothing else says. In that order:
|
|
|
250
251
|
3. **`detail.source`** — who asked. A button therefore opens the popup on
|
|
251
252
|
itself, and a popup passed as its own source is its own anchor.
|
|
252
253
|
|
|
253
|
-
For `Dialog` the anchor does nothing at all unless `sizeFromAnchor`
|
|
254
|
-
(
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
For `Dialog` the anchor does nothing at all unless `sizeFromAnchor` asks for
|
|
255
|
+
it (it then feeds the `--anchor-width`/`--anchor-height` CSS vars) or
|
|
256
|
+
`animation="lifting"` does (the box the dialog comes out of, see
|
|
257
|
+
[popup_lift.md](./popup_lift.md)); for `Popover`, which really is positioned
|
|
258
|
+
relative to its anchor, say what the anchor is:
|
|
257
259
|
|
|
258
260
|
```jsx
|
|
259
261
|
<Popover ref={popoverRef} anchor={rowRef}>
|
|
@@ -261,8 +263,9 @@ anchor is:
|
|
|
261
263
|
|
|
262
264
|
`anchorCustomEventDetail="ignore"` drops the open's side of that order
|
|
263
265
|
entirely, leaving the `anchor` prop alone — for a popover that must never be
|
|
264
|
-
anchored to whatever opened it (`SidePanel` does this), and for a
|
|
265
|
-
|
|
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"`).
|
|
266
269
|
|
|
267
270
|
## Opening it ON something
|
|
268
271
|
|
|
@@ -272,8 +275,8 @@ row is one dialog with two modes, and the press is the only thing that knows
|
|
|
272
275
|
which one — so the press says it, with its own value:
|
|
273
276
|
|
|
274
277
|
```jsx
|
|
275
|
-
<Button command="--navi-open"
|
|
276
|
-
<Button value={radar.id} command="--navi-open"
|
|
278
|
+
<Button command="--navi-open" commandFor="radar-dialog">Nouveau radar</Button>
|
|
279
|
+
<Button value={radar.id} command="--navi-open" commandFor="radar-dialog" />
|
|
277
280
|
|
|
278
281
|
<Dialog
|
|
279
282
|
id="radar-dialog"
|
|
@@ -629,7 +632,7 @@ command once it succeeded.
|
|
|
629
632
|
```jsx
|
|
630
633
|
// Stays open while save() runs, closes when it resolves, stays open if it
|
|
631
634
|
// throws — with what was typed still there and the error on the button.
|
|
632
|
-
<Button command="--navi-close"
|
|
635
|
+
<Button command="--navi-close" commandFor="note-dialog" action={save}>
|
|
633
636
|
Save
|
|
634
637
|
</Button>
|
|
635
638
|
```
|
|
@@ -689,12 +692,12 @@ the popup without closing it, and the error callout stays where the press was.
|
|
|
689
692
|
The gestures that close a popup do not all mean the same thing, and that is on
|
|
690
693
|
purpose:
|
|
691
694
|
|
|
692
|
-
| gesture | what it means | who decides
|
|
693
|
-
| ------------------------------ | ------------- |
|
|
694
|
-
| Escape | cancel | `escapeEffect="cancel"` (default) |
|
|
695
|
-
| a press outside | close, keep | `pressOutside="close"`
|
|
696
|
-
| a close cross (`--navi-close`) | close, keep | [the cross](#the-close-cross)
|
|
697
|
-
| `--navi-cancel` on a button | cancel | the button
|
|
695
|
+
| gesture | what it means | who decides |
|
|
696
|
+
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------ |
|
|
697
|
+
| Escape | cancel | always, for a `Dialog`/`Popover`; `escapeEffect="cancel"` (the default) for a `Picker` |
|
|
698
|
+
| a press outside | close, keep | `pressOutside="close"` — the default of a `Dialog` and of a `Picker`; a bare `Popover` says `"ignore"` |
|
|
699
|
+
| a close cross (`--navi-close`) | close, keep | [the cross](#the-close-cross) |
|
|
700
|
+
| `--navi-cancel` on a button | cancel | the button |
|
|
698
701
|
|
|
699
702
|
Escape says "forget it". It is the one gesture that has meant that everywhere,
|
|
700
703
|
for as long as there have been dialogs, and navi keeps it that way. **A popup
|
|
@@ -757,7 +760,8 @@ empty.
|
|
|
757
760
|
|
|
758
761
|
### `escapeEffect="close"`, and why it is a last resort
|
|
759
762
|
|
|
760
|
-
`escapeEffect="close"`
|
|
763
|
+
`escapeEffect="close"` — a `Picker` prop; a `Dialog`/`Popover` has no such
|
|
764
|
+
switch — makes Escape say what a click outside says. It exists,
|
|
761
765
|
and it is almost never what you want: it takes away the only key that undoes,
|
|
762
766
|
and a popup with no way back is one people stop opening. Reach for a close
|
|
763
767
|
cross first.
|
|
@@ -996,7 +1000,8 @@ one fill above.
|
|
|
996
1000
|
|
|
997
1001
|
A press on another card is not an outside press once the card names the panel
|
|
998
1002
|
with `data-navi-popup-inside` (see [popup_backdrop.md](./popup_backdrop.md)).
|
|
999
|
-
The full example is `src/layout/demos/7_slot_demo.html`, "
|
|
1003
|
+
The full example is `src/layout/demos/7_slot_demo.html`, "Two side-panel
|
|
1004
|
+
slots".
|
|
1000
1005
|
|
|
1001
1006
|
## A popup that loads data
|
|
1002
1007
|
|
|
@@ -1116,14 +1121,20 @@ half-typed form where it left them.
|
|
|
1116
1121
|
"Closed" is two states, not one — never opened yet, and closed again after an
|
|
1117
1122
|
opening — so the `mount` prop answers both at once:
|
|
1118
1123
|
|
|
1119
|
-
| `mount` | before the first open
|
|
1120
|
-
| ----------------------------- |
|
|
1121
|
-
| `"always"` | mounted
|
|
1122
|
-
| `"
|
|
1123
|
-
| `"
|
|
1124
|
+
| `mount` | before the first open | after a close |
|
|
1125
|
+
| ----------------------------- | --------------------------- | ------------- |
|
|
1126
|
+
| `"always"` | mounted | mounted |
|
|
1127
|
+
| `"idle"` | mounted once the page idles | mounted |
|
|
1128
|
+
| `"from-first-open"` (default) | not mounted | mounted |
|
|
1129
|
+
| `"while-opened"` | not mounted | not mounted |
|
|
1124
1130
|
|
|
1125
1131
|
`"always"` is for content something depends on before any opening: a value read
|
|
1126
1132
|
off it, fields a surrounding form submits, a size measured from outside.
|
|
1133
|
+
`"idle"` is `"always"` minus the cost on the render that draws the page.
|
|
1134
|
+
|
|
1135
|
+
Whatever the value but `"while-opened"`, intent on the anchor — a pointer
|
|
1136
|
+
entering it, focus landing in it — builds the content ahead of the click that
|
|
1137
|
+
will open it.
|
|
1127
1138
|
|
|
1128
1139
|
`"while-opened"` is what an uncontrolled field seeded from a `defaultValue`
|
|
1129
1140
|
needs: without it, a popup reopened after the underlying value changed still
|
package/docs/resource.md
CHANGED
|
@@ -93,14 +93,15 @@ const GAME = resource("game", {
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
The callback receives the bound params merged with the range the list asks for
|
|
96
|
-
(`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and
|
|
97
|
-
as second argument — aborted when the list stops wanting those rows.
|
|
98
|
-
a range the way a `Content-Range` does: **`{ items, start, count }`**
|
|
99
|
-
rows, at this place, out of that many. `start` may be omitted when the
|
|
100
|
-
asked for
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
(`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and `{ signal }`
|
|
97
|
+
as second argument — a signal aborted when the list stops wanting those rows.
|
|
98
|
+
It returns a range the way a `Content-Range` does: **`{ items, start, count }`**
|
|
99
|
+
— these rows, at this place, out of that many. `start` may be omitted when the
|
|
100
|
+
list asked for one at or after `0` (a negative `start` counts back from the end,
|
|
101
|
+
and the source must then say where the slice landed); `count` defaults to
|
|
102
|
+
`start + items.length` (a source that does not know its total). The items are
|
|
103
|
+
upserted on their way in, so the list draws store items — never copies of the
|
|
104
|
+
JSON — and a request sent from a row is read back on that row.
|
|
104
105
|
|
|
105
106
|
A row that must follow its **own fields** through a write reads them from the
|
|
106
107
|
store rather than from the object it was handed:
|
|
@@ -131,9 +132,12 @@ this collection again after its screen went away finds it drawn and revalidates
|
|
|
131
132
|
it rather than starting over
|
|
132
133
|
([list_refresh.md](./list_refresh.md#leaving-the-screen-and-coming-back)).
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
Membership is what a re-read answers: a verb listed in `rerunOn.GET_RANGE`
|
|
136
|
+
(`["POST", "DELETE"]` by default) tells the run the collection moved, and it
|
|
137
|
+
asks again for the window it draws, rows staying on screen meanwhile. A write
|
|
138
|
+
that moves an item in or out through another verb — a `PATCH` that archives —
|
|
139
|
+
is added there, or `reader.invalidate()` is called; a `key` on the run is not the
|
|
140
|
+
way ([list_refresh.md](./list_refresh.md#a-paginated-list-stays-on-screen-too)).
|
|
137
141
|
|
|
138
142
|
It reads a collection, so it lives on the resource itself (or on a
|
|
139
143
|
`withParams()` of it), not on a relation.
|
|
@@ -252,7 +256,8 @@ These are not guessable — each relationship method has its own shape.
|
|
|
252
256
|
|
|
253
257
|
### `.one(propertyName, childResource, { GET, PUT, DELETE })`
|
|
254
258
|
|
|
255
|
-
|
|
259
|
+
`GET` and `PUT` return the **parent** object with the child nested inside;
|
|
260
|
+
`DELETE` returns the parent id, or `{ id }`:
|
|
256
261
|
|
|
257
262
|
```js
|
|
258
263
|
const USER_SESSION = USER.one("session", SESSION, {
|
|
@@ -411,14 +416,18 @@ rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
|
|
|
411
416
|
reruns the parent. Declare it explicitly with `dependencies` if you need it
|
|
412
417
|
(see [`dependencies`](#dependencies-rerun-after-another-resource-writes)).
|
|
413
418
|
- Within a relationship resource, the usual defaults still apply: its own
|
|
414
|
-
`GET_MANY` reruns after its own `POST
|
|
415
|
-
`DELETE
|
|
416
|
-
|
|
419
|
+
`GET_MANY` reruns after its own `POST`. A `.one`/`.many` `GET` is reset (not
|
|
420
|
+
rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
|
|
421
|
+
its data simply resolves to nothing once the child is dropped. Override per
|
|
422
|
+
relation with `rerunOn`/`dependencies`, which every relationship method
|
|
423
|
+
accepts; what a relation leaves out is its parent resource's (`rerunOn` key
|
|
424
|
+
by key).
|
|
417
425
|
|
|
418
426
|
Splitting a sub-resource out of a parent `PATCH` therefore changes the refresh
|
|
419
|
-
graph:
|
|
420
|
-
only by these rules. When a parent field
|
|
421
|
-
say so with `dependencies` rather than
|
|
427
|
+
graph: a parent field the parent's own response kept fresh is, once the relation
|
|
428
|
+
is a resource of its own, refreshed only by these rules. When a parent field
|
|
429
|
+
genuinely depends on a child mutation, say so with `dependencies` rather than
|
|
430
|
+
relying on a rerun that will not happen.
|
|
422
431
|
|
|
423
432
|
## `withParams()`: a scope with reruns of its own
|
|
424
433
|
|
|
@@ -453,8 +462,10 @@ membership of each list is a question its own scope answers.
|
|
|
453
462
|
`withParams()` chains, merging the params —
|
|
454
463
|
`USER.withParams({ role: "admin" }).withParams({ gender: "male" })` is
|
|
455
464
|
`USER.withParams({ role: "admin", gender: "male" })`. Its second argument takes
|
|
456
|
-
`rerunOn` and `dependencies` for that scope;
|
|
457
|
-
resource's
|
|
465
|
+
`rerunOn` and `dependencies` for that scope; what it leaves out is the
|
|
466
|
+
resource's — a `rerunOn` key by key, so `{ rerunOn: { GET: ["PATCH"] } }` keeps
|
|
467
|
+
the resource's `GET_MANY` and `GET_RANGE`. Empty params throw: a scope has to be
|
|
468
|
+
about something.
|
|
458
469
|
|
|
459
470
|
It is not the tool for a param the user types. A search word bound with
|
|
460
471
|
`withParams()` would keep its results from refreshing after a write on the
|
|
@@ -132,8 +132,9 @@ relation was written for. Two situations, two answers:
|
|
|
132
132
|
deserves a direction. This is the case the export exists for.
|
|
133
133
|
|
|
134
134
|
A default has no direction (nothing says which of two arbitrary pages is
|
|
135
|
-
"before" the other), so only directionless movements make sense there
|
|
136
|
-
|
|
135
|
+
"before" the other), so only directionless movements make sense there: navi's
|
|
136
|
+
`slide-*` and `cover-*` are written on the direction, and as the default they
|
|
137
|
+
play nothing. Written relations, and `"none"`, always win over it.
|
|
137
138
|
|
|
138
139
|
## When one navigation knows better
|
|
139
140
|
|
|
@@ -179,7 +180,8 @@ the same forms `defineRouteTransition` takes, plus `direction`. It overrides
|
|
|
179
180
|
answers for. So:
|
|
180
181
|
|
|
181
182
|
- `{ direction: "back" }` keeps the pair's movement and only turns it round;
|
|
182
|
-
- `"zoom"` swaps the movement
|
|
183
|
+
- `"zoom"` swaps the movement; the way round and the pace are still the
|
|
184
|
+
pair's;
|
|
183
185
|
- `"none"` cuts, where the pair — or the default — would have played;
|
|
184
186
|
- `{ duration: 500 }` re-times what was already going to play.
|
|
185
187
|
|
|
@@ -525,12 +527,13 @@ await page.goBack();
|
|
|
525
527
|
The window is **one frame**: the hold is given back inside the view
|
|
526
528
|
transition's callback, which the browser runs at its next rendering
|
|
527
529
|
opportunity. `tests/route_transition_list_revisit/` walks it the wrong way on
|
|
528
|
-
purpose in its last case —
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
same millisecond. This is a testing trap, not
|
|
530
|
+
purpose in its last case — a back pressed the instant the URL changes — and
|
|
531
|
+
which side of that frame a cycle lands on belongs to the machine's load: under
|
|
532
|
+
a movement the list that comes back is either the element that never left,
|
|
533
|
+
asking for nothing, or one truly remounted, asking once, while the same walk
|
|
534
|
+
without a movement is a real one every time. No thumb moves in one frame; an
|
|
535
|
+
automated click continues in the same millisecond. This is a testing trap, not
|
|
536
|
+
a user-facing behaviour.
|
|
534
537
|
|
|
535
538
|
## The rest, briefly
|
|
536
539
|
|
|
@@ -538,6 +541,6 @@ same millisecond. This is a testing trap, not a user-facing behaviour.
|
|
|
538
541
|
a per-relation `{ type, duration }` for one relation.
|
|
539
542
|
- The URL leads: transitions play on navigations somebody else started (a
|
|
540
543
|
`<Link>`, the back button, `history.back()`). Nothing here navigates.
|
|
541
|
-
- A browser without view transitions
|
|
544
|
+
- A browser without view transitions navigates with a cut. The app
|
|
542
545
|
must remain fully usable that way — which it is, if the transitions state
|
|
543
546
|
relations rather than carry information.
|
package/docs/safe_area.md
CHANGED
|
@@ -75,13 +75,8 @@ So the app states its own screen once, and never names a component:
|
|
|
75
75
|
|
|
76
76
|
The bands fall out of it (centered), `--navi-app-width` follows, and `FixedBar`
|
|
77
77
|
pins itself to the column's edges rather than the glass. An app wanting them
|
|
78
|
-
uneven writes `--navi-app-inset-left` / `-right` directly instead
|
|
79
|
-
|
|
80
|
-
In pixels: popup placement reads this value back from CSS to compute its own
|
|
81
|
-
margins, and a custom property computes to a token stream rather than to a
|
|
82
|
-
length, so `40rem` would arrive there as the string `"40rem"`. A non-px value
|
|
83
|
-
still caps the popup's size (that part is pure CSS) but leaves the margins
|
|
84
|
-
viewport-sized, and says so in the console.
|
|
78
|
+
uneven writes `--navi-app-inset-left` / `-right` directly instead; everything
|
|
79
|
+
below follows those the same way, popup placement included.
|
|
85
80
|
|
|
86
81
|
Every popup follows: `Dialog`, `Popover`, and everything built on them
|
|
87
82
|
(`Picker`, `Select`…). It is a ceiling and nothing more — on a screen narrower
|
|
@@ -98,11 +93,13 @@ Two ways NOT to get this:
|
|
|
98
93
|
popovers would keep sizing themselves against 1500px;
|
|
99
94
|
- setting `--dialog-max-width` on `.navi_dialog` from the app. It is a
|
|
100
95
|
`--component-*` token, declared on the element (see
|
|
101
|
-
[css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
[css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go)):
|
|
97
|
+
every dialog resets it on itself in an unlayered rule, so that a nested
|
|
98
|
+
dialog does not inherit its parent's size, and a `maxWidth` prop writes it
|
|
99
|
+
inline (a `Picker` under `dialogSizeFromAnchor` does) — an app rule of the
|
|
100
|
+
same specificity does not reliably win, and the cap silently disappears. It
|
|
101
|
+
is also the knob a single popup uses to ask for a specific size, not a
|
|
102
|
+
ceiling:
|
|
106
103
|
`--navi-app-max-width` feeds `--dialog-maxmax-width`, the hard ceiling _under_
|
|
107
104
|
that knob, so a popup that genuinely needs its own `maxWidth` can still say
|
|
108
105
|
so without escaping the app's screen.
|
|
@@ -114,15 +111,19 @@ price.
|
|
|
114
111
|
|
|
115
112
|
#### Placement follows the same rectangle
|
|
116
113
|
|
|
117
|
-
|
|
114
|
+
The app's rectangle moves where a popup is placed, not only how big it may
|
|
118
115
|
get. Placement is computed against the visual viewport narrowed to the level-1
|
|
119
|
-
bands: `getAppInsets` (`src/layout/responsive.js`)
|
|
120
|
-
|
|
116
|
+
bands: `getAppInsets` (`src/layout/responsive.js`) reads `--navi-app-inset-*`
|
|
117
|
+
back off the computed style — the bands `--navi-app-max-width` centers and the
|
|
118
|
+
ones an app writes by hand alike, in any length unit — navi hands them to
|
|
119
|
+
`@jsenv/dom` once (`setPlacementViewportInsets`, wired in
|
|
121
120
|
`navi_css_vars.js`), and `pickPositionRelativeTo` reads them on every
|
|
122
|
-
placement.
|
|
123
|
-
`bottom`, `top`, which is what a dialog does nearly
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
placement. With centered bands it is invisible for anything centered on its
|
|
122
|
+
cross axis — `center`, `bottom`, `top`, which is what a dialog does nearly
|
|
123
|
+
always — and with uneven ones it is what centers that dialog on the app column
|
|
124
|
+
rather than on the window. Either way it is what puts anything anchored to an
|
|
125
|
+
edge (a `positionArea` like `bottom-start`, a `SidePanel`) flush against the
|
|
126
|
+
app column's edge rather than the window's.
|
|
126
127
|
`FixedBar` reads the same description through CSS instead: it is pinned to
|
|
127
128
|
`--navi-app-inset-*`, which says where the app's rectangle is in the window
|
|
128
129
|
rather than how wide it may be.
|
|
@@ -154,18 +155,25 @@ Beware of making that container scrollable by accident — see
|
|
|
154
155
|
`var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
|
|
155
156
|
or not the app ever mounts a bar.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
`
|
|
159
|
-
`
|
|
160
|
-
|
|
158
|
+
From **JS**, both levels are registered as lengths (`@property`, in
|
|
159
|
+
`safe_area.js`), so
|
|
160
|
+
`getComputedStyle(document.documentElement).getPropertyValue("--navi-safe-area-inset-bottom")`
|
|
161
|
+
gives pixels, and so does `--navi-app-inset-*`.
|
|
161
162
|
|
|
162
163
|
### Putting something new into it
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
Level 2 has one slot per edge, `--navi-fixed-bar-space-*`, and it belongs to
|
|
166
|
+
`FixedBar`: each bar measures its own border box (notch included), and navi
|
|
167
|
+
writes the largest one on that edge inline on `<html>`
|
|
168
|
+
(`src/layout/fixed_bar/fixed_bar_space.js`). The slot is combined with
|
|
169
|
+
`env(safe-area-inset-*)` through `max()`, not added to it, since a bar measured
|
|
170
|
+
with its notch already covers the notch.
|
|
171
|
+
|
|
172
|
+
Something else taking an edge — a native banner, an OS strip — is published by
|
|
173
|
+
being drawn as a `FixedBar`; every component reading the safe area then clears
|
|
174
|
+
it without learning it exists. Writing the slot by hand holds only while no bar
|
|
175
|
+
takes that edge: the first bar mounting there replaces the value rather than
|
|
176
|
+
adding to it.
|
|
169
177
|
|
|
170
178
|
## What already reads it
|
|
171
179
|
|
|
@@ -182,24 +190,36 @@ truth:
|
|
|
182
190
|
|
|
183
191
|
## The trap: which viewport
|
|
184
192
|
|
|
185
|
-
Three heights are in play and they are not the same one
|
|
193
|
+
Three heights are in play and they are not the same one — and which of them
|
|
194
|
+
the keyboard moves depends on the browser.
|
|
195
|
+
|
|
196
|
+
| what | shrinks when the keyboard opens |
|
|
197
|
+
| -------------------------------- | ----------------------------------------------- |
|
|
198
|
+
| `window.innerHeight` / `100dvh` | no |
|
|
199
|
+
| `visualViewport.height` | yes, except where the keyboard overlays (below) |
|
|
200
|
+
| `--navi-vvh` (tracks the visual) | same as `visualViewport.height` |
|
|
186
201
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
Wherever the browser has the VirtualKeyboard API (Chromium), navi makes the
|
|
203
|
+
keyboard overlay the page rather than shrink it (`src/layout/virtual_keyboard.js`):
|
|
204
|
+
no viewport shrinks, and the keyboard arrives as `--navi-keyboard-inset-bottom`
|
|
205
|
+
(`env(keyboard-inset-height)`), which `--navi-app-inset-bottom` adds. Firefox,
|
|
206
|
+
Safari, and an app that called `disableVirtualKeyboardOverlay()` shrink the
|
|
207
|
+
visual viewport instead, and `--navi-keyboard-inset-bottom` stays 0. Either way
|
|
208
|
+
`--navi-app-height` and the popup ceilings answer the part of the screen left
|
|
209
|
+
visible.
|
|
192
210
|
|
|
193
211
|
`position: fixed` — so every `FixedBar` — is laid out against the **layout**
|
|
194
|
-
viewport.
|
|
195
|
-
|
|
196
|
-
nothing reduced the layout viewport.
|
|
212
|
+
viewport. Where the visual viewport is what shrinks, a bottom bar therefore
|
|
213
|
+
stays at the bottom of a window the keyboard is covering: it ends up _behind_
|
|
214
|
+
the keyboard, and no inset says so, because nothing reduced the layout viewport.
|
|
215
|
+
Where the keyboard overlays, the bar is pinned to `--navi-app-inset-bottom`,
|
|
216
|
+
which counts it, and sits above it.
|
|
197
217
|
|
|
198
218
|
The consequence for anything measuring against the insets: mix the two families
|
|
199
219
|
and you get a drift that only appears with a keyboard open. `getBoundingClientRect`
|
|
200
220
|
is in layout-viewport coordinates, so what is compared to it must be too
|
|
201
|
-
(`100dvh`), while `--navi-app-*` derives from `--navi-vvh`
|
|
202
|
-
_sizes_ must fit what is actually visible.
|
|
221
|
+
(`100dvh`), while `--navi-app-*` derives from `--navi-vvh` (plus the keyboard
|
|
222
|
+
inset) because what navi _sizes_ must fit what is actually visible.
|
|
203
223
|
|
|
204
224
|
`src/layout/demos/fixed_bar/keyboard.html` puts all of these on screen at once
|
|
205
225
|
and turns the bottom bar's number red when it goes under the keyboard. On a
|
package/docs/scroll.md
CHANGED
|
@@ -47,7 +47,7 @@ Two shapes, and they do not behave the same:
|
|
|
47
47
|
| `header` / `footer` alone | the container itself scrolls, and they are `position: sticky` at its edges — the content scrolls under them |
|
|
48
48
|
| a `body` as well | the container becomes a flex column, its own overflow turns to `hidden`, and the **body is the only thing that scrolls**; header and footer sit outside it (`position: static`, `flex-shrink: 0`) |
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Three consequences worth knowing before fighting them:
|
|
51
51
|
|
|
52
52
|
- the body is `flex: 0 1 auto` — **it shrinks, it never grows**. A short body
|
|
53
53
|
leaves the footer right under it rather than pushed to the bottom of a box it
|
|
@@ -61,7 +61,8 @@ Two consequences worth knowing before fighting them:
|
|
|
61
61
|
them — positioned or not. Write `style={{ "--box-header-z-index": "auto" }}`
|
|
62
62
|
(`--box-footer-z-index` likewise) at the call site that needs the opposite: a
|
|
63
63
|
badge or a stamp overflowing a row is otherwise sliced by a header it never
|
|
64
|
-
scrolls under. `isolation: isolate`
|
|
64
|
+
scrolls under. The box is `isolation: isolate` already, so either value stays
|
|
65
|
+
local to it.
|
|
65
66
|
See `docs/z_index.md` and `src/box/demos/9_scrollable_z_index_demo.html`.
|
|
66
67
|
|
|
67
68
|
Padding belongs on the parts, not on the scrolling box: padding on a scroller
|
|
@@ -237,10 +238,11 @@ A dialog is already bounded by the room its container leaves it
|
|
|
237
238
|
(`--dialog-maxmax-height`), so a `maxHeight` is only for making it smaller than
|
|
238
239
|
that.
|
|
239
240
|
|
|
240
|
-
`dialog.jsx` deliberately declares no `overflow` of its own:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
`dialog.jsx` deliberately declares no `overflow` of its own: the
|
|
242
|
+
`[data-scrollable]` the dialog carries is its scrolling rule. A modal dialog
|
|
243
|
+
would get `auto` from the UA stylesheet anyway, but a `layer="local"` one gets
|
|
244
|
+
nothing, and without that rule its `max-height` would only decide how big the
|
|
245
|
+
box looks while the content kept painting straight through it.
|
|
244
246
|
|
|
245
247
|
### `scrollCapture`
|
|
246
248
|
|
|
@@ -309,7 +311,7 @@ So: nothing scrollable between the cap and the slides. A `<Box body>` around
|
|
|
309
311
|
them is a scroller (see the table at the top of this file) — and so is a bare
|
|
310
312
|
`overflow="auto"` on a wrapper. The dialog keeps a shared `header` if the tabs
|
|
311
313
|
are shared, with an explicit `flexShrink="0"` since the rule that gives it for
|
|
312
|
-
free
|
|
314
|
+
free applies only next to a `body`.
|
|
313
315
|
|
|
314
316
|
**Padding goes on the slide** — or on its parts, since the slide is now the
|
|
315
317
|
scroller (see the top of this file) — but never on the container nor on
|
|
@@ -393,8 +395,8 @@ browser paints first — what a phone screen shows, plus a few — and `after` f
|
|
|
393
395
|
the paint on. The switch waits for the paint itself, not for an effect: preact
|
|
394
396
|
runs a component's pending effects early when that component renders again,
|
|
395
397
|
and something always re-renders before a popup has painted. Do not rebuild
|
|
396
|
-
this by hand with a `useEffect` that widens a slice
|
|
397
|
-
|
|
398
|
+
this by hand with a `useEffect` that widens a slice: that effect is one preact
|
|
399
|
+
runs early, and it fails for that reason. The runs ask their source for `after`
|
|
398
400
|
rows from the start, so the smaller first window costs no second request.
|
|
399
401
|
|
|
400
402
|
### Doing it well
|
package/docs/testid.md
CHANGED
|
@@ -85,7 +85,9 @@ found by nothing, while the host is what the user focuses and what
|
|
|
85
85
|
`getByRole(role, { name })` and `getByLabel()` read.
|
|
86
86
|
|
|
87
87
|
```jsx
|
|
88
|
-
<Picker variant="icon" aria-label="Aide" ui={<Icon>…</Icon>}
|
|
88
|
+
<Picker variant="icon" aria-label="Aide" ui={<Icon>…</Icon>}>
|
|
89
|
+
…
|
|
90
|
+
</Picker>
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
```html
|
|
@@ -122,6 +124,13 @@ error message, a section, a row — put the testid on the surrounding component
|
|
|
122
124
|
(`<Field data-testid="email-field">`, `<Box data-testid="cart-row">`); anything
|
|
123
125
|
built on `Box` forwards it to its own element.
|
|
124
126
|
|
|
127
|
+
A selectable `List.Item` is a control too, and the rule holds there with a
|
|
128
|
+
consequence worth knowing: its host is the real checkbox or radio the row
|
|
129
|
+
selects with, an input navi draws invisible (`navi-visually-hidden`) and lays
|
|
130
|
+
under the row's content. A `data-testid` on the item names that input — what
|
|
131
|
+
`toBeChecked()` reads, not what the eye sees. A test that presses the row, or
|
|
132
|
+
waits for it to show, targets what the row draws: a testid on its content.
|
|
133
|
+
|
|
125
134
|
A picker's popup is the one element the application does not render itself:
|
|
126
135
|
`<Picker>` builds it, so a `data-testid` on the picker names the trigger and
|
|
127
136
|
nothing names the popup. `popupTestId` does:
|
package/docs/typography.md
CHANGED
|
@@ -84,7 +84,7 @@ The prop is the same everywhere: a number of lines, cut with an ellipsis.
|
|
|
84
84
|
- **`Picker`** — its value, `maxLines={1}` by default; `variant="text"` and
|
|
85
85
|
`variant="bare"` draw the caller's own thing and are not clamped, a `Text` in
|
|
86
86
|
their `ui` cuts itself with its own `maxLines`.
|
|
87
|
-
- **`
|
|
87
|
+
- **`Spin`** (`DaySpin`, `NumberSpin`…) — its value; left out, a long value
|
|
88
88
|
wraps and the box grows.
|
|
89
89
|
- **`Binder`** — its tab labels, `maxLines={1}` by default, `false` lets them
|
|
90
90
|
wrap; overridable per item.
|
|
@@ -104,9 +104,8 @@ width must say it too**, or the whole chain grows instead of truncating:
|
|
|
104
104
|
|
|
105
105
|
```jsx
|
|
106
106
|
<Box flex width="300" spacing="s">
|
|
107
|
+
{/* without minWidth the row just grows */}
|
|
107
108
|
<Box flex expandX minWidth="0">
|
|
108
|
-
{" "}
|
|
109
|
-
{/* without minWidth the row just grows */}
|
|
110
109
|
<Text maxLines={1}>…</Text>
|
|
111
110
|
</Box>
|
|
112
111
|
</Box>
|
package/docs/view_transitions.md
CHANGED
|
@@ -75,15 +75,6 @@ does not handle a rejection, an unhandled one is what it leaves behind, and
|
|
|
75
75
|
`.then(done, done)` is the shape that ends it whichever way it went — while a
|
|
76
76
|
`ready` waiter must expect the skip: `ready.then(onPictures, () => {})`.
|
|
77
77
|
|
|
78
|
-
## See also
|
|
79
|
-
|
|
80
|
-
- [route_transitions.md](./route_transitions.md) — pages moving against each
|
|
81
|
-
other on navigation
|
|
82
|
-
- [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger,
|
|
83
|
-
and what the main thread cannot read of it
|
|
84
|
-
- [drag_interactions.md](./drag_interactions.md#naming-what-travels) — naming
|
|
85
|
-
what moves when something is dropped on a place
|
|
86
|
-
|
|
87
78
|
## The top layer is painted through the root's picture
|
|
88
79
|
|
|
89
80
|
A modal `<dialog>`, its `::backdrop`, a popover: the browser paints the top
|
|
@@ -137,3 +128,12 @@ Two consequences for the switch itself:
|
|
|
137
128
|
`display` on: hidden before the call, a stand-in measures as a zero rect and
|
|
138
129
|
lands in the wrong place; and on the capture frame it is displayed while the
|
|
139
130
|
real thing is still there.
|
|
131
|
+
|
|
132
|
+
## See also
|
|
133
|
+
|
|
134
|
+
- [route_transitions.md](./route_transitions.md) — pages moving against each
|
|
135
|
+
other on navigation
|
|
136
|
+
- [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger,
|
|
137
|
+
and what the main thread cannot read of it
|
|
138
|
+
- [drag_interactions.md](./drag_interactions.md#naming-what-travels) — naming
|
|
139
|
+
what moves when something is dropped on a place
|