@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/dist/jsenv_navi.js
CHANGED
|
@@ -14476,7 +14476,11 @@ const createOpenController = (
|
|
|
14476
14476
|
const applyChange = () => {
|
|
14477
14477
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
14478
14478
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
14479
|
-
|
|
14479
|
+
// An opening records it inside the change, once its content is built
|
|
14480
|
+
// (see open() below).
|
|
14481
|
+
if (!opened) {
|
|
14482
|
+
controller.openedInDom = false;
|
|
14483
|
+
}
|
|
14480
14484
|
change();
|
|
14481
14485
|
};
|
|
14482
14486
|
const { transitionChange } = controller;
|
|
@@ -14756,15 +14760,17 @@ const createOpenController = (
|
|
|
14756
14760
|
controller.mountContent?.();
|
|
14757
14761
|
// Only now — after the content has been built, before openEffect
|
|
14758
14762
|
// shows it. Dialog/Popover recompute aria-expanded and navi-hidden
|
|
14759
|
-
// from
|
|
14763
|
+
// from `openedInDom` on every render, and mountContent above renders
|
|
14760
14764
|
// synchronously: flipping it any earlier commits an already-open DOM
|
|
14761
14765
|
// (aria-expanded "true", navi-hidden gone) before openEffect has run
|
|
14762
14766
|
// a single statement, so the "closed" frame it pins to transition
|
|
14763
14767
|
// from is in fact the open one and the entrance animation has
|
|
14764
|
-
// nothing to play
|
|
14765
|
-
//
|
|
14766
|
-
//
|
|
14767
|
-
// popup_content_mount.js and
|
|
14768
|
+
// nothing to play — on every first opening that builds its content.
|
|
14769
|
+
// It also gives the content it just built the opening it is
|
|
14770
|
+
// documented to observe — mounted while the popup reads as closed,
|
|
14771
|
+
// told it opened right after (see popup_content_mount.js and
|
|
14772
|
+
// use_displayed_layout_effect.js).
|
|
14773
|
+
controller.openedInDom = true;
|
|
14768
14774
|
controller.opened = true;
|
|
14769
14775
|
// Which press it opened during, so the release of that press is not
|
|
14770
14776
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
@@ -15155,7 +15161,14 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15155
15161
|
lastRunOpenRef.current = open;
|
|
15156
15162
|
|
|
15157
15163
|
if (isFirstRun) {
|
|
15158
|
-
|
|
15164
|
+
// "interaction" is said even over what the caller holds reading open: a
|
|
15165
|
+
// popup built by the request that opens it — a dialog standing in until
|
|
15166
|
+
// its signal said open, see DialogStandIn in dialog.jsx — mounts BECAUSE
|
|
15167
|
+
// of that opening.
|
|
15168
|
+
const mountOpenReason =
|
|
15169
|
+
open && defaultOpen === "interaction"
|
|
15170
|
+
? "interaction"
|
|
15171
|
+
: open || defaultOpen;
|
|
15159
15172
|
if (mountOpenReason) {
|
|
15160
15173
|
// Whether this popup being open is something that just happened, or
|
|
15161
15174
|
// something that was already true when the page appeared. "interaction"
|
|
@@ -27804,6 +27817,18 @@ const useAsyncData = (promiseOrAction, {
|
|
|
27804
27817
|
|
|
27805
27818
|
// ─── useAction ────────────────────────────────────────────────────────────────
|
|
27806
27819
|
|
|
27820
|
+
// subscribe() calls back at once with the value the signal holds; what these
|
|
27821
|
+
// subscriptions are for is what changes after.
|
|
27822
|
+
const subscribeToChanges = (signal, onChange) => {
|
|
27823
|
+
let initial = true;
|
|
27824
|
+
return signal.subscribe(value => {
|
|
27825
|
+
if (initial) {
|
|
27826
|
+
initial = false;
|
|
27827
|
+
return;
|
|
27828
|
+
}
|
|
27829
|
+
onChange(value);
|
|
27830
|
+
});
|
|
27831
|
+
};
|
|
27807
27832
|
const LoadingContext = createContext(null);
|
|
27808
27833
|
const actionPendingPromiseWeakMap = new WeakMap();
|
|
27809
27834
|
const dismissedActionWeakSet = new WeakSet();
|
|
@@ -27848,6 +27873,15 @@ const useActionAsyncData = (action, {
|
|
|
27848
27873
|
const runningState = action.runningStateSignal.peek();
|
|
27849
27874
|
const [, setTick] = useState(0);
|
|
27850
27875
|
const routePage = useContext(RoutePageContext);
|
|
27876
|
+
// What this render read of the action, for the subscriptions below to tell
|
|
27877
|
+
// a change that came between the render and them from none at all.
|
|
27878
|
+
const readRef = useRef(null);
|
|
27879
|
+
readRef.current = {
|
|
27880
|
+
runningState,
|
|
27881
|
+
data: action.dataSignal.peek(),
|
|
27882
|
+
params: action.paramsSignal.peek(),
|
|
27883
|
+
settling: action.paramsSettlingSignal.peek()
|
|
27884
|
+
};
|
|
27851
27885
|
useEffect(() => {
|
|
27852
27886
|
let stopOwingRender = null;
|
|
27853
27887
|
const rerender = () => {
|
|
@@ -27875,7 +27909,12 @@ const useActionAsyncData = (action, {
|
|
|
27875
27909
|
}
|
|
27876
27910
|
setTick(n => n + 1);
|
|
27877
27911
|
};
|
|
27878
|
-
|
|
27912
|
+
// A run under way is a new try: an error dismissed before it is not the
|
|
27913
|
+
// one it may end on.
|
|
27914
|
+
if (action.runningStateSignal.peek() === RUNNING) {
|
|
27915
|
+
dismissedActionWeakSet.delete(action);
|
|
27916
|
+
}
|
|
27917
|
+
const unsubscribeFromRunningState = subscribeToChanges(action.runningStateSignal, state => {
|
|
27879
27918
|
if (state === RUNNING) {
|
|
27880
27919
|
dismissedActionWeakSet.delete(action);
|
|
27881
27920
|
}
|
|
@@ -27886,38 +27925,24 @@ const useActionAsyncData = (action, {
|
|
|
27886
27925
|
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
27887
27926
|
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
27888
27927
|
// through the same controlled path as the run state, instead of `.value`.
|
|
27889
|
-
|
|
27890
|
-
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
27891
|
-
if (dataNotificationIsInitial) {
|
|
27892
|
-
// subscribe() calls back synchronously with the current value
|
|
27893
|
-
dataNotificationIsInitial = false;
|
|
27894
|
-
return;
|
|
27895
|
-
}
|
|
27896
|
-
rerender();
|
|
27897
|
-
});
|
|
27928
|
+
const unsubscribeFromData = subscribeToChanges(action.dataSignal, rerender);
|
|
27898
27929
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27899
27930
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
27900
27931
|
// retargeting from no question to a question — a filter chosen, a first
|
|
27901
27932
|
// character typed — is announced by nothing above: a fresh target is IDLE
|
|
27902
27933
|
// holding no data, the very state the hook already sees.
|
|
27903
|
-
|
|
27904
|
-
const unsubscribeFromParams = action.paramsSignal.subscribe(() => {
|
|
27905
|
-
if (paramsNotificationIsInitial) {
|
|
27906
|
-
paramsNotificationIsInitial = false;
|
|
27907
|
-
return;
|
|
27908
|
-
}
|
|
27909
|
-
rerender();
|
|
27910
|
-
});
|
|
27934
|
+
const unsubscribeFromParams = subscribeToChanges(action.paramsSignal, rerender);
|
|
27911
27935
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27912
27936
|
// while the delay runs — but what is on screen is already out of date.
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
27937
|
+
const unsubscribeFromParamsSettling = subscribeToChanges(action.paramsSettlingSignal, rerender);
|
|
27938
|
+
// Subscribed once the browser has painted, so the action may have moved
|
|
27939
|
+
// since the render: a run that very render started may be over already,
|
|
27940
|
+
// and nothing would say so again. What the render read, against what is
|
|
27941
|
+
// there now — and nothing moved, nothing to draw again.
|
|
27942
|
+
const read = readRef.current;
|
|
27943
|
+
if (action.runningStateSignal.peek() !== read.runningState || action.dataSignal.peek() !== read.data || action.paramsSignal.peek() !== read.params || action.paramsSettlingSignal.peek() !== read.settling) {
|
|
27919
27944
|
rerender();
|
|
27920
|
-
}
|
|
27945
|
+
}
|
|
27921
27946
|
return () => {
|
|
27922
27947
|
unsubscribeFromRunningState();
|
|
27923
27948
|
unsubscribeFromData();
|
|
@@ -39345,6 +39370,17 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
39345
39370
|
decisionDeferredToRef.current = popupMounting;
|
|
39346
39371
|
return;
|
|
39347
39372
|
}
|
|
39373
|
+
if (
|
|
39374
|
+
(el.tagName === "DIALOG" || el.hasAttribute("popover")) &&
|
|
39375
|
+
!isAncestorOpen(el)
|
|
39376
|
+
) {
|
|
39377
|
+
// Itself a closed <dialog> or [popover]: display:none by the UA's own
|
|
39378
|
+
// rules, which the markup says without asking the layout — asked, every
|
|
39379
|
+
// closed popup mounting would force a style recalculation, and a list
|
|
39380
|
+
// whose rows each hold a few mounts hundreds of them. The observer
|
|
39381
|
+
// below fires as for any element the ancestor hides.
|
|
39382
|
+
return;
|
|
39383
|
+
}
|
|
39348
39384
|
if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
|
|
39349
39385
|
// Ancestor is closed and took this element off screen with it — skip
|
|
39350
39386
|
// now; the observeAncestorOpenState call below will fire once it
|
|
@@ -51447,9 +51483,9 @@ const FixedBar = ({
|
|
|
51447
51483
|
* A page that overflows horizontally is a bug wherever it happens, and on
|
|
51448
51484
|
* Chrome Android it is a catastrophic one: the layout viewport inflates to the
|
|
51449
51485
|
* content and `position: fixed` centering goes with it (see
|
|
51450
|
-
* docs/
|
|
51451
|
-
* `overflow-x: clip`, and it works — at the price of making the
|
|
51452
|
-
* invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
51486
|
+
* docs/safe_area.md#a-document-wider-than-the-screen). The remedy there is a
|
|
51487
|
+
* wrapper in `overflow-x: clip`, and it works — at the price of making the
|
|
51488
|
+
* cause invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
51453
51489
|
* `min-width` or an unbreakable string is still oversized. This puts the
|
|
51454
51490
|
* signal back, without giving up the net.
|
|
51455
51491
|
*
|
|
@@ -64379,7 +64415,9 @@ const css$E = /* css */`
|
|
|
64379
64415
|
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
64380
64416
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
64381
64417
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
64382
|
-
* them afterwards
|
|
64418
|
+
* them afterwards — and the dialog itself is built then too: until its first
|
|
64419
|
+
* request (an open, a press on what opens it, its signal saying open) it is
|
|
64420
|
+
* a bare `<dialog>` holding its id (see docs/popup_open.md). `"always"` builds them right away, for content something
|
|
64383
64421
|
* depends on while the popup is still closed: a value read off it, fields a
|
|
64384
64422
|
* surrounding form collects on submit, a size measured from outside.
|
|
64385
64423
|
* `"idle"` builds them in a browser idle moment after load — "always" minus
|
|
@@ -64400,11 +64438,122 @@ const Dialog = props => {
|
|
|
64400
64438
|
...props
|
|
64401
64439
|
});
|
|
64402
64440
|
}
|
|
64403
|
-
return jsx(
|
|
64441
|
+
return jsx(DialogStandIn, {
|
|
64404
64442
|
...props
|
|
64405
64443
|
});
|
|
64406
64444
|
};
|
|
64407
64445
|
|
|
64446
|
+
// A dialog nobody has asked for yet stands in as its bare <dialog>. That
|
|
64447
|
+
// element is all that reaches a closed dialog before its first opening: a
|
|
64448
|
+
// command finds it by id (`commandfor`) and asks it to open, a press on what
|
|
64449
|
+
// opens it announces itself on it (navi_open_press), --navi-toggle reads its
|
|
64450
|
+
// aria-expanded. The dialog proper — its open controller, its box, its focus
|
|
64451
|
+
// and pseudo-class wiring — is built at the first request: an open, a press
|
|
64452
|
+
// about to open it, its signal or its `open` saying open. A list whose rows
|
|
64453
|
+
// each hold a few dialogs mounts hundreds of them, and would pay for every one
|
|
64454
|
+
// at every mount, for openings that mostly never come. Built once, the dialog
|
|
64455
|
+
// stays: the next opening finds it there.
|
|
64456
|
+
const DialogStandIn = props => {
|
|
64457
|
+
const {
|
|
64458
|
+
ref,
|
|
64459
|
+
id,
|
|
64460
|
+
signal,
|
|
64461
|
+
value,
|
|
64462
|
+
open
|
|
64463
|
+
} = props;
|
|
64464
|
+
const ownRef = useRef();
|
|
64465
|
+
const dialogRef = ref || ownRef;
|
|
64466
|
+
// Why the dialog proper exists, once it does; never undone.
|
|
64467
|
+
const builtRef = useRef(null);
|
|
64468
|
+
const mountedRef = useRef(false);
|
|
64469
|
+
const isMount = !mountedRef.current;
|
|
64470
|
+
mountedRef.current = true;
|
|
64471
|
+
// Its own reading of the signal: the dialog of one card among many, told
|
|
64472
|
+
// apart by `value`, follows whether IT is the one open rather than every
|
|
64473
|
+
// write the others cause.
|
|
64474
|
+
const openedSignal = useMemo(() => signal ? computed(() => readOpened(signal.value, value)) : null, [signal, value]);
|
|
64475
|
+
const openedBySignal = openedSignal ? openedSignal.value : false;
|
|
64476
|
+
if (builtRef.current === null) {
|
|
64477
|
+
if (isMount && (needsDialogFromMount(props) || open || openedBySignal)) {
|
|
64478
|
+
builtRef.current = "mount";
|
|
64479
|
+
} else if (open || openedBySignal) {
|
|
64480
|
+
// Asked open while this stood in: the opening is happening now, and it
|
|
64481
|
+
// plays like any other (see defaultOpen "interaction").
|
|
64482
|
+
builtRef.current = "interaction";
|
|
64483
|
+
}
|
|
64484
|
+
}
|
|
64485
|
+
const [, setBuildCount] = useState(0);
|
|
64486
|
+
const warmRef = useRef(null);
|
|
64487
|
+
const build = reason => {
|
|
64488
|
+
if (builtRef.current !== null) {
|
|
64489
|
+
return;
|
|
64490
|
+
}
|
|
64491
|
+
builtRef.current = reason;
|
|
64492
|
+
flushSyncRendering(() => {
|
|
64493
|
+
setBuildCount(count => count + 1);
|
|
64494
|
+
});
|
|
64495
|
+
};
|
|
64496
|
+
const builtReason = builtRef.current;
|
|
64497
|
+
if (builtReason !== null) {
|
|
64498
|
+
return jsx(UncontrolledDialog, {
|
|
64499
|
+
...props,
|
|
64500
|
+
ref: dialogRef,
|
|
64501
|
+
defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
|
|
64502
|
+
});
|
|
64503
|
+
}
|
|
64504
|
+
return jsx("dialog", {
|
|
64505
|
+
ref: dialogRef,
|
|
64506
|
+
id: id,
|
|
64507
|
+
className: "navi_dialog"
|
|
64508
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64509
|
+
,
|
|
64510
|
+
"navi-hidden": ""
|
|
64511
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64512
|
+
,
|
|
64513
|
+
"navi-out-of-flow": "",
|
|
64514
|
+
"aria-expanded": "false",
|
|
64515
|
+
"data-testid": props["data-testid"]
|
|
64516
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64517
|
+
,
|
|
64518
|
+
onnavi_request_open: e => {
|
|
64519
|
+
// Built in this same event, and asked again: the opening is the one
|
|
64520
|
+
// the dialog proper would have had — its source, its anchor, its value.
|
|
64521
|
+
build("request");
|
|
64522
|
+
const opened = dispatchCustomEvent(dialogRef.current, "navi_request_open", e.detail);
|
|
64523
|
+
if (!opened) {
|
|
64524
|
+
e.preventDefault();
|
|
64525
|
+
}
|
|
64526
|
+
}
|
|
64527
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64528
|
+
,
|
|
64529
|
+
onnavi_open_press: () => {
|
|
64530
|
+
// What the opening will read is asked for at once, as the dialog
|
|
64531
|
+
// proper does (see open_controller.js); the dialog itself two frames
|
|
64532
|
+
// later, so the pressed trigger is painted first — the click that
|
|
64533
|
+
// follows opens it built, or builds it itself when it comes sooner.
|
|
64534
|
+
if (signal) {
|
|
64535
|
+
preloadState(signal, readOpenValue(value));
|
|
64536
|
+
}
|
|
64537
|
+
if (warmRef.current) {
|
|
64538
|
+
return;
|
|
64539
|
+
}
|
|
64540
|
+
warmRef.current = requestFrameAfterNext(() => {
|
|
64541
|
+
warmRef.current = null;
|
|
64542
|
+
build("press");
|
|
64543
|
+
});
|
|
64544
|
+
}
|
|
64545
|
+
});
|
|
64546
|
+
};
|
|
64547
|
+
// What needs the dialog proper from the start: an opening at mount, content
|
|
64548
|
+
// built ahead of any opening, a history entry to read, an anchor whose hover
|
|
64549
|
+
// or focus builds the content ahead of the click.
|
|
64550
|
+
const needsDialogFromMount = props => {
|
|
64551
|
+
if (props.defaultOpen || props.navState || props.anchor !== undefined) {
|
|
64552
|
+
return true;
|
|
64553
|
+
}
|
|
64554
|
+
return props.mount !== undefined && props.mount !== "from-first-open";
|
|
64555
|
+
};
|
|
64556
|
+
|
|
64408
64557
|
// No openController passed: this Dialog is used declaratively (e.g. driven
|
|
64409
64558
|
// by --navi-toggle/--navi-open/--navi-close commands, the `open` prop, or
|
|
64410
64559
|
// `defaultOpen`) rather than owned by a parent component.
|
|
@@ -76033,10 +76182,10 @@ const useBadgeRegistry = (children, enabled) => {
|
|
|
76033
76182
|
* list renders nothing at all. In a <Picker> this is the only placeholder
|
|
76034
76183
|
* the user gets — a picker given a `ui` does not draw its own — so pass the
|
|
76035
76184
|
* placeholder text: `fallback="Select skills…"`. Plain text is the right
|
|
76036
|
-
* choice there: it reads at the picker's size, in the
|
|
76037
|
-
*
|
|
76038
|
-
* transparent <Badge> matches the slot to the pixel instead, at the
|
|
76039
|
-
* badge-sized text. See docs/
|
|
76185
|
+
* choice there: it reads at the picker's size, in the normal text color (a
|
|
76186
|
+
* caller's `ui` is never greyed as a placeholder), and the box stays the same
|
|
76187
|
+
* height. A transparent <Badge> matches the slot to the pixel instead, at the
|
|
76188
|
+
* price of badge-sized text. See docs/typography.md#badgelist.
|
|
76040
76189
|
* @param {boolean} [shrinkWrap]
|
|
76041
76190
|
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
76042
76191
|
* Defaults to true inside a <Picker> — the trigger draws a border around the
|