@jsenv/navi 0.29.100 → 0.29.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +649 -46
- package/dist/jsenv_navi.js.map +21 -18
- package/docs/control_group.md +14 -9
- package/docs/interactions.md +14 -0
- package/docs/navigation.md +127 -0
- package/docs/z_index.md +8 -8
- package/package.json +1 -1
package/docs/control_group.md
CHANGED
|
@@ -47,14 +47,19 @@ Live examples: `src/control/demos/15_group_demo.html`.
|
|
|
47
47
|
last one loses it on the other side, and any member in between loses all
|
|
48
48
|
four. A single member keeps its own radius — a group of one looks like the
|
|
49
49
|
control alone. The ask is made twice, in two forms — see below.
|
|
50
|
-
- **Overlap order**: the member under the pointer,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
- **Overlap order**: the member under the pointer, the member showing a focus
|
|
51
|
+
ring, and the member holding something open (`aria-expanded="true"`) paint
|
|
52
|
+
above their neighbours (`position: relative` plus
|
|
53
|
+
`--navi-z-index-control-hovered` / `-focused` / `-expanded`). Without it the
|
|
54
|
+
border color change and the focus ring of the active member would be sliced
|
|
55
|
+
by whichever neighbour is painted after it. The focused member is matched
|
|
56
|
+
whether it wears `data-focus-visible` itself or merely contains it — a
|
|
57
|
+
control that wraps a real input (`Picker`, `Spin`) draws the ring on its own
|
|
58
|
+
frame while the keyboard is held inside; the expanded member is matched the
|
|
59
|
+
same way, since it can be wrapped in an enrobage. Expanded ranks highest: a
|
|
60
|
+
`Picker` opened by a click shows no focus ring, hands the focus to its popup
|
|
61
|
+
and lets the pointer travel to a neighbour, yet its border keeps saying it is
|
|
62
|
+
the one open. There is deliberately no `isolation: isolate` — see
|
|
58
63
|
[z_index.md](./z_index.md).
|
|
59
64
|
|
|
60
65
|
Nothing else: a group does not restyle its members, does not impose a size,
|
|
@@ -113,7 +118,7 @@ opened it. `Popover` and `Dialog` stop it too, at their own root: nothing a
|
|
|
113
118
|
popup holds is at a seam.
|
|
114
119
|
|
|
115
120
|
Reference: `.navi_button_content` in `button_ui.jsx` (a button reads then
|
|
116
|
-
stops), `.
|
|
121
|
+
stops), `.navi_picker_box`, `.navi_input_slot`, `.navi_popover`,
|
|
117
122
|
`.navi_dialog`.
|
|
118
123
|
|
|
119
124
|
A control that declares its radius on an inner element instead is invisible to
|
package/docs/interactions.md
CHANGED
|
@@ -105,6 +105,20 @@ interaction happened, and nothing runs. A `Box` with no control anywhere near it
|
|
|
105
105
|
still answers a callback; only `"request_action"` has nothing to ask, and says so
|
|
106
106
|
in dev.
|
|
107
107
|
|
|
108
|
+
The one thing the gate weighs besides the control's state is what the
|
|
109
|
+
interaction would do to it. Everything writes unless it says otherwise; an
|
|
110
|
+
interaction that only shows what is already there declares `intent: "read"`, and
|
|
111
|
+
a control held read-only lets that one through. That is how a read-only
|
|
112
|
+
`<Picker>` still opens: the popup is where its answer is really drawn, so it
|
|
113
|
+
opens and everything inside it is held read-only in turn. Disabled and busy go
|
|
114
|
+
on refusing either way — one is out of service, the other is mid-operation, and
|
|
115
|
+
neither has anything to show.
|
|
116
|
+
|
|
117
|
+
Which controls let a read through is theirs to say, not the caller's: a picker
|
|
118
|
+
with no popup of its own opens the browser's, which cannot be held read-only, so
|
|
119
|
+
that one refuses. `openWhileReadOnly={false}` is how a caller says the popup is
|
|
120
|
+
a form with nothing to read.
|
|
121
|
+
|
|
108
122
|
## What a swipe draws, and what it leaves to you
|
|
109
123
|
|
|
110
124
|
navi makes the element follow the finger — there is nothing to decide about
|
package/docs/navigation.md
CHANGED
|
@@ -139,6 +139,133 @@ export const ADMIN_SETTINGS_ROUTE = route(`/admin/settings/:tab=${tabSignal}`);
|
|
|
139
139
|
So the rule is the one you would want: name a section and it becomes a place;
|
|
140
140
|
leave it unnamed and it stays a setting carried along.
|
|
141
141
|
|
|
142
|
+
### Which values a param accepts
|
|
143
|
+
|
|
144
|
+
A param says which segments it accepts, and a segment it declines is not a
|
|
145
|
+
half-match to be sorted out later — the route simply does not match:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
|
|
149
|
+
params: { gameId: /^W-[A-Z0-9]{8}$/i },
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A constraint is a regexp, the list of accepted values, or a `(value) => boolean`
|
|
154
|
+
— the list is compared as strings, so it can be the very `oneOf` given to the
|
|
155
|
+
signal bound to that param:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const SECTIONS = ["candidate", "to_come", "done"];
|
|
159
|
+
const sectionSignal = stateSignal("to_come", {
|
|
160
|
+
id: "section",
|
|
161
|
+
oneOf: SECTIONS,
|
|
162
|
+
});
|
|
163
|
+
export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`, {
|
|
164
|
+
params: { section: SECTIONS },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This is what makes a param usable at the root, where it would otherwise swallow
|
|
169
|
+
every single-segment address: `/cgu` and `/me` stay other routes' urls,
|
|
170
|
+
`<Route fallback>` is reachable for `/whatever`, and no signal is written for a
|
|
171
|
+
url this route has nothing to do with.
|
|
172
|
+
|
|
173
|
+
A constrained param is also **required** — no segment is not one of the values
|
|
174
|
+
it accepts — so `/:gameId` does not match `/`. The address with no segment is a
|
|
175
|
+
route of its own, which is the shape you want anyway.
|
|
176
|
+
|
|
177
|
+
#### Constrain the shape, never the existence
|
|
178
|
+
|
|
179
|
+
A constraint answers one question: **is this segment addressed to this route?**
|
|
180
|
+
It is decided on the url alone, before anything is written, so it can only be
|
|
181
|
+
about shape — that a segment looks like a game code, not that the game exists.
|
|
182
|
+
|
|
183
|
+
Whether the value is any good is a different question, asked later and answered
|
|
184
|
+
by different things: the signal's own validation (`oneOf`, `autoFix`) and the
|
|
185
|
+
route action's data. That question belongs to a route that **did** match, with a
|
|
186
|
+
page free to repair itself, show a not-found screen, offer a way out:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
// ✅ /W-ZZZZZZZZ matches, the action 404s, the page says so
|
|
190
|
+
// ❌ constraining gameId to the codes that exist — matching cannot ask a server,
|
|
191
|
+
// and "no route matched" is a worse answer than "this game is gone"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
So the signal never takes part in matching. It knows what to make of a value;
|
|
195
|
+
the route decides whether the url is its own.
|
|
196
|
+
|
|
197
|
+
#### Why order stops being load-bearing
|
|
198
|
+
|
|
199
|
+
When several routes match one url and bind the **same signal** on a param of the
|
|
200
|
+
same name, they all write it, in declaration order — the last one wins:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
route(`/games/:gameId=${gameIdSignal}`); // declared first
|
|
204
|
+
route(`/:gameId=${gameIdSignal}/:state`); // declared later
|
|
205
|
+
// on /games/W-ABC234PQ the second one matches too and writes "games"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Constraining `gameId` removes that second match entirely, which is the fix.
|
|
209
|
+
Where a param genuinely cannot be constrained, the routes must not share a
|
|
210
|
+
signal.
|
|
211
|
+
|
|
212
|
+
### An address that only sends elsewhere
|
|
213
|
+
|
|
214
|
+
Some addresses are not pages: the root of an app whose home screen is « my
|
|
215
|
+
games », the old address of a section that moved, the share link of a game
|
|
216
|
+
carrying a segment only WhatsApp cares about. They exist to be resolved, and a
|
|
217
|
+
route says so itself:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
export const HOME_ROUTE = route("/", { redirectRoute: MY_GAMES_ROUTE });
|
|
221
|
+
export const GAME_SHARED_ROUTE = route("/:gameId/:shareState", {
|
|
222
|
+
redirectRoute: GAME_ROUTE,
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The params found in the url carry over to the ones the target route declares
|
|
227
|
+
under the same name — `gameId` above needs no help — and what it cannot place
|
|
228
|
+
is left behind, `shareState` included. `redirectRouteParams` says the rest:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
// renaming, when the two routes do not call it the same thing
|
|
232
|
+
route("/partie/:id", {
|
|
233
|
+
redirectRoute: GAME_ROUTE,
|
|
234
|
+
redirectRouteParams: ({ id }) => ({ gameId: id }),
|
|
235
|
+
});
|
|
236
|
+
// dropping one, keeping the others
|
|
237
|
+
route("/:gameId/invite", {
|
|
238
|
+
redirectRoute: MY_GAMES_ROUTE,
|
|
239
|
+
redirectRouteParams: { gameId: undefined },
|
|
240
|
+
});
|
|
241
|
+
// carrying nothing over
|
|
242
|
+
route("/tri", { redirectRoute: MY_GAMES_ROUTE, redirectRouteParams: null });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Why it is not a page rendering `null`
|
|
246
|
+
|
|
247
|
+
The redirection is resolved at the door of the navigation, before the url is
|
|
248
|
+
written anywhere. Nothing about that address ever happens: no history entry, no
|
|
249
|
+
route matching, no route action loading data for a screen nobody will see, no
|
|
250
|
+
element mounted, nothing painted — and going back lands on the page before it
|
|
251
|
+
rather than replaying the redirection forever.
|
|
252
|
+
|
|
253
|
+
A page doing it in an effect gets none of that. It has to be routed to first,
|
|
254
|
+
which means the address exists, its action runs, and the app is on a screen
|
|
255
|
+
nobody should see for one paint — one a route transition can even animate _to_.
|
|
256
|
+
Anything reached by rendering is already too late, so a redirection is declared
|
|
257
|
+
with the address and never appears in the `<Route>` tree at all.
|
|
258
|
+
|
|
259
|
+
It fires on the route's own address only. `/` catches everything below it when
|
|
260
|
+
it renders a container, and would carry `/cgu` away with it if redirecting
|
|
261
|
+
followed the same reading — so redirecting asks the stricter question: is this
|
|
262
|
+
url exactly that route's address?
|
|
263
|
+
|
|
264
|
+
Where several redirecting routes answer for one url, the more specific wins —
|
|
265
|
+
`/:gameId/invite` over `/:gameId/:shareState`, the same reading the rest of the
|
|
266
|
+
router uses. Chains collapse into one navigation, and a cycle throws naming the
|
|
267
|
+
addresses it goes through.
|
|
268
|
+
|
|
142
269
|
### Search params
|
|
143
270
|
|
|
144
271
|
A param that qualifies a page rather than naming it — a zoom level, a sort, a
|
package/docs/z_index.md
CHANGED
|
@@ -74,14 +74,14 @@ file is the overview, this table is its summary. Bands are a decade apart so
|
|
|
74
74
|
one can grow without reaching the next, and so a value seen in devtools says
|
|
75
75
|
which band it came from.
|
|
76
76
|
|
|
77
|
-
| Band | Token
|
|
78
|
-
| ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
-
| Top layer (`Dialog`/`Popover` with `layer="top"`) | —
|
|
80
|
-
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout`
|
|
81
|
-
| `FixedBar` | `--navi-z-index-bar`
|
|
82
|
-
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky`
|
|
83
|
-
| A `Group` member under the pointer, then the one holding focus
|
|
84
|
-
| `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js`
|
|
77
|
+
| Band | Token | Value |
|
|
78
|
+
| ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
79
|
+
| Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
|
|
80
|
+
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
|
|
81
|
+
| `FixedBar` | `--navi-z-index-bar` | 100 |
|
|
82
|
+
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
|
|
83
|
+
| A `Group` member under the pointer, then the one holding focus, then the one holding a popup open | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused`, `--navi-z-index-control-expanded` | 1, 2, 3 |
|
|
84
|
+
| `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
|
|
85
85
|
|
|
86
86
|
What to read from it:
|
|
87
87
|
|