@jsenv/navi 0.29.417 → 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 +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- 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 +291 -454
- package/docs/drag_to_travel.md +152 -748
- 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 +170 -326
- 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 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- 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/control_group.md
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Grouping controls (`<Group>`)
|
|
2
|
-
|
|
3
|
-
What we want: **several controls reading as one object.** A search input and
|
|
4
|
-
its button, a row of segmented buttons, a column of setting rows — when they
|
|
5
|
-
belong together, the eye should see one frame with divisions inside it, not
|
|
6
|
-
three boxes that happen to touch. Concretely that means: one border along each
|
|
7
|
-
seam instead of two side by side, and a radius only on the four outer corners.
|
|
8
|
-
|
|
9
|
-
`<Group>` is what produces that. Reach for it whenever you place controls
|
|
10
|
-
against each other and any of them has a border or a radius — never hand-write
|
|
11
|
-
negative margins, `border-radius: 0` overrides, or a `borderRadius` prop set to
|
|
12
|
-
`0` on the middle members. Those spellings look right on the case you are
|
|
13
|
-
looking at and break on the next one: a member added at the end, a member
|
|
14
|
-
hidden by a condition (the "first" is then the second child), a switch from row
|
|
15
|
-
to column.
|
|
16
|
-
|
|
17
|
-
```jsx
|
|
18
|
-
<Group>
|
|
19
|
-
<Input name="search" placeholder="Search…" />
|
|
20
|
-
<Button>Go</Button>
|
|
21
|
-
</Group>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Stacked, one setting per row:
|
|
25
|
-
|
|
26
|
-
```jsx
|
|
27
|
-
<Group row>
|
|
28
|
-
<Picker name="side" ui={…} expandX />
|
|
29
|
-
<Picker name="level" ui={…} expandX />
|
|
30
|
-
<Picker name="city" ui={…} expandX />
|
|
31
|
-
</Group>
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
`row` (or `vertical`, the same prop — each member is a row, so they stack
|
|
35
|
-
vertically and meet along horizontal seams) is the only prop that changes the
|
|
36
|
-
arrangement; everything else is read off the members themselves. Say it rather
|
|
37
|
-
than `flex="y"`: the seams follow `row`, not the layout.
|
|
38
|
-
|
|
39
|
-
Live examples: `src/control/demos/15_group_demo.html`.
|
|
40
|
-
|
|
41
|
-
## What Group does to its members
|
|
42
|
-
|
|
43
|
-
- **Seams**: a member with a member before it is pulled back by one border
|
|
44
|
-
width, so the two borders along a seam become one line. The width comes from
|
|
45
|
-
the member's own `--border-width` when it declares one, else from the group's
|
|
46
|
-
`--group-border-width` (default: `--navi-control-border-width`).
|
|
47
|
-
- **Corners**: a member loses the radius on each side where it faces another
|
|
48
|
-
member — the first one on the side that joins, the last one on the other
|
|
49
|
-
side, one in between on all four. A single member keeps its own radius — a
|
|
50
|
-
group of one looks like the control alone. The ask is made twice, in two
|
|
51
|
-
forms — see below.
|
|
52
|
-
- **Members only**: a child out of the group's flow is none of the group's
|
|
53
|
-
business, and there are more of those than it looks. A popup renders inside
|
|
54
|
-
its opener's own subtree, so a `Dialog` or `Popover` written next to the
|
|
55
|
-
button that opens it is a child of the group itself, and brings its backdrop
|
|
56
|
-
along; a callout anchored on a `<button>` is mounted in that button's parent.
|
|
57
|
-
They mark themselves `navi-out-of-flow`, and the group reads that: none of
|
|
58
|
-
them takes a corner of its own, none moves anyone else's (a row of one button
|
|
59
|
-
and the dialog it opens is still a group of one), none is pulled back by a
|
|
60
|
-
border width it does not share.
|
|
61
|
-
- **Overlap order**: the member under the pointer, the member showing a focus
|
|
62
|
-
ring, and the member holding something open (`aria-expanded="true"`) paint
|
|
63
|
-
above their neighbours (`position: relative` plus
|
|
64
|
-
`--navi-z-index-control-hovered` / `-focused` / `-expanded`). Without it the
|
|
65
|
-
border color change and the focus ring of the active member would be sliced
|
|
66
|
-
by whichever neighbour is painted after it. The focused member is matched
|
|
67
|
-
whether it wears `data-focus-visible` itself or merely contains it — a
|
|
68
|
-
control that wraps a real input (`Picker`, `Spin`) draws the ring on its own
|
|
69
|
-
frame while the keyboard is held inside; the expanded member is matched the
|
|
70
|
-
same way, since it can be wrapped in an enrobage. Expanded ranks highest: a
|
|
71
|
-
`Picker` opened by a click shows no focus ring, hands the focus to its popup
|
|
72
|
-
and lets the pointer travel to a neighbour, yet its border keeps saying it is
|
|
73
|
-
the one open. There is deliberately no `isolation: isolate` — see
|
|
74
|
-
[z_index.md](./z_index.md).
|
|
75
|
-
|
|
76
|
-
Nothing else: a group does not restyle its members, does not impose a size,
|
|
77
|
-
and takes any `Box` prop for its own layout.
|
|
78
|
-
|
|
79
|
-
## Writing a control that belongs in a group
|
|
80
|
-
|
|
81
|
-
A group never writes a selector that reaches inside a member — a member's
|
|
82
|
-
subtree holds more than the member (a `Picker` renders its popup inside
|
|
83
|
-
itself, not in a portal; a control carries buttons of its own, like the clear
|
|
84
|
-
cross in a slot), and a rule matching "some descendant" finds all of them. It
|
|
85
|
-
asks for a square corner in two forms instead, and a control answers with
|
|
86
|
-
whichever fits.
|
|
87
|
-
|
|
88
|
-
**The property, on the member itself.** A control declares the radius of its
|
|
89
|
-
frame on its own root, and whatever inner element paints that frame takes
|
|
90
|
-
`border-radius: inherit`. The group sets the property on the root; the frame
|
|
91
|
-
follows.
|
|
92
|
-
|
|
93
|
-
```css
|
|
94
|
-
.navi_thing {
|
|
95
|
-
/* Declared here even though the box below is what draws it */
|
|
96
|
-
border-radius: var(--thing-border-radius);
|
|
97
|
-
|
|
98
|
-
.navi_thing_box {
|
|
99
|
-
border: ...;
|
|
100
|
-
border-radius: inherit;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**The custom property, which travels.** A member is not always the control
|
|
106
|
-
that carries the frame: a button can arrive wrapped in a tooltip or a link, at
|
|
107
|
-
any depth. So the group also sets `--x-corner-top-left-radius` and its three
|
|
108
|
-
siblings on the member, and a control that can arrive wrapped reads them as an
|
|
109
|
-
override of its own radius. The `--x-` prefix says what they are: navi's
|
|
110
|
-
internal wiring between a group and its members, not a surface an app writes
|
|
111
|
-
to (an app changes a radius with the `borderRadius` prop, which lands in the
|
|
112
|
-
fallback below and is what the corner keeps everywhere the group has no claim):
|
|
113
|
-
|
|
114
|
-
```css
|
|
115
|
-
.navi_thing {
|
|
116
|
-
border-top-left-radius: var(
|
|
117
|
-
--x-corner-top-left-radius,
|
|
118
|
-
var(--thing-border-radius)
|
|
119
|
-
);
|
|
120
|
-
/* …and the three others */
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Whoever answers the ask also stops it.** Custom properties inherit all the
|
|
125
|
-
way down, so the control that consumed a corner sets the four back to
|
|
126
|
-
`initial` on the first element inside it — otherwise a button in a slot, or the
|
|
127
|
-
Save button of a form in an open popup, reads a corner meant for the row that
|
|
128
|
-
opened it. `Popover` and `Dialog` stop it too, at their own root: nothing a
|
|
129
|
-
popup holds is at a seam.
|
|
130
|
-
|
|
131
|
-
Reference: `.navi_button_content` in `button_ui.jsx` (a button reads then
|
|
132
|
-
stops), `.navi_picker_box`, `.navi_input_slot`, `.navi_popover`,
|
|
133
|
-
`.navi_dialog`.
|
|
134
|
-
|
|
135
|
-
**A member drawing the frame itself stops it too**, without being a control. A
|
|
136
|
-
row of settings — a `Field`, a `Label`, a `Box` — that paints a background or a
|
|
137
|
-
border, or that insets what it holds with padding, is what the outer corner
|
|
138
|
-
belongs to; the switch at the right end of that row is not on it. `Box` marks
|
|
139
|
-
itself with `navi-box-frame` for exactly those props and stops the claims for
|
|
140
|
-
everything inside. A wrapper adding neither is only an enrobage — a tooltip, a
|
|
141
|
-
link, a bare `<Box>` — and lets them through to the control that really carries
|
|
142
|
-
the frame.
|
|
143
|
-
|
|
144
|
-
A control that declares its radius on an inner element instead is invisible to
|
|
145
|
-
`Group`: it keeps round corners in the middle of the row, and no rule written
|
|
146
|
-
in `group.jsx` can reach it without naming that private class — which is how a
|
|
147
|
-
layout component ends up knowing the internals of every control. When you meet
|
|
148
|
-
that symptom, fix the control, not the group.
|
|
149
|
-
|
|
150
|
-
The controls shipped by navi all follow this: `Button`, `Input`, `Select`,
|
|
151
|
-
`Picker`, `Spin`, checkbox, range.
|
|
152
|
-
|
|
153
|
-
## When it is not a Group
|
|
154
|
-
|
|
155
|
-
- Controls separated by space, each with its own frame — that is a `Box` with
|
|
156
|
-
`spacing`, they were never one object.
|
|
157
|
-
- A label and its control — that is `Field`.
|
|
158
|
-
- Several controls making ONE value between them (an address out of three
|
|
159
|
-
fields, a day and two wheels out of one moment) — that is `ControlGroup`,
|
|
160
|
-
which is about the value and draws nothing. Same word, other subject: see
|
|
161
|
-
[control_object.md](./control_object.md). Both at once is fine — a `Group`
|
|
162
|
-
around the members of a `ControlGroup`.
|
|
163
|
-
- Radio buttons or checkboxes sharing a name and a validation — that is
|
|
164
|
-
`RadioGroup` / `CheckboxGroup`, which is about the value, not the frame. They
|
|
165
|
-
can be put inside a `Group` if you also want them to share a frame.
|
|
166
|
-
- Controls that are ONE value between them, with a word written inside the frame
|
|
167
|
-
(the hours and the minutes of "07h30") — that is `SpinGroup`, which drops the
|
|
168
|
-
members' frames rather than joining them.
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# Mobile layout pitfalls
|
|
2
|
-
|
|
3
|
-
## Horizontal overflow on mobile breaks `<dialog>` positioning
|
|
4
|
-
|
|
5
|
-
### What happens
|
|
6
|
-
|
|
7
|
-
On Chrome Android, if **any element causes the document to overflow horizontally**,
|
|
8
|
-
the browser inflates the layout viewport (`window.innerWidth/innerHeight`) to match
|
|
9
|
-
the full content dimensions instead of the visible screen dimensions.
|
|
10
|
-
|
|
11
|
-
Example on a 412×915px phone with a 2000px-wide grid:
|
|
12
|
-
|
|
13
|
-
- `window.innerWidth` → `1648` (inflated, ~4× the real width)
|
|
14
|
-
- `window.innerHeight` → `3660` (inflated, ~4× the real height)
|
|
15
|
-
- `window.visualViewport.width` → `412` (the actual screen)
|
|
16
|
-
|
|
17
|
-
**This creates a large invisible empty area below the real content** — the browser
|
|
18
|
-
has "expanded" the document to a size that doesn't correspond to anything visible.
|
|
19
|
-
|
|
20
|
-
### Why `<dialog>` breaks
|
|
21
|
-
|
|
22
|
-
`position: fixed; margin: auto` is supposed to center an element in the viewport.
|
|
23
|
-
But for obscure reasons, Chrome Android centers it relative to the inflated **layout
|
|
24
|
-
viewport** instead of the visual viewport.
|
|
25
|
-
|
|
26
|
-
With `innerHeight: 3660`, `margin: auto` places the dialog at ~1830px from the top —
|
|
27
|
-
the user scrolls down and finds the dialog sitting far below the visible area, miscentered
|
|
28
|
-
or completely invisible.
|
|
29
|
-
|
|
30
|
-
**To make things worse**, `showModal()` internally calls `.focus()` on the dialog element,
|
|
31
|
-
which triggers the browser's native `scrollIntoView`. So after opening, the page automatically
|
|
32
|
-
scrolls to bring the dialog into view — scrolling the user away from where they were.
|
|
33
|
-
|
|
34
|
-
### The fix: clip the horizontal overflow in a wrapper
|
|
35
|
-
|
|
36
|
-
The root cause is the document overflowing horizontally. The fix is to **never let the
|
|
37
|
-
document itself overflow in X** — instead, contain the overflow inside a child wrapper.
|
|
38
|
-
|
|
39
|
-
```html
|
|
40
|
-
<body>
|
|
41
|
-
<!-- This wrapper absorbs the horizontal overflow of the whole app -->
|
|
42
|
-
<div style="overflow-x: clip;">
|
|
43
|
-
<!-- all app content goes here -->
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<!-- Dialog is outside the wrapper, directly in body, placed first -->
|
|
47
|
-
<dialog>...</dialog>
|
|
48
|
-
</body>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
With this structure:
|
|
52
|
-
|
|
53
|
-
- The document never overflows horizontally → layout viewport = visual viewport
|
|
54
|
-
- `position: fixed; margin: auto` centers the dialog correctly
|
|
55
|
-
- No ghost empty space at the bottom
|
|
56
|
-
|
|
57
|
-
As a safety net, add `overflow-x: clip` on `html` and `body` to prevent any content
|
|
58
|
-
that forgets to use a wrapper from inflating the layout viewport:
|
|
59
|
-
|
|
60
|
-
```css
|
|
61
|
-
html,
|
|
62
|
-
body {
|
|
63
|
-
overflow-x: clip;
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
What it costs: content wider than the screen is cut off instead of reachable by dragging.
|
|
68
|
-
When some element genuinely needs to be scrolled horizontally (a wide table, a carousel),
|
|
69
|
-
give **that element** its own `overflow-x: auto` — the wrapper stays `clip`.
|
|
70
|
-
|
|
71
|
-
### Why `clip` and not `auto` or `hidden`
|
|
72
|
-
|
|
73
|
-
`clip` is the only value that clips without turning the box into a **scroll container**.
|
|
74
|
-
`auto`, `scroll` and `hidden` all create one, and that has two consequences that show up
|
|
75
|
-
far from the wrapper:
|
|
76
|
-
|
|
77
|
-
- **Every `position: sticky` in the app sticks to that wrapper**, because sticky resolves
|
|
78
|
-
against the nearest scroll container in the DOM — not against the box the app considers
|
|
79
|
-
its scroller. The wrapper grows with its content and never scrolls, so nothing sticks
|
|
80
|
-
anymore: sticky headers and `<List groupBy>` group labels just scroll away with the
|
|
81
|
-
content.
|
|
82
|
-
- **Worse than not sticking: the sticky element is offset downwards.** The rectangle a
|
|
83
|
-
sticky element sticks within is the scroll container's box shrunk by its
|
|
84
|
-
`scroll-padding` (CSS Position L3), and Chromium applies that for an element scroll
|
|
85
|
-
container. A wrapper carrying `data-navi-safe-area` has
|
|
86
|
-
`scroll-padding-top: var(--navi-safe-area-inset-top)`, so labels come to rest at
|
|
87
|
-
`scroll-padding-top + top` — a group label floating a hundred pixels below the bar,
|
|
88
|
-
covering the content above it.
|
|
89
|
-
|
|
90
|
-
`overflow-x: clip` also lets `overflow-y` stay `visible`, where `hidden`/`auto` force the
|
|
91
|
-
other axis to `auto`.
|
|
92
|
-
|
|
93
|
-
Note: `overflow: hidden` on `<html>` would additionally create a new containing block,
|
|
94
|
-
which could break `position: fixed` in edge cases. A dialog in the top layer is out of
|
|
95
|
-
reach of that: the top layer takes it out of every ancestor's containing block, which is
|
|
96
|
-
why navi's `Dialog` stays where it is written, in its opener's subtree.
|
|
97
|
-
|
|
98
|
-
### The wrapper is a net, not a fix: find what overflows
|
|
99
|
-
|
|
100
|
-
Clipping makes the symptom disappear, and with it the signal. Something wider than
|
|
101
|
-
the screen is a layout bug wherever it happens — a width in px, a `min-width`, a grid
|
|
102
|
-
of fixed columns, an unbreakable string coming from the data. The wrapper only keeps
|
|
103
|
-
that bug from taking the whole mobile viewport down with it.
|
|
104
|
-
|
|
105
|
-
So in dev, ask who overflows:
|
|
106
|
-
|
|
107
|
-
```js
|
|
108
|
-
import { detectHorizontalOverflow } from "@jsenv/navi";
|
|
109
|
-
|
|
110
|
-
if (import.meta.dev) {
|
|
111
|
-
detectHorizontalOverflow({ root: document.querySelector("#main") });
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
It outlines the culprits in red and names them in the console, at load and whenever
|
|
116
|
-
the layout changes. It reports the **outermost** box that sticks out (its children
|
|
117
|
-
stick out because it does), and stays quiet about what cannot reach the document:
|
|
118
|
-
anything inside a box that scrolls or clips on its own — a wide table in its own
|
|
119
|
-
`overflow-x: auto` container is doing the right thing — and anything `position: fixed`
|
|
120
|
-
or in the top layer.
|
|
121
|
-
|
|
122
|
-
Measuring against the wrapper matters here: once it is in `clip` there is no scrollable
|
|
123
|
-
overflow left to read, so `scrollWidth > clientWidth` reports nothing. The rectangles of
|
|
124
|
-
the descendants are what tells.
|
|
125
|
-
|
|
126
|
-
### Also: place `<dialog>` before content in the DOM
|
|
127
|
-
|
|
128
|
-
Even with the viewport fix, `showModal()` triggers `scrollIntoView` on the dialog element.
|
|
129
|
-
If the `<dialog>` is placed at the end of the DOM (after all the app content), the page
|
|
130
|
-
scrolls down to it when it opens.
|
|
131
|
-
|
|
132
|
-
Fix: **place `<dialog>` as the first child of `<body>`**, before any scrollable content.
|
|
133
|
-
`scrollIntoView` on an element already at the top of the document has no effect.
|
|
134
|
-
|
|
135
|
-
This is advice for a `<dialog>` written by hand. navi's `Dialog` is not moved: it
|
|
136
|
-
renders in its opener's subtree (see
|
|
137
|
-
[css_architecture.md](./css_architecture.md#counting-children-what-navi-puts-in-your-tree)).
|
|
138
|
-
|
|
139
|
-
### Summary
|
|
140
|
-
|
|
141
|
-
| Cause | Symptom | Fix |
|
|
142
|
-
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
143
|
-
| Document overflows horizontally | Layout viewport inflated → ghost space below → dialog miscentered | Wrap app content in `overflow-x: clip` container |
|
|
144
|
-
| `<dialog>` at end of DOM | Page scrolls to dialog on `showModal()` | Place `<dialog>` first in `<body>` |
|
|
145
|
-
| Wrapper uses `auto`/`hidden` | It becomes a scroll container: sticky headers and group labels stick to it (and get offset by its `scroll-padding`) | Use `clip`; put `overflow-x: auto` on the wide element itself |
|