@jsenv/navi 0.29.478 → 0.29.480

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
@@ -57,7 +59,8 @@ start when unsure which export solves a problem.
57
59
  ### Routing and movement
58
60
 
59
61
  - `navigation.md` — the position of the user belongs in the URL by default:
60
- routes and which branch renders, search params, redirections, the back arrow
62
+ routes and which branch renders, a page (`/x`) or a frame (`/x/`), search
63
+ params, redirections, the back arrow
61
64
  (`navBack`), where a navigation lands (a `#id` for an element; a page not
62
65
  whole yet holds its position with `useHoldScrollRestoration`), a
63
66
  `SlideContainer` read from the URL, what a layer drawn over the screen may say
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
@@ -5,6 +5,7 @@ linking to them, and turning them into tabs.
5
5
 
6
6
  - [The rule that decides everything else: the position belongs in the URL](#the-rule-that-decides-everything-else-the-position-belongs-in-the-url)
7
7
  - [Declaring routes](#declaring-routes)
8
+ - [A page or a frame: the trailing slash](#a-page-or-a-frame-the-trailing-slash)
8
9
  - [A document that is not at the root: `setBaseUrl`](#a-document-that-is-not-at-the-root-setbaseurl)
9
10
  - [A section is allowed to be a route of its own](#a-section-is-allowed-to-be-a-route-of-its-own)
10
11
  - [Which values a param accepts](#which-values-a-param-accepts)
@@ -68,6 +69,32 @@ application, and an import line says which places a component deals with. Routes
68
69
  are plain objects usable outside any component (`buildUrl()`, `navTo()`,
69
70
  `redirectTo()`, `matching`), which is why they are declared apart from the JSX.
70
71
 
72
+ ### A page or a frame: the trailing slash
73
+
74
+ The last character of a pattern is part of what the route stands for:
75
+
76
+ - `route("/map")` is a **page**: it matches `/map` and nothing below it.
77
+ - `route("/map/")` is a **frame**: it matches `/map` and every address below it,
78
+ `/map/flow` and `/map/isochrone/compare` included. The root `/` is always one.
79
+
80
+ A frame is what stays on screen while one moves below it: the map under its
81
+ panels, an admin shell around its sections. A page is one screen among others,
82
+ even when its address starts theirs: the profile at `/me`, beside the address
83
+ book at `/me/players`. To choose, ask whether the route is still on screen at
84
+ the addresses below it. The answer decides more than what renders:
85
+
86
+ | | page `route("/map")` | frame `route("/map/")` |
87
+ | ------------------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------- |
88
+ | matches `/map/flow` | no | yes |
89
+ | its search params, on `/map/flow` | absent: they belong to `/map` alone | held: the url keeps them, the signal follows them |
90
+ | `/map/:panel` defaulting to `"isochrone"`, beside `/map/isochrone` | `/map` is this page alone, the panel lives at `/map/isochrone` | `/map` opens on the isochrone panel, and a link to it builds `/map` |
91
+
92
+ So a frame written without its slash shows itself in two ways: its search params
93
+ are left out of the addresses below it (not written there, not read from there),
94
+ and its address does not open on the default section. Details in
95
+ [Search params](#search-params) and in
96
+ [Declaring the sections](#declaring-the-sections-is-what-makes-them-places).
97
+
71
98
  ### A document that is not at the root: `setBaseUrl`
72
99
 
73
100
  A second document in the same site — an admin panel at `/admin/admin.html`
@@ -125,6 +152,25 @@ already open — a tab pointing at the current page cannot be clicked. The defau
125
152
  section has no segment, so its literal route is the **parent** of the
126
153
  parameterized one, and still does not inherit the param.
127
154
 
155
+ The default section can also have an address of its own, when the bare address
156
+ is another page:
157
+
158
+ ```js
159
+ export const PROFILE_ROUTE = route("/me"); // a page, not a frame
160
+ export const ADDRESS_BOOK_ROUTE = route(`/me/:section=${bookSectionSignal}`); // default "players"
161
+ export const PLAYERS_ROUTE = route("/me/players");
162
+ export const GROUPS_ROUTE = route("/me/groups");
163
+ ```
164
+
165
+ A page at the bare address (see
166
+ [A page or a frame](#a-page-or-a-frame-the-trailing-slash)) and a literal for the
167
+ default are what say so: the param is then required, `/me` is the profile alone,
168
+ and `ADDRESS_BOOK_ROUTE.buildUrl()` gives `/me/players` or `/me/groups`, never
169
+ `/me`. A visit to the profile leaves the section where it was. The bare address
170
+ is the default section in the two other cases: when nothing is declared there
171
+ but a frame (`/map/` around its panels opens on the default one), and when no
172
+ literal is declared for the default (`/games/me` above).
173
+
128
174
  That the literals exist is what tells navi these values name pages. Where none is
129
175
  declared, the value stays a qualifier, carried by an ancestor url:
130
176
 
@@ -157,8 +203,10 @@ export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
157
203
  This is what lets a param sit at the root without swallowing every single-segment
158
204
  address: `/cgu` stays another route's url, `<Route fallback>` is reachable, and
159
205
  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.
206
+ param is also **required**: `/:gameId` does not match `/` — unless its default
207
+ stands for the bare address, as `/games/me` does for `/games/me/:section` (see
208
+ [Declaring the sections](#declaring-the-sections-is-what-makes-them-places)).
209
+ The accepted forms are in `route()`'s JSDoc.
162
210
 
163
211
  #### Constrain the shape, never the existence
164
212
 
@@ -257,7 +305,13 @@ export const HOME_ROUTE = route("/", { searchParams: { vue: vueSignal } });
257
305
 
258
306
  The signal and the URL are the same state: never keep a `useState` beside a route
259
307
  param for the same fact. Declared on the **root route**, a search param holds
260
- wherever one is in the application; declared on one route, it exists only there.
308
+ wherever one is in the application; declared on one route, it exists only there:
309
+ on its own address for a page, on its own address and every address below it for
310
+ a frame (see [A page or a frame](#a-page-or-a-frame-the-trailing-slash)). Two
311
+ pages may each have a `?q=` of their own: the list at `/places` and the clubs at
312
+ `/places/clubs` keep two searches, even with a `/places/:section` matching both.
313
+ A param several pages need, and no frame holds, is declared on each of them,
314
+ with the same signal.
261
315
 
262
316
  Writing it AMENDS the history entry one is on rather than stacking a new one: a
263
317
  param that qualifies a screen is not a place, and one entry per write turns a
@@ -300,7 +354,8 @@ the branch taken when no sibling matches.
300
354
  A route matches its own address and nothing below it — `/games/:gameId` does not
301
355
  match `/games/2/edit` — except `/` and a pattern ending in `/`, which match
302
356
  everything below them too (`route("/games/")` matches `/games/2/edit`): that is
303
- what keeps a section's container on screen while one is inside it. Two patterns
357
+ what keeps a section's container on screen while one is inside it (see
358
+ [A page or a frame](#a-page-or-a-frame-the-trailing-slash)). Two patterns
304
359
  can still match one url — `/games/my_games` is also a `/games/:gameId` — and a
305
360
  container renders the **first** branch that matches, in the order written, not
306
361
  the most specific. So the branches go from the most precise to the widest, and
@@ -398,6 +453,14 @@ section it does NOT stand for. Both amend what a link claims about itself and
398
453
  nothing else — neither is an answer to a route that lies about where the reader
399
454
  is (see [what its address may say](#a-layer-over-the-screen-what-its-address-may-say)).
400
455
 
456
+ The routing takes a plain click that moves this tab, and nothing else. A link
457
+ with `target="_blank"` (or any `target` other than `_self`) opens the app in that
458
+ tab or window, which loads it and routes there; a link with `download` saves its
459
+ address; a click with a modifier key (a new tab, a new window, a download) is the
460
+ browser's. None of them costs the page the reader is on — a half-filled form
461
+ beside a link to the terms stays filled. Nothing is fetched ahead for such a
462
+ link either: the other tab loads its own code.
463
+
401
464
  ## The back arrow: `navBack`
402
465
 
403
466
  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.480",
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",