@jsenv/navi 0.29.478 → 0.29.479

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.
@@ -26,7 +26,9 @@ source of knowledge has one job:
26
26
  - **The built code, `dist/jsenv_navi.js`** — the exhaustive truth about the API,
27
27
  JSDoc included, always under `node_modules/@jsenv/navi/`. When a signature, a
28
28
  default or an accepted value is what you need, read it there rather than
29
- guessing.
29
+ guessing. The formatters navi re-exports from `@jsenv/humanize` (`format*`,
30
+ `createI18n`, `interpolateText`) are a re-export there: their JSDoc is in
31
+ `node_modules/@jsenv/humanize/dist/browser/jsenv_humanize_browser.js`.
30
32
  - **Sources, demos and tests** — how an export is really used:
31
33
  `src/**/demos/*_demo.html` exercise one component per page, prop by prop. They
32
34
  are not published to npm, so they live only in the repo
package/docs/actions.md CHANGED
@@ -22,6 +22,7 @@ them by hand — see [resource.md](./resource.md).
22
22
 
23
23
  - [Params: `bindParams`, and calling the action](#params-bindparams-and-calling-the-action)
24
24
  - [The answer is kept on the instance that ran](#the-answer-is-kept-on-the-instance-that-ran)
25
+ - [Nothing to ask, and coming back](#nothing-to-ask-and-coming-back)
25
26
  - [The in-between states of a gesture are real params](#the-in-between-states-of-a-gesture-are-real-params)
26
27
  - [Running: `run`, `rerun`, `prerun`, `reset`](#running-run-rerun-prerun-reset)
27
28
  - [Aborting saves resources, it does not undo](#aborting-saves-resources-it-does-not-undo)
@@ -124,6 +125,28 @@ verb — `computed(() => ME.GET.dataSignal.value)` — only works for a verb
124
125
  something runs **directly**, with no params; anything else derives from the
125
126
  instance: `computed(() => COUNTS.dataSignal.value)`.
126
127
 
128
+ ### Nothing to ask, and coming back
129
+
130
+ A binding has nothing to ask while its params as a whole are `undefined` or
131
+ `null` — a question computed from a form still missing a field.
132
+ `{ id: undefined }` is not that: it is a question, and it is asked. With nothing
133
+ to ask, the binding reads an instance holding nothing, idle: no data, no wait.
134
+ The previous answer stands in only while a newer one is coming, and none is.
135
+
136
+ What the previous params got stays on the instance that ran. A run still out
137
+ for them is called off by `actionRunEffect` (so by a route action) and carries
138
+ on under a plain binding; either way nothing is reset — an action does not
139
+ start over because its params went away.
140
+
141
+ Whether coming back to those params finds that answer depends on the instance
142
+ still being there. Without `keep` it lives as long as something references it,
143
+ and once nothing does it goes at a moment nothing in the code decides: coming
144
+ back may find it or ask again, and neither is something to build on.
145
+ `createAction(callback, { keep: { ms } })` is what decides. The answer stays
146
+ good for `ms` from the moment it landed, and the first screen asking for those
147
+ params after that asks again: a read worth asking at each visit — a
148
+ measurement, a quota — declares a short one.
149
+
127
150
  ### The in-between states of a gesture are real params
128
151
 
129
152
  The delay decides _when_ a question goes out, never _which_ one is coherent: it
@@ -79,9 +79,15 @@ are read at the press, before the box moves:
79
79
  [interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions));
80
80
  and a popover or a `<dialog>` inside the box, which is a layer over it. A
81
81
  button, a link or a checkbox is not among them on purpose: a drag from one
82
- travels, and the click it would have made is swallowed. A field that only
83
- reads the press — a picker's façade is an `<input>` that opens a popup — says
84
- so with `data-press-only`, and a travel starts there like anywhere else.
82
+ travels, and the click it would have made is swallowed before any listener
83
+ hears it, the ancestors' included: an `onClick` on something that travels
84
+ needs no distance check of its own. For the same reason
85
+ navi refuses the browser's own drag of a link or an image for that press: it
86
+ starts a few pixels in and would take the pointer before the travel forms.
87
+ An app that wants an image dragged out of the page marks it
88
+ `data-no-drag-travel`. A field that only reads the press — a picker's façade
89
+ is an `<input>` that opens a popup — says so with `data-press-only`, and a
90
+ travel starts there like anywhere else.
85
91
  2. **A scroller in between with room left that way.** It keeps the gesture until
86
92
  it has no room left, and only then hands the travel over — so a row that
87
93
  scrolls sideways inside a page still scrolls sideways. A row swiped with
@@ -53,9 +53,10 @@ for.
53
53
 
54
54
  ### A popup that stays open
55
55
 
56
- A popup closes once a form inside it is sent. When it is a place one keeps
57
- working in — a panel where a push is sent, two fields are fixed, an account is
58
- linked — the form says it stays:
56
+ A popup closes once a form inside it is sent, even when the send took the form
57
+ out of the page. When it is a place one keeps working in — a panel where a push
58
+ is sent, two fields are fixed, an account is linked, an answer drawn where the
59
+ form was ("link sent, check your inbox") — the form says it stays:
59
60
 
60
61
  ```jsx
61
62
  <SidePanel signal={openUserSignal} value={user.id}>
@@ -478,10 +478,14 @@ teardown, so a travelling container above it does not take the gesture (see
478
478
  selection refused only for the length of its gesture — in time for a mouse,
479
479
  too late for a finger held still — so it says `user-select: none` itself (see
480
480
  [drag_interactions.md](./drag_interactions.md#the-text-inside-user-select-none)).
481
- - **A swipe cannot also be dragged out of the page.** An element declaring a
482
- swipe gets `draggable={false}` and its `dragstart` refused — a native drag _is_
483
- press-and-move, and a link or an image is draggable without anyone asking. One
484
- gesture cannot mean both.
481
+ - **What a press moves cannot also be dragged out of the page.** A swipe, a
482
+ travel (a `SlideContainer`, a `RouteTravel`, a sheet pushed back) and a `pan`
483
+ surface refuse the browser's own drag of the link or image a press lands on —
484
+ a native drag _is_ press-and-move, a link or an image is draggable without
485
+ anyone asking, and the browser starts it a few pixels in, before any of them
486
+ has decided. One gesture cannot mean both. What they leave alone keeps its
487
+ drag: an element inside marked `data-no-drag-travel` reads the pointer itself.
488
+ An element declaring a swipe also gets `draggable={false}`.
485
489
  - **A popup can open while the finger is still down** — a menu under a waiting
486
490
  finger is the native gesture (see
487
491
  [popup_open.md](./popup_open.md#opening-while-the-finger-is-still-down), and
@@ -125,6 +125,22 @@ already open — a tab pointing at the current page cannot be clicked. The defau
125
125
  section has no segment, so its literal route is the **parent** of the
126
126
  parameterized one, and still does not inherit the param.
127
127
 
128
+ The default section can also have an address of its own, when the bare address
129
+ is another page:
130
+
131
+ ```js
132
+ export const PROFILE_ROUTE = route("/me");
133
+ export const ADDRESS_BOOK_ROUTE = route(`/me/:section=${bookSectionSignal}`); // default "players"
134
+ export const PLAYERS_ROUTE = route("/me/players");
135
+ export const GROUPS_ROUTE = route("/me/groups");
136
+ ```
137
+
138
+ Declaring both `/me` and a literal for the default is what says so: the param
139
+ is then required, `/me` is the profile alone, and `ADDRESS_BOOK_ROUTE.buildUrl()`
140
+ gives `/me/players` or `/me/groups`, never `/me`. A visit to the profile leaves
141
+ the section where it was. Leave out the literal for the default and `/me` is the
142
+ default section, as with `/games/me` above.
143
+
128
144
  That the literals exist is what tells navi these values name pages. Where none is
129
145
  declared, the value stays a qualifier, carried by an ancestor url:
130
146
 
@@ -157,8 +173,10 @@ export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
157
173
  This is what lets a param sit at the root without swallowing every single-segment
158
174
  address: `/cgu` stays another route's url, `<Route fallback>` is reachable, and
159
175
  no signal is written for a url this route has nothing to do with. A constrained
160
- param is also **required**: `/:gameId` does not match `/`. The accepted forms are
161
- in `route()`'s JSDoc.
176
+ param is also **required**: `/:gameId` does not match `/` — unless its default
177
+ stands for the bare address, as `/games/me` does for `/games/me/:section` (see
178
+ [Declaring the sections](#declaring-the-sections-is-what-makes-them-places)).
179
+ The accepted forms are in `route()`'s JSDoc.
162
180
 
163
181
  #### Constrain the shape, never the existence
164
182
 
@@ -258,6 +276,9 @@ export const HOME_ROUTE = route("/", { searchParams: { vue: vueSignal } });
258
276
  The signal and the URL are the same state: never keep a `useState` beside a route
259
277
  param for the same fact. Declared on the **root route**, a search param holds
260
278
  wherever one is in the application; declared on one route, it exists only there.
279
+ A route declared "this and below" (`/map/`) lends its params to every address
280
+ below it; `/places` lends nothing, so a `?q=` of its own and the `?q=` of
281
+ `/places/clubs` are two states, even with a `/places/:section` matching both.
261
282
 
262
283
  Writing it AMENDS the history entry one is on rather than stacking a new one: a
263
284
  param that qualifies a screen is not a place, and one entry per write turns a
@@ -398,6 +419,14 @@ section it does NOT stand for. Both amend what a link claims about itself and
398
419
  nothing else — neither is an answer to a route that lies about where the reader
399
420
  is (see [what its address may say](#a-layer-over-the-screen-what-its-address-may-say)).
400
421
 
422
+ The routing takes a plain click that moves this tab, and nothing else. A link
423
+ with `target="_blank"` (or any `target` other than `_self`) opens the app in that
424
+ tab or window, which loads it and routes there; a link with `download` saves its
425
+ address; a click with a modifier key (a new tab, a new window, a download) is the
426
+ browser's. None of them costs the page the reader is on — a half-filled form
427
+ beside a link to the terms stays filled. Nothing is fetched ahead for such a
428
+ link either: the other tab loads its own code.
429
+
401
430
  ## The back arrow: `navBack`
402
431
 
403
432
  An arrow drawn inside the app promises the screen the reader came from — never
package/docs/pan_zoom.md CHANGED
@@ -91,10 +91,12 @@ is not, and asking again is asking three times for one sentence:
91
91
  <Box data-pan-after-hold="kept" interactions={{ pan, zoom }} />
92
92
  ```
93
93
 
94
- From the moment the surface is given the hand — the hold, a pinch, a mouse
95
- travelling — it pans on contact, the way the same plan does opened full screen,
96
- until a press lands away from it; navi watches for that press itself, so nothing
97
- on your side listens to the window. The paid wait is a state to draw, and only a
94
+ From the moment a finger gives the surface the hand — the hold, or a pinch — it
95
+ pans on contact, the way the same plan does opened full screen, until a press
96
+ lands away from it; navi watches for that press itself, so nothing on your side
97
+ listens to the window. A mouse never owed the wait, so it neither spends it nor
98
+ marks it: after a mouse pan the next finger still waits, and the attribute is
99
+ safe to write for every device. The paid wait is a state to draw, and only a
98
100
  `"kept"` surface carries it: `[data-hand-kept]`, for as long as it holds the
99
101
  hand, a finger on it or not.
100
102
 
@@ -138,6 +138,11 @@ wheel above is painted, so a press on it is a press on the popup — which is wh
138
138
  lets one marker cover a whole row without swallowing the controls it holds. A
139
139
  nested box that is see-through too needs its own marker.
140
140
 
141
+ **Inside a box that travels by drag, the marker answers the click.** A press on
142
+ a row of slides, or on a sheet that swipes closed, may be the start of that
143
+ travel; the popup closes on the click that ends the press, which a travel
144
+ swallows, rather than at the press itself.
145
+
141
146
  **Space beside something, not space between two things.** What the wheel does
142
147
  not cover is plainly nothing, and a press there reads as a press on the page.
143
148
  The gap between two rows of a column does not read that way — it is a seam of
@@ -218,9 +218,10 @@ closing, and a name built from it at that render names no tile: build it from
218
218
  the last key the walk held — a ref written while the signal holds one.
219
219
 
220
220
  **A press on the surface that dismisses is `data-navi-popup-outside`** (see
221
- [popup_backdrop.md](./popup_backdrop.md#where-the-outside-begins)), read on the
222
- press itself. A close written by hand on a click has to tell a click from the
223
- end of a swipe — and that guard is the sign the marker was missed.
221
+ [popup_backdrop.md](./popup_backdrop.md#where-the-outside-begins)). The row
222
+ travels, so the marker answers the press's click rather than its mousedown: a
223
+ swipe that starts on the free space walks, and only a press that stayed one
224
+ closes.
224
225
 
225
226
  ## Lifting on the way back only
226
227
 
@@ -287,9 +287,10 @@ Four misconfigurations are silent enough to be worth a console warning, each sai
287
287
  once: an area that was not captured (the case above); several elements marked at
288
288
  once — they share one `view-transition-name`, and the browser refuses every route
289
289
  transition while they do; pages travelling on the document while something else
290
- is captured on its own — the blank band; and, on a browser without nested groups,
291
- a `view-transition-name` written inside the area, whose element escapes the pages'
292
- picture (see
290
+ is captured on its own — the blank band; and a `view-transition-name` written
291
+ inside the area that only one of the two pages has: with nothing to morph into,
292
+ its element stands still and fades while the pages move, in every browser, and
293
+ without nested groups it is not even clipped (see
293
294
  [view_transitions.md](./view_transitions.md#a-name-is-unique-per-document)). A
294
295
  warning here is a bug to fix, not a mechanism to lean on.
295
296
 
@@ -474,6 +475,9 @@ What that means for a page and for the application around it:
474
475
  it was (`<List id>`) under the same `id` as a list of the page arriving is the
475
476
  same list to navi, and the two overwrite each other's position (see
476
477
  [scroll.md](./scroll.md#a-list-that-comes-back-one-id-one-list)).
478
+ They let go of the document once hidden: a list scrolling the page stops
479
+ placing it on the item it opened on, and stops answering its scrolls and
480
+ resizes, so the page arriving lands where it was left, or at its top.
477
481
  - **Never while a modal dialog is open.** An open modal keeps everything but
478
482
  itself out of reach — the page arriving could not even take the focus — and one
479
483
  in a page kept while leaving would stay open for the whole movement. The page is
package/docs/scroll.md CHANGED
@@ -117,8 +117,8 @@ say so with `"document"` or the element itself.
117
117
  ### Where the list opens, and where it is
118
118
 
119
119
  `defaultScrolled` (or `scrolled`, held) takes `{id, offset}` — what
120
- `onScrolledChange` hands out — and asks for the row BY NAME, then puts it back
121
- by MEASURING it: it lands where it was even if rows were inserted before it,
120
+ `onScrolledChange` hands out — and asks for the item BY NAME, then puts it back
121
+ by MEASURING it: it lands where it was even if items were inserted before it,
122
122
  whatever the screen it was saved on. "Reopen a thread where I left it" is
123
123
  already provided; keep the position whole, its `visibleCount` sizes the first
124
124
  paint. A `defaultScrolled` that changes before anyone has moved the list is
@@ -129,9 +129,9 @@ must name one list of the app, see [below](#a-list-that-comes-back-one-id-one-li
129
129
  **`scrollResetOnNavigation`** opts out. What it draws there before its source
130
130
  answers is another matter
131
131
  ([What a run knows when it opens](#what-a-run-knows-when-it-opens)). A list scrolling the document that
132
- opens on a row places the document itself: the offset the page kept for its url
133
- is not put back over it — the rows held off screen are fillers of an estimated
134
- height, and the same pixels now fall on other rows.
132
+ opens on an item places the document itself: the offset the page kept for its url
133
+ is not put back over it — the items held off screen are fillers of an estimated
134
+ height, and the same pixels now fall on other items.
135
135
 
136
136
  ### A list that comes back: one `id`, one list
137
137
 
package/docs/testid.md CHANGED
@@ -76,11 +76,14 @@ Navi hands the host the props it knows belong there (`id`, `name`, `type`,
76
76
  root box instead — so a `data-test-id` or a `data-qa` of your own would name
77
77
  the wrapper, which is the second reason to keep the standard spelling.
78
78
 
79
- `aria-label` and `aria-labelledby` take the same road, and for the reason the
80
- section above gives: the name has to be where the role is. The root box has no
81
- role — it is a wrapper — so a name left on it would be announced by nothing and
82
- found by nothing, while the host is what the user focuses and what
83
- `getByRole(role, { name })` and `getByLabel()` read.
79
+ `aria-label` and `aria-labelledby` follow the role, for the reason the section
80
+ above gives: the name has to be where the role is. For most controls that is the
81
+ host. The root box has no role — it is a wrapper — so a name left on it would be
82
+ announced by nothing and found by nothing, while the host is what the user
83
+ focuses and what `getByRole(role, { name })` and `getByLabel()` read. A `Wheel`
84
+ is the other way round: its root is the `spinbutton` the user focuses, and its
85
+ host is an `aria-hidden` input carrying the value, so the name lands on the
86
+ root.
84
87
 
85
88
  ```jsx
86
89
  <Picker variant="icon" aria-label="Aide" ui={<Icon>…</Icon>}>
@@ -33,8 +33,9 @@ and `List` unnames its rows for the length of any route transition or travel. A
33
33
  name the app puts inside the pages is given only for the gesture it serves, or
34
34
  dropped while they move: `view-transition-name: none` under
35
35
  `:root:is([data-navi-route-transition], [data-navi-route-travel])`. Without nested
36
- groups it also escapes the pages' clip, and navi warns about one inside a
37
- transition area.
36
+ groups it also escapes the pages' clip. navi warns about a name inside a
37
+ transition area that only one of the two pages has: with nothing to morph into,
38
+ it can only be this trap.
38
39
 
39
40
  ## Nested groups, and the fallback fade
40
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.478",
3
+ "version": "0.29.479",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -35,8 +35,8 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@jsenv/dom": "0.17.94",
38
- "@jsenv/humanize": "1.8.7",
39
- "@jsenv/validity": "0.4.5"
38
+ "@jsenv/humanize": "1.8.8",
39
+ "@jsenv/validity": "0.4.6"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@jsenv/assert": "4.5.7",