@jsenv/navi 0.29.74 → 0.29.76
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 +406 -61
- package/dist/jsenv_navi.js.map +18 -14
- package/docs/AI_INSTRUCTIONS.md +11 -0
- package/docs/dialog_shape.md +223 -0
- package/docs/z_index.md +35 -9
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -99,6 +99,17 @@ consistency across the app, not from any single call site.
|
|
|
99
99
|
before hand-writing an offset to clear a `FixedBar`, before reaching for
|
|
100
100
|
`env(safe-area-inset-*)` directly, and before making an app simulate a phone
|
|
101
101
|
screen.
|
|
102
|
+
- `docs/dialog_shape.md` — where a `Dialog` sits and how big it gets: bounds
|
|
103
|
+
rather than a width, the container ceiling no prop can exceed, the two shapes
|
|
104
|
+
one dialog has (centered box vs. bottom sheet under
|
|
105
|
+
`dockedOnSmallTouchScreen`) and which bounds apply to which — `maxWidth`
|
|
106
|
+
describes the centered shape and docking withdraws it, so both can be stated
|
|
107
|
+
at once. Also `expandX`/`expandY` vs. docking, `marginWithContainer` deciding
|
|
108
|
+
the gap and the ceiling together, `sizing="frozen"`, `layer="local"`, and the
|
|
109
|
+
`dialog*` props a `Picker`/`SplitButton` forwards. Read it before deriving
|
|
110
|
+
`smallTouchScreenSignal` in an app to change a dialog's size, before passing
|
|
111
|
+
`expandX={false}` to stop a dialog sprawling, and before writing CSS to make a
|
|
112
|
+
dialog fit the screen.
|
|
102
113
|
- `docs/scroll.md` — where scrolling happens: what turns `Box`
|
|
103
114
|
`header`/`body`/`footer` on, `FixedBar` space, `List`'s `scroller`, scroll
|
|
104
115
|
inside a `Dialog`/`Popover`, and what a scroll does to hover
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# The shape of a dialog
|
|
2
|
+
|
|
3
|
+
Where a `Dialog` sits and how big it gets. What happens _inside_ it (scrolling,
|
|
4
|
+
`header`/`body`/`footer`) is [`scroll.md`](./scroll.md); what opens it is
|
|
5
|
+
[`popup_open.md`](./popup_open.md).
|
|
6
|
+
|
|
7
|
+
- [A dialog is sized by its content, never by a `width`](#a-dialog-is-sized-by-its-content-never-by-a-width)
|
|
8
|
+
- [The ceiling nobody sets](#the-ceiling-nobody-sets)
|
|
9
|
+
- [One dialog, two shapes](#one-dialog-two-shapes)
|
|
10
|
+
- [Saying the two shapes at once](#saying-the-two-shapes-at-once)
|
|
11
|
+
- [`expand` is not "docked", and `expandX={false}` is not "do not sprawl"](#expand-is-not-docked-and-expandxfalse-is-not-do-not-sprawl)
|
|
12
|
+
- [`marginWithContainer` decides the gap AND the ceiling](#marginwithcontainer-decides-the-gap-and-the-ceiling)
|
|
13
|
+
- [Holding a size while the dialog is open](#holding-a-size-while-the-dialog-is-open)
|
|
14
|
+
- [A `layer="local"` dialog answers to its container](#a-layerlocal-dialog-answers-to-its-container)
|
|
15
|
+
- [Reaching all of this through a `Picker` or a `SplitButton`](#reaching-all-of-this-through-a-picker-or-a-splitbutton)
|
|
16
|
+
|
|
17
|
+
## A dialog is sized by its content, never by a `width`
|
|
18
|
+
|
|
19
|
+
There is no `width` prop, and that is the whole design: a dialog is a surface
|
|
20
|
+
laid over the app, and what it holds is what knows how wide it should be. What
|
|
21
|
+
a caller states are **bounds** — a floor under a dialog too narrow for its
|
|
22
|
+
content, a ceiling over one that would sprawl:
|
|
23
|
+
|
|
24
|
+
| what you want to say | how |
|
|
25
|
+
| --------------------------------------- | ------------------------- |
|
|
26
|
+
| "not narrower than this" | `minWidth` / `minHeight` |
|
|
27
|
+
| "not wider than this" | `maxWidth` / `maxHeight` |
|
|
28
|
+
| "as wide as the container allows" | `expandX` / `expandY` |
|
|
29
|
+
| "as wide as the control that opened it" | `sizeFromAnchor` (opt-in) |
|
|
30
|
+
|
|
31
|
+
`sizeFromAnchor` is off by default and that is deliberate: unlike a `Popover`,
|
|
32
|
+
a dialog is not attached to what opened it, so following that element's box is
|
|
33
|
+
a choice, not the norm. It only ever sets a **floor**
|
|
34
|
+
(`--anchor-width`/`--anchor-height`), never a width.
|
|
35
|
+
|
|
36
|
+
## The ceiling nobody sets
|
|
37
|
+
|
|
38
|
+
Above every bound a caller passes there is one navi always applies: the
|
|
39
|
+
container, minus `marginWithContainer` on both sides
|
|
40
|
+
(`--dialog-maxmax-width` / `--dialog-maxmax-height`). It is not a default that
|
|
41
|
+
a larger `maxWidth` overrides — it wins, always. A `minWidth` too large for the
|
|
42
|
+
screen is clamped by it too, so **no combination of props can produce a dialog
|
|
43
|
+
that overflows its container.** Stop trying to defend against that case.
|
|
44
|
+
|
|
45
|
+
Two consequences worth knowing before writing CSS of your own:
|
|
46
|
+
|
|
47
|
+
- the caps are applied to the dialog's **size**, not merely to its position.
|
|
48
|
+
That is what makes a centered dialog follow the mobile virtual keyboard for
|
|
49
|
+
free: the ceiling is expressed against the app's live screen
|
|
50
|
+
(`--navi-app-width`/`--navi-app-height`, which track the visual viewport), so
|
|
51
|
+
the browser reflows the dialog as the keyboard opens. Nothing to wire.
|
|
52
|
+
- "the container" is the **app's own screen** for `layer="top"` — the visual
|
|
53
|
+
viewport, or the narrower one the app declared with `--navi-app-max-width`
|
|
54
|
+
(see [`safe_area.md`](./safe_area.md)) — and the positioned ancestor for
|
|
55
|
+
`layer="local"`.
|
|
56
|
+
|
|
57
|
+
## One dialog, two shapes
|
|
58
|
+
|
|
59
|
+
`dockedOnSmallTouchScreen` is the whole small-screen story in one prop: on a
|
|
60
|
+
small touch screen the dialog stops being a centered box and becomes a bottom
|
|
61
|
+
sheet; everywhere else nothing changes.
|
|
62
|
+
|
|
63
|
+
Both halves of the name matter. Touch alone would dock a tablet or a kiosk
|
|
64
|
+
panel — a whole screen away from where the finger just tapped. Size alone would
|
|
65
|
+
dock a narrow desktop window, which is still a mouse. `smallTouchScreenSignal`
|
|
66
|
+
answers both, by shape rather than by a box of maximum dimensions (a phone is a
|
|
67
|
+
narrow slab, in either orientation), and it is **live**: turning the phone
|
|
68
|
+
re-resolves the dialog.
|
|
69
|
+
|
|
70
|
+
What docking supplies — defaults only, so any single axis of the sheet can be
|
|
71
|
+
adjusted without giving up the rest:
|
|
72
|
+
|
|
73
|
+
| supplied | value | why |
|
|
74
|
+
| --------------------- | ---------- | ----------------------------------------------------- |
|
|
75
|
+
| `positionArea` | `"bottom"` | where the thumbs rest and where the keyboard comes up |
|
|
76
|
+
| `marginWithContainer` | `0` | a sheet is flush, or it is not a sheet |
|
|
77
|
+
| `expandX` | `true` | container-wide, same reason |
|
|
78
|
+
| `scrollCapture` | `true` | a drag past the sheet's edge must not reach the page |
|
|
79
|
+
|
|
80
|
+
Plus a swipe-down-to-close, held by the sheet's `header` (a `Box` with the
|
|
81
|
+
`header` prop) and by anything carrying `data-swipe-grip` — never by the whole
|
|
82
|
+
sheet, so a board something is dragged across keeps its own gestures. See
|
|
83
|
+
[`drag_to_travel.md`](./drag_to_travel.md).
|
|
84
|
+
|
|
85
|
+
**`expandY` (or `expand`) cancels docking outright.** A dialog already filling
|
|
86
|
+
the height is on the bottom edge docking would bring it to; all docking could
|
|
87
|
+
still do is take away the shape the caller asked for, and arm a swipe-down on
|
|
88
|
+
something that never rose.
|
|
89
|
+
|
|
90
|
+
## Saying the two shapes at once
|
|
91
|
+
|
|
92
|
+
The sentence an app almost always wants is two sentences:
|
|
93
|
+
|
|
94
|
+
> Keep this dialog between 12 and 16rem so it does not sprawl on a wide window
|
|
95
|
+
> and does not collapse to its shortest line. **And when it is a bottom sheet,
|
|
96
|
+
> forget all that: a sheet is flush and full width.**
|
|
97
|
+
|
|
98
|
+
Both halves are written together, and each applies where it means something:
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
<Dialog dockedOnSmallTouchScreen minWidth="12rem" maxWidth="16rem">
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| prop | centered box | docked sheet |
|
|
105
|
+
| ----------- | ------------ | ----------------------------------------------------- |
|
|
106
|
+
| `maxWidth` | applies | **withdrawn** — the sheet is container-wide |
|
|
107
|
+
| `minWidth` | applies | stops mattering (the floor is below full width) |
|
|
108
|
+
| `maxHeight` | applies | applies — a sheet is content-tall, not container-tall |
|
|
109
|
+
| `minHeight` | applies | applies |
|
|
110
|
+
|
|
111
|
+
`maxWidth` is an answer about the _centered_ shape: "do not sprawl on a wide
|
|
112
|
+
window". A sheet spanning its container's full width, flush against the two
|
|
113
|
+
side edges, **is** the docked mode — so docking withdraws that ceiling rather
|
|
114
|
+
than capping the sheet with it. The container ceiling still holds, as always.
|
|
115
|
+
|
|
116
|
+
> **Trap: do not re-derive the docking condition in the app.** This looks like
|
|
117
|
+
> the way to say it and is subtly wrong:
|
|
118
|
+
>
|
|
119
|
+
> ```jsx
|
|
120
|
+
> // WRONG
|
|
121
|
+
> maxWidth={smallTouchScreenSignal.value ? undefined : "16rem"}
|
|
122
|
+
> ```
|
|
123
|
+
>
|
|
124
|
+
> Docking is not `smallTouchScreenSignal` — it is
|
|
125
|
+
> `dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY`. A
|
|
126
|
+
> dialog that also sets `expandY` never docks, so the cap must never be
|
|
127
|
+
> withdrawn there, and the line above withdraws it anyway. Beyond being wrong,
|
|
128
|
+
> it duplicates a condition navi owns at every call site (it drifts the day
|
|
129
|
+
> "docked" gains a rule), and it reads as a bug: it says nothing about bottom
|
|
130
|
+
> sheets to the next person. State both bounds plainly and let the dialog
|
|
131
|
+
> resolve its own shape.
|
|
132
|
+
|
|
133
|
+
## `expand` is not "docked", and `expandX={false}` is not "do not sprawl"
|
|
134
|
+
|
|
135
|
+
`expandX`/`expandY` mean "grow to the ceiling" — the container ceiling above,
|
|
136
|
+
capped in turn by `maxWidth`/`maxHeight` when they apply. `expand` is the
|
|
137
|
+
shorthand for both.
|
|
138
|
+
|
|
139
|
+
Since docking _supplies_ `expandX`, passing it explicitly takes the caller out
|
|
140
|
+
of that default:
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
// The sheet stops being flush: a floating box at the bottom of the screen.
|
|
144
|
+
<Dialog dockedOnSmallTouchScreen expandX={false} />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
That is consistent — an explicitly passed prop wins over a docked default — and
|
|
148
|
+
it is still the trap, because the two props read as answering different
|
|
149
|
+
questions (one about the centered shape, one about the phone) when they answer
|
|
150
|
+
the same one. If what you meant was "cap the centered box", that is `maxWidth`,
|
|
151
|
+
and docking withdraws it on its own.
|
|
152
|
+
|
|
153
|
+
## `marginWithContainer` decides the gap AND the ceiling
|
|
154
|
+
|
|
155
|
+
One prop, because they are one fact: the gap a dialog keeps with the edges of
|
|
156
|
+
its container is also what its size ceiling is computed from. Writing them
|
|
157
|
+
separately is how a dialog ends up flush on one side and inset on the other.
|
|
158
|
+
|
|
159
|
+
It defaults to a share of whatever holds the dialog (`3appw` for `layer="top"`,
|
|
160
|
+
`3cqw` for `layer="local"`) and accepts a spacing token (`"s"`, `"m"`…), a
|
|
161
|
+
number of pixels, or a viewport length — `appw`/`apph` being the app's own
|
|
162
|
+
screen, `vvw`/`vvh` the visual viewport, which shrinks when the keyboard opens.
|
|
163
|
+
Pass `0` for something meant to sit flush (a side panel), and note that docking
|
|
164
|
+
already passes `0` for you.
|
|
165
|
+
|
|
166
|
+
## Holding a size while the dialog is open
|
|
167
|
+
|
|
168
|
+
`sizing="frozen"` measures the dialog once and holds it until it closes; what
|
|
169
|
+
no longer fits — or no longer fills it — becomes the scroll's business. It is
|
|
170
|
+
for a surface acted upon while it is open: marking a notification read,
|
|
171
|
+
emptying a queue, swapping between two slides of different heights. The row
|
|
172
|
+
being aimed at must not move under the finger.
|
|
173
|
+
|
|
174
|
+
Two things make it safe to reach for:
|
|
175
|
+
|
|
176
|
+
- the freeze writes a `width`/`height`, never a `min-*` — the caps above keep
|
|
177
|
+
winning, so a frozen dialog still fits when the phone is turned;
|
|
178
|
+
- the measure is taken at the first render where the prop says `"frozen"`, so a
|
|
179
|
+
dialog opening on skeletons says `sizing={loading ? "auto" : "frozen"}` and
|
|
180
|
+
is measured once the real content has arrived. Closing releases it.
|
|
181
|
+
|
|
182
|
+
## A `layer="local"` dialog answers to its container
|
|
183
|
+
|
|
184
|
+
`layer="top"` (the default) is a real `<dialog>` in the browser's top layer:
|
|
185
|
+
native focus trap, `Escape`, hardware back-button dismissal, the rest of the
|
|
186
|
+
document made inert. `layer="local"` stays in normal document flow, confined to
|
|
187
|
+
and clipped by its own positioned ancestor.
|
|
188
|
+
|
|
189
|
+
For the shape, that swaps what every bound is measured against: the container
|
|
190
|
+
becomes that ancestor's box, read through
|
|
191
|
+
`--container-position-remaining-width`/`-height`, and the default margin is
|
|
192
|
+
read in container units. Its own container's scroll is always locked while it
|
|
193
|
+
is open — its backdrop covers the scrollport, not the scrolled content, so
|
|
194
|
+
scrolling would slide the dialog away and reveal what the backdrop does not
|
|
195
|
+
cover. `scrollCapture` is what extends that lock to the whole page.
|
|
196
|
+
|
|
197
|
+
One accepted limitation, not an oversight: a local dialog **cannot** be
|
|
198
|
+
dismissed by the hardware/gesture back button. No web API hooks into that
|
|
199
|
+
outside the browser's own modal-dismissal stack, which only a genuine
|
|
200
|
+
`showModal()` element joins.
|
|
201
|
+
|
|
202
|
+
## Reaching all of this through a `Picker` or a `SplitButton`
|
|
203
|
+
|
|
204
|
+
A picker's popup is a popover or a dialog depending on the screen, so it
|
|
205
|
+
exposes both sets of bounds under prefixed names, and they mean exactly what
|
|
206
|
+
they mean on `Dialog` itself:
|
|
207
|
+
|
|
208
|
+
`dialogMinWidth`, `dialogMinHeight`, `dialogMaxWidth`, `dialogMaxHeight`,
|
|
209
|
+
`dialogExpand`, `dialogExpandX`, `dialogExpandY`, `dockedOnSmallTouchScreen`,
|
|
210
|
+
`marginWithContainer` — plus `popoverMaxHeight` and `popupWidthFitContent` for
|
|
211
|
+
the other shape.
|
|
212
|
+
|
|
213
|
+
`SplitButton` forwards the same set to the picker it wraps. Anything not in
|
|
214
|
+
that set lands on the split button's own box instead — so a prop that seems to
|
|
215
|
+
do nothing to the menu is worth checking against that list first (see
|
|
216
|
+
`POPUP_PROP_SET` in `src/control/input/split_button.jsx`).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
Reference: `src/layout/dialog.jsx` (the stylesheet at the top of the file holds
|
|
221
|
+
the cap arithmetic), `src/layout/responsive.js` (`smallTouchScreenSignal`),
|
|
222
|
+
`src/control/picker/picker_custom.jsx` (how `--picker-dialog-*` bridges to
|
|
223
|
+
`--dialog-*`), and `src/layout/demos/1_dialog_demo.html`.
|
package/docs/z_index.md
CHANGED
|
@@ -74,14 +74,14 @@ file is the overview, this table is its summary. Bands are a decade apart so
|
|
|
74
74
|
one can grow without reaching the next, and so a value seen in devtools says
|
|
75
75
|
which band it came from.
|
|
76
76
|
|
|
77
|
-
| Band
|
|
78
|
-
|
|
|
79
|
-
| Top layer (`Dialog`/`Popover` with `layer="top"`)
|
|
80
|
-
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts
|
|
81
|
-
| `FixedBar`
|
|
82
|
-
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer | `--navi-z-index-sticky` | 10 |
|
|
83
|
-
| A `Group` member under the pointer, then the one holding focus
|
|
84
|
-
| `Table` sticky cells, drag, resize
|
|
77
|
+
| Band | Token | Value |
|
|
78
|
+
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------- |
|
|
79
|
+
| Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
|
|
80
|
+
| `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
|
|
81
|
+
| `FixedBar` | `--navi-z-index-bar` | 100 |
|
|
82
|
+
| Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
|
|
83
|
+
| A `Group` member under the pointer, then the one holding focus | `--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
85
|
|
|
86
86
|
What to read from it:
|
|
87
87
|
|
|
@@ -138,7 +138,7 @@ the label behind the rows, it puts it behind that background and out of sight.
|
|
|
138
138
|
See the "Sticky parts" chapter of
|
|
139
139
|
[12_list_demo.html](../src/control/demos/12_list_demo.html).
|
|
140
140
|
|
|
141
|
-
`Box`'s own `header`/`footer` take the opposite default, and for a reason worth
|
|
141
|
+
`<Box sticky>` and `Box`'s own `header`/`footer` take the opposite default, and for a reason worth
|
|
142
142
|
knowing: they are in the band **always**, not only while stuck. `List` can tell
|
|
143
143
|
— it measures its parts against its own scroller. A `Box` cannot: it is the
|
|
144
144
|
generic scrolling area, its content is whatever the app puts in it, and a
|
|
@@ -151,6 +151,32 @@ one call site that knows nothing inside is positioned.
|
|
|
151
151
|
shows the band, what `auto` would look like, and what the band costs, side by
|
|
152
152
|
side.
|
|
153
153
|
|
|
154
|
+
`<Box sticky>` — the one an app writes by hand — is that same generic case, so
|
|
155
|
+
it carries the band too, from the `sticky` prop itself (`position="sticky"`
|
|
156
|
+
included):
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
// position: sticky + z-index: var(--navi-z-index-sticky). Nothing to write.
|
|
160
|
+
<Box sticky bottom>
|
|
161
|
+
<SubmitButton />
|
|
162
|
+
</Box>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Without it the box is a positioned element at `z-index: auto`, and anything the
|
|
166
|
+
page raised passes in front of it — a `Group` member holding focus is at 2, so
|
|
167
|
+
it is seen crossing a submit bar the box was written to keep last. DOM order
|
|
168
|
+
cannot answer that (2 beats `auto` whatever the order) and `isolation: isolate`
|
|
169
|
+
neither (the common parent holds both, isolating it does not reorder them), so
|
|
170
|
+
before this the app had no move left but a literal of its own.
|
|
171
|
+
|
|
172
|
+
An explicit `zIndex` wins, `zIndex="auto"` included — the way
|
|
173
|
+
`--box-header-z-index` writes the band back at a call site that knows better:
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
// Back to auto: this one is meant to slide under the card that follows it.
|
|
177
|
+
<Box sticky top zIndex="auto" />
|
|
178
|
+
```
|
|
179
|
+
|
|
154
180
|
### Why a `Group` member is not isolated
|
|
155
181
|
|
|
156
182
|
`Group` overlaps its members by one border width, so the one the user is on has
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.76",
|
|
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.25",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.2"
|
|
35
35
|
},
|