@jsenv/navi 0.29.18 → 0.29.20
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 +129 -57
- package/dist/jsenv_navi.js.map +10 -9
- package/docs/AI_INSTRUCTIONS.md +4 -0
- package/docs/list_refresh.md +131 -0
- package/docs/resource.md +2 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -7990,12 +7990,40 @@ const requestPseudoStateCheck = (element, detail) => {
|
|
|
7990
7990
|
);
|
|
7991
7991
|
}
|
|
7992
7992
|
};
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7993
|
+
/**
|
|
7994
|
+
* Called back whenever `element`'s pseudo state changes.
|
|
7995
|
+
*
|
|
7996
|
+
* Kept beside the element rather than announced to the DOM: an element that
|
|
7997
|
+
* changes state has at most one or two interested parties, each known by name
|
|
7998
|
+
* (the box drawing it, the accent color reading its computed style), and a
|
|
7999
|
+
* CustomEvent costs its allocation and its capture/bubble walk whether anyone
|
|
8000
|
+
* listens or not — paid on every element, on every state change. A lookup that
|
|
8001
|
+
* finds nothing costs nothing.
|
|
8002
|
+
*
|
|
8003
|
+
* @param {Element} element
|
|
8004
|
+
* @param {(pseudoState: object, oldPseudoState: object) => void} callback
|
|
8005
|
+
* @returns {() => void} teardown
|
|
8006
|
+
*/
|
|
8007
|
+
const subscribeToPseudoState = (element, callback) => {
|
|
8008
|
+
let subscriberSet = pseudoStateSubscriberSetWeakMap.get(element);
|
|
8009
|
+
if (!subscriberSet) {
|
|
8010
|
+
subscriberSet = new Set();
|
|
8011
|
+
pseudoStateSubscriberSetWeakMap.set(element, subscriberSet);
|
|
8012
|
+
}
|
|
8013
|
+
subscriberSet.add(callback);
|
|
8014
|
+
return () => {
|
|
8015
|
+
subscriberSet.delete(callback);
|
|
8016
|
+
};
|
|
8017
|
+
};
|
|
8018
|
+
const pseudoStateSubscriberSetWeakMap = new WeakMap();
|
|
8019
|
+
const notifyPseudoStateSubscribers = (element, value, oldValue) => {
|
|
8020
|
+
const subscriberSet = pseudoStateSubscriberSetWeakMap.get(element);
|
|
8021
|
+
if (!subscriberSet) {
|
|
8022
|
+
return;
|
|
8023
|
+
}
|
|
8024
|
+
for (const subscriber of subscriberSet) {
|
|
8025
|
+
subscriber(value, oldValue);
|
|
8026
|
+
}
|
|
7999
8027
|
};
|
|
8000
8028
|
|
|
8001
8029
|
const PSEUDO_CLASSES = {};
|
|
@@ -8809,7 +8837,7 @@ const initPseudoStyles = (
|
|
|
8809
8837
|
const onStateChange = (value, oldValue) => {
|
|
8810
8838
|
effect?.(value, oldValue);
|
|
8811
8839
|
if (elementListeningPseudoState) {
|
|
8812
|
-
|
|
8840
|
+
notifyPseudoStateSubscribers(
|
|
8813
8841
|
elementListeningPseudoState,
|
|
8814
8842
|
value,
|
|
8815
8843
|
oldValue,
|
|
@@ -8892,11 +8920,12 @@ const initPseudoStyles = (
|
|
|
8892
8920
|
onStateChange(state, oldState);
|
|
8893
8921
|
};
|
|
8894
8922
|
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8923
|
+
addTeardown(
|
|
8924
|
+
subscribeToPseudoState(element, (pseudoState, oldPseudoState) => {
|
|
8925
|
+
state = pseudoState;
|
|
8926
|
+
onStateChange(state, oldPseudoState);
|
|
8927
|
+
}),
|
|
8928
|
+
);
|
|
8900
8929
|
element.addEventListener("navi_pseudo_state_request_check", () => {
|
|
8901
8930
|
checkPseudoClasses();
|
|
8902
8931
|
});
|
|
@@ -9014,18 +9043,7 @@ const updateStyle = (element, style, preventInitialTransition) => {
|
|
|
9014
9043
|
styleKeySetToApply = new Set(styleKeySet);
|
|
9015
9044
|
styleKeySetToApply.delete("transition");
|
|
9016
9045
|
}
|
|
9017
|
-
|
|
9018
|
-
if (elementTransitionWeakMap.has(element)) {
|
|
9019
|
-
const transitionToRestore = elementTransitionWeakMap.get(element);
|
|
9020
|
-
if (transitionToRestore === undefined) {
|
|
9021
|
-
element.style.transition = "";
|
|
9022
|
-
} else {
|
|
9023
|
-
element.style.transition = transitionToRestore;
|
|
9024
|
-
}
|
|
9025
|
-
elementTransitionWeakMap.delete(element);
|
|
9026
|
-
}
|
|
9027
|
-
elementRenderedWeakSet.add(element);
|
|
9028
|
-
});
|
|
9046
|
+
afterFirstFrame(element);
|
|
9029
9047
|
}
|
|
9030
9048
|
|
|
9031
9049
|
// Apply all styles normally (excluding transition during anti-flicker)
|
|
@@ -9056,6 +9074,39 @@ const updateStyle = (element, style, preventInitialTransition) => {
|
|
|
9056
9074
|
styleKeySetWeakMap.set(element, styleKeySet);
|
|
9057
9075
|
};
|
|
9058
9076
|
|
|
9077
|
+
// One frame for every element waiting for its first one, not one frame each.
|
|
9078
|
+
// What this owes each element — put back the transition suppressed above, and
|
|
9079
|
+
// remember it has now been painted once — is a few microseconds, while asking
|
|
9080
|
+
// the browser for a frame costs more than that, and a page mounting thousands of
|
|
9081
|
+
// boxes asks thousands of times in the same tick. A Set, so an element styled
|
|
9082
|
+
// twice before the frame arrives is still one entry.
|
|
9083
|
+
const elementSetWaitingFirstFrame = new Set();
|
|
9084
|
+
let firstFrameScheduled = false;
|
|
9085
|
+
const afterFirstFrame = (element) => {
|
|
9086
|
+
elementSetWaitingFirstFrame.add(element);
|
|
9087
|
+
if (firstFrameScheduled) {
|
|
9088
|
+
return;
|
|
9089
|
+
}
|
|
9090
|
+
firstFrameScheduled = true;
|
|
9091
|
+
requestAnimationFrame(() => {
|
|
9092
|
+
firstFrameScheduled = false;
|
|
9093
|
+
const elements = [...elementSetWaitingFirstFrame];
|
|
9094
|
+
elementSetWaitingFirstFrame.clear();
|
|
9095
|
+
for (const element of elements) {
|
|
9096
|
+
if (elementTransitionWeakMap.has(element)) {
|
|
9097
|
+
const transitionToRestore = elementTransitionWeakMap.get(element);
|
|
9098
|
+
if (transitionToRestore === undefined) {
|
|
9099
|
+
element.style.transition = "";
|
|
9100
|
+
} else {
|
|
9101
|
+
element.style.transition = transitionToRestore;
|
|
9102
|
+
}
|
|
9103
|
+
elementTransitionWeakMap.delete(element);
|
|
9104
|
+
}
|
|
9105
|
+
elementRenderedWeakSet.add(element);
|
|
9106
|
+
}
|
|
9107
|
+
});
|
|
9108
|
+
};
|
|
9109
|
+
|
|
9059
9110
|
/**
|
|
9060
9111
|
* Keeps a DOM element in sync with `syncElement(el)` whenever syncElement changes.
|
|
9061
9112
|
* - If element is already mounted: runs syncElement immediately during render.
|
|
@@ -11465,9 +11516,12 @@ const useAccentColorAttributes = (
|
|
|
11465
11516
|
}
|
|
11466
11517
|
};
|
|
11467
11518
|
updateAttributes();
|
|
11468
|
-
|
|
11519
|
+
const unsubscribeFromPseudoState = subscribeToPseudoState(
|
|
11520
|
+
el,
|
|
11521
|
+
updateAttributes,
|
|
11522
|
+
);
|
|
11469
11523
|
return () => {
|
|
11470
|
-
|
|
11524
|
+
unsubscribeFromPseudoState();
|
|
11471
11525
|
el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
|
|
11472
11526
|
el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
|
|
11473
11527
|
el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
|
|
@@ -12491,10 +12545,17 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12491
12545
|
action.debug(`${action}.prerun(${stringifyForDisplay(options)})`);
|
|
12492
12546
|
return dispatchSingleAction(action, "prerun", options);
|
|
12493
12547
|
};
|
|
12548
|
+
/**
|
|
12549
|
+
* Requests the action's data. An action that is already RUNNING or
|
|
12550
|
+
* COMPLETED already has it, so the request is a no-op there: use `rerun()`
|
|
12551
|
+
* to force a fresh run ("refresh", "check now", any explicit user intent to
|
|
12552
|
+
* go back to the network).
|
|
12553
|
+
*/
|
|
12494
12554
|
const run = (options) => {
|
|
12495
12555
|
action.debug(`${action}.run(${stringifyForDisplay(options)})`);
|
|
12496
12556
|
return dispatchSingleAction(action, "run", options);
|
|
12497
12557
|
};
|
|
12558
|
+
/** Resets the action and runs it again, whatever state it is in. */
|
|
12498
12559
|
const rerun = (options) => {
|
|
12499
12560
|
action.debug(`${action}.rerun(${stringifyForDisplay(options)})`);
|
|
12500
12561
|
return dispatchSingleAction(action, "rerun", options);
|
|
@@ -17891,6 +17952,10 @@ const useExecuteAction = (
|
|
|
17891
17952
|
*/
|
|
17892
17953
|
|
|
17893
17954
|
const CLICK_TO_EXPAND_SELECTOR = "summary, [aria-expanded]";
|
|
17955
|
+
// A popup is written inside whatever opened it, but it is not part of it on
|
|
17956
|
+
// screen: a control inside a popup must not be read as a click on the region
|
|
17957
|
+
// the popup happens to be nested in.
|
|
17958
|
+
const POPUP_SELECTOR = "[navi-control='popover'], [navi-control='dialog']";
|
|
17894
17959
|
|
|
17895
17960
|
/**
|
|
17896
17961
|
* Cancels `event` when the control consumed a click that a surrounding
|
|
@@ -17916,13 +17981,29 @@ const preventClickToExpand = (element, event) => {
|
|
|
17916
17981
|
}
|
|
17917
17982
|
// From the parent: a control that opens something carries its own
|
|
17918
17983
|
// `aria-expanded` and would find itself.
|
|
17919
|
-
const clickToExpandRegion = parentElement
|
|
17984
|
+
const clickToExpandRegion = findClickToExpandRegion(parentElement);
|
|
17920
17985
|
if (!clickToExpandRegion) {
|
|
17921
17986
|
return;
|
|
17922
17987
|
}
|
|
17923
17988
|
event.preventDefault();
|
|
17924
17989
|
};
|
|
17925
17990
|
|
|
17991
|
+
const findClickToExpandRegion = (element) => {
|
|
17992
|
+
let ancestor = element;
|
|
17993
|
+
while (ancestor) {
|
|
17994
|
+
// Tested first: a popup carries `aria-expanded` of its own, so it would
|
|
17995
|
+
// otherwise pass for the region containing its own content.
|
|
17996
|
+
if (ancestor.matches(POPUP_SELECTOR)) {
|
|
17997
|
+
return null;
|
|
17998
|
+
}
|
|
17999
|
+
if (ancestor.matches(CLICK_TO_EXPAND_SELECTOR)) {
|
|
18000
|
+
return ancestor;
|
|
18001
|
+
}
|
|
18002
|
+
ancestor = ancestor.parentElement;
|
|
18003
|
+
}
|
|
18004
|
+
return null;
|
|
18005
|
+
};
|
|
18006
|
+
|
|
17926
18007
|
const clickDefaultActionIsInert = (element, event) => {
|
|
17927
18008
|
if (!isInertOnClick(element)) {
|
|
17928
18009
|
return false;
|
|
@@ -22470,6 +22551,20 @@ const useControlProps = (props, {
|
|
|
22470
22551
|
uiStateController
|
|
22471
22552
|
}];
|
|
22472
22553
|
};
|
|
22554
|
+
/**
|
|
22555
|
+
* Whether the caller has told this control what it holds. Three ways, and only
|
|
22556
|
+
* three — the controlled `value`, the uncontrolled `defaultValue`, a bound
|
|
22557
|
+
* `signal` — the same three createControlInfo below reads, in that precedence
|
|
22558
|
+
* order, to seed the state.
|
|
22559
|
+
*
|
|
22560
|
+
* None of them and the control starts with nothing: whatever it ends up holding
|
|
22561
|
+
* has to come from somewhere else — a parent form distributing its own value,
|
|
22562
|
+
* or, for a picker, the control sitting in its popup (see
|
|
22563
|
+
* useUIFacadeStateController). Anyone who needs to know whether a control can
|
|
22564
|
+
* answer for itself before anything mounts asks this rather than re-listing the
|
|
22565
|
+
* props, which is how `signal` came to be forgotten.
|
|
22566
|
+
*/
|
|
22567
|
+
const isControlValueGivenByProps = props => Object.hasOwn(props, "value") || Object.hasOwn(props, "defaultValue") || Object.hasOwn(props, "signal");
|
|
22473
22568
|
const createControlInfo = (props, {
|
|
22474
22569
|
controlType
|
|
22475
22570
|
}) => {
|
|
@@ -29754,37 +29849,14 @@ const getActionResultProperties = (action) => {
|
|
|
29754
29849
|
return actionResultPropertiesMap.get(action);
|
|
29755
29850
|
};
|
|
29756
29851
|
|
|
29757
|
-
|
|
29758
|
-
|
|
29759
|
-
|
|
29760
|
-
|
|
29761
|
-
|
|
29762
|
-
* - DELETE operation on the displayed item would display nothing in the UI (action is in IDLE state)
|
|
29763
|
-
* - PUT/PATCH operations update UI via signals, no rerun needed
|
|
29764
|
-
* - This approach minimizes unnecessary API calls
|
|
29765
|
-
*
|
|
29766
|
-
* How to handle:
|
|
29767
|
-
* - Applications can provide custom UI for deleted items (e.g., "Item not found")
|
|
29768
|
-
* - Or redirect users to appropriate pages (e.g., back to list view)
|
|
29769
|
-
*
|
|
29770
|
-
* Alternative (NOT RECOMMENDED):
|
|
29771
|
-
* - Use GET: ["DELETE"] to rerun and display 404 error received from backend
|
|
29772
|
-
* - Poor UX: users expect immediate feedback, not loading + error state
|
|
29773
|
-
*
|
|
29774
|
-
* GET_MANY: ["POST"]
|
|
29775
|
-
* - POST: New items may or may not appear in lists (depends on filters, pagination, etc.)
|
|
29776
|
-
* Backend determines visibility better than client-side logic
|
|
29777
|
-
* - DELETE: Excluded by default because:
|
|
29778
|
-
* • UI handles deletions via store signals (selectAll filters out deleted items)
|
|
29779
|
-
* • DELETE operations rarely change list content beyond item removal
|
|
29780
|
-
* • Avoids unnecessary API calls (can be overridden if needed)
|
|
29781
|
-
*/
|
|
29852
|
+
// PUT/PATCH results update the UI through the store, and DELETE resets the GET
|
|
29853
|
+
// instead of rerunning it, so a GET rerun would only ever cost a request.
|
|
29854
|
+
// A POST is the one case the client cannot decide alone: whether a new item
|
|
29855
|
+
// belongs to a list depends on filters/pagination the backend owns.
|
|
29856
|
+
// Rationale in full, plus when to override: docs/list_refresh.md
|
|
29782
29857
|
const defaultRerunOn = {
|
|
29783
29858
|
GET: false,
|
|
29784
|
-
GET_MANY: [
|
|
29785
|
-
"POST",
|
|
29786
|
-
// "DELETE"
|
|
29787
|
-
],
|
|
29859
|
+
GET_MANY: ["POST"],
|
|
29788
29860
|
};
|
|
29789
29861
|
|
|
29790
29862
|
// This handles ALL resource lifecycle logic (rerun/reset) across all resources
|
|
@@ -46296,7 +46368,7 @@ const PickerCustom = props => {
|
|
|
46296
46368
|
// opens anything. Told a value — even an empty one — the picker owns it
|
|
46297
46369
|
// and pushes it down instead, leaving the popup free to build its
|
|
46298
46370
|
// content only when it is first opened (see popup_content_mount.js).
|
|
46299
|
-
mountWhenClosed: !
|
|
46371
|
+
mountWhenClosed: !isControlValueGivenByProps(props),
|
|
46300
46372
|
// Not on pickerProps (the trigger): commands.js's own
|
|
46301
46373
|
// resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
|
|
46302
46374
|
// find where to dispatch navi_request_open/navi_request_close — and
|