@jsenv/navi 0.29.28 → 0.29.30
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 +17306 -15851
- package/dist/jsenv_navi.js.map +168 -149
- package/dist/jsenv_navi_side_effects.js +0 -1
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +13 -1
- package/docs/control_group.md +95 -0
- package/docs/interactions.md +405 -0
- package/docs/z_index.md +40 -16
- package/package.json +8 -5
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# `interactions` — a component that answers more than a click
|
|
2
|
+
|
|
3
|
+
## What we want
|
|
4
|
+
|
|
5
|
+
An element should be able to answer a gesture — a row swiped aside to archive it,
|
|
6
|
+
a card held down to open a menu, a shortcut that sends a form — and the person
|
|
7
|
+
writing that element should only have to **name** the gesture and say what it
|
|
8
|
+
does.
|
|
9
|
+
|
|
10
|
+
Everything hard about a gesture is not the detection. It is the four things
|
|
11
|
+
around it:
|
|
12
|
+
|
|
13
|
+
- **who owns the press** when boxes are nested (a row swiped sideways inside a
|
|
14
|
+
container that travels sideways);
|
|
15
|
+
- **which of several gestures a single press turns out to be** (a swipe, a hold, a
|
|
16
|
+
click — one press, one arbiter);
|
|
17
|
+
- **the click the browser fires afterwards**, which would follow the link the
|
|
18
|
+
gesture started from;
|
|
19
|
+
- **whether the element is allowed to be interacted with at all** (disabled,
|
|
20
|
+
read-only, waiting on something).
|
|
21
|
+
|
|
22
|
+
navi owns those four. An application that reads the pointer itself gets two of
|
|
23
|
+
them wrong by construction, because two of them can only be decided from inside
|
|
24
|
+
navi and before the first pixel moves.
|
|
25
|
+
|
|
26
|
+
## The prop
|
|
27
|
+
|
|
28
|
+
`interactions` is a prop of `Box`, so it is available on anything built from
|
|
29
|
+
one — `Box`, `List.Item`, `Button`, `Link`, the field components. Its keys are
|
|
30
|
+
**event types**, its values say what that interaction does.
|
|
31
|
+
|
|
32
|
+
```jsx
|
|
33
|
+
<Box
|
|
34
|
+
interactions={{
|
|
35
|
+
"swipe_right": "request_action",
|
|
36
|
+
"swipe_left": (event) => markUnread(event),
|
|
37
|
+
"longpress": (event) => openMenu(event),
|
|
38
|
+
"keyboard:ctrl+backspace": "request_action",
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`action` is untouched by this and keeps its own wiring — a click on a button, a
|
|
44
|
+
change on a field. `interactions` is the other half: everything that is not that
|
|
45
|
+
natural one.
|
|
46
|
+
|
|
47
|
+
### The three values
|
|
48
|
+
|
|
49
|
+
| Value | Meaning |
|
|
50
|
+
| --------------------- | ---------------------------------------------------- |
|
|
51
|
+
| `"request_action"` | ask the nearest control for its `action` prop |
|
|
52
|
+
| `"request_ui_action"` | ask it for a ui action (what says "the user acted") |
|
|
53
|
+
| a function | do this, with the interaction event as only argument |
|
|
54
|
+
|
|
55
|
+
A falsy value means "not this one", so an interaction can be declared under a
|
|
56
|
+
condition: `{ swipe_right: canArchive && archive }`.
|
|
57
|
+
|
|
58
|
+
### The interactions navi detects
|
|
59
|
+
|
|
60
|
+
| Key | Read from |
|
|
61
|
+
| ------------------------------------------------------ | ---------------------------------------------- |
|
|
62
|
+
| `mousedown` `mouseup` `click` `dblclick` `contextmenu` | the browser's own events |
|
|
63
|
+
| `swipe_left` `swipe_right` `swipe_up` `swipe_down` | a press that travels |
|
|
64
|
+
| `longpress` | a press held still |
|
|
65
|
+
| `move` `reorder` `toss` | the element carried, and what letting go means |
|
|
66
|
+
| `"keyboard:<shortcut>"` | keys, e.g. `"keyboard:ctrl+backspace"` |
|
|
67
|
+
|
|
68
|
+
A name nothing knows how to detect produces a dev warning naming the detectors
|
|
69
|
+
that exist.
|
|
70
|
+
|
|
71
|
+
## Which interaction asked
|
|
72
|
+
|
|
73
|
+
An interaction navi makes is **dispatched as an event of its own name** —
|
|
74
|
+
bubbling, cancelable, chained onto the event it was read from. So an action does
|
|
75
|
+
not need to be told which interaction asked for it: it reads the event it already
|
|
76
|
+
receives.
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
<Button
|
|
80
|
+
action={(value, { event }) => {
|
|
81
|
+
const swipe = findEvent(event, "swipe_right");
|
|
82
|
+
if (swipe) {
|
|
83
|
+
const { axis, sign, pulled, size, progress } = swipe.detail;
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
interactions={{ swipe_right: "request_action" }}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`findEvent` is exported from `@jsenv/navi`. Because these are real events, an
|
|
91
|
+
ancestor can also listen for one, and `preventDefault()` on it means "not this
|
|
92
|
+
time".
|
|
93
|
+
|
|
94
|
+
The lower-level event the interaction was read from is reachable too:
|
|
95
|
+
`interactionEvent.detail.event` is the `pointerdown` a swipe or a hold was made
|
|
96
|
+
of — which is how a menu is opened at the point the press happened.
|
|
97
|
+
|
|
98
|
+
## Reaching the control
|
|
99
|
+
|
|
100
|
+
Everything goes through the interaction gate of the **nearest control** — itself,
|
|
101
|
+
an ancestor, or a descendant, in that order. So a disabled, read-only or busy
|
|
102
|
+
control answers a swipe the way it answers a click: it says why, where the
|
|
103
|
+
interaction happened, and nothing runs. A `Box` with no control anywhere near it
|
|
104
|
+
still answers a callback; only `"request_action"` has nothing to ask, and says so
|
|
105
|
+
in dev.
|
|
106
|
+
|
|
107
|
+
## What a swipe draws, and what it leaves to you
|
|
108
|
+
|
|
109
|
+
navi makes the element follow the finger — there is nothing to decide about
|
|
110
|
+
that — and says where the gesture is up to:
|
|
111
|
+
|
|
112
|
+
| Written on the element | Meaning |
|
|
113
|
+
| ---------------------------------------- | ----------------------------------------- |
|
|
114
|
+
| `--swipe-pulled` | how far it has come, signed, in px |
|
|
115
|
+
| `--swipe-progress` | the same as a fraction, signed, inherited |
|
|
116
|
+
| `[data-swiping="left\|right\|up\|down"]` | which way, while a finger holds it |
|
|
117
|
+
| `[data-swipe-past-threshold]` | letting go now would go through with it |
|
|
118
|
+
|
|
119
|
+
WHAT is revealed behind is yours: navi does not know what putting a row away
|
|
120
|
+
looks like. A trail is usually a child of the swiped element sized off
|
|
121
|
+
`--swipe-pulled` — which is what makes those values reachable from CSS at all, a
|
|
122
|
+
sibling could not read them.
|
|
123
|
+
|
|
124
|
+
```css
|
|
125
|
+
.trail {
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 0;
|
|
128
|
+
right: 100%; /* the strip the row just left */
|
|
129
|
+
bottom: 0;
|
|
130
|
+
width: var(--swipe-pulled);
|
|
131
|
+
opacity: calc(var(--swipe-progress) * 3);
|
|
132
|
+
}
|
|
133
|
+
[data-swipe-past-threshold] .trail {
|
|
134
|
+
background: var(--ok-color);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
While the answer takes time, the element **stays where the gesture left it**, and
|
|
139
|
+
comes back once it settles — a failure leaves the row in place so it can be tried
|
|
140
|
+
again. What a success does to the element is yours (a list that redemands its
|
|
141
|
+
rows, a row that leaves): navi does not make it disappear.
|
|
142
|
+
|
|
143
|
+
## Carrying something: `move`, `reorder`, `toss`
|
|
144
|
+
|
|
145
|
+
All three are the same gesture — the element is picked up and carried — and what
|
|
146
|
+
differs is the release. One detector reads them all, because it is one press.
|
|
147
|
+
|
|
148
|
+
`reorder` and `toss` **combine**: dropped on another item the element changes
|
|
149
|
+
places, thrown far and fast it is gotten rid of. `move` does **not** combine with
|
|
150
|
+
`reorder` — an element either goes where it is put or takes a place in a list,
|
|
151
|
+
and one release cannot mean both (a dev warning says so).
|
|
152
|
+
|
|
153
|
+
`move` carries the element ITSELF and leaves it where it was put; the other two
|
|
154
|
+
carry a copy and put the original back. That is the same difference said in layout
|
|
155
|
+
terms: something moved has a new place of its own, something reordered had its
|
|
156
|
+
place taken by the list.
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
<Box
|
|
160
|
+
id={token.id}
|
|
161
|
+
interactions={{
|
|
162
|
+
move: (event) => remember(event.detail.x, event.detail.y),
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`data-drag-free` on the element or a container lets it leave; by default a `move`
|
|
168
|
+
stays inside what one can SEE of its container — which requires that container to
|
|
169
|
+
be a scroll container at all (`overflow` anything but `visible`), since there is
|
|
170
|
+
nothing else for "inside" to mean. A `move` whose answer rejects travels back, because a
|
|
171
|
+
place the application would not accept must not stay on screen as if it had.
|
|
172
|
+
|
|
173
|
+
**Declaring `toss` frees the area by itself.** What is dragged is otherwise kept
|
|
174
|
+
inside its scroll area — right for a reorder, since a row belongs to its list, and
|
|
175
|
+
fatal for a throw: the copy hits the edge of the list, no distance is ever covered,
|
|
176
|
+
so no throw can happen and no sideways movement is even visible. So the two
|
|
177
|
+
together let it leave.
|
|
178
|
+
|
|
179
|
+
```jsx
|
|
180
|
+
<List.Item
|
|
181
|
+
id={task.id}
|
|
182
|
+
data-view-transition-name={`task_${task.id}`}
|
|
183
|
+
interactions={{
|
|
184
|
+
reorder: (event) => {
|
|
185
|
+
const { fromId, toId, syncCloneWithDropTarget } = event.detail;
|
|
186
|
+
return document.startViewTransition(() => {
|
|
187
|
+
syncCloneWithDropTarget();
|
|
188
|
+
setOrder(moveBefore(order, fromId, toId));
|
|
189
|
+
}).finished;
|
|
190
|
+
},
|
|
191
|
+
toss: (event) => remove(event.detail.id),
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The gesture is `startDragTo`'s, whole: `move` carries the element itself, the
|
|
197
|
+
other two carry a copy above the page while the original keeps its place, with a
|
|
198
|
+
drop hint, drop targets found by intersection, no-op drops filtered out, and the
|
|
199
|
+
flight of a thrown copy plus its return when the answer refuses. Only what the
|
|
200
|
+
declared outcomes need runs — no copy for a move, no hint for something
|
|
201
|
+
that can only be thrown away.
|
|
202
|
+
|
|
203
|
+
Every element declaring `reorder` marks itself, so the set of items IS the set of
|
|
204
|
+
elements that declared it — no selector to pass, and an item that must not move
|
|
205
|
+
simply does not declare it. An element declaring only `toss` marks nothing: it is
|
|
206
|
+
not a place anything lands. Items are named by their `id`.
|
|
207
|
+
|
|
208
|
+
`toId` is null for a drop at the end. `syncCloneWithDropTarget` must be called
|
|
209
|
+
synchronously inside the transition callback, next to the state change, so the copy
|
|
210
|
+
is captured where it lands rather than where it was let go of.
|
|
211
|
+
|
|
212
|
+
**The promise matters in both cases**: the gesture holds its copy until the answer
|
|
213
|
+
settles. Returning the transition is what makes a landing continuous; a `toss` that
|
|
214
|
+
rejects brings the copy back, because the thing still exists and the screen has to
|
|
215
|
+
say so.
|
|
216
|
+
|
|
217
|
+
A throw is asked about before a landing: a hand that sent something across the
|
|
218
|
+
screen has not asked for it to swap places with whatever it flew over.
|
|
219
|
+
|
|
220
|
+
Starting a document transition is the application's call, not navi's: a
|
|
221
|
+
`view-transition-name` must be unique per document, so only the application can
|
|
222
|
+
name what moves.
|
|
223
|
+
|
|
224
|
+
| Attribute | Meaning |
|
|
225
|
+
| -------------------------------------------------------- | -------------------------------------- |
|
|
226
|
+
| `data-drag-axis="x"\|"y"\|"xy"` | which axes the drag walks |
|
|
227
|
+
| `data-drag-delay` `data-drag-slop` `data-drag-threshold` | when the press becomes a grab |
|
|
228
|
+
| `data-toss-distance` `data-toss-speed` | how far and how fast counts as a throw |
|
|
229
|
+
|
|
230
|
+
### Dressing the clone
|
|
231
|
+
|
|
232
|
+
What the pointer carries is a copy, and a copy of a transparent element is
|
|
233
|
+
invisible — an element has no background unless something gave it one, and a row
|
|
234
|
+
usually gets its own from the list around it, which the copy has left. So the
|
|
235
|
+
clone's look is the page's to declare, through the attributes the gesture puts on
|
|
236
|
+
it:
|
|
237
|
+
|
|
238
|
+
| Attribute | On |
|
|
239
|
+
| ------------------------- | ------------------------------------------------------ |
|
|
240
|
+
| `navi-drag-clone` | the copy being carried |
|
|
241
|
+
| `navi-drag-clone-wrapper` | what positions it (already shadowed, in the top layer) |
|
|
242
|
+
| `navi-drag-clone-source` | the original, still in place (already hidden) |
|
|
243
|
+
|
|
244
|
+
```css
|
|
245
|
+
.task[navi-drag-clone] {
|
|
246
|
+
background: white;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Reusing the item's own class is the point: the copy is that item, so it is styled
|
|
252
|
+
as that item plus whatever being carried changes. The copy is a real element in the
|
|
253
|
+
page, in the top layer, and everything about its look is reachable from CSS —
|
|
254
|
+
including these, read off the dragged element so a whole list or a single item can
|
|
255
|
+
answer:
|
|
256
|
+
|
|
257
|
+
| Variable | What it changes |
|
|
258
|
+
| --------------------- | ------------------------------------------------------------- |
|
|
259
|
+
| `--drag-clone-shadow` | what being lifted casts; `none` for something that flies flat |
|
|
260
|
+
| `--drag-clone-scale` | how much bigger it gets once picked up |
|
|
261
|
+
|
|
262
|
+
**What stays behind is the source, not a hole.** The original is never taken out of
|
|
263
|
+
the page — it keeps its place in the layout and wears `navi-drag-clone-source`,
|
|
264
|
+
which only makes it `visibility: hidden`. So a mark left where the thing was — an
|
|
265
|
+
imprint, a dashed outline, the shape a note was pinned on — is drawn ON that
|
|
266
|
+
element and not next to it, and its parts have to say `visibility: visible` to come
|
|
267
|
+
back from the hidden source:
|
|
268
|
+
|
|
269
|
+
```css
|
|
270
|
+
.paper[navi-drag-clone-source]::after {
|
|
271
|
+
position: absolute;
|
|
272
|
+
inset: 0;
|
|
273
|
+
border: 1px dashed currentColor;
|
|
274
|
+
opacity: 0.35;
|
|
275
|
+
visibility: visible;
|
|
276
|
+
content: "";
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
It stays until the answer settles, which is what makes it say where the thing left
|
|
281
|
+
from for as long as the question is open — and if the answer refuses, the copy comes
|
|
282
|
+
back to it.
|
|
283
|
+
|
|
284
|
+
`data-drag-axis` says which axes the drag walks, and its default is not the same
|
|
285
|
+
for every outcome: `reorder` alone walks the list (`y`, or `x` for a list that runs
|
|
286
|
+
sideways), while a `move` goes wherever it is put and a `toss` wherever it was
|
|
287
|
+
thrown (`xy`). `data-drag-delay`, `data-drag-slop`, `data-drag-threshold` tune when
|
|
288
|
+
the press becomes a grab.
|
|
289
|
+
|
|
290
|
+
## Tuning
|
|
291
|
+
|
|
292
|
+
Read off the element or any ancestor carrying the attribute, so a whole list is
|
|
293
|
+
tuned in one place and a stylesheet can read the same value.
|
|
294
|
+
|
|
295
|
+
| Attribute | Default | Meaning |
|
|
296
|
+
| ---------------------- | ------- | ----------------------------------------- |
|
|
297
|
+
| `data-swipe-threshold` | `0.33` | fraction of the element to pull to commit |
|
|
298
|
+
| `data-longpress-delay` | `450` | ms the press must be held |
|
|
299
|
+
| `data-longpress-slop` | `8` | px the pointer may drift during the wait |
|
|
300
|
+
|
|
301
|
+
A threshold is a **fraction and never a distance**: the same gesture must mean
|
|
302
|
+
the same thing on a phone and on a wide screen. Speed answers on its own on top
|
|
303
|
+
of it — a brief flick counts whatever the distance covered.
|
|
304
|
+
|
|
305
|
+
## Registering an interaction navi does not have
|
|
306
|
+
|
|
307
|
+
The registry holds no detector of its own: navi's swipes, holds and shortcuts go
|
|
308
|
+
through the same door an application uses.
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
import { defineInteractionDetector } from "@jsenv/navi";
|
|
312
|
+
|
|
313
|
+
defineInteractionDetector({
|
|
314
|
+
name: "triple_click",
|
|
315
|
+
claims: (type) => type === "triple_click",
|
|
316
|
+
setup: (element, trigger) => {
|
|
317
|
+
let count = 0;
|
|
318
|
+
let timeout = null;
|
|
319
|
+
const onClick = (clickEvent) => {
|
|
320
|
+
count++;
|
|
321
|
+
clearTimeout(timeout);
|
|
322
|
+
timeout = setTimeout(() => {
|
|
323
|
+
count = 0;
|
|
324
|
+
}, 1000);
|
|
325
|
+
if (count < 3) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
count = 0;
|
|
329
|
+
trigger(clickEvent);
|
|
330
|
+
};
|
|
331
|
+
element.addEventListener("click", onClick);
|
|
332
|
+
return () => {
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
element.removeEventListener("click", onClick);
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`setup(element, trigger, { types, readConfig })` runs **once per element** and
|
|
341
|
+
returns how to undo whatever it did. Listeners, attributes, anything: it is a
|
|
342
|
+
plain setup and teardown, so a detector counts what it needs in its own closure
|
|
343
|
+
and nothing has to hold state on its behalf.
|
|
344
|
+
|
|
345
|
+
`claims` takes a **set** of names rather than one, because interactions sharing an
|
|
346
|
+
input have to be arbitrated together — a swipe, a hold and a click dispute the
|
|
347
|
+
same press, and read apart they walk over each other. `types` (third argument) is
|
|
348
|
+
which of them were actually declared here.
|
|
349
|
+
|
|
350
|
+
`trigger(type, originalEvent, detail)` says the interaction happened. Called with
|
|
351
|
+
a single event — `trigger(event)` — the type is the detector's own, which only
|
|
352
|
+
works when exactly one of its names is declared. When `originalEvent.type` is
|
|
353
|
+
already the interaction's name (a native one), that event IS the interaction and
|
|
354
|
+
no second one is dispatched.
|
|
355
|
+
|
|
356
|
+
It returns **`null` when nothing ran** (the gate refused, no control to ask, the
|
|
357
|
+
interaction event was prevented) and otherwise a **promise**: resolved once the
|
|
358
|
+
effect worked, rejected when it did not. Those two answers are not the same and a
|
|
359
|
+
detector usually treats them differently — a row pulled out comes back either
|
|
360
|
+
way, something thrown off the screen only comes back if the throw failed.
|
|
361
|
+
|
|
362
|
+
`readConfig(attribute, defaultValue)` reads a number off the element or any
|
|
363
|
+
ancestor carrying that attribute, so a whole list is tuned in one place.
|
|
364
|
+
|
|
365
|
+
A detector that reads the pointer must mark itself in the DOM so a travelling
|
|
366
|
+
container above it does not take the gesture:
|
|
367
|
+
`element.setAttribute("data-no-drag-travel", "")`, undone in the teardown (see
|
|
368
|
+
`docs/drag_to_travel.md`). navi's own swipes do the equivalent with
|
|
369
|
+
`data-travel-by-drag`.
|
|
370
|
+
|
|
371
|
+
## Things worth knowing before guessing
|
|
372
|
+
|
|
373
|
+
- **A hold does not take the context menu.** Declaring `longpress` says what a
|
|
374
|
+
held finger does; a right click comes from the other button and keeps opening
|
|
375
|
+
the browser's menu. Declare `contextmenu` beside it to make the right click do
|
|
376
|
+
the same thing. (A held _finger_ is the system's own context-menu gesture, and
|
|
377
|
+
that one is refused while the wait runs.)
|
|
378
|
+
- **A swipe cannot also be dragged out of the page.** An element declaring a
|
|
379
|
+
swipe gets `draggable={false}` and its `dragstart` refused — a native drag _is_
|
|
380
|
+
press-and-move, and a link or an image is draggable without anyone asking. One
|
|
381
|
+
gesture cannot mean both.
|
|
382
|
+
- **`interactions` adds, it does not replace.** A control's own wiring stays:
|
|
383
|
+
`actionEvent` / `actionOnMouseDown` are still how you change what triggers
|
|
384
|
+
`action` by default.
|
|
385
|
+
- **A popup can open while the finger is still down.** navi's `Popover` is
|
|
386
|
+
`popover="manual"` and owns its dismissal, so the `pointerup` ending a hold is
|
|
387
|
+
not read as an interaction outside it — a menu can appear under a waiting
|
|
388
|
+
finger, which is the native gesture. To place it at the press point rather than
|
|
389
|
+
on the element:
|
|
390
|
+
`triggerNaviCommand(target, "--navi-open", interactionEvent, { anchor })`.
|
|
391
|
+
- **A swipe has no keyboard equivalent.** There is nothing to press that means
|
|
392
|
+
"swipe right", so a swipe is only reachable if something else on the element
|
|
393
|
+
offers the same thing — a `"keyboard:<shortcut>"`, a `contextmenu`, or the
|
|
394
|
+
control's own action.
|
|
395
|
+
|
|
396
|
+
## Reference
|
|
397
|
+
|
|
398
|
+
- `src/control/interaction/interaction_registry.js` — the prop, the three values,
|
|
399
|
+
the registry.
|
|
400
|
+
- `src/control/interaction/interaction_press.js` — swipes and holds, and what a
|
|
401
|
+
swipe writes on the element.
|
|
402
|
+
- `src/control/interaction/interaction_keyboard.js`,
|
|
403
|
+
`interaction_native.js` — the other two detectors.
|
|
404
|
+
- `src/control/demos/38_interactions_demo.html` — every case above, plus a
|
|
405
|
+
mailbox and a custom `swipe_out` gesture registered from the page.
|
package/docs/z_index.md
CHANGED
|
@@ -69,22 +69,46 @@ context is how a value ends up tuned to a symptom.
|
|
|
69
69
|
|
|
70
70
|
## 5. The values navi plays with
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
They all live in `src/navi_z_indexes.js`, as tokens, in one ordered list — the
|
|
73
|
+
file is the overview, this table is its summary. Bands are a decade apart so
|
|
74
|
+
one can grow without reaching the next, and so a value seen in devtools says
|
|
75
|
+
which band it came from.
|
|
76
|
+
|
|
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 | `--navi-z-index-sticky` | 10 |
|
|
83
|
+
| A `Group` member under the pointer, then the one holding focus | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused` | 1, 2 |
|
|
84
|
+
| `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
|
|
85
|
+
|
|
86
|
+
What to read from it:
|
|
87
|
+
|
|
88
|
+
- **The order matters more than the numbers.** A bar is above anything the page
|
|
89
|
+
scrolls, a popup above the bar, and a control raising itself above its
|
|
90
|
+
neighbour is at the bottom — a hovered control crossing the top bar is the
|
|
91
|
+
bug the gaps exist to make impossible.
|
|
92
|
+
- **A z-index that only orders a component's own parts stays a literal** next to
|
|
93
|
+
the rule that needs it. Tokens are for what is decided against another
|
|
94
|
+
component; putting "above my own sibling" in the global list would only
|
|
95
|
+
dilute it.
|
|
96
|
+
- **An app writing its own number is competing with this scale.** Write
|
|
97
|
+
`isolation: isolate` on the parent instead, and the number stops meaning
|
|
98
|
+
anything outside it.
|
|
99
|
+
|
|
100
|
+
### Why a `Group` member is not isolated
|
|
101
|
+
|
|
102
|
+
`Group` overlaps its members by one border width, so the one the user is on has
|
|
103
|
+
to paint over its neighbour — otherwise its focus ring is sliced in half by the
|
|
104
|
+
member that comes after it in the DOM. DOM order cannot express "whichever one
|
|
105
|
+
is hovered", so this is a legitimate `z-index`.
|
|
106
|
+
|
|
107
|
+
`isolation: isolate` on the group would contain those two values, but it would
|
|
108
|
+
also contain the popup of a `Picker` held in the group: its 1000 would become
|
|
109
|
+
local, and the popup would be capped inside the group instead of covering the
|
|
110
|
+
page. So the group is deliberately not isolated, and what keeps its 1 and 2
|
|
111
|
+
harmless is the scale above them.
|
|
88
112
|
|
|
89
113
|
## A card that stacks three layers with no `z-index`
|
|
90
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@jsenv/dom": "0.17.
|
|
32
|
+
"@jsenv/dom": "0.17.10",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.2"
|
|
35
35
|
},
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@jsenv/snapshot": "../../tooling/snapshot",
|
|
42
42
|
"@jsenv/terminal-table": "../../tooling/terminal-table",
|
|
43
43
|
"@jsenv/urls": "../../tooling/urls",
|
|
44
|
-
"@preact/signals": "2.
|
|
45
|
-
"playwright": "1.
|
|
44
|
+
"@preact/signals": "2.11.1",
|
|
45
|
+
"playwright": "1.62.1",
|
|
46
46
|
"preact": "11.0.0-beta.2"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
},
|
|
51
51
|
"sideEffects": [
|
|
52
52
|
"./src/navi_css_vars.js",
|
|
53
|
-
"./
|
|
53
|
+
"./src/navi_z_indexes.js",
|
|
54
|
+
"./src/control/interaction/*.js",
|
|
55
|
+
"./dist/jsenv_navi_side_effects.js",
|
|
56
|
+
"./dist/jsenv_navi.js"
|
|
54
57
|
]
|
|
55
58
|
}
|