@jsenv/navi 0.29.301 → 0.29.303

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.
@@ -77,6 +77,14 @@ adjusted without giving up the rest:
77
77
  | `expandX` | `true` | container-wide, same reason |
78
78
  | `scrollCapture` | `true` | a drag past the sheet's edge must not reach the page |
79
79
 
80
+ A `layer="top"` dialog flush with an edge of the screen — a docked sheet on the
81
+ bottom, anything with `marginWithContainer={0}` — keeps the band the device
82
+ reserves there (`env(safe-area-inset-*)`: the notch, the home indicator,
83
+ Safari's floating bar). The surface still reaches the edge; what it holds stops
84
+ at the band. Only the device's own inset: the dialog is in front of the app's
85
+ fixed bars, so `--navi-safe-area-inset-*`, which counts them too (see
86
+ [`safe_area.md`](./safe_area.md)), is not what it reads.
87
+
80
88
  Plus a swipe-down-to-close, held by the sheet's `header` (a `Box` with the
81
89
  `header` prop) and by anything carrying `data-swipe-grip` — never by the whole
82
90
  sheet, so a board something is dragged across keeps its own gestures. See
@@ -181,10 +189,17 @@ Two things make it safe to reach for:
181
189
 
182
190
  ## A `layer="local"` dialog answers to its container
183
191
 
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.
192
+ `layer="top"` (the default) is a real `<dialog>` in the browser's top layer,
193
+ placed against the screen and clipped by nothing — modal, with everything
194
+ behind made inert, unless `backdrop={false}` says otherwise (see
195
+ [`popup_backdrop.md`](./popup_backdrop.md)). `layer="local"` stays in normal
196
+ document flow, confined to and clipped by its own positioned ancestor.
197
+
198
+ For a **docked** dialog that is the choice of which edge the sheet rests on:
199
+ `layer` names the container, and docking makes the sheet flush and full width
200
+ against it. A sheet meant for the bottom of the screen is `layer="top"`, even
201
+ when the page behind it must stay live — that is `backdrop={false}`, not
202
+ `layer="local"`.
188
203
 
189
204
  For the shape, that swaps what every bound is measured against: the container
190
205
  becomes that ancestor's box, read through
@@ -194,10 +209,10 @@ is open — its backdrop covers the scrollport, not the scrolled content, so
194
209
  scrolling would slide the dialog away and reveal what the backdrop does not
195
210
  cover. `scrollCapture` is what extends that lock to the whole page.
196
211
 
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.
212
+ One accepted limitation, not an oversight: a dialog that is not modal —
213
+ `layer="local"`, or `backdrop={false}` — **cannot** be dismissed by the
214
+ hardware/gesture back button. No web API hooks into that outside the browser's
215
+ own modal-dismissal stack, which only a genuine `showModal()` element joins.
201
216
 
202
217
  ## Reaching all of this through a `Picker` or a `SplitButton`
203
218
 
@@ -7,6 +7,8 @@
7
7
  - [Which interaction asked](#which-interaction-asked)
8
8
  - [Reaching the control](#reaching-the-control)
9
9
  - [What a swipe draws, and what it leaves to you](#what-a-swipe-draws-and-what-it-leaves-to-you)
10
+ - [Twice, whichever hand it is](#twice-whichever-hand-it-is)
11
+ - [The click on the way there](#the-click-on-the-way-there)
10
12
  - [Carrying something, or a surface under the hand](#carrying-something-or-a-surface-under-the-hand)
11
13
  - [An affordance inside somebody else's box: `selfInteractions`](#an-affordance-inside-somebody-elses-box-selfinteractions)
12
14
  - [Why it is a list, and why it is required](#why-it-is-a-list-and-why-it-is-required)
@@ -87,16 +89,17 @@ which the carrying interactions answer with).
87
89
 
88
90
  ### The interactions navi detects
89
91
 
90
- | Key | Read from |
91
- | ------------------------------------------------------ | ----------------------------------------------------------- |
92
- | `mousedown` `mouseup` `click` `dblclick` `contextmenu` | the browser's own events |
93
- | `swipe_left` `swipe_right` `swipe_up` `swipe_down` | a press that travels |
94
- | `longpress` | a press held still |
95
- | `move` `reorder` `land` `toss` `leave` | the element carried, and what letting go means |
96
- | `moving` | the same carry, told on every frame |
97
- | `grab` `release` `refuse` | the instants a drag takes hold, lets go, or does not happen |
98
- | `pan` `zoom` | a surface under the hand, or under a wheel |
99
- | `"keyboard:<shortcut>"` | keys, e.g. `"keyboard:ctrl+backspace"` |
92
+ | Key | Read from |
93
+ | -------------------------------------------------- | ----------------------------------------------------------- |
94
+ | `mousedown` `mouseup` `click` `contextmenu` | the browser's own events |
95
+ | `swipe_left` `swipe_right` `swipe_up` `swipe_down` | a press that travels |
96
+ | `longpress` | a press held still |
97
+ | `double_click` `single_click` | two presses in a row, or one that stayed alone |
98
+ | `move` `reorder` `land` `toss` `leave` | the element carried, and what letting go means |
99
+ | `moving` | the same carry, told on every frame |
100
+ | `grab` `release` `refuse` | the instants a drag takes hold, lets go, or does not happen |
101
+ | `pan` `zoom` | a surface under the hand, or under a wheel |
102
+ | `"keyboard:<shortcut>"` | keys, e.g. `"keyboard:ctrl+backspace"` |
100
103
 
101
104
  Two holds on one press — a `longpress` declared on something inside an
102
105
  element that declares one too — are answered by the nearer one, the way a
@@ -104,6 +107,15 @@ click is the innermost target's: the inner hold fires, the outer wait is given
104
107
  up. Delays being equal; an outer hold made shorter than the inner one fires
105
108
  first.
106
109
 
110
+ There is no `dblclick` in that table, on purpose: the browser fires it for a
111
+ mouse and never for a finger, so an element declaring it would answer half the
112
+ hands that reach it. `double_click` is the same gesture counted from the
113
+ pointer — see [Twice, whichever hand it is](#twice-whichever-hand-it-is). navi says so in dev, once per element, the first time it happens: the
114
+ outer hold is a declaration that can never fire, and a picker opened by
115
+ `openOn="longpress"` around a card that already holds one is exactly where it
116
+ happens. One hold has one meaning — two things on one card want two gestures
117
+ (a hold and a click), not two holds.
118
+
107
119
  A name nothing knows how to detect produces a dev warning naming the detectors
108
120
  that exist. The carrying family and the two surface streams each have a file of
109
121
  their own — [drag_interactions.md](./drag_interactions.md) and
@@ -196,6 +208,67 @@ comes back once it settles — a failure leaves the row in place so it can be tr
196
208
  again. What a success does to the element is yours (a list that redemands its
197
209
  rows, a row that leaves): navi does not make it disappear.
198
210
 
211
+ ## Twice, whichever hand it is
212
+
213
+ At the finger there is no `dblclick`. There is not even a second `click`: two
214
+ taps in the same place are a gesture the browser keeps for itself (its own
215
+ zoom), so it withholds them — measured on a phone, two taps 120 ms apart give
216
+ two `pointerup`, ONE `click` and no `dblclick` at all. Anything that wants to
217
+ mean "twice" therefore has to count presses, which is what `double_click` is:
218
+
219
+ ```jsx
220
+ <Box interactions={{ pan, zoom, double_click: (event) => open(event) }} />
221
+ ```
222
+
223
+ One name for the mouse and for the finger — a tap is a click, and the same
224
+ gesture opens the plan at a desk and on a phone.
225
+
226
+ The rhythm is one window, opened by the FIRST press: the second has to land
227
+ inside it, and near enough to it.
228
+
229
+ | Attribute | Default | Meaning |
230
+ | ------------------------- | ------- | ------------------------------------------- |
231
+ | `data-double-click-delay` | `400` | ms the window stays open |
232
+ | `data-double-click-slop` | `30` | px the second press may land from the first |
233
+
234
+ The two numbers are read against the rest of the family rather than picked. 400
235
+ sits under the hold's 450: a pause longer than the wait navi calls "held" is
236
+ longer than one gesture — which also means a press slow enough to be a hold
237
+ cannot start a double click, and a hold answered on the second press takes that
238
+ press back. 30 px is a fingertip and not a pixel, because between the two the
239
+ finger leaves the glass and lands again where it means to; the 8 px of
240
+ `data-longpress-slop` answer a different question (has this finger stood
241
+ still?).
242
+
243
+ ### The click on the way there
244
+
245
+ By default both happen, the way they do in a browser: the first press is a
246
+ `click`, and the second is answered by the `double_click` alone — the click it
247
+ would have left behind is swallowed, the way every gesture swallows the one it
248
+ leaves.
249
+
250
+ `single_click` is for the case where even the first one is too much: two
251
+ answers that exclude each other — a plan that opens on the double must not do
252
+ whatever a lone tap does on the way there. It is the same window read the other
253
+ way, the tap that STAYED alone, said once the window has closed on it:
254
+
255
+ ```jsx
256
+ <Box
257
+ interactions={{
258
+ single_click: (event) => select(event),
259
+ double_click: (event) => open(event),
260
+ }}
261
+ />
262
+ ```
263
+
264
+ It costs that wait, which is why it is a name a caller picks rather than
265
+ something a declared `double_click` imposes on `click`. And it hands the
266
+ element's click over: the click each tap leaves behind is swallowed, so a
267
+ control that wants its action on a lone click asks for it there —
268
+ `single_click: "request_action"`, not `action` reached by a click that no longer
269
+ arrives. A keyboard activation is not held: nothing can double it, so it is said
270
+ at once.
271
+
199
272
  ## Carrying something, or a surface under the hand
200
273
 
201
274
  Two families of interaction have a file of their own, because each is a subject:
@@ -414,6 +487,10 @@ tuned in one place and a stylesheet can read the same value.
414
487
  | `data-longpress-delay` | `450` | ms the press must be held |
415
488
  | `data-longpress-slop` | `8` | px the pointer may drift during the wait |
416
489
 
490
+ Plus the two the double click is counted with, `data-double-click-delay` and
491
+ `data-double-click-slop` (see [Twice, whichever hand it
492
+ is](#twice-whichever-hand-it-is)).
493
+
417
494
  A threshold is a **fraction and never a distance**: the same gesture must mean
418
495
  the same thing on a phone and on a wide screen. Speed answers on its own on top
419
496
  of it — a brief flick counts whatever the distance covered.
@@ -522,17 +599,18 @@ container above it does not take the gesture:
522
599
  finger, which is the native gesture. To place it at the press point rather than
523
600
  on the element:
524
601
  `triggerNaviCommand(target, "--navi-open", interactionEvent, { anchor })`.
525
- - **A swipe has no keyboard equivalent.** There is nothing to press that means
526
- "swipe right", so a swipe is only reachable if something else on the element
527
- offers the same thing — a `"keyboard:<shortcut>"`, a `contextmenu`, or the
528
- control's own action.
602
+ - **A swipe has no keyboard equivalent, and neither has a double click.** There
603
+ is nothing to press that means "swipe right", and no key that means "twice",
604
+ so either is only reachable if something else on the element offers the same
605
+ thing — a `"keyboard:<shortcut>"`, a `contextmenu`, or the control's own
606
+ action.
529
607
 
530
608
  ## Reference
531
609
 
532
610
  - `src/control/interaction/interaction_registry.js` — the prop, the four values,
533
611
  the registry.
534
- - `src/control/interaction/interaction_press.js` — swipes and holds, and what a
535
- swipe writes on the element.
612
+ - `src/control/interaction/interaction_press.js` — swipes, holds and the two
613
+ taps, and what a swipe writes on the element.
536
614
  - `src/control/interaction/interaction_keyboard.js`,
537
615
  `interaction_native.js` — the other two detectors.
538
616
  - `src/control/demos/38_interactions_demo.html` — every case above, plus a
package/docs/pan_zoom.md CHANGED
@@ -43,8 +43,8 @@ when it lands; the pan stepping back for what is carried across the surface (a
43
43
  for); the pinch not beginning as a pan under its first finger; the wheel and the
44
44
  pinch writing one `zoom`; the capture, the pointer the browser drops, the click
45
45
  the release leaves behind. A pointer pans only once it has travelled a few px
46
- (`data-drag-threshold`), so a tap stays a tap and a `longpress` declared beside
47
- `pan` still gets its hold.
46
+ (`data-drag-threshold`), so a tap stays a tap: a `longpress` declared beside
47
+ `pan` still gets its hold, and a `double_click` still gets its two presses.
48
48
 
49
49
  Declared alone, `zoom` takes two fingers and the wheel and leaves one pointer to
50
50
  whatever else reads it; `pan` alone leaves the wheel to the page.
@@ -61,13 +61,34 @@ questions. `"invisible"` is for a popup that must absorb — a menu whose
61
61
  dismissing click must not also press what is under it — without dimming the
62
62
  page for it. `backdrop={false}` is for a popup that must not absorb at all.
63
63
 
64
- **Only a non-modal popup can honour it.** `Popover` always can, either layer. A
65
- `Dialog` can only in `layer="local"`: the default `layer="top"` is shown with
66
- `showModal()`, which makes everything behind genuinely inert before any of
67
- navi's code runs — there is no press left to let through, and asking for one
68
- warns. `Popup` follows from that: it forwards `backdrop` to its popover and
69
- drops it for a top-layer dialog, so the same usage that gives one press on a
70
- desktop costs two once the small-screen resolution picks dialog mode.
64
+ **It is what decides whether a popup is modal.** `Popover` is never modal,
65
+ either layer. A `Dialog` is modal exactly when it has a wall: with one, a
66
+ top-layer dialog is `showModal()`'d and the browser makes everything behind
67
+ genuinely inert; without one it goes to the same top layer through the Popover
68
+ API (`popover="manual"`), placed against the screen just the same, over a page
69
+ that stays live. `Popup` forwards `backdrop` in both modes, so which of popover
70
+ or dialog the small-screen resolution picks says nothing about whether one
71
+ press or two are needed.
72
+
73
+ That is what a bottom sheet on a phone is made of. `dockedOnSmallTouchScreen`
74
+ docks the dialog against its container, and `layer` is what that container is —
75
+ `"top"` for the screen, `"local"` for the box the popup was declared in. A
76
+ sheet flush with the bottom of the screen over a map still being read object by
77
+ object is `layer="top"` for the shape and `backdrop={false}` for the map:
78
+
79
+ ```jsx
80
+ <Popup dockedOnSmallTouchScreen layer="top" backdrop={false}>
81
+ ```
82
+
83
+ Going `layer="local"` to keep the map live instead confines the sheet to the
84
+ box it was declared in — full width and flush against _that_, which on a
85
+ partial container reads as a sheet that failed rather than as a sheet.
86
+
87
+ What a wall-less dialog gives up is the one thing only a modal gets natively:
88
+ the hardware/gesture back button no longer dismisses it (same accepted
89
+ limitation as `layer="local"`, see [`dialog_shape.md`](./dialog_shape.md)).
90
+ Focus is not trapped either, deliberately — a page meant to be reachable is
91
+ meant to be reachable with the keyboard too.
71
92
 
72
93
  `pointerInteractionOutsideEffect="capture"` and `backdrop={false}` contradict
73
94
  each other — absorbing is what a wall does — and navi warns rather than
@@ -468,6 +468,23 @@ card out of its place and puts it back (`data-grow` on the card inside the
468
468
  sheet; see Dialog's own `animation`). `12_picker_card_demo.html` shows all of
469
469
  it, against a backend that answers when told to.
470
470
 
471
+ A gesture inside the sheet that is not a field — cancel the game, delete it —
472
+ is still an answer the sheet gives. A named button says which:
473
+
474
+ ```jsx
475
+ <Button name="op" value="cancel" command="--navi-send">
476
+ Cancel the game
477
+ </Button>
478
+ ```
479
+
480
+ Its name and value travel with the fields (the group takes the sender's
481
+ value, see `wantRequesterButtonState`), the sheet leaves on the send, and the
482
+ picker's `action` receives `{ …fields, op: "cancel" }` — so this write is
483
+ worn by the trigger exactly like the others, callout included, and a drawing
484
+ reading `usePickerState()` can already show the "cancelled" stamp while the
485
+ request is out. No button inside the popup needs an action of its own: a run
486
+ started in there would belong to a control the close takes away.
487
+
471
488
  This is not `optimistic`. `optimistic` on a control is "draw no wait at all";
472
489
  here the wait is drawn, on the trigger, and the popup was never what held it.
473
490
  An optimistic picker is for a write not worth showing — the card then reads
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.301",
3
+ "version": "0.29.303",
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.61",
37
+ "@jsenv/dom": "0.17.62",
38
38
  "@jsenv/humanize": "1.8.2",
39
39
  "@jsenv/validity": "0.4.3"
40
40
  },