@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/safe_area.md
CHANGED
|
@@ -60,9 +60,8 @@ Declared in `src/layout/safe_area.js`.
|
|
|
60
60
|
An app that never spans the whole window — a phone-shaped column centered in a
|
|
61
61
|
wide one, bands on the sides — has one problem with popups: a dialog lives in
|
|
62
62
|
the browser's top layer, so it is calibrated on the _viewport_, and would paint
|
|
63
|
-
1500px of modal over a 600px app.
|
|
64
|
-
|
|
65
|
-
that, because the app would then have to know which components exist.
|
|
63
|
+
1500px of modal over a 600px app. Popups must not need to be told, or the app
|
|
64
|
+
would have to know which components exist.
|
|
66
65
|
|
|
67
66
|
So the app states its own screen once, and never names a component:
|
|
68
67
|
|
|
@@ -76,57 +75,28 @@ So the app states its own screen once, and never names a component:
|
|
|
76
75
|
The bands fall out of it (centered), `--navi-app-width` follows, and `FixedBar`
|
|
77
76
|
pins itself to the column's edges rather than the glass. An app wanting them
|
|
78
77
|
uneven writes `--navi-app-inset-left` / `-right` directly instead; everything
|
|
79
|
-
below follows those the same way
|
|
80
|
-
|
|
81
|
-
Every popup follows
|
|
82
|
-
(`Picker`, `Select`…)
|
|
83
|
-
than the app it never binds
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
is also the knob a single popup uses to ask for a specific size, not a
|
|
102
|
-
ceiling:
|
|
103
|
-
`--navi-app-max-width` feeds `--dialog-maxmax-width`, the hard ceiling _under_
|
|
104
|
-
that knob, so a popup that genuinely needs its own `maxWidth` can still say
|
|
105
|
-
so without escaping the app's screen.
|
|
106
|
-
|
|
107
|
-
An app can also get all of it by rendering itself in an iframe of the target
|
|
108
|
-
width: the viewport then genuinely _is_ the app's screen and no token is needed.
|
|
109
|
-
`--navi-app-max-width` is the answer for an app that does not want to pay that
|
|
110
|
-
price.
|
|
111
|
-
|
|
112
|
-
#### Placement follows the same rectangle
|
|
113
|
-
|
|
114
|
-
The app's rectangle moves where a popup is placed, not only how big it may
|
|
115
|
-
get. Placement is computed against the visual viewport narrowed to the level-1
|
|
116
|
-
bands: `getAppInsets` (`src/layout/responsive.js`) reads `--navi-app-inset-*`
|
|
117
|
-
back off the computed style — the bands `--navi-app-max-width` centers and the
|
|
118
|
-
ones an app writes by hand alike, in any length unit — navi hands them to
|
|
119
|
-
`@jsenv/dom` once (`setPlacementViewportInsets`, wired in
|
|
120
|
-
`navi_css_vars.js`), and `pickPositionRelativeTo` reads them on every
|
|
121
|
-
placement. With centered bands it is invisible for anything centered on its
|
|
122
|
-
cross axis — `center`, `bottom`, `top`, which is what a dialog does nearly
|
|
123
|
-
always — and with uneven ones it is what centers that dialog on the app column
|
|
124
|
-
rather than on the window. Either way it is what puts anything anchored to an
|
|
125
|
-
edge (a `positionArea` like `bottom-start`, a `SidePanel`) flush against the
|
|
126
|
-
app column's edge rather than the window's.
|
|
127
|
-
`FixedBar` reads the same description through CSS instead: it is pinned to
|
|
128
|
-
`--navi-app-inset-*`, which says where the app's rectangle is in the window
|
|
129
|
-
rather than how wide it may be.
|
|
78
|
+
below follows those the same way.
|
|
79
|
+
|
|
80
|
+
Every popup follows — `Dialog`, `Popover`, and everything built on them
|
|
81
|
+
(`Picker`, `Select`…) — as a ceiling and nothing more: on a screen narrower
|
|
82
|
+
than the app it never binds. A `Dialog` keeps its `marginWithContainer` inside
|
|
83
|
+
that screen, and its default gap is a share of the app's screen, not of the
|
|
84
|
+
window (see
|
|
85
|
+
[dialog_shape.md](./dialog_shape.md#marginwithcontainer-decides-the-gap-and-the-ceiling));
|
|
86
|
+
a `Popover` is capped at 95% of it. Placement uses the same rectangle: a dialog
|
|
87
|
+
centers on the app column, and anything anchored to an edge (a `positionArea`
|
|
88
|
+
like `bottom-start`, a `SidePanel`) sits flush with the column's edge rather
|
|
89
|
+
than the window's.
|
|
90
|
+
|
|
91
|
+
Two ways NOT to get this: empty `FixedBar area="left"/"right"` reserve the
|
|
92
|
+
room, but the app's rectangle stays the whole window and popups keep sizing
|
|
93
|
+
themselves against it; and `--dialog-max-width` set from the app is a
|
|
94
|
+
`--component-*` token every dialog resets on itself (a `maxWidth` prop writes it
|
|
95
|
+
inline), so the cap silently disappears — see
|
|
96
|
+
[css_architecture.md](./css_architecture.md#--navi--vs---component--where-the-override-has-to-go).
|
|
97
|
+
`--navi-app-max-width` feeds the hard ceiling _under_ that knob, so a popup that
|
|
98
|
+
genuinely needs its own `maxWidth` can still say so without escaping the app's
|
|
99
|
+
screen.
|
|
130
100
|
|
|
131
101
|
### Something that scrolls under the furniture
|
|
132
102
|
|
|
@@ -148,16 +118,15 @@ gets the `scroll-padding` unconditionally, since the document is the scrollport
|
|
|
148
118
|
in the common case — and, while nothing is marked, the keyboard's room at its
|
|
149
119
|
end (see below).
|
|
150
120
|
|
|
151
|
-
Beware of making that container
|
|
152
|
-
[
|
|
121
|
+
Beware of making that container a scroller by accident — see
|
|
122
|
+
[A document wider than the screen](#a-document-wider-than-the-screen).
|
|
153
123
|
|
|
154
124
|
### Reading it yourself
|
|
155
125
|
|
|
156
126
|
`var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
|
|
157
127
|
or not the app ever mounts a bar.
|
|
158
128
|
|
|
159
|
-
From **JS**, both levels are registered as lengths (`@property
|
|
160
|
-
`safe_area.js`), so
|
|
129
|
+
From **JS**, both levels are registered as lengths (`@property`), so
|
|
161
130
|
`getComputedStyle(document.documentElement).getPropertyValue("--navi-safe-area-inset-bottom")`
|
|
162
131
|
gives pixels, and so does `--navi-app-inset-*`.
|
|
163
132
|
|
|
@@ -165,10 +134,7 @@ gives pixels, and so does `--navi-app-inset-*`.
|
|
|
165
134
|
|
|
166
135
|
Level 2 has one slot per edge, `--navi-fixed-bar-space-*`, and it belongs to
|
|
167
136
|
`FixedBar`: each bar measures its own border box (notch included), and navi
|
|
168
|
-
writes the largest one on that edge inline on `<html
|
|
169
|
-
(`src/layout/fixed_bar/fixed_bar_space.js`). The slot is combined with
|
|
170
|
-
`env(safe-area-inset-*)` through `max()`, not added to it, since a bar measured
|
|
171
|
-
with its notch already covers the notch.
|
|
137
|
+
writes the largest one on that edge inline on `<html>`.
|
|
172
138
|
|
|
173
139
|
Something else taking an edge — a native banner, an OS strip — is published by
|
|
174
140
|
being drawn as a `FixedBar`; every component reading the safe area then clears
|
|
@@ -176,19 +142,6 @@ it without learning it exists. Writing the slot by hand holds only while no bar
|
|
|
176
142
|
takes that edge: the first bar mounting there replaces the value rather than
|
|
177
143
|
adding to it.
|
|
178
144
|
|
|
179
|
-
## What already reads it
|
|
180
|
-
|
|
181
|
-
Pointers, not a list to keep in sync — grep `--navi-safe-area-inset` for the
|
|
182
|
-
truth:
|
|
183
|
-
|
|
184
|
-
- `FixedBar` pins itself to `--navi-app-inset-*`.
|
|
185
|
-
- `List` offsets its sticky group labels when `scroller="document"`, so a label
|
|
186
|
-
comes to rest in front of the bar and not behind it.
|
|
187
|
-
- `RouteTravel` clips the pictures of a travel to the safe area. It has to: a
|
|
188
|
-
view transition paints in the top layer, where no `overflow` of the document
|
|
189
|
-
reaches it, and the box pages travel in runs _under_ the bars by design — so
|
|
190
|
-
a page scrolled by one pixel would be watched painting over them.
|
|
191
|
-
|
|
192
145
|
## The trap: which viewport
|
|
193
146
|
|
|
194
147
|
Three heights are in play and they are not the same one — and which of them
|
|
@@ -203,24 +156,20 @@ the keyboard moves depends on the browser.
|
|
|
203
156
|
By default the keyboard shrinks the visual viewport, on every browser, and
|
|
204
157
|
`--navi-keyboard-inset-bottom` stays 0. Where the browser has the VirtualKeyboard
|
|
205
158
|
API (Chromium), an app can call `enableVirtualKeyboardOverlay()` to make the
|
|
206
|
-
keyboard overlay the page instead
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
visible.
|
|
159
|
+
keyboard overlay the page instead: no viewport shrinks, and the keyboard arrives
|
|
160
|
+
as `--navi-keyboard-inset-bottom` (`env(keyboard-inset-height)`), which
|
|
161
|
+
`--navi-app-inset-bottom` adds. Either way `--navi-app-height` and the popup
|
|
162
|
+
ceilings answer the part of the screen left visible.
|
|
211
163
|
|
|
212
164
|
An overlaying keyboard is one the browser no longer scrolls the focused field
|
|
213
165
|
out from under, so navi does: when the keyboard rises or resizes, and when focus
|
|
214
166
|
moves to another field with the keyboard up, a focused field that is not fully
|
|
215
|
-
inside the document's `scroll-padding` box is centered in it.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
element's `padding-bottom`, or, on a page that marked nothing, from a block navi
|
|
222
|
-
adds at the end of the document while the keyboard is up — both count the
|
|
223
|
-
keyboard and the allowance.
|
|
167
|
+
inside the document's `scroll-padding` box is centered in it.
|
|
168
|
+
`scroll-padding-bottom` also adds `--navi-keyboard-strip-allowance`, for the
|
|
169
|
+
suggestion strip Chrome paints above the keyboard and no inset counts. The room
|
|
170
|
+
to scroll into comes from the marked element's `padding-bottom`, or, on a page
|
|
171
|
+
that marked nothing, from a block navi adds at the end of the document while
|
|
172
|
+
the keyboard is up.
|
|
224
173
|
|
|
225
174
|
`position: fixed` — so every `FixedBar` — is laid out against the **layout**
|
|
226
175
|
viewport. Where the visual viewport is what shrinks, a bottom bar therefore
|
|
@@ -238,3 +187,67 @@ inset) because what navi _sizes_ must fit what is actually visible.
|
|
|
238
187
|
`src/layout/demos/fixed_bar/keyboard.html` puts all of these on screen at once
|
|
239
188
|
and turns the bottom bar's number red when it goes under the keyboard. On a
|
|
240
189
|
phone; a desktop has no keyboard to open.
|
|
190
|
+
|
|
191
|
+
## A document wider than the screen
|
|
192
|
+
|
|
193
|
+
On Chrome Android, **anything that makes the document overflow horizontally
|
|
194
|
+
inflates the layout viewport** to the size of the content: a 2000px-wide grid
|
|
195
|
+
on a 412px phone takes `window.innerWidth` and `innerHeight` to about four times
|
|
196
|
+
the screen, while `visualViewport.width` still says 412. The document grows a
|
|
197
|
+
large empty area below the real content, and what is centered in the layout
|
|
198
|
+
viewport — a `<dialog>`'s `position: fixed; margin: auto` — lands far below the
|
|
199
|
+
visible area, miscentered or out of sight.
|
|
200
|
+
|
|
201
|
+
So the document itself never overflows in x. Wrap the app in a box that clips,
|
|
202
|
+
and clip `html` and `body` as a net for anything that escapes the wrapper:
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<body>
|
|
206
|
+
<div style="overflow-x: clip">
|
|
207
|
+
<!-- all app content goes here -->
|
|
208
|
+
</div>
|
|
209
|
+
</body>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```css
|
|
213
|
+
html,
|
|
214
|
+
body {
|
|
215
|
+
overflow-x: clip;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
What it costs: content wider than the screen is cut off instead of reachable by
|
|
220
|
+
dragging. An element that genuinely needs to scroll horizontally (a wide table,
|
|
221
|
+
a carousel) gets its own `overflow-x: auto` — the wrapper stays `clip`.
|
|
222
|
+
|
|
223
|
+
**`clip`, never `auto`, `hidden` or `scroll`.** `clip` is the only value that
|
|
224
|
+
clips without turning the box into a scroll container (and it leaves
|
|
225
|
+
`overflow-y` at `visible`, where the others force it to `auto`). A wrapper that
|
|
226
|
+
scrolls, even by accident, takes every `position: sticky` in the app with it:
|
|
227
|
+
sticky resolves against the nearest scroll container, and this one grows with
|
|
228
|
+
its content and never scrolls, so sticky headers and `<List groupBy>` labels
|
|
229
|
+
scroll away with the content. Worse, a sticky element sticks within its scroll
|
|
230
|
+
container's box shrunk by that container's `scroll-padding`: a wrapper carrying
|
|
231
|
+
`data-navi-safe-area` has `scroll-padding-top: var(--navi-safe-area-inset-top)`,
|
|
232
|
+
so the labels come to rest that far below the bar, covering the content above
|
|
233
|
+
them.
|
|
234
|
+
|
|
235
|
+
**The wrapper is a net, not a fix.** Clipping makes the symptom disappear, and
|
|
236
|
+
with it the signal: something wider than the screen — a width in px, a
|
|
237
|
+
`min-width`, a grid of fixed columns, an unbreakable string coming from the
|
|
238
|
+
data — is still a layout bug. In dev, ask who overflows:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
import { detectHorizontalOverflow } from "@jsenv/navi";
|
|
242
|
+
|
|
243
|
+
if (import.meta.dev) {
|
|
244
|
+
detectHorizontalOverflow({ root: document.querySelector("#main") });
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
It outlines the culprits in red and names them in the console, at load and
|
|
249
|
+
whenever the layout changes. It reports the **outermost** box that sticks out
|
|
250
|
+
(its children stick out because it does), and stays quiet about what cannot
|
|
251
|
+
reach the document: anything inside a box that scrolls or clips on its own — a
|
|
252
|
+
wide table in its own `overflow-x: auto` container is doing the right thing —
|
|
253
|
+
and anything `position: fixed` or in the top layer.
|