@jsenv/navi 0.29.418 → 0.29.419
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 +1 -1
- package/dist/dev/jsenv_navi.js +192 -43
- package/dist/dev/jsenv_navi.js.map +10 -10
- package/dist/jsenv_navi.js +190 -41
- package/dist/jsenv_navi.js.map +10 -10
- package/docs/AI_INSTRUCTIONS.md +177 -261
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +115 -174
- package/docs/drag_to_travel.md +10 -9
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +6 -4
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -482
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -335
- package/docs/state_binding.md +2 -1
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/view_transitions.md
CHANGED
|
@@ -10,6 +10,7 @@ they are met once.
|
|
|
10
10
|
- [Nested groups, and the fallback fade](#nested-groups-and-the-fallback-fade)
|
|
11
11
|
- [Rendering is suspended for the whole callback](#rendering-is-suspended-for-the-whole-callback)
|
|
12
12
|
- [`ready` rejects when the transition is skipped, `finished` when the update fails](#ready-rejects-when-the-transition-is-skipped-finished-when-the-update-fails)
|
|
13
|
+
- [An element captured in a view transition cannot be pointed at](#an-element-captured-in-a-view-transition-cannot-be-pointed-at)
|
|
13
14
|
- [The top layer is painted through the root's picture](#the-top-layer-is-painted-through-the-roots-picture)
|
|
14
15
|
- [Two frames show the live document](#two-frames-show-the-live-document)
|
|
15
16
|
- [What makes a transition restyle the whole document](#what-makes-a-transition-restyle-the-whole-document)
|
|
@@ -20,18 +21,28 @@ they are met once.
|
|
|
20
21
|
A `view-transition-name` must be unique per document: a duplicate aborts the
|
|
21
22
|
transition, silently. Scope any name the app adds — per item id, per screen.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the
|
|
24
|
+
A named element is photographed on its own and animated by the browser from where
|
|
25
|
+
it was to where it is, during any transition, whoever started it. Hence the trap:
|
|
26
|
+
**an element named inside a page that moves does not move with it.** The name
|
|
27
|
+
lifts it out of its page's picture into a picture of its own, which stands where
|
|
28
|
+
it was captured, fading, while the pages slide away under it — right for a morph
|
|
29
|
+
between the two pages, wrong for anything that belongs to its page. navi's own
|
|
30
|
+
components drop their names for such movements: `Nav` names its current-tab bar
|
|
31
|
+
only for a movement between two tabs of its own row (`currentIndicatorSlides`),
|
|
32
|
+
and `List` unnames its rows for the length of any route transition or travel. A
|
|
33
|
+
name the app puts inside the pages is given only for the gesture it serves, or
|
|
34
|
+
dropped while they move: `view-transition-name: none` under
|
|
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.
|
|
28
38
|
|
|
29
39
|
## Nested groups, and the fallback fade
|
|
30
40
|
|
|
31
41
|
List and grid transitions rely on nested groups (`view-transition-group:
|
|
32
|
-
contain`, Chrome/Edge 140+). On a browser without
|
|
33
|
-
unconditional `startViewTransition` falls back to a full-page cross-fade.
|
|
34
|
-
that fade is
|
|
42
|
+
contain`, Chrome/Edge 140+). On a browser without them nothing is named, so an
|
|
43
|
+
unconditional `startViewTransition` falls back to a full-page cross-fade. Whether
|
|
44
|
+
that fade is a decent default or a glitch is the application's call, never a
|
|
45
|
+
component's, and turning it off is one rule (see `itemTransition` on `List`):
|
|
35
46
|
|
|
36
47
|
```css
|
|
37
48
|
@supports not (view-transition-group: contain) {
|
|
@@ -41,9 +52,6 @@ that fade is unwanted, the app — not a component — writes:
|
|
|
41
52
|
}
|
|
42
53
|
```
|
|
43
54
|
|
|
44
|
-
Only the application knows whether a page-wide fade is a decent default or a
|
|
45
|
-
glitch there.
|
|
46
|
-
|
|
47
55
|
## Rendering is suspended for the whole callback
|
|
48
56
|
|
|
49
57
|
The document's rendering is suspended for the whole update callback, from the
|
|
@@ -53,103 +61,99 @@ callback awaits something that cannot happen, until the browser gives up on the
|
|
|
53
61
|
transition entirely (`Transition was aborted because of timeout in DOM update`).
|
|
54
62
|
Await a microtask, a task or a render — never a frame.
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
app, opened on its own, shows nothing. Keep the callback short, and suspect the
|
|
62
|
-
frame before the code when a scrollbar blinks.
|
|
64
|
+
**In a sub-document the suspension takes the scrollbar with it**: an iframe's
|
|
65
|
+
scrollbar is painted by the framed document, so it goes and comes back for the
|
|
66
|
+
length of the callback, shifting the layout by its width — a top-level page is
|
|
67
|
+
spared, its root scrollbar being the compositor's. Keep the callback short, and
|
|
68
|
+
suspect the frame before the code when a scrollbar blinks.
|
|
63
69
|
|
|
64
70
|
## `ready` rejects when the transition is skipped, `finished` when the update fails
|
|
65
71
|
|
|
66
72
|
There is only ever one transition per document, so another one starting SKIPS
|
|
67
73
|
this one — as does the document going hidden or its snapshot containing block
|
|
68
|
-
changing size mid-movement (a phone's address bar folding, its keyboard
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
So ending a transition means handling both ways of `finished` — `.finally()`
|
|
76
|
-
|
|
74
|
+
changing size mid-movement (a phone's address bar folding, its keyboard opening).
|
|
75
|
+
A skip rejects `viewTransition.ready`, with a stackless DOMException (`AbortError`
|
|
76
|
+
"skipped", or `InvalidStateError` "aborted because of invalid state").
|
|
77
|
+
`viewTransition.finished` still FULFILLS on a skip: the end state is reached, only
|
|
78
|
+
the animation is not. `finished` (and `updateCallbackDone`) reject for one reason
|
|
79
|
+
only, the update callback throwing.
|
|
80
|
+
|
|
81
|
+
So ending a transition means handling both ways of `finished` — `.finally()` does
|
|
82
|
+
not handle a rejection, an unhandled one is what it leaves behind, and
|
|
77
83
|
`.then(done, done)` is the shape that ends it whichever way it went — while a
|
|
78
84
|
`ready` waiter must expect the skip: `ready.then(onPictures, () => {})`.
|
|
79
85
|
|
|
86
|
+
## An element captured in a view transition cannot be pointed at
|
|
87
|
+
|
|
88
|
+
A captured element is not painted where it stands, and nothing hit-tests to it: a
|
|
89
|
+
press over it falls through to the nearest ancestor still being painted, whatever
|
|
90
|
+
the pseudo-elements are told about `pointer-events`. Under the root's default
|
|
91
|
+
name, that is the whole page for the length of the movement. It is why a
|
|
92
|
+
`RouteTravel` keeps the root out of its pictures, so the page around the box stays
|
|
93
|
+
live, and catches a press on the travelling box at the document, handing it to the
|
|
94
|
+
box it fell inside; and why a door in a bar a route transition photographs needs
|
|
95
|
+
`pressableDuringRouteTransition` to stay pressable (see
|
|
96
|
+
[route_transitions.md](./route_transitions.md#pages-between-fixed-bars-the-transition-area)).
|
|
97
|
+
|
|
80
98
|
## The top layer is painted through the root's picture
|
|
81
99
|
|
|
82
|
-
A modal `<dialog>`, its `::backdrop`, a popover: the browser paints the top
|
|
83
|
-
|
|
84
|
-
view-transition-name: none }`, the wall and the dialog go unpainted for
|
|
85
|
-
length of the movement, and a named element inside the dialog is photographed
|
|
86
|
-
empty
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
each capture, and one `position: fixed` element in the live document for what
|
|
99
|
-
no picture covers, with a hole cut where each picture stands. The live one is
|
|
100
|
-
not a picture, so nothing cross-fades it: it is animated on the movement's own
|
|
101
|
-
clock. Where a picture stands changes over the movement — the frame the first
|
|
102
|
-
picture is taken on shows the leaving state, the last frame the arriving one,
|
|
103
|
-
and in between only the intersection of the two states' rectangles is under a
|
|
104
|
-
picture at every moment — so the holes are cut three times. Reference:
|
|
105
|
-
`paintTransitionWalls` and `paintRestWalls` in `nav/transition_furniture.js`;
|
|
106
|
-
the measured story is in [route_transitions.md](./route_transitions.md#pages-between-fixed-bars-the-transition-area).
|
|
100
|
+
A modal `<dialog>`, its `::backdrop`, a popover: the browser paints the top layer
|
|
101
|
+
during a transition only as part of the root's picture. With
|
|
102
|
+
`:root { view-transition-name: none }`, the wall and the dialog go unpainted for
|
|
103
|
+
the length of the movement, and a named element inside the dialog is photographed
|
|
104
|
+
empty. A transition that opens or closes a top-layer surface keeps the root's
|
|
105
|
+
default name and pays the frozen page — under a modal wall that costs nothing.
|
|
106
|
+
navi's own lift ([popup_lift.md](./popup_lift.md)) does exactly that.
|
|
107
|
+
|
|
108
|
+
When the root cannot be kept — a route transition splits the bars from the pages,
|
|
109
|
+
which only works with the root opted out — the top layer has to be **stood in
|
|
110
|
+
for**. A dialog's box is an element, so it can be named and photographed on its
|
|
111
|
+
own; its wall cannot, `::backdrop` wears no name. What works is painting what the
|
|
112
|
+
wall paints into the pictures themselves, plus a live element in the document for
|
|
113
|
+
what no picture covers — not a picture, so animated on the movement's own clock,
|
|
114
|
+
and switched on by the same DOM write that switches the real wall off (see
|
|
115
|
+
[Two frames show the live document](#two-frames-show-the-live-document)).
|
|
107
116
|
|
|
108
117
|
## Two frames show the live document
|
|
109
118
|
|
|
110
119
|
A view transition is not pictures from the call to `finished`. **The frame the
|
|
111
|
-
first picture is taken on is rendered and shown**, with the top layer painted
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
wall twice as dark under the press, read as a flash).
|
|
120
|
+
first picture is taken on is rendered and shown**, with the top layer painted as
|
|
121
|
+
usual, and `:active-view-transition` already matches on it (it matches from the
|
|
122
|
+
`startViewTransition` call). **The frame after the pictures are dropped is shown
|
|
123
|
+
too**, before the `finished` callbacks run — they are a frame late. Whatever is
|
|
124
|
+
switched on for the movement is therefore on screen one frame before the pictures
|
|
125
|
+
and one frame after; anything it stands in for must be switched off for exactly
|
|
126
|
+
the same span, or that frame shows both (a wall twice as dark: a flash).
|
|
119
127
|
|
|
120
128
|
Two consequences for the switch itself:
|
|
121
129
|
|
|
122
130
|
- **Switch both on the same DOM write**, at the same moment: an attribute set
|
|
123
131
|
before the call and removed in `finished`. The late frame then shows the
|
|
124
|
-
stand-in alone, which is what the real thing looked like — one frame nobody
|
|
125
|
-
|
|
126
|
-
- **`:active-view-transition` is the only same-frame signal at the end**: it
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
real thing is still there.
|
|
132
|
+
stand-in alone, which is what the real thing looked like — one frame nobody can
|
|
133
|
+
see.
|
|
134
|
+
- **`:active-view-transition` is the only same-frame signal at the end**: it stops
|
|
135
|
+
matching in the rendering step that drops the pictures, before that frame is
|
|
136
|
+
painted. It is what to use when the last frame must differ from the movement. It
|
|
137
|
+
is NOT what to gate a stand-in's `display` on: hidden before the call, a
|
|
138
|
+
stand-in measures as a zero rect and lands in the wrong place; and on the
|
|
139
|
+
capture frame it is displayed while the real thing is still there.
|
|
133
140
|
|
|
134
141
|
## What makes a transition restyle the whole document
|
|
135
142
|
|
|
136
|
-
Starting a transition restyles what changed and nothing more: in a bare page,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
state is photographed —
|
|
140
|
-
and 60–90 ms with the CPU throttled 4×, added to every transition before
|
|
141
|
-
anything moves:
|
|
143
|
+
Starting a transition restyles what changed and nothing more: in a bare page, the
|
|
144
|
+
capture of the old state restyles a handful of elements, however long the page
|
|
145
|
+
is. Two things make it restyle every element of the page, in the frame the old
|
|
146
|
+
state is photographed — added to every transition before anything moves:
|
|
142
147
|
|
|
143
148
|
- **A `::highlight()` rule every element matches.** Written for the whole
|
|
144
|
-
document, `::highlight(x) { … }` makes Chrome restyle every element when a
|
|
145
|
-
|
|
146
|
-
once the rule is scoped). Scope it to the elements that hold the ranges —
|
|
149
|
+
document, `::highlight(x) { … }` makes Chrome restyle every element when a view
|
|
150
|
+
transition starts. Scope it to the elements that hold the ranges —
|
|
147
151
|
`.results::highlight(x), .results ::highlight(x)` (both: a browser without
|
|
148
152
|
highlight inheritance only paints what the rule matches). navi's own search
|
|
149
153
|
highlight is scoped to list rows.
|
|
150
|
-
- **A custom property changed on `:root`.** Every element inherits it, so
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
- **A custom property changed on `:root`.** Every element inherits it, so changing
|
|
155
|
+
one restyles the document — and a movement changes its values as it starts.
|
|
156
|
+
What the pictures read belongs on `::view-transition` instead: the
|
|
153
157
|
pseudo-element tree inherits from it, and no element of the document does.
|
|
154
158
|
|
|
155
159
|
```css
|
|
@@ -160,41 +164,39 @@ anything moves:
|
|
|
160
164
|
}
|
|
161
165
|
```
|
|
162
166
|
|
|
163
|
-
A value computed in JS cannot get there through an inline style, which
|
|
164
|
-
|
|
165
|
-
`:root::view-transition {}`, set with `rule.style.setProperty()
|
|
166
|
-
this in `nav/transition_values.js`).
|
|
167
|
+
A value computed in JS cannot get there through an inline style, which cannot
|
|
168
|
+
target a pseudo-element: it goes in a rule of a sheet of its own,
|
|
169
|
+
`:root::view-transition {}`, set with `rule.style.setProperty()`.
|
|
167
170
|
|
|
168
171
|
A value JS has to read back is the exception: what JS reads off
|
|
169
172
|
`::view-transition` is not reliable outside a live transition —
|
|
170
|
-
`getComputedStyle(root, "::view-transition")` answers while none runs, but
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
`getComputedStyle(root, "::view-transition")` answers while none runs, but in
|
|
174
|
+
Chrome and WebKit alike it went on answering empty for rules added after its
|
|
175
|
+
first read. Such a value stays on `:root`, registered with `@property` and
|
|
176
|
+
`inherits: false` so that the document does not inherit it, and is handed to the
|
|
177
|
+
pictures explicitly: `--x: inherit` on `::view-transition` and on the
|
|
178
|
+
pseudo-elements below it.
|
|
176
179
|
|
|
177
180
|
Whatever does depend on the root is paid once, by the frame of the capture —
|
|
178
|
-
unless a style read follows a write on the root, which forces the restyle on
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
unless a style read follows a write on the root, which forces the restyle on the
|
|
182
|
+
spot, in the click. Read what the transition needs (names, rectangles) before
|
|
183
|
+
writing anything on the root.
|
|
181
184
|
|
|
182
185
|
## A hidden element is not photographed, even in the top layer
|
|
183
186
|
|
|
184
187
|
A popover or a modal `<dialog>` whose ancestor is `display: none` still matches
|
|
185
|
-
`:popover-open` or `:modal`, but it is not rendered: `checkVisibility()` is
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
`:popover-open` or `:modal`, but it is not rendered: `checkVisibility()` is false,
|
|
189
|
+
its rectangle is empty, and a transition taking its pictures then has it on the
|
|
190
|
+
old side only — it leaves, as if it were gone. `isConnected` says nothing about
|
|
191
|
+
it; `checkVisibility()` is what tells which of the two states an element is part
|
|
192
|
+
of. navi relies on it to keep the page left by a route transition mounted but out
|
|
193
|
+
of the second picture (see
|
|
191
194
|
[route_transitions.md](./route_transitions.md#the-page-being-left-stays-until-its-movement-is-over)).
|
|
192
195
|
|
|
193
196
|
## See also
|
|
194
197
|
|
|
195
198
|
- [route_transitions.md](./route_transitions.md) — pages moving against each
|
|
196
199
|
other on navigation
|
|
197
|
-
- [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
what moves when something is dropped on a place
|
|
200
|
+
- [drag_to_travel.md](./drag_to_travel.md) — a transition held under a finger
|
|
201
|
+
- [drag_interactions.md](./drag_interactions.md#naming-what-travels) — naming what
|
|
202
|
+
moves when something is dropped on a place
|
package/docs/z_index.md
CHANGED
|
@@ -11,15 +11,8 @@ Reach for the tools in this order.
|
|
|
11
11
|
|
|
12
12
|
Between positioned elements that all have `z-index: auto`, the last one written
|
|
13
13
|
paints in front. Moving a tag is the cheapest way to reorder, and it can never
|
|
14
|
-
affect anything outside its parent
|
|
15
|
-
|
|
16
|
-
```jsx
|
|
17
|
-
// The stamp paints over the content because it comes after it. No z-index.
|
|
18
|
-
<Box position="relative">
|
|
19
|
-
<CardContent />
|
|
20
|
-
<Stamp />
|
|
21
|
-
</Box>
|
|
22
|
-
```
|
|
14
|
+
affect anything outside its parent (see
|
|
15
|
+
[the card at the end](#a-card-that-stacks-three-layers-with-no-z-index)).
|
|
23
16
|
|
|
24
17
|
If the element that must be in front cannot move in the DOM (it is a slot, it
|
|
25
18
|
is written by a consumer), that is a real reason to go further — "I did not
|
|
@@ -30,11 +23,8 @@ think about the order" is not.
|
|
|
30
23
|
`z-index: 5` does not mean "in front of my siblings". It means "in front of
|
|
31
24
|
everything painted lower **in the nearest stacking context**", and when no
|
|
32
25
|
ancestor opens one, that context is the document root — including `FixedBar`,
|
|
33
|
-
sticky list-group labels, and popups.
|
|
34
|
-
|
|
35
|
-
This is the failure that keeps happening: a small `z-index` inside a card wins
|
|
36
|
-
against a bar written at the other end of the page, because both are competing
|
|
37
|
-
in the same, page-wide context.
|
|
26
|
+
sticky list-group labels, and popups. That is how a small `z-index` inside a
|
|
27
|
+
card wins against a bar written at the other end of the page.
|
|
38
28
|
|
|
39
29
|
## 3. If a `z-index` is genuinely needed, isolate
|
|
40
30
|
|
|
@@ -51,74 +41,51 @@ takes its place among its own siblings.
|
|
|
51
41
|
|
|
52
42
|
A `z-index` inside a reusable component without this is a bug waiting for its
|
|
53
43
|
call site: the component behaves differently depending on where it is dropped.
|
|
44
|
+
An app writing its own number without it is competing with navi's scale (§5).
|
|
54
45
|
|
|
55
46
|
## 4. What creates a stacking context without you asking
|
|
56
47
|
|
|
57
48
|
`opacity` below 1, `transform`, `filter`, `backdrop-filter`, `will-change`,
|
|
58
49
|
`contain: paint`, `mix-blend-mode`, and a positioned element with a `z-index`
|
|
59
|
-
other than `auto` all open one
|
|
50
|
+
other than `auto` all open one: something you faded or moved suddenly paints as
|
|
51
|
+
a block, and a `z-index` written deeper inside stops reaching where you
|
|
52
|
+
expected. The answer is still DOM order — write the layer that must be on top
|
|
53
|
+
last — not a `z-index` "to repair it": adding one on top of an unnoticed
|
|
54
|
+
stacking context is how a value ends up tuned to a symptom.
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
behind a sibling it used to interleave with;
|
|
63
|
-
- a `z-index` you wrote deeper inside stops reaching where you expected,
|
|
64
|
-
because one of these ancestors now caps it.
|
|
56
|
+
## 5. The values navi plays with
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
They live in `src/navi_z_indexes.js`, as tokens, in bands a decade apart — one
|
|
59
|
+
can grow without reaching the next, and a value seen in devtools says which
|
|
60
|
+
band it came from. From the bottom:
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
1, 2, 3 (a `Group` member hovered, focused, holding a popup open) < 10 (sticky
|
|
63
|
+
parts, while something scrolls under them) < 100 (`FixedBar`) < 1000
|
|
64
|
+
(`Dialog`/`Popover` with `layer="local"` and their backdrops, plus stack order)
|
|
65
|
+
< 10000 (a modal's wall painted into the page for the length of a route
|
|
66
|
+
transition, standing in for the top layer) < the top layer itself
|
|
67
|
+
(`Dialog`/`Popover` with `layer="top"`, callouts).
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
79
|
-
| Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
|
|
80
|
-
| A modal's wall painted into the page for the length of a route transition, standing in for the top layer | `--navi-z-index-top-layer` | 10000 |
|
|
81
|
-
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
|
|
82
|
-
| `FixedBar` | `--navi-z-index-bar` | 100 |
|
|
83
|
-
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
|
|
84
|
-
| 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 |
|
|
85
|
-
| `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
|
|
86
|
-
|
|
87
|
-
What to read from it:
|
|
88
|
-
|
|
89
|
-
- **The order matters more than the numbers.** A bar is above anything the page
|
|
90
|
-
scrolls, a popup above the bar, and a control raising itself above its
|
|
91
|
-
neighbour is at the bottom — a hovered control crossing the top bar is the
|
|
92
|
-
bug the gaps exist to make impossible.
|
|
93
|
-
- **A z-index that only orders a component's own parts stays a literal** next to
|
|
94
|
-
the rule that needs it. Tokens are for what is decided against another
|
|
95
|
-
component; putting "above my own sibling" in the global list would only
|
|
96
|
-
dilute it.
|
|
97
|
-
- **An app writing its own number is competing with this scale.** Write
|
|
98
|
-
`isolation: isolate` on the parent instead, and the number stops meaning
|
|
99
|
-
anything outside it.
|
|
69
|
+
**The order matters more than the numbers.** A bar is above anything the page
|
|
70
|
+
scrolls, a popup above the bar, and a control raising itself above its
|
|
71
|
+
neighbour is at the bottom — a hovered control crossing the top bar is the bug
|
|
72
|
+
the gaps exist to make impossible. **A z-index that only orders a component's
|
|
73
|
+
own parts stays a literal** next to the rule that needs it (`Table` orders its
|
|
74
|
+
sticky cells, drag and resize on a 1–7 scale of its own).
|
|
100
75
|
|
|
101
76
|
### A sticky part is only in the band while it is stuck
|
|
102
77
|
|
|
103
78
|
`--navi-z-index-sticky` says "kept stuck while something scrolls under it", and
|
|
104
|
-
the second half of that sentence is a condition
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`.navi_list_item_group_label`) with `navi-stuck`, against its own scroller and
|
|
115
|
-
not the window, updated on scroll, on resize, and on every commit (a virtualized
|
|
116
|
-
list changes which labels exist without anything scrolling).
|
|
117
|
-
|
|
118
|
-
navi's own rule is the first reader: the band applies under `[navi-stuck]`,
|
|
119
|
-
`auto` applies at rest. That is what puts the decision back within reach of an
|
|
120
|
-
app — a card whose badge overflows into the label below it gets past it with a
|
|
121
|
-
literal in the card, against its own neighbour, exactly as §1–3 above ask:
|
|
79
|
+
the second half of that sentence is a condition. A `List` group label at rest
|
|
80
|
+
is a block in the flow: nothing passes under it, and painting it at 10 there is
|
|
81
|
+
what slices a focus ring, a badge or a stamp that a neighbouring row lets out of
|
|
82
|
+
its box — including a `Group` member raising itself to 1, 2 or 3.
|
|
83
|
+
|
|
84
|
+
An element cannot read its own stuck state in CSS, so `List` measures it and
|
|
85
|
+
marks its header, footer and group labels `navi-stuck`: the band applies there,
|
|
86
|
+
`auto` at rest. That puts the decision back within reach of an app — a card
|
|
87
|
+
whose badge overflows into the label below it gets past it with a literal in
|
|
88
|
+
the card, against its own neighbour, exactly as §1–3 ask:
|
|
122
89
|
|
|
123
90
|
```jsx
|
|
124
91
|
// Reaches past a label at rest (auto), loses to one that is stuck (10).
|
|
@@ -127,48 +94,32 @@ literal in the card, against its own neighbour, exactly as §1–3 above ask:
|
|
|
127
94
|
|
|
128
95
|
For what a literal cannot reach, each part has a pair of variables —
|
|
129
96
|
`--list-header-z-index`, `--list-footer-z-index`, `--list-group-label-z-index`,
|
|
130
|
-
each with a `-stuck` counterpart defaulting to the band — settable on `<List
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
See the "Sticky parts" chapter of
|
|
97
|
+
each with a `-stuck` counterpart defaulting to the band — settable on `<List>`.
|
|
98
|
+
Reach for them last, and remember a negative value is compared against the page
|
|
99
|
+
like any other: without a stacking context between the label and the nearest
|
|
100
|
+
opaque background, `-1` does not put the label behind the rows, it puts it
|
|
101
|
+
behind that background and out of sight. See the "Sticky parts" chapter of
|
|
137
102
|
[12_list_demo.html](../src/control/demos/12_list_demo.html).
|
|
138
103
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
generic scrolling area, its content is whatever the app puts in it, and a
|
|
143
|
-
sticky part
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
104
|
+
`Box`'s own sticky `header`/`footer` — the shape without a `body`; beside a
|
|
105
|
+
body they are plain blocks — and `<Box sticky>` take the opposite default: they
|
|
106
|
+
are in the band **always**, not only while stuck. A `Box` cannot tell: it is
|
|
107
|
+
the generic scrolling area, its content is whatever the app puts in it, and a
|
|
108
|
+
sticky part at `auto` loses to anything that content positioned, a `transform`
|
|
109
|
+
or an `opacity` below 1 included. The scrolling box is `isolation: isolate`, so
|
|
110
|
+
the band stays local to it. At the one call site that knows nothing inside is
|
|
111
|
+
positioned — a badge or a stamp overflowing a row is otherwise sliced by a
|
|
112
|
+
header it never scrolls under — `--box-header-z-index: auto`
|
|
113
|
+
(`--box-footer-z-index` likewise) writes it back.
|
|
148
114
|
[9_scrollable_z_index_demo.html](../src/box/demos/9_scrollable_z_index_demo.html)
|
|
149
115
|
shows the band, what `auto` would look like, and what the band costs, side by
|
|
150
116
|
side.
|
|
151
117
|
|
|
152
|
-
`<Box sticky>`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// position: sticky + z-index: var(--navi-z-index-sticky). Nothing to write.
|
|
158
|
-
<Box sticky bottom>
|
|
159
|
-
<SubmitButton />
|
|
160
|
-
</Box>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Without it the box is a positioned element at `z-index: auto`, and anything the
|
|
164
|
-
page raised passes in front of it — a `Group` member holding focus is at 2, so
|
|
165
|
-
it is seen crossing a submit bar the box was written to keep last. DOM order
|
|
166
|
-
cannot answer that (2 beats `auto` whatever the order) and `isolation: isolate`
|
|
167
|
-
neither (the common parent holds both, isolating it does not reorder them), so
|
|
168
|
-
without the band the app would have no move left but a literal of its own.
|
|
169
|
-
|
|
170
|
-
An explicit `zIndex` wins, `zIndex="auto"` included — the way
|
|
171
|
-
`--box-header-z-index` writes the band back at a call site that knows better:
|
|
118
|
+
`<Box sticky>` carries the band from the prop itself. Without it, a `Group`
|
|
119
|
+
member holding focus (2) would be seen crossing a submit bar the box was written
|
|
120
|
+
to keep last, and neither DOM order nor `isolation` can answer that: 2 beats
|
|
121
|
+
`auto` whatever the order, and the common parent holds both. An explicit
|
|
122
|
+
`zIndex` wins, `"auto"` included:
|
|
172
123
|
|
|
173
124
|
```jsx
|
|
174
125
|
// Back to auto: this one is meant to slide under the card that follows it.
|
|
@@ -183,10 +134,11 @@ member that comes after it in the DOM. DOM order cannot express "whichever one
|
|
|
183
134
|
is hovered", so this is a legitimate `z-index`.
|
|
184
135
|
|
|
185
136
|
`isolation: isolate` on the group would contain those values, but it would
|
|
186
|
-
also contain the popup of a `Picker` held in the group
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
137
|
+
also contain the popup of a `Picker` held in the group when that popup stacks
|
|
138
|
+
in the page (`popupLayer="local"`): its 1000 would become local, and the popup
|
|
139
|
+
would be capped inside the group instead of covering the page. A top-layer
|
|
140
|
+
popup, the default, escapes any stacking context. So the group is deliberately
|
|
141
|
+
not isolated, and what keeps its 1, 2 and 3 harmless is the scale above them.
|
|
190
142
|
|
|
191
143
|
## A card that stacks three layers with no `z-index`
|
|
192
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.419",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"preact": "^11.0.0-rc.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/dom": "0.17.
|
|
37
|
+
"@jsenv/dom": "0.17.85",
|
|
38
38
|
"@jsenv/humanize": "1.8.6",
|
|
39
39
|
"@jsenv/validity": "0.4.5"
|
|
40
40
|
},
|
package/docs/badge_list.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# BadgeList
|
|
2
|
-
|
|
3
|
-
A row of badges that wraps. What it does at runtime depends on what it is asked
|
|
4
|
-
for, and nothing is set up for a case that cannot happen:
|
|
5
|
-
|
|
6
|
-
- **plain** — no `max`, no `maxLines`, no `fallback`, no `shrinkWrap`: one
|
|
7
|
-
element holding its children as-is. No registry, no effect, no measurement.
|
|
8
|
-
- **`max` / `fallback`** — the badges are counted (see below), nothing is
|
|
9
|
-
measured, no DOM is watched.
|
|
10
|
-
- **`shrinkWrap`** — a hidden clone of the list is laid out to find the widest
|
|
11
|
-
wrapped row, and the list is narrowed to it so the last row isn't ragged.
|
|
12
|
-
Opt-in outside a `Picker` (a picker draws a border around the list, so the
|
|
13
|
-
ragged edge shows; elsewhere the work would often go unseen). With `maxLines`
|
|
14
|
-
the rows are read first, at the full width, and the list is narrowed once the
|
|
15
|
-
surplus is gone — in place, no clone.
|
|
16
|
-
- **`maxLines`** — every badge is laid out once, hidden; where the rows fell is
|
|
17
|
-
read; the list is rendered again with the badges that fit and a `+N more`
|
|
18
|
-
badge for the rest. Both renders land in the same frame. A width change of
|
|
19
|
-
the room around the list triggers another measure — not a change of the
|
|
20
|
-
list's own width, which is what it produces, not what it is given.
|
|
21
|
-
|
|
22
|
-
## How badges are counted
|
|
23
|
-
|
|
24
|
-
Once the list has something to decide — a `max`, a `fallback`, `maxLines` — a
|
|
25
|
-
`Badge` inside it does not render itself: it hands its props to the list
|
|
26
|
-
through `BadgeListContext` and renders nothing (a plain list leaves its badges
|
|
27
|
-
to render themselves). Badges register in tree order, so by the time the list
|
|
28
|
-
gets to its own content it holds them all
|
|
29
|
-
and knows how many there are before deciding what to show — without walking
|
|
30
|
-
children vnodes, and without rendering a badge it then has to take back.
|
|
31
|
-
|
|
32
|
-
Two consequences:
|
|
33
|
-
|
|
34
|
-
- a `Badge` must be a **direct** child of the list — its rendering is moved
|
|
35
|
-
into the list, so `<div><Badge /></div>` takes the badge out of its wrapper;
|
|
36
|
-
- a badge's own `key` goes to the registering vnode and not to the badge the
|
|
37
|
-
list draws, which is keyed by position. Reordering recreates the nodes rather
|
|
38
|
-
than moving them.
|
|
39
|
-
|
|
40
|
-
## `maxLines` in a Picker
|
|
41
|
-
|
|
42
|
-
A picker clamps its value with `maxLines`, which is CSS line-clamp — it counts
|
|
43
|
-
line boxes of inline text and never sees a wrapped flex row. So a `BadgeList`
|
|
44
|
-
rendered as a picker's `ui` reads the number from `MaxLinesContext` instead
|
|
45
|
-
(the picker provides it, default 1) and caps its own rows to it, and the picker
|
|
46
|
-
turns its own clamp off (`.navi_picker_value:has(.navi_badge_list)`). Nothing
|
|
47
|
-
to set on the list; `maxLines` on the picker is enough, and a `maxLines` on the
|
|
48
|
-
list itself is a local override.
|
|
49
|
-
|
|
50
|
-
`max` is a different cap: a number of badges, whatever the rows. The two
|
|
51
|
-
compose — the `+N` badge takes one of the `max` slots.
|
|
52
|
-
|
|
53
|
-
## The `fallback`
|
|
54
|
-
|
|
55
|
-
Without a `fallback`, an empty list renders **nothing**. Inside a picker there
|
|
56
|
-
is one more thing to know: a picker given a `ui` draws that and only that, its
|
|
57
|
-
own `placeholder` is **not** drawn next to it. An empty `BadgeList` in a picker
|
|
58
|
-
is a blank picker — so the list's `fallback` is the placeholder, and the
|
|
59
|
-
placeholder text is what to pass:
|
|
60
|
-
|
|
61
|
-
```jsx
|
|
62
|
-
<BadgeList fallback="Select skills…">
|
|
63
|
-
{selected.map((skill) => (
|
|
64
|
-
<Badge key={skill}>{skill}</Badge>
|
|
65
|
-
))}
|
|
66
|
-
</BadgeList>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Plain text is the right shape for it. It reads at the picker's own size, in the
|
|
70
|
-
placeholder color — the picker colors its value slot that way while it holds
|
|
71
|
-
nothing — and the picker box stays the same height empty and filled: the box is
|
|
72
|
-
held one line tall by the picker's own line (the right slot is `1lh`), whatever
|
|
73
|
-
the slot inside holds. That slot is a fraction of a pixel taller with a line of
|
|
74
|
-
text than with a row of badges (0.89px at the default font size — a line is set
|
|
75
|
-
by the picker's font and line-height, a badge by its own padding and smaller
|
|
76
|
-
font), which nothing shows.
|
|
77
|
-
|
|
78
|
-
A transparent `Badge` as fallback matches the slot to the pixel instead, but its
|
|
79
|
-
text is badge-sized — smaller than the picker's — and reads as a badge with
|
|
80
|
-
nothing in it. Only worth it where the slot itself is what something else is
|
|
81
|
-
sized on. Section 4 of `src/control/demos/picker/2_select_multiple_demo.html`
|
|
82
|
-
measures the three cases (no fallback, plain text, transparent badge), box and
|
|
83
|
-
slot side by side.
|