@jsenv/navi 0.29.18 → 0.29.19

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.
@@ -7990,12 +7990,40 @@ const requestPseudoStateCheck = (element, detail) => {
7990
7990
  );
7991
7991
  }
7992
7992
  };
7993
- const NAVI_PSEUDO_STATE_CUSTOM_EVENT = "navi_pseudo_state";
7994
- const dispatchPseudoStateCustomEvent = (element, value, oldValue) => {
7995
- dispatchInternalCustomEvent(element, NAVI_PSEUDO_STATE_CUSTOM_EVENT, {
7996
- pseudoState: value,
7997
- oldPseudoState: oldValue,
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
- dispatchPseudoStateCustomEvent(
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
- element.addEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, (event) => {
8896
- const oldState = event.detail.oldPseudoState;
8897
- state = event.detail.pseudoState;
8898
- onStateChange(state, oldState);
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
- requestAnimationFrame(() => {
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
- el.addEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttributes);
11519
+ const unsubscribeFromPseudoState = subscribeToPseudoState(
11520
+ el,
11521
+ updateAttributes,
11522
+ );
11469
11523
  return () => {
11470
- el.removeEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttributes);
11524
+ unsubscribeFromPseudoState();
11471
11525
  el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
11472
11526
  el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
11473
11527
  el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
@@ -22470,6 +22524,20 @@ const useControlProps = (props, {
22470
22524
  uiStateController
22471
22525
  }];
22472
22526
  };
22527
+ /**
22528
+ * Whether the caller has told this control what it holds. Three ways, and only
22529
+ * three — the controlled `value`, the uncontrolled `defaultValue`, a bound
22530
+ * `signal` — the same three createControlInfo below reads, in that precedence
22531
+ * order, to seed the state.
22532
+ *
22533
+ * None of them and the control starts with nothing: whatever it ends up holding
22534
+ * has to come from somewhere else — a parent form distributing its own value,
22535
+ * or, for a picker, the control sitting in its popup (see
22536
+ * useUIFacadeStateController). Anyone who needs to know whether a control can
22537
+ * answer for itself before anything mounts asks this rather than re-listing the
22538
+ * props, which is how `signal` came to be forgotten.
22539
+ */
22540
+ const isControlValueGivenByProps = props => Object.hasOwn(props, "value") || Object.hasOwn(props, "defaultValue") || Object.hasOwn(props, "signal");
22473
22541
  const createControlInfo = (props, {
22474
22542
  controlType
22475
22543
  }) => {
@@ -46296,7 +46364,7 @@ const PickerCustom = props => {
46296
46364
  // opens anything. Told a value — even an empty one — the picker owns it
46297
46365
  // and pushes it down instead, leaving the popup free to build its
46298
46366
  // content only when it is first opened (see popup_content_mount.js).
46299
- mountWhenClosed: !Object.hasOwn(props, "value") && !Object.hasOwn(props, "defaultValue"),
46367
+ mountWhenClosed: !isControlValueGivenByProps(props),
46300
46368
  // Not on pickerProps (the trigger): commands.js's own
46301
46369
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
46302
46370
  // find where to dispatch navi_request_open/navi_request_close — and