@jsenv/navi 0.29.39 → 0.29.41

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.
@@ -75,8 +75,9 @@ consistency across the app, not from any single call site.
75
75
  supported ways to override component styles (props > CSS variables > direct
76
76
  rule overrides, in that preference order).
77
77
  - `docs/scroll.md` — where scrolling happens: what turns `Box`
78
- `header`/`body`/`footer` on, `FixedBar` space, `List`'s `scroller`, and
79
- scroll inside a `Dialog`/`Popover`. Read it before writing CSS to make
78
+ `header`/`body`/`footer` on, `FixedBar` space, `List`'s `scroller`, scroll
79
+ inside a `Dialog`/`Popover`, and what a scroll does to hover
80
+ (`hoverWhileScrolling`, `isScrolling()`). Read it before writing CSS to make
80
81
  something scroll — navi almost certainly already has the prop.
81
82
  - `docs/control_value.md` — who holds a control's value: nobody, a bound
82
83
  `signal` (two-way, in both directions), or you (`value`/`checked`). What
package/docs/actions.md CHANGED
@@ -97,6 +97,43 @@ callback:
97
97
  </Button>
98
98
  ```
99
99
 
100
+ ## `action` or `uiAction`
101
+
102
+ Both fire when a control's value changes, and they are not two ways of writing
103
+ the same thing:
104
+
105
+ | | `uiAction(value, event)` | `action` |
106
+ | ----------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
107
+ | what it is | a plain callback | an action bound to the control's value |
108
+ | while it runs | nothing | the control is busy (`aria-busy`, its loading state) |
109
+ | if it fails | an unhandled rejection — nothing on screen | an error callout on the control, and the control goes back to what it held |
110
+ | a popup around it | closes | refuses to close until it is done |
111
+
112
+ `uiAction` is a notification: the value has changed, here it is. Use it for what
113
+ cannot fail — logging, moving something else on screen, keeping a local
114
+ variable.
115
+
116
+ Anything that can fail or take time is an `action`, and it does not have to be
117
+ an action instance: a plain function is wrapped into one, bound to the control's
118
+ value, so the callback receives what the control now holds.
119
+
120
+ ```jsx
121
+ // ✓ the box shows it is saving, says so if the save fails, and goes back to
122
+ // where it was — nothing to write for any of it
123
+ <Input type="checkbox" action={(visibility) => saveMe({ visibility })} />
124
+
125
+ // ✗ same save, and the user learns nothing: no pending state, and a failure
126
+ // leaves the box showing something the server never accepted
127
+ <Input type="checkbox" uiAction={(visibility) => saveMe({ visibility })} />
128
+ ```
129
+
130
+ The give-away is an `async` `uiAction`, or one that calls something that writes:
131
+ `uiAction` never waits for what it starts, so nobody is left holding the result.
132
+
133
+ To merely REMEMBER the value rather than send it, neither is the answer: bind a
134
+ signal and drop the callback entirely — see
135
+ [control_value.md](./control_value.md).
136
+
100
137
  ## Reruns
101
138
 
102
139
  Actions do not stay stale on their own: a resource's `POST` reruns the
@@ -126,7 +126,7 @@ directions; it just has nothing extra to say.
126
126
 
127
127
  - [control_group.md](./control_group.md) — several controls reading as one
128
128
  framed object
129
- - [actions.md](./actions.md) — `action` vs `uiAction`: what carries loading and
130
- error
129
+ - [actions.md](./actions.md#action-or-uiaction) — `action` or `uiAction`: which
130
+ one carries loading and error, and which one carries nothing
131
131
  - [popup_open.md](./popup_open.md#escape-cancels-the-other-gestures-keep) — what
132
132
  a cancelled popup does to the value inside it
package/docs/scroll.md CHANGED
@@ -7,6 +7,7 @@ scrolling area (`Box header/body/footer`, `List`, a popup) are told about it.
7
7
  - [1. The document scrolls](#1-the-document-scrolls)
8
8
  - [2. A part of the document scrolls](#2-a-part-of-the-document-scrolls)
9
9
  - [3. A popup scrolls](#3-a-popup-scrolls)
10
+ - [Hover while scrolling](#hover-while-scrolling)
10
11
  - [The list border](#the-list-border)
11
12
 
12
13
  ## What makes header/body/footer work: the overflow
@@ -236,6 +237,58 @@ changes screen mid-reading.
236
237
  Reference: `src/layout/dialog.jsx`, `src/layout/popover.jsx`,
237
238
  `src/layout/slide_container.jsx`.
238
239
 
240
+ ## Hover while scrolling
241
+
242
+ A scroll moves the content under a pointer that does not move. The browser
243
+ reports that as hover: it fires `mouseleave` + `mouseenter` for **every element
244
+ crossing the cursor** — a dozen per wheel tick. None of it was asked for; the
245
+ user asked to scroll.
246
+
247
+ It is free as long as hover only paints a background. It stops being free the
248
+ moment hover triggers real work — a highlight somewhere else in the tree, a
249
+ prefetch, a map redrawing a layer — because that work then lands on the main
250
+ thread exactly while a scroll animation is running, and the scroll stutters.
251
+
252
+ ### The fact is in the DOM: `navi-scrolling`
253
+
254
+ While an element scrolls it carries `navi-scrolling`, written by one capturing
255
+ listener on the document (`scroll` does not bubble, but it does propagate in
256
+ the capture phase) and removed once it has been quiet for a moment — scroll
257
+ events stop before the movement does. Nothing subscribes to anything: whoever
258
+ is concerned says so in CSS.
259
+
260
+ ```css
261
+ /* my rows answer the pointer only when nothing is moving them */
262
+ [navi-scrolling] .my_row {
263
+ pointer-events: none;
264
+ }
265
+ ```
266
+
267
+ `pointer-events` is what does the work, and it does the whole of it: enter,
268
+ move and leave at once, in the browser, at no cost per element. Hand-written in
269
+ JS the same suppression takes three handlers — once `mouseenter` has been
270
+ swallowed the pointer is already inside the element, so only `mousemove` can
271
+ ever bring the hover back.
272
+
273
+ The page scroll carries the attribute on `document.scrollingElement`, so an
274
+ ancestor rule covers it too. In JS the same fact reads as `isScrolling()` /
275
+ `isScrolling(element)`, or `scrollActivitySignal` to react to it.
276
+
277
+ ### In a `List`: nothing to do
278
+
279
+ `List` rows leave hit-testing while anything scrolling them moves — its own
280
+ scroll box, the panel around it, the page.
281
+
282
+ ```jsx
283
+ <List hoverWhileScrolling> {/* opt back in */}
284
+ ```
285
+
286
+ The default costs one thing, and it is the honest half of the same trade: right
287
+ after a scroll, the row under the pointer lights up only once the pointer moves
288
+ by a pixel.
289
+
290
+ Reference: `src/utils/scroll_activity.js`.
291
+
239
292
  ## The list border
240
293
 
241
294
  Not scroll, but the same family of problem — a reasonable default nobody knows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.39",
3
+ "version": "0.29.41",
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.13",
32
+ "@jsenv/dom": "0.17.14",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.2"
35
35
  },