@jsenv/navi 0.29.104 → 0.29.105

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, hasCSSSizeUnit, resolveOklchLightness, contrastColor, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, resolveCSSSize, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, hasCSSSizeUnit, resolveOklchLightness, contrastColor, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
10
10
  import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
@@ -2430,9 +2430,13 @@ const useOwnTargetHidden = (props) => {
2430
2430
  *
2431
2431
  * Beyond recursive object/array comparison it covers the edge cases `===` gets
2432
2432
  * "wrong" for equality purposes: NaN equals NaN, Date compared by time value,
2433
- * cycles don't loop (a seen-set guards circular refs), and same-type is required
2434
- * before descending. Cheap paths run first: reference equality, then the identity
2435
- * short-circuit below, then array length before element-by-element.
2433
+ * cycles don't loop (a set of the pairs being compared guards circular refs),
2434
+ * and same-type is required
2435
+ * before descending. Functions, and objects with nothing enumerable to compare
2436
+ * (a Set, a Map, an element, a URL), are equal by reference only — nothing in
2437
+ * them says whether two are "the same". Cheap paths run first: reference
2438
+ * equality, then the identity short-circuit below, then array length before
2439
+ * element-by-element.
2436
2440
  *
2437
2441
  * SYMBOL_IDENTITY. Two *different* object instances can be declared "conceptually
2438
2442
  * the same" by sharing a SYMBOL_IDENTITY value; the comparison then treats them as
@@ -2494,6 +2498,11 @@ const compareTwoJsValues = (
2494
2498
  if (aType !== bType) {
2495
2499
  return false;
2496
2500
  }
2501
+ if (aType === "function") {
2502
+ // Not the same function (reference equality came first), and nothing in
2503
+ // a function says whether two of them do the same thing.
2504
+ return false;
2505
+ }
2497
2506
  const aIsPrimitive =
2498
2507
  a === null || (aType !== "object" && aType !== "function");
2499
2508
  const bIsPrimitive =
@@ -2504,14 +2513,22 @@ const compareTwoJsValues = (
2504
2513
  if (aIsPrimitive && bIsPrimitive) {
2505
2514
  return a === b;
2506
2515
  }
2507
- if (seenSet.has(a)) {
2508
- return false;
2509
- }
2510
- if (seenSet.has(b)) {
2516
+ // Back on something still being compared: a cycle. No loop, and no answer
2517
+ // either — equal by a route that never ends is not equal.
2518
+ if (seenSet.has(a) || seenSet.has(b)) {
2511
2519
  return false;
2512
2520
  }
2521
+ // Held only while a and b are being compared, not for the rest of the
2522
+ // walk: the same object is rightly met again elsewhere — in an unordered
2523
+ // array every element of a is tried against every element of b.
2513
2524
  seenSet.add(a);
2514
2525
  seenSet.add(b);
2526
+ const result = compareComposite(a, b);
2527
+ seenSet.delete(a);
2528
+ seenSet.delete(b);
2529
+ return result;
2530
+ };
2531
+ const compareComposite = (a, b) => {
2515
2532
  const aIsArray = Array.isArray(a);
2516
2533
  const bIsArray = Array.isArray(b);
2517
2534
  if (aIsArray !== bIsArray) {
@@ -2569,25 +2586,25 @@ const compareTwoJsValues = (
2569
2586
  return true;
2570
2587
  }
2571
2588
  // Date objects must be compared by time value, not by enumerable keys (which are empty)
2572
- {
2573
- const aIsDate = a instanceof Date;
2574
- const bIsDate = b instanceof Date;
2575
- if (aIsDate !== bIsDate) {
2576
- return false;
2577
- }
2578
- if (aIsDate && bIsDate) {
2579
- const aTime = a.getTime();
2580
- const bTime = b.getTime();
2581
- if (aTime !== bTime) {
2582
- return false;
2583
- }
2584
- }
2589
+ const aIsDate = a instanceof Date;
2590
+ const bIsDate = b instanceof Date;
2591
+ if (aIsDate !== bIsDate) {
2592
+ return false;
2593
+ }
2594
+ if (aIsDate) {
2595
+ return a.getTime() === b.getTime();
2585
2596
  }
2586
2597
  const aKeys = Object.keys(a);
2587
2598
  const bKeys = Object.keys(b);
2588
2599
  if (aKeys.length !== bKeys.length) {
2589
2600
  return false;
2590
2601
  }
2602
+ if (aKeys.length === 0 && (!isPlainObject$2(a) || !isPlainObject$2(b))) {
2603
+ // A Set, a Map, an element, a URL: nothing enumerable to tell two of them
2604
+ // apart, so they are the same one or they are not — and they are not,
2605
+ // reference equality came first.
2606
+ return false;
2607
+ }
2591
2608
  if (lightKeySet) {
2592
2609
  // compare light keys first, then remaining keys
2593
2610
  // (optimization for cases where some keys are more likely to differ and/or faster to compare)
@@ -2625,6 +2642,11 @@ const compareTwoJsValues = (
2625
2642
  return compare(rootA, rootB);
2626
2643
  };
2627
2644
 
2645
+ const isPlainObject$2 = (value) => {
2646
+ const prototype = Object.getPrototypeOf(value);
2647
+ return prototype === Object.prototype || prototype === null;
2648
+ };
2649
+
2628
2650
  // Global signal registry for route template detection
2629
2651
  const globalSignalRegistry = new Map();
2630
2652
  let signalIdCounter = 0;
@@ -16708,16 +16730,10 @@ const getHowToHandleStyleProp = (name) => {
16708
16730
  }
16709
16731
  return getStyle;
16710
16732
  };
16711
- const prepareStyleValue = (
16712
- existingValue,
16713
- value,
16714
- name,
16715
- styleContext,
16716
- context,
16717
- ) => {
16733
+ const prepareStyleValue = (existingValue, value, name, styleContext) => {
16718
16734
  const stringifier = getStringifier(name);
16719
- const cssValue = stringifier(value, name, styleContext, context);
16720
- const mergedValue = mergeOneStyle(existingValue, cssValue, name, context);
16735
+ const cssValue = stringifier(value, name, styleContext);
16736
+ const mergedValue = mergeOneStyle(existingValue, cssValue, name, "css");
16721
16737
  return mergedValue;
16722
16738
  };
16723
16739
 
@@ -17806,30 +17822,61 @@ const isKeyboardModality = () => keyboardNavigationUsed;
17806
17822
  return false;
17807
17823
  };
17808
17824
 
17809
- // Shared setup for :focus and :focus-visible. Both need focusin/focusout
17810
- // listeners + a MutationObserver on aria-controls so that when the attribute
17811
- // changes while the element is focused, old and new controlled elements are
17812
- // notified to re-check their own focus state.
17813
- // extraSetup: optional (el, callback) => teardown for pseudo-class-specific
17814
- // listeners (e.g. keydown/keyup for :focus-visible).
17815
- const setupFocus = (el, callback) => {
17816
- const onFocusChange = (e) => {
17817
- callback();
17818
- notifyAriaControlled(el, e);
17819
- };
17820
- el.addEventListener("focusin", onFocusChange);
17821
- el.addEventListener("focusout", onFocusChange);
17822
- // Only observe aria-controls mutations when the element already has the
17823
- // attribute at setup time. If aria-controls is guaranteed to be set before
17824
- // initPseudoStyles runs (e.g. passed as a prop in box.jsx), this covers all
17825
- // real cases without paying the MutationObserver cost for every element.
17826
- let observer;
17827
- // if (el.hasAttribute("aria-controls")) {
17828
- observer = new MutationObserver((mutations) => {
17829
- if (!el.matches(":focus-within")) {
17830
- return;
17825
+ // One registration per element for everything focus-related, shared by
17826
+ // :focus, :focus-visible and :focus-within: they all react to the same
17827
+ // focusin/focusout, and they all re-check through the same callback (see
17828
+ // initPseudoStyles), which the Set turns into one call.
17829
+ const focusTrackingWeakMap = new WeakMap();
17830
+ const trackFocus = (el, callback) => {
17831
+ let tracking = focusTrackingWeakMap.get(el);
17832
+ if (!tracking) {
17833
+ const callbackSet = new Set();
17834
+ const onFocusChange = (e) => {
17835
+ for (const trackedCallback of callbackSet) {
17836
+ trackedCallback();
17837
+ }
17838
+ notifyAriaControlled(el, e);
17839
+ };
17840
+ el.addEventListener("focusin", onFocusChange);
17841
+ el.addEventListener("focusout", onFocusChange);
17842
+ tracking = {
17843
+ callbackSet,
17844
+ teardown: () => {
17845
+ el.removeEventListener("focusin", onFocusChange);
17846
+ el.removeEventListener("focusout", onFocusChange);
17847
+ focusTrackingWeakMap.delete(el);
17848
+ },
17849
+ };
17850
+ focusTrackingWeakMap.set(el, tracking);
17851
+ observeAriaControls();
17852
+ }
17853
+ tracking.callbackSet.add(callback);
17854
+ return () => {
17855
+ tracking.callbackSet.delete(callback);
17856
+ if (tracking.callbackSet.size === 0) {
17857
+ tracking.teardown();
17831
17858
  }
17859
+ };
17860
+ };
17861
+ // When aria-controls changes on a focused element, what it used to control
17862
+ // and what it controls now both re-check their inherited focus. One observer
17863
+ // on the document rather than one per tracked element: the attribute changes
17864
+ // rarely, on few elements, while thousands are tracked — and it can be set
17865
+ // after the element was, so "has it at setup" would miss it.
17866
+ let ariaControlsObserver = null;
17867
+ const observeAriaControls = () => {
17868
+ if (ariaControlsObserver) {
17869
+ return;
17870
+ }
17871
+ ariaControlsObserver = new MutationObserver((mutations) => {
17832
17872
  for (const mutation of mutations) {
17873
+ const el = mutation.target;
17874
+ if (!focusTrackingWeakMap.has(el)) {
17875
+ continue;
17876
+ }
17877
+ if (!el.matches(":focus-within")) {
17878
+ continue;
17879
+ }
17833
17880
  const oldIds = (mutation.oldValue || "").split(" ").filter(Boolean);
17834
17881
  for (const id of oldIds) {
17835
17882
  const controlled = document.getElementById(id);
@@ -17837,30 +17884,20 @@ const isKeyboardModality = () => keyboardNavigationUsed;
17837
17884
  requestPseudoStateCheck(controlled, {});
17838
17885
  }
17839
17886
  }
17887
+ notifyAriaControlled(el, {});
17840
17888
  }
17841
- notifyAriaControlled(el, {});
17842
17889
  });
17843
- observer.observe(el, {
17890
+ ariaControlsObserver.observe(document.documentElement, {
17891
+ subtree: true,
17844
17892
  attributes: true,
17845
17893
  attributeFilter: ["aria-controls"],
17846
17894
  attributeOldValue: true,
17847
17895
  });
17848
- // }
17849
- return () => {
17850
- el.removeEventListener("focusin", onFocusChange);
17851
- el.removeEventListener("focusout", onFocusChange);
17852
- observer?.disconnect();
17853
- };
17854
17896
  };
17855
17897
 
17856
17898
  definePseudoClass(":focus", {
17857
17899
  attribute: "data-focus",
17858
- setup: (el, callback) => {
17859
- const cleanup = setupFocus(el, callback);
17860
- return () => {
17861
- cleanup();
17862
- };
17863
- },
17900
+ setup: trackFocus,
17864
17901
  test: (el) => {
17865
17902
  if (el.matches(":focus")) {
17866
17903
  return true;
@@ -17876,9 +17913,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
17876
17913
  // No per-element keydown/keyup listener: the shared recheckFocusChainOnKey
17877
17914
  // handler re-checks the focused element (the only one a keystroke can turn
17878
17915
  // focus-visible) so a keypress stays O(1), not O(number-of-boxes).
17879
- setup: (el, callback) => {
17880
- return setupFocus(el, callback);
17881
- },
17916
+ setup: trackFocus,
17882
17917
  test: (el) => {
17883
17918
  if (isMatchingFocusVisible(el)) {
17884
17919
  return true;
@@ -17891,18 +17926,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
17891
17926
  });
17892
17927
  definePseudoClass(":focus-within", {
17893
17928
  attribute: "data-focus-within",
17894
- setup: (el, callback) => {
17895
- const onFocusChange = (e) => {
17896
- callback();
17897
- notifyAriaControlled(el, e);
17898
- };
17899
- el.addEventListener("focusin", onFocusChange);
17900
- el.addEventListener("focusout", onFocusChange);
17901
- return () => {
17902
- el.removeEventListener("focusin", onFocusChange);
17903
- el.removeEventListener("focusout", onFocusChange);
17904
- };
17905
- },
17929
+ setup: trackFocus,
17906
17930
  test: (el) => {
17907
17931
  if (el.matches(":focus-within")) {
17908
17932
  return true;
@@ -18197,8 +18221,18 @@ const initPseudoStyles = (
18197
18221
  onStateChange(state, oldPseudoState);
18198
18222
  }),
18199
18223
  );
18200
- element.addEventListener("navi_pseudo_state_request_check", () => {
18224
+ // One function for every way a re-check can be asked, so that setups
18225
+ // registering it side by side (the focus tracking, for one) hold the same
18226
+ // callback and call it once.
18227
+ const requestCheck = () => {
18201
18228
  checkPseudoClasses();
18229
+ };
18230
+ element.addEventListener("navi_pseudo_state_request_check", requestCheck);
18231
+ addTeardown(() => {
18232
+ element.removeEventListener(
18233
+ "navi_pseudo_state_request_check",
18234
+ requestCheck,
18235
+ );
18202
18236
  });
18203
18237
 
18204
18238
  for (const pseudoClass of pseudoClasses) {
@@ -18209,9 +18243,7 @@ const initPseudoStyles = (
18209
18243
  }
18210
18244
  const { setup } = pseudoClassDefinition;
18211
18245
  if (setup) {
18212
- const cleanup = setup(element, () => {
18213
- checkPseudoClasses();
18214
- });
18246
+ const cleanup = setup(element, requestCheck);
18215
18247
  addTeardown(cleanup);
18216
18248
  }
18217
18249
  }
@@ -18231,6 +18263,11 @@ const applyStyle = (
18231
18263
  return;
18232
18264
  }
18233
18265
  const styleToApply = getStyleToApply(style, pseudoState, pseudoNamedStyles);
18266
+ // The same object comes back for every state change of a box whose inline
18267
+ // style has no pseudo entry: nothing to write.
18268
+ if (appliedStyleWeakMap.get(element) === styleToApply) {
18269
+ return;
18270
+ }
18234
18271
  updateStyle(element, styleToApply, preventInitialTransition);
18235
18272
  };
18236
18273
 
@@ -18245,52 +18282,74 @@ const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
18245
18282
  ) {
18246
18283
  return styles;
18247
18284
  }
18248
-
18249
- const isMatching = (pseudoKey) => {
18250
- if (pseudoKey.startsWith("::")) {
18251
- const nextColonIndex = pseudoKey.indexOf(":", 2);
18252
- if (nextColonIndex === -1) {
18253
- return true;
18254
- }
18255
- // Handle pseudo-elements with states like "::-navi-loader:checked:disabled"
18256
- const pseudoStatesString = pseudoKey.slice(nextColonIndex);
18257
- return isMatching(pseudoStatesString);
18285
+ let style = styles;
18286
+ for (const pseudoKey of Object.keys(pseudoNamedStyles)) {
18287
+ const requiredStates = getPseudoKeyRequiredStates(pseudoKey);
18288
+ if (!requiredStates.every((state) => pseudoState[state])) {
18289
+ continue;
18258
18290
  }
18259
- const nextColonIndex = pseudoKey.indexOf(":", 1);
18260
- if (nextColonIndex === -1) {
18261
- return pseudoState[pseudoKey];
18291
+ if (style === styles) {
18292
+ style = { ...styles };
18262
18293
  }
18263
- // Handle compound pseudo-states like ":checked:disabled"
18264
- return pseudoKey
18265
- .slice(1)
18266
- .split(":")
18267
- .every((state) => pseudoState[state]);
18268
- };
18269
-
18270
- const styleToAddSet = new Set();
18271
- for (const pseudoKey of Object.keys(pseudoNamedStyles)) {
18272
- if (isMatching(pseudoKey)) {
18273
- const stylesToApply = pseudoNamedStyles[pseudoKey];
18274
- styleToAddSet.add(stylesToApply);
18294
+ // Both sides are already normalized for CSS by the box; only the
18295
+ // properties that compose (a press scale on top of a translate) go through
18296
+ // a merge, the rest is a plain override.
18297
+ const styleToAdd = pseudoNamedStyles[pseudoKey];
18298
+ for (const key of Object.keys(styleToAdd)) {
18299
+ const value = styleToAdd[key];
18300
+ if (value === undefined) {
18301
+ continue;
18302
+ }
18303
+ if (key === "transform" || key === "willChange") {
18304
+ style[key] = mergeOneStyle(style[key], value, key, "css");
18305
+ } else {
18306
+ style[key] = value;
18307
+ }
18275
18308
  }
18276
18309
  }
18277
- if (styleToAddSet.size === 0) {
18278
- return styles;
18310
+ return style;
18311
+ };
18312
+
18313
+ // The state names a pseudo key asks for, parsed once: the same few keys come
18314
+ // back on every state change of every box that has them. "::x" alone always
18315
+ // matches; "::x:a:b" and ":a:b" ask for ":a" and ":b" — the state keys as
18316
+ // checkPseudoClasses writes them, colon included.
18317
+ const pseudoKeyRequiredStatesMap = new Map();
18318
+ const getPseudoKeyRequiredStates = (pseudoKey) => {
18319
+ const cached = pseudoKeyRequiredStatesMap.get(pseudoKey);
18320
+ if (cached) {
18321
+ return cached;
18279
18322
  }
18280
- let style = styles || {};
18281
- for (const styleToAdd of styleToAddSet) {
18282
- style = mergeTwoStyles(style, styleToAdd, "css");
18323
+ let requiredStates;
18324
+ if (pseudoKey.startsWith("::")) {
18325
+ const nextColonIndex = pseudoKey.indexOf(":", 2);
18326
+ requiredStates =
18327
+ nextColonIndex === -1
18328
+ ? []
18329
+ : getPseudoKeyRequiredStates(pseudoKey.slice(nextColonIndex));
18330
+ } else {
18331
+ const nextColonIndex = pseudoKey.indexOf(":", 1);
18332
+ requiredStates =
18333
+ nextColonIndex === -1
18334
+ ? [pseudoKey]
18335
+ : pseudoKey
18336
+ .slice(1)
18337
+ .split(":")
18338
+ .map((state) => `:${state}`);
18283
18339
  }
18284
- return style;
18340
+ pseudoKeyRequiredStatesMap.set(pseudoKey, requiredStates);
18341
+ return requiredStates;
18285
18342
  };
18286
18343
 
18287
- const styleKeySetWeakMap = new WeakMap();
18344
+ // element the style object last written to it, so the next one is written
18345
+ // as a difference: the values that changed, the keys it no longer has.
18346
+ const appliedStyleWeakMap = new WeakMap();
18288
18347
  const elementTransitionWeakMap = new WeakMap();
18289
18348
  const elementRenderedWeakSet = new WeakSet();
18290
- const NO_STYLE_KEY_SET = new Set();
18349
+ const NO_STYLE = {};
18291
18350
  const updateStyle = (element, style, preventInitialTransition) => {
18292
- const styleKeySet = style ? new Set(Object.keys(style)) : NO_STYLE_KEY_SET;
18293
- const oldStyleKeySet = styleKeySetWeakMap.get(element) || NO_STYLE_KEY_SET;
18351
+ const styleToApply = style || NO_STYLE;
18352
+ const styleApplied = appliedStyleWeakMap.get(element) || NO_STYLE;
18294
18353
  // TRANSITION ANTI-FLICKER STRATEGY:
18295
18354
  // Problem: When setting both transition and styled properties simultaneously
18296
18355
  // (e.g., el.style.transition = "border-radius 0.3s ease"; el.style.borderRadius = "20px"),
@@ -18300,32 +18359,32 @@ const updateStyle = (element, style, preventInitialTransition) => {
18300
18359
  // transition to "none", then restore the intended transition after the frame completes.
18301
18360
  // We handle multiple updateStyle calls in the same frame gracefully - only one
18302
18361
  // requestAnimationFrame is scheduled per element, and the final transition value wins.
18303
- let styleKeySetToApply = styleKeySet;
18362
+ let skipTransition = false;
18304
18363
  if (!elementRenderedWeakSet.has(element)) {
18305
- const hasTransition = styleKeySet.has("transition");
18364
+ const hasTransition = Object.hasOwn(styleToApply, "transition");
18306
18365
  if (hasTransition || preventInitialTransition) {
18307
- if (elementTransitionWeakMap.has(element)) {
18308
- elementTransitionWeakMap.set(element, style?.transition);
18309
- } else {
18366
+ if (!elementTransitionWeakMap.has(element)) {
18310
18367
  element.style.transition = "none";
18311
- elementTransitionWeakMap.set(element, style?.transition);
18312
18368
  }
18313
- // Don't apply the transition property now - we've set it to "none" temporarily
18314
- styleKeySetToApply = new Set(styleKeySet);
18315
- styleKeySetToApply.delete("transition");
18369
+ elementTransitionWeakMap.set(element, styleToApply.transition);
18370
+ // Stays "none" until the first frame puts the intended value back
18371
+ skipTransition = true;
18316
18372
  }
18317
18373
  afterFirstFrame(element);
18318
18374
  }
18319
18375
 
18320
- // Apply all styles normally (excluding transition during anti-flicker)
18321
- const keysToDelete = new Set(oldStyleKeySet);
18322
- for (const key of styleKeySetToApply) {
18323
- const value = style[key];
18376
+ for (const key of Object.keys(styleToApply)) {
18377
+ const value = styleToApply[key];
18324
18378
  if (value === undefined || value === null) {
18325
- // Treat undefined/null as "remove" leave key in keysToDelete
18379
+ // a removal: handled below with the keys this style no longer has
18380
+ continue;
18381
+ }
18382
+ if (skipTransition && key === "transition") {
18383
+ continue;
18384
+ }
18385
+ if (styleApplied[key] === value) {
18326
18386
  continue;
18327
18387
  }
18328
- keysToDelete.delete(key);
18329
18388
  if (key.startsWith("--")) {
18330
18389
  element.style.setProperty(key, value);
18331
18390
  } else {
@@ -18333,8 +18392,15 @@ const updateStyle = (element, style, preventInitialTransition) => {
18333
18392
  }
18334
18393
  }
18335
18394
 
18336
- // Remove obsolete styles
18337
- for (const key of keysToDelete) {
18395
+ for (const key of Object.keys(styleApplied)) {
18396
+ const previousValue = styleApplied[key];
18397
+ if (previousValue === undefined || previousValue === null) {
18398
+ continue;
18399
+ }
18400
+ const value = styleToApply[key];
18401
+ if (value !== undefined && value !== null) {
18402
+ continue;
18403
+ }
18338
18404
  if (key.startsWith("--")) {
18339
18405
  element.style.removeProperty(key);
18340
18406
  } else {
@@ -18342,7 +18408,7 @@ const updateStyle = (element, style, preventInitialTransition) => {
18342
18408
  }
18343
18409
  }
18344
18410
 
18345
- styleKeySetWeakMap.set(element, styleKeySet);
18411
+ appliedStyleWeakMap.set(element, styleToApply);
18346
18412
  };
18347
18413
 
18348
18414
  // One frame for every element waiting for its first one, not one frame each.
@@ -18394,8 +18460,10 @@ const useComposeElementRef = (syncElement, externalRef) => {
18394
18460
  const cleanupRef = useRef(null);
18395
18461
  const elRef = useRef(null);
18396
18462
  const prevSyncElementRef = useRef(undefined);
18397
- const refCallbackRef = useRef(null);
18463
+ const stableRef = useRef(null);
18398
18464
  const externalRefRef = useRef(externalRef);
18465
+ const syncElementRef = useRef(syncElement);
18466
+ syncElementRef.current = syncElement;
18399
18467
  // Detect external ref identity change between renders. The refCallback is
18400
18468
  // stable across renders, so when the parent passes a new ref object (or
18401
18469
  // switches from null to a ref), Preact does NOT re-fire the callback while
@@ -18419,24 +18487,24 @@ const useComposeElementRef = (syncElement, externalRef) => {
18419
18487
  }
18420
18488
  externalRefRef.current = externalRef;
18421
18489
 
18422
- const runSync = (el) => {
18423
- if (cleanupRef.current) {
18424
- cleanupRef.current();
18425
- cleanupRef.current = null;
18426
- }
18427
- prevSyncElementRef.current = syncElement;
18428
- const cleanup = syncElement(el);
18429
- if (typeof cleanup === "function") {
18430
- cleanupRef.current = cleanup;
18431
- }
18432
- };
18433
-
18434
- // If element already mounted, re-sync when syncElement reference changed.
18435
- if (elRef.current && syncElement !== prevSyncElementRef.current) {
18436
- runSync(elRef.current);
18437
- }
18438
-
18439
- if (!refCallbackRef.current) {
18490
+ if (!stableRef.current) {
18491
+ // Created once, like the ref callback that calls it, and reading the sync
18492
+ // function through a ref for that reason: the element can be replaced long
18493
+ // after the first render — a tag that changes, a box hidden then shown —
18494
+ // and what the new element gets must be the current render's sync, not
18495
+ // the one the first render closed over.
18496
+ const runSync = (el) => {
18497
+ if (cleanupRef.current) {
18498
+ cleanupRef.current();
18499
+ cleanupRef.current = null;
18500
+ }
18501
+ const syncElementNow = syncElementRef.current;
18502
+ prevSyncElementRef.current = syncElementNow;
18503
+ const cleanup = syncElementNow(el);
18504
+ if (typeof cleanup === "function") {
18505
+ cleanupRef.current = cleanup;
18506
+ }
18507
+ };
18440
18508
  const refCallback = (el) => {
18441
18509
  elRef.current = el;
18442
18510
  // Keep .current in sync immediately so useEffect callbacks that read
@@ -18460,10 +18528,15 @@ const useComposeElementRef = (syncElement, externalRef) => {
18460
18528
  prevSyncElementRef.current = undefined;
18461
18529
  }
18462
18530
  };
18463
- refCallbackRef.current = refCallback;
18531
+ stableRef.current = { refCallback, runSync };
18532
+ }
18533
+ const { refCallback, runSync } = stableRef.current;
18534
+
18535
+ // If element already mounted, re-sync when syncElement reference changed.
18536
+ if (elRef.current && syncElement !== prevSyncElementRef.current) {
18537
+ runSync(elRef.current);
18464
18538
  }
18465
18539
 
18466
- const refCallback = refCallbackRef.current;
18467
18540
  refCallback.current = elRef.current;
18468
18541
  return refCallback;
18469
18542
  };
@@ -18657,12 +18730,6 @@ import.meta.css = [/* css */`
18657
18730
  told it may, and without that the body grows instead of scrolling */
18658
18731
  min-height: 0;
18659
18732
  flex: 0 1 auto;
18660
- /* Overflow makes it focusable via tab: apply the outline styles */
18661
- outline-width: var(--navi-focus-outline-width);
18662
- /* Outline must appear ON the body, not outside */
18663
- /* Because for instance when body is within dialog or slide with overflow: hidden it would not be visible */
18664
- outline-offset: calc(-1 * var(--navi-focus-outline-width));
18665
- overflow: auto;
18666
18733
 
18667
18734
  /* The same reading as the header's corners above, on all four: a body
18668
18735
  follows the corners of the box it is drawn in — which is also what
@@ -18671,6 +18738,12 @@ import.meta.css = [/* css */`
18671
18738
  border-top-right-radius: inherit;
18672
18739
  border-bottom-right-radius: inherit;
18673
18740
  border-bottom-left-radius: inherit;
18741
+ /* Overflow makes it focusable via tab: apply the outline styles */
18742
+ outline-width: var(--navi-focus-outline-width);
18743
+ /* Outline must appear ON the body, not outside */
18744
+ /* Because for instance when body is within dialog or slide with overflow: hidden it would not be visible */
18745
+ outline-offset: calc(-1 * var(--navi-focus-outline-width));
18746
+ overflow: auto;
18674
18747
 
18675
18748
  &:focus-visible {
18676
18749
  outline-style: solid;
@@ -18817,6 +18890,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
18817
18890
  * childPropSet?: Set<string>,
18818
18891
  * preventInitialTransition?: boolean,
18819
18892
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
18893
+ * ownTarget?: boolean | "refuse" | "always",
18820
18894
  * children?: import("ignore:preact").ComponentChildren,
18821
18895
  * [key: string]: any,
18822
18896
  * }>}
@@ -18824,10 +18898,94 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
18824
18898
  const Box = props => {
18825
18899
  const {
18826
18900
  ref,
18901
+ children,
18902
+ separator,
18903
+ interactions,
18904
+ ...computeProps
18905
+ } = props;
18906
+ const parentBoxFlow = useContext(BoxFlowContext);
18907
+ // Which interactions this box answers, and with what. Read here rather than
18908
+ // on the control, so a swipe or a hold can be declared on anything — a row, a
18909
+ // card, a block of text — and reach the control it belongs to (which is what
18910
+ // carries the action, and what knows it is disabled) by looking for it.
18911
+ // Read through a ref by the effect below: what an interaction DOES is this
18912
+ // render, while WHEN it happens is wired once, at mount (see
18913
+ // useInteractionsEffect).
18914
+ const interactionsRef = useRef(null);
18915
+ interactionsRef.current = resolveInteractions(interactions);
18916
+
18917
+ // What the props say about this box is worked out once per distinct set of
18918
+ // them: a parent re-rendering hands every box below it a new props object,
18919
+ // and most of the time nothing in it has changed. Handlers are the
18920
+ // exception — a closure is new on every render — so they are compared by
18921
+ // name only and put back fresh, see withCurrentHandlers.
18922
+ const renderMemoRef = useRef(null);
18923
+ const renderMemo = renderMemoRef.current;
18924
+ let computed;
18925
+ if (renderMemo && renderMemo.parentBoxFlow === parentBoxFlow && arePropsEquivalent(renderMemo.props, computeProps)) {
18926
+ computed = withCurrentHandlers(renderMemo.computed, computeProps);
18927
+ } else {
18928
+ computed = computeBox(computeProps, parentBoxFlow);
18929
+ }
18930
+ renderMemoRef.current = {
18931
+ props: computeProps,
18932
+ parentBoxFlow,
18933
+ computed
18934
+ };
18935
+ const {
18936
+ TagName,
18937
+ boxFlow,
18938
+ boxFlowIsDefault,
18939
+ row,
18940
+ column,
18941
+ aspectRatio,
18942
+ visualSelector,
18943
+ innerClassName,
18944
+ selfForwardedProps,
18945
+ childForwardedProps,
18946
+ styleDeps
18947
+ } = computed;
18948
+ const syncBox = useCallback(computed.syncBox, styleDeps);
18949
+ const finalRef = useComposeElementRef(syncBox, ref);
18950
+ useInteractionsEffect(finalRef, interactionsRef);
18951
+ let innerChildren = children;
18952
+ if (separator) {
18953
+ // Flatten nested arrays (e.g., from .map()) to treat each element as individual child
18954
+ innerChildren = applySeparatorOnChildren(innerChildren, separator);
18955
+ }
18956
+
18957
+ // When hasChildUsingForwardedProps is used it means
18958
+ // Some/all the children needs to access remainingProps
18959
+ // to render and will provide a function to do so.
18960
+ if (props.hasChildUsingForwardedProps) {
18961
+ innerChildren = jsx(BoxForwardedPropsContext.Provider, {
18962
+ value: childForwardedProps,
18963
+ children: innerChildren
18964
+ });
18965
+ }
18966
+ return jsx(TagName, {
18967
+ ref: finalRef,
18968
+ className: innerClassName,
18969
+ "navi-box-flow": boxFlowIsDefault ? undefined : boxFlow,
18970
+ "navi-box-flow-row": row ? "" : undefined,
18971
+ "navi-box-flow-column": column ? "" : undefined,
18972
+ "navi-aspect-ratio": aspectRatio ? aspectRatio : undefined,
18973
+ "data-visual-selector": visualSelector,
18974
+ ...selfForwardedProps,
18975
+ children: jsx(BoxFlowContext.Provider, {
18976
+ value: boxFlow,
18977
+ children: innerChildren
18978
+ })
18979
+ });
18980
+ };
18981
+
18982
+ // Everything the props decide, for the JSX and for the DOM sync. Pure, which
18983
+ // is what lets Box keep the previous result when the props are equivalent.
18984
+ const computeBox = (props, parentBoxFlow) => {
18985
+ const {
18827
18986
  as: asProp = "div",
18828
18987
  baseClassName,
18829
18988
  className,
18830
- baseStyle,
18831
18989
  // style management
18832
18990
  style,
18833
18991
  styleCSSVars = STYLE_CSS_VARS_DEFAULT,
@@ -18853,8 +19011,6 @@ const Box = props => {
18853
19011
  // (when transition is set via props, this is done automatically)
18854
19012
  // so this prop is useful only when transition is enabled from "outside" (via CSS)
18855
19013
  preventInitialTransition,
18856
- children,
18857
- separator,
18858
19014
  // Layout roles inside a scrolling container (a Dialog, a Popover): the
18859
19015
  // header stays at the top and the footer at the bottom while the rest
18860
19016
  // scrolls, or — when a body is present — the body is what scrolls and the
@@ -18864,22 +19020,24 @@ const Box = props => {
18864
19020
  header,
18865
19021
  footer,
18866
19022
  body,
18867
- // Which interactions this box answers, and with what. Read here rather than
18868
- // on the control, so a swipe or a hold can be declared on anything — a row, a
18869
- // card, a block of text and reach the control it belongs to (which is what
18870
- // carries the action, and what knows it is disabled) by looking for it.
18871
- interactions,
19023
+ // A press landing here is aimed AT this box, not at whatever it sits in — a
19024
+ // cross an application draws in a card's corner, a badge on a row that
19025
+ // travels. Writing the attribute is the whole of it here: it is read off
19026
+ // the DOM by the controls above and by the gesture readers, and what an
19027
+ // affordance makes of the read-only around it is a question only a control
19028
+ // can answer (see own_target.js).
19029
+ ownTarget,
18872
19030
  ...rest
18873
19031
  } = props;
19032
+ if (ownTarget) {
19033
+ rest[OWN_TARGET_ATTRIBUTE] = typeof ownTarget === "string" ? ownTarget : "";
19034
+ }
18874
19035
  let as = asProp;
18875
19036
 
18876
19037
  // A box that scrolls is what gives header/footer/body their meaning, and
18877
19038
  // saying overflow="auto" is already saying it — no second prop for the same
18878
19039
  // fact. Dialog and Popover get it the same way, by asking for that overflow.
18879
- const scrolls = ["overflow", "overflowX", "overflowY"].some(name => {
18880
- const value = rest[name];
18881
- return value === "auto" || value === "scroll";
18882
- });
19040
+ const scrolls = isScrollingOverflow(rest.overflow) || isScrollingOverflow(rest.overflowX) || isScrollingOverflow(rest.overflowY);
18883
19041
  // <header>/<footer> rather than a div: the role is exactly what those tags
18884
19042
  // mean, and a screen reader gets it for free. The body stays a div — <main>
18885
19043
  // means "the main content of the document", which a popup's body is not.
@@ -18915,8 +19073,6 @@ const Box = props => {
18915
19073
  }
18916
19074
  }
18917
19075
  const defaultDisplay = getDefaultDisplay(TagName);
18918
- // Read the parent flow early so we can use it when display="inherit" is requested.
18919
- const parentBoxFlow = useContext(BoxFlowContext);
18920
19076
  let {
18921
19077
  inline,
18922
19078
  block,
@@ -18992,12 +19148,6 @@ const Box = props => {
18992
19148
  }
18993
19149
  const boxFlowIsDefault = boxFlow === defaultDisplay;
18994
19150
 
18995
- // Read through a ref by the effect below: what an interaction DOES is this
18996
- // render, while WHEN it happens is wired once, at mount (see
18997
- // useInteractionsEffect).
18998
- const interactionsRef = useRef(null);
18999
- interactionsRef.current = resolveInteractions(interactions);
19000
- const remainingPropKeySet = new Set(Object.keys(rest));
19001
19151
  // The box is only a frame and one of its descendants IS the component (a Button
19002
19152
  // and its content): everything, event handlers included, belongs to that
19003
19153
  // descendant.
@@ -19005,15 +19155,20 @@ const Box = props => {
19005
19155
  const innerClassName = withPropsClassName(baseClassName, className);
19006
19156
  const selfForwardedProps = {};
19007
19157
  const childForwardedProps = {};
19008
- let finalRef;
19158
+ let styleDeps;
19159
+ let syncBox;
19009
19160
  {
19010
- const styleDeps = [
19161
+ styleDeps = [
19011
19162
  // Layout and alignment props
19012
19163
  parentBoxFlow, boxFlow,
19013
19164
  // Style context dependencies
19014
19165
  styleCSSVars, pseudoClasses, pseudoElements,
19015
19166
  // Selectors
19016
19167
  visualSelector, pseudoStateSelector, preventInitialTransition];
19168
+ // The pseudo state goes into the deps as key/value entries and never as the
19169
+ // object itself: a control builds that object on every render, and its
19170
+ // identity in the deps would re-run the sync — listeners, observers, style
19171
+ // writes — on each of them, for a state that has not changed.
19017
19172
  let innerPseudoState;
19018
19173
  if (basePseudoState && pseudoState) {
19019
19174
  innerPseudoState = {};
@@ -19023,30 +19178,28 @@ const Box = props => {
19023
19178
  if (pseudoStateKeySet.has(key)) {
19024
19179
  pseudoStateKeySet.delete(key);
19025
19180
  const value = pseudoState[key];
19026
- styleDeps.push(value);
19181
+ styleDeps.push(key, value);
19027
19182
  innerPseudoState[key] = value;
19028
19183
  } else {
19029
19184
  const value = basePseudoState[key];
19030
- styleDeps.push(value);
19185
+ styleDeps.push(key, value);
19031
19186
  innerPseudoState[key] = value;
19032
19187
  }
19033
19188
  }
19034
19189
  for (const key of pseudoStateKeySet) {
19035
19190
  const value = pseudoState[key];
19036
- styleDeps.push(value);
19191
+ styleDeps.push(key, value);
19037
19192
  innerPseudoState[key] = value;
19038
19193
  }
19039
19194
  } else if (basePseudoState) {
19040
19195
  innerPseudoState = basePseudoState;
19041
19196
  for (const key of Object.keys(basePseudoState)) {
19042
- const value = basePseudoState[key];
19043
- styleDeps.push(value);
19197
+ styleDeps.push(key, basePseudoState[key]);
19044
19198
  }
19045
19199
  } else if (pseudoState) {
19046
19200
  innerPseudoState = pseudoState;
19047
19201
  for (const key of Object.keys(pseudoState)) {
19048
- const value = pseudoState[key];
19049
- styleDeps.push(value);
19202
+ styleDeps.push(key, pseudoState[key]);
19050
19203
  }
19051
19204
  } else {
19052
19205
  innerPseudoState = PSEUDO_STATE_DEFAULT;
@@ -19064,8 +19217,8 @@ const Box = props => {
19064
19217
  };
19065
19218
  let boxPseudoNamedStyles = PSEUDO_NAMED_STYLES_DEFAULT;
19066
19219
  const canForwardToChild = hasChildUsingForwardedProps;
19067
- const addStyle = (value, name, styleContext, stylesTarget, context) => {
19068
- const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext, context);
19220
+ const addStyle = (value, name, styleContext, stylesTarget) => {
19221
+ const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext);
19069
19222
  const cssVar = styleContext.styleCSSVars[name];
19070
19223
  if (cssVar) {
19071
19224
  addCSSVar(mergedValue, cssVar, stylesTarget);
@@ -19087,20 +19240,20 @@ const Box = props => {
19087
19240
  styleDeps.push(name, value); // impact box style -> add to deps
19088
19241
  stylesTarget[name] = value;
19089
19242
  };
19090
- const addStyleMaybeForwarding = (value, name, styleContext, stylesTarget, context, visualChildPropStrategy) => {
19243
+ const addStyleMaybeForwarding = (value, name, styleContext, stylesTarget, visualChildPropStrategy) => {
19091
19244
  if (!visualChildPropStrategy) {
19092
- addStyle(value, name, styleContext, stylesTarget, context);
19245
+ addStyle(value, name, styleContext, stylesTarget);
19093
19246
  return false;
19094
19247
  }
19095
19248
  const cssVar = styleCSSVars[name];
19096
19249
  if (cssVar) {
19097
19250
  // css var wins over visual child handling
19098
- addStyle(value, name, styleContext, stylesTarget, context);
19251
+ addStyle(value, name, styleContext, stylesTarget);
19099
19252
  return false;
19100
19253
  }
19101
19254
  if (visualChildPropStrategy === "copy") {
19102
19255
  // we stylyze ourself + forward prop to the child
19103
- addStyle(value, name, styleContext, stylesTarget, context);
19256
+ addStyle(value, name, styleContext, stylesTarget);
19104
19257
  }
19105
19258
  if (!canForwardToChild) {
19106
19259
  return false;
@@ -19113,13 +19266,15 @@ const Box = props => {
19113
19266
  // style={{ ":hover": { backgroundColor: "red" } }}
19114
19267
  // then we'll track ":hover" state changes even for basic elements like <div>
19115
19268
  const pseudoClassesFromStyleSet = new Set();
19116
- boxPseudoNamedStyles = {};
19117
19269
  const visitProp = (value, name, styleContext, boxStylesTarget, styleOrigin) => {
19118
19270
  const isPseudoElement = name.startsWith("::");
19119
19271
  const isPseudoClass = name.startsWith(":");
19120
19272
  if (isPseudoElement || isPseudoClass) {
19121
19273
  styleDeps.push(name);
19122
19274
  pseudoClassesFromStyleSet.add(name);
19275
+ if (boxPseudoNamedStyles === PSEUDO_NAMED_STYLES_DEFAULT) {
19276
+ boxPseudoNamedStyles = {};
19277
+ }
19123
19278
  const pseudoStyleContext = {
19124
19279
  ...styleContext,
19125
19280
  styleCSSVars: {
@@ -19140,18 +19295,16 @@ const Box = props => {
19140
19295
  const pseudoClassStyles = {};
19141
19296
  for (const key of pseudoStyleKeys) {
19142
19297
  visitProp(value[key], key, pseudoStyleContext, pseudoClassStyles, "pseudo_style");
19143
- boxPseudoNamedStyles[name] = pseudoClassStyles;
19144
19298
  }
19299
+ boxPseudoNamedStyles[name] = pseudoClassStyles;
19145
19300
  return;
19146
19301
  }
19147
- const context = styleOrigin === "base_style" ? "js" : "css";
19148
- const isCss = styleOrigin === "base_style" || styleOrigin === "style";
19149
- if (isCss) {
19150
- addStyle(value, name, styleContext, boxStylesTarget, context);
19302
+ if (styleOrigin === "style") {
19303
+ addStyle(value, name, styleContext, boxStylesTarget);
19151
19304
  return;
19152
19305
  }
19153
19306
  if (name.startsWith("--")) {
19154
- addStyle(value, name, styleContext, boxStylesTarget, context);
19307
+ addStyle(value, name, styleContext, boxStylesTarget);
19155
19308
  return;
19156
19309
  }
19157
19310
  const isPseudoStyle = styleOrigin === "pseudo_style";
@@ -19163,7 +19316,7 @@ const Box = props => {
19163
19316
  if (
19164
19317
  // prop name === css style name
19165
19318
  !getStyle) {
19166
- const needForwarding = addStyleMaybeForwarding(value, name, styleContext, boxStylesTarget, context, visualChildPropStrategy);
19319
+ const needForwarding = addStyleMaybeForwarding(value, name, styleContext, boxStylesTarget, visualChildPropStrategy);
19167
19320
  if (needForwarding) {
19168
19321
  if (isPseudoStyle) ; else {
19169
19322
  childForwardedProps[name] = value;
@@ -19178,7 +19331,7 @@ const Box = props => {
19178
19331
  let needForwarding = false;
19179
19332
  for (const styleName of Object.keys(cssValues)) {
19180
19333
  const cssValue = cssValues[styleName];
19181
- needForwarding = addStyleMaybeForwarding(cssValue, styleName, styleContext, boxStylesTarget, context, visualChildPropStrategy);
19334
+ needForwarding = addStyleMaybeForwarding(cssValue, styleName, styleContext, boxStylesTarget, visualChildPropStrategy);
19182
19335
  }
19183
19336
  if (needForwarding) {
19184
19337
  if (isPseudoStyle) ; else {
@@ -19214,13 +19367,7 @@ const Box = props => {
19214
19367
  }
19215
19368
  return;
19216
19369
  };
19217
- if (baseStyle) {
19218
- for (const key of baseStyle) {
19219
- const value = baseStyle[key];
19220
- visitProp(value, key, styleContext, boxStyles, "baseStyle");
19221
- }
19222
- }
19223
- for (const propName of remainingPropKeySet) {
19370
+ for (const propName of Object.keys(rest)) {
19224
19371
  const propValue = rest[propName];
19225
19372
  if (baseChildPropSet?.has(propName) || childPropSet?.has(propName)) {
19226
19373
  if (canForwardToChild) {
@@ -19230,9 +19377,6 @@ const Box = props => {
19230
19377
  }
19231
19378
  continue;
19232
19379
  }
19233
- if (canForwardToChild && toCopySet.has(propName)) {
19234
- childForwardedProps[propName] = propValue;
19235
- }
19236
19380
  const isDataAttribute = propName.startsWith("data-");
19237
19381
  if (isDataAttribute) {
19238
19382
  selfForwardedProps[propName] = propValue;
@@ -19274,7 +19418,6 @@ const Box = props => {
19274
19418
  visitProp(styleValue, styleName, styleContext, boxStyles, "style");
19275
19419
  }
19276
19420
  }
19277
- styleDeps.push(pseudoStateSelector, innerPseudoState);
19278
19421
  let innerPseudoClasses;
19279
19422
  if (pseudoClassesFromStyleSet.size) {
19280
19423
  innerPseudoClasses = [...pseudoClasses];
@@ -19291,7 +19434,7 @@ const Box = props => {
19291
19434
  styleDeps.push(...pseudoClasses);
19292
19435
  }
19293
19436
  }
19294
- const syncBox = useCallback(boxEl => {
19437
+ syncBox = boxEl => {
19295
19438
  const pseudoStateEl = pseudoStateSelector ? boxEl.querySelector(pseudoStateSelector) : boxEl;
19296
19439
  if (!pseudoStateEl) {
19297
19440
  console.error(`pseudoStateSelector "${pseudoStateSelector}" did not match any element inside the box`, boxEl);
@@ -19306,42 +19449,24 @@ const Box = props => {
19306
19449
  elementToImpact: boxEl,
19307
19450
  elementListeningPseudoState: visualEl === pseudoStateEl ? null : visualEl
19308
19451
  });
19309
- }, styleDeps);
19310
- finalRef = useComposeElementRef(syncBox, ref);
19311
- }
19312
- useInteractionsEffect(finalRef, interactionsRef);
19313
- let innerChildren = children;
19314
- if (separator) {
19315
- // Flatten nested arrays (e.g., from .map()) to treat each element as individual child
19316
- innerChildren = applySeparatorOnChildren(innerChildren, separator);
19317
- }
19318
-
19319
- // When hasChildUsingForwardedProps is used it means
19320
- // Some/all the children needs to access remainingProps
19321
- // to render and will provide a function to do so.
19322
- if (hasChildUsingForwardedProps) {
19323
- innerChildren = jsx(BoxForwardedPropsContext.Provider, {
19324
- value: childForwardedProps,
19325
- children: innerChildren
19326
- });
19452
+ };
19327
19453
  }
19328
19454
  const aspectRatio = rest.square || rest.circle ? "1/1" : rest.aspectRatio;
19329
- return jsx(TagName, {
19330
- ref: finalRef,
19331
- className: innerClassName,
19332
- "navi-box-flow": boxFlowIsDefault ? undefined : boxFlow,
19333
- "navi-box-flow-row": row ? "" : undefined,
19334
- "navi-box-flow-column": column ? "" : undefined,
19335
- "navi-aspect-ratio": aspectRatio ? aspectRatio : undefined,
19336
- "data-visual-selector": visualSelector,
19337
- ...selfForwardedProps,
19338
- children: jsx(BoxFlowContext.Provider, {
19339
- value: boxFlow,
19340
- children: innerChildren
19341
- })
19342
- });
19455
+ return {
19456
+ TagName,
19457
+ boxFlow,
19458
+ boxFlowIsDefault,
19459
+ row,
19460
+ column,
19461
+ aspectRatio,
19462
+ visualSelector,
19463
+ innerClassName,
19464
+ selfForwardedProps,
19465
+ childForwardedProps,
19466
+ styleDeps,
19467
+ syncBox
19468
+ };
19343
19469
  };
19344
- const toCopySet = new Set([]);
19345
19470
  const applySeparatorOnChildren = (children, separator) => {
19346
19471
  const flattenedChildren = toChildArray(children);
19347
19472
  if (flattenedChildren.length <= 1) {
@@ -19387,6 +19512,62 @@ const isNonZeroSpacing = value => {
19387
19512
  }
19388
19513
  return true;
19389
19514
  };
19515
+ const isScrollingOverflow = value => value === "auto" || value === "scroll";
19516
+
19517
+ // Same props as far as computeBox is concerned. Handlers count by name only,
19518
+ // see withCurrentHandlers; no style or state key starts with "on".
19519
+ const arePropsEquivalent = (previousProps, props) => compareTwoJsValues(previousProps, props, {
19520
+ keyComparator: comparePropAt
19521
+ });
19522
+ const comparePropAt = (a, b, key, recurse) => {
19523
+ if (typeof key === "string" && key.startsWith("on")) {
19524
+ return true;
19525
+ }
19526
+ return recurse(a, b);
19527
+ };
19528
+ // The previous computation with this render's handlers: a handler goes where
19529
+ // its name went the previous time, self or child, the split being decided by
19530
+ // props that are the same. The forwarded objects keep their identity when no
19531
+ // handler changed, so what reads them through context sees nothing new.
19532
+ const withCurrentHandlers = (computed, props) => {
19533
+ let {
19534
+ selfForwardedProps,
19535
+ childForwardedProps
19536
+ } = computed;
19537
+ for (const key of Object.keys(props)) {
19538
+ if (!key.startsWith("on")) {
19539
+ continue;
19540
+ }
19541
+ const value = props[key];
19542
+ if (Object.hasOwn(childForwardedProps, key)) {
19543
+ if (childForwardedProps[key] !== value) {
19544
+ if (childForwardedProps === computed.childForwardedProps) {
19545
+ childForwardedProps = {
19546
+ ...childForwardedProps
19547
+ };
19548
+ }
19549
+ childForwardedProps[key] = value;
19550
+ }
19551
+ } else if (Object.hasOwn(selfForwardedProps, key)) {
19552
+ if (selfForwardedProps[key] !== value) {
19553
+ if (selfForwardedProps === computed.selfForwardedProps) {
19554
+ selfForwardedProps = {
19555
+ ...selfForwardedProps
19556
+ };
19557
+ }
19558
+ selfForwardedProps[key] = value;
19559
+ }
19560
+ }
19561
+ }
19562
+ if (selfForwardedProps === computed.selfForwardedProps && childForwardedProps === computed.childForwardedProps) {
19563
+ return computed;
19564
+ }
19565
+ return {
19566
+ ...computed,
19567
+ selfForwardedProps,
19568
+ childForwardedProps
19569
+ };
19570
+ };
19390
19571
 
19391
19572
  const useDebounceTrue = (value, delay = 300) => {
19392
19573
  const [debouncedTrue, setDebouncedTrue] = useState(false);
@@ -20844,14 +21025,6 @@ const shouldInjectSpacingBetween = (left, right) => {
20844
21025
  * internally for overlays such as the skeleton container.
20845
21026
  */
20846
21027
  const Text = props => {
20847
- const defaultRef = useRef();
20848
- const ref = props.ref || defaultRef;
20849
- return jsx(TextDispatcher, {
20850
- ...props,
20851
- ref: ref
20852
- });
20853
- };
20854
- const TextDispatcher = props => {
20855
21028
  if (props.loading || props.skeleton) {
20856
21029
  return jsx(TextSkeleton, {
20857
21030
  ...props
@@ -20877,9 +21050,8 @@ const TextDispatcher = props => {
20877
21050
  });
20878
21051
  };
20879
21052
  const TextShrinkWrap = props => {
20880
- const {
20881
- ref
20882
- } = props;
21053
+ const defaultRef = useRef();
21054
+ const ref = props.ref || defaultRef;
20883
21055
  const applyWidth = () => {
20884
21056
  const text = ref.current;
20885
21057
  // Reset any previously forced width so we measure the natural size
@@ -20920,8 +21092,9 @@ const TextShrinkWrap = props => {
20920
21092
  window.removeEventListener("resize", applyWidth);
20921
21093
  };
20922
21094
  }, []);
20923
- return jsx(TextDispatcher, {
21095
+ return jsx(Text, {
20924
21096
  ...props,
21097
+ ref: ref,
20925
21098
  "data-shrinkwrap": "",
20926
21099
  shrinkWrap: undefined
20927
21100
  });
@@ -21037,7 +21210,7 @@ const TextSkeleton = ({
21037
21210
  "aria-hidden": "true",
21038
21211
  children: "W"
21039
21212
  });
21040
- return jsx(TextDispatcher, {
21213
+ return jsx(Text, {
21041
21214
  "data-skeleton": "",
21042
21215
  "data-loading": loading ? "" : undefined,
21043
21216
  ...props,
@@ -21053,7 +21226,7 @@ const TextOverflow = ({
21053
21226
  children,
21054
21227
  ...rest
21055
21228
  }) => {
21056
- return jsx(TextDispatcher, {
21229
+ return jsx(Text, {
21057
21230
  block: true,
21058
21231
  as: "div",
21059
21232
  pre: noWrap === undefined ? true : undefined
@@ -21075,10 +21248,12 @@ const TextWithSelectRange = ({
21075
21248
  selectRange,
21076
21249
  ...props
21077
21250
  }) => {
21078
- useInitialTextSelection(ref, selectRange);
21079
- return jsx(TextDispatcher, {
21251
+ const defaultRef = useRef();
21252
+ const innerRef = ref || defaultRef;
21253
+ useInitialTextSelection(innerRef, selectRange);
21254
+ return jsx(Text, {
21080
21255
  ...props,
21081
- ref: ref,
21256
+ ref: innerRef,
21082
21257
  selectRange: undefined
21083
21258
  });
21084
21259
  };
@@ -63256,7 +63431,8 @@ const useBadgeRegistry = (children, enabled) => {
63256
63431
  * not the same in each. Nothing is set up for a case that cannot happen: a
63257
63432
  * plain list is one element holding its children as-is — no registry, no
63258
63433
  * effect —, a capped one collects its badges but measures nothing, and only
63259
- * shrinkWrap ever builds the measurement ghost.
63434
+ * shrinkWrap on its own builds the measurement ghost: under maxLines the list
63435
+ * is measured in place, once the rows are known.
63260
63436
  *
63261
63437
  * @param {import("ignore:preact").ComponentChildren} [fallback]
63262
63438
  * Rendered in place of the badges when there is none. Without it an empty
@@ -63271,8 +63447,9 @@ const useBadgeRegistry = (children, enabled) => {
63271
63447
  * Narrows the list down to its widest row so the last row isn't ragged.
63272
63448
  * Defaults to true inside a <Picker> — the trigger draws a border around the
63273
63449
  * list, so the ragged edge shows — and false elsewhere, where the work would
63274
- * often go unseen: opt in where an edge is visible. Ignored when maxLines is
63275
- * in play, which needs the full width to know where the rows fall.
63450
+ * often go unseen: opt in where an edge is visible. Composes with maxLines:
63451
+ * the rows are read at the full width first, and the list is narrowed once
63452
+ * the surplus is gone.
63276
63453
  * @param {number} [max]
63277
63454
  * Caps how many badges are rendered; the surplus becomes a "+N" badge, which
63278
63455
  * takes one of the max slots.
@@ -63296,11 +63473,10 @@ const BadgeList = props => {
63296
63473
  shrinkWrap = maxLinesFromAbove !== undefined
63297
63474
  } = props;
63298
63475
  if (maxLinesResolved !== undefined) {
63299
- // shrinkWrap is dropped on purpose: it narrows the list down to its widest
63300
- // row, which would re-wrap the badges under the cap just measured.
63301
63476
  return jsx(BadgeListMaxLines, {
63302
63477
  ...props,
63303
- maxLines: maxLinesResolved
63478
+ maxLines: maxLinesResolved,
63479
+ shrinkWrap: shrinkWrap
63304
63480
  });
63305
63481
  }
63306
63482
  if (shrinkWrap) {
@@ -63441,6 +63617,7 @@ const BadgeListMaxLines = ({
63441
63617
  children,
63442
63618
  max,
63443
63619
  maxLines,
63620
+ shrinkWrap,
63444
63621
  ...boxProps
63445
63622
  }) => {
63446
63623
  const registry = useBadgeRegistry(children, true);
@@ -63462,8 +63639,29 @@ const BadgeListMaxLines = ({
63462
63639
  // out of the cap and nothing has to be watched.
63463
63640
  const watchesResize = fit !== null && fit.count > 1;
63464
63641
 
63642
+ // The list's own width. While the rows are being read the list takes all the
63643
+ // room it is given — a width kept from an earlier shrink wrap would fold the
63644
+ // badges under it. Once the surplus is gone, shrinkWrap narrows the list to
63645
+ // its widest row, measured where the badges stand: nothing is painted before
63646
+ // the effect below has run, so no clone is needed.
63647
+ useLayoutEffect(() => {
63648
+ const visibleEl = visibleRef.current;
63649
+ if (!visibleEl) {
63650
+ return;
63651
+ }
63652
+ visibleEl.style.width = "";
63653
+ if (fit === null || !shrinkWrap) {
63654
+ return;
63655
+ }
63656
+ const widestRowWidth = measureWidestChildRow(visibleEl);
63657
+ if (widestRowWidth !== null) {
63658
+ visibleEl.style.width = `${Math.ceil(widestRowWidth)}px`;
63659
+ }
63660
+ }, [fit, shrinkWrap]);
63661
+
63465
63662
  // Runs after every render, which is when the badges have registered and the
63466
- // DOM holds whatever this render asked for.
63663
+ // DOM holds whatever this render asked for, at the width the effect above
63664
+ // gave it.
63467
63665
  useLayoutEffect(() => {
63468
63666
  const visibleEl = visibleRef.current;
63469
63667
  if (!visibleEl) {
@@ -63498,7 +63696,8 @@ const BadgeListMaxLines = ({
63498
63696
  // And not every width change either. Nothing guarantees an ancestor whose
63499
63697
  // width does not follow its content (a column with align-items: start
63500
63698
  // sizes every row to what is inside it), so rendering every badge widens
63501
- // what is being watched and dropping the surplus narrows it right back.
63699
+ // what is being watched and dropping the surplus then shrink wrapping
63700
+ // what is left — narrows it right back.
63502
63701
  // Those two widths are this list talking to itself; measuring again on
63503
63702
  // them never ends. Any other width is the room around it changing.
63504
63703
  const width = outerParent.getBoundingClientRect().width;