@jsenv/navi 0.29.175 → 0.29.177

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.
@@ -34356,6 +34356,15 @@ const useUIStateController = (
34356
34356
  ) {
34357
34357
  continue;
34358
34358
  }
34359
+ // Only the sibling that IS checked has anything to let go of.
34360
+ // Asked anyway, an already-unchecked sibling still runs the
34361
+ // tail of its own setUIState (uiAction and all) — and a group
34362
+ // of N radios then pays N-1 of those on every selection, which
34363
+ // is what made every row of a selectable list re-render per
34364
+ // click.
34365
+ if (siblingController.uiState === undefined) {
34366
+ continue;
34367
+ }
34359
34368
  siblingController.setUIState(undefined, siblingUncheckEvent);
34360
34369
  }
34361
34370
  }
@@ -36368,7 +36377,7 @@ const ControlgroupChildrenWrapper = ({
36368
36377
  readOnly,
36369
36378
  loading,
36370
36379
  boundAction,
36371
- actionRequester
36380
+ actionRequesterSignal
36372
36381
  }) => jsx(MessagePropsRefContext.Provider, {
36373
36382
  value: undefined,
36374
36383
  children: jsx(ControlIdContext.Provider, {
@@ -36388,7 +36397,7 @@ const ControlgroupChildrenWrapper = ({
36388
36397
  children: jsx(ActionContext.Provider, {
36389
36398
  value: boundAction,
36390
36399
  children: jsx(ActionRequesterContext.Provider, {
36391
- value: actionRequester,
36400
+ value: actionRequesterSignal,
36392
36401
  children: children
36393
36402
  })
36394
36403
  })
@@ -37480,7 +37489,20 @@ const useControlgroupProps = (props, {
37480
37489
  if (implicitReadOnly && !props.readOnly) {
37481
37490
  props.readOnly = true;
37482
37491
  }
37483
- const [actionRequester, setActionRequester] = useState();
37492
+
37493
+ // Who pressed for the action the group is running, held in a SIGNAL and not
37494
+ // in render state: every control in the group reads it (to wear the group's
37495
+ // busy state when it is the one that asked), so as a plain context value a
37496
+ // new requester would force-re-render every one of them — a click on one row
37497
+ // of a selectable list re-rendering the whole list. The signal keeps the
37498
+ // context value stable; each control subscribes to its own "is it me"
37499
+ // computed (see loadingFromParent below), so a requester change re-renders
37500
+ // exactly the control it leaves and the one it lands on.
37501
+ const actionRequesterSignalRef = useRef(null);
37502
+ if (actionRequesterSignalRef.current === null) {
37503
+ actionRequesterSignalRef.current = signal(undefined);
37504
+ }
37505
+ const actionRequesterSignal = actionRequesterSignalRef.current;
37484
37506
  const [controlRootProps, controlgroupProps] = useInteractiveProps(props, {
37485
37507
  uiStateController: uiGroupStateController,
37486
37508
  boundAction,
@@ -37504,8 +37526,8 @@ const useControlgroupProps = (props, {
37504
37526
  readOnly,
37505
37527
  loading,
37506
37528
  boundAction,
37507
- actionRequester
37508
- }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequester]);
37529
+ actionRequesterSignal
37530
+ }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequesterSignal]);
37509
37531
  return [controlRootProps, {
37510
37532
  ...controlgroupProps,
37511
37533
  "name": undefined,
@@ -37517,7 +37539,7 @@ const useControlgroupProps = (props, {
37517
37539
  // any element wears: a <fieldset maxlength> means nothing.
37518
37540
  "maxLength": undefined,
37519
37541
  "onnavi_action_allowed": e => {
37520
- setActionRequester(e.detail.requester);
37542
+ actionRequesterSignal.value = e.detail.requester;
37521
37543
  controlgroupProps.onnavi_action_allowed(e);
37522
37544
  },
37523
37545
  "navi-control-group": ""
@@ -37680,7 +37702,13 @@ const useInteractiveProps = (props, {
37680
37702
  const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
37681
37703
  const controlRequired = useContext(RequiredContext);
37682
37704
  const controlLoadingFromAbove = useContext(LoadingContext$1);
37683
- const parentActionRequester = useContext(ActionRequesterContext);
37705
+ // A SIGNAL holding the element whose press the group's action is wearing
37706
+ // (see useControlgroupProps). Subscribed to through a per-control computed
37707
+ // rather than read raw: `.value` read here directly would re-render every
37708
+ // control of the group on each new requester — the computed's boolean only
37709
+ // changes for the control the requester leaves and the one it lands on.
37710
+ const parentActionRequesterSignal = useContext(ActionRequesterContext);
37711
+ const isActionRequesterComputed = useMemo(() => parentActionRequesterSignal ? computed(() => parentActionRequesterSignal.value === ref.current) : null, [parentActionRequesterSignal]);
37684
37712
  const parallelGuard = useContext(ParallelGuardContext);
37685
37713
  const parentAction = useContext(ActionContext);
37686
37714
  const actionStatus = useActionStatus(boundAction);
@@ -37707,7 +37735,7 @@ const useInteractiveProps = (props, {
37707
37735
  // Busy because the group above is running the action THIS control asked
37708
37736
  // for (a submit button wearing its form's submission), as opposed to busy
37709
37737
  // on its own say-so.
37710
- const loadingFromParent = Boolean(controlLoading && parentActionRequester === ref.current);
37738
+ const loadingFromParent = Boolean(controlLoading && isActionRequesterComputed?.value);
37711
37739
  const loadingBase = loading || loadingFromParent;
37712
37740
  // Read-only because the selection above guards its length
37713
37741
  // (`maxLengthGuard`) and this one would make it longer: it can be pointed
@@ -42414,6 +42442,20 @@ const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
42414
42442
  // in pixels. A rough line, deliberately: nothing here needs to agree with a
42415
42443
  // paragraph, only to move the row about as far as a notch moves a page.
42416
42444
  const WHEEL_LINE_SIZE = 16;
42445
+
42446
+ // The animation a slide container puts on this row to bring the travel's
42447
+ // progress home (see animateTravelProgress in slide_container.jsx) — the cheap
42448
+ // source of that number while a travel plays (see syncTabScroll). Its starting
42449
+ // value is read off the keyframes here, once: the frame loop only ever asks
42450
+ // the animation how far along it is.
42451
+ const rememberProgressAnimation = (navElement, travel) => {
42452
+ const animation = navElement.getAnimations().find(candidate => {
42453
+ const keyframes = candidate.effect?.getKeyframes?.();
42454
+ return keyframes?.[0] && "--slide-travel-progress" in keyframes[0];
42455
+ });
42456
+ travel.progressAnimation = animation;
42457
+ travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
42458
+ };
42417
42459
  const css$W = /* css */`@layer navi {
42418
42460
  .navi_nav {
42419
42461
  --nav-border: none;
@@ -42740,8 +42782,27 @@ const Nav = ({
42740
42782
  if (currentIndex === -1) {
42741
42783
  // On a slide no tab in this row names: there is no tab to sit under.
42742
42784
  navElement.removeAttribute("data-nav-indicator-measured");
42785
+ indicatorPaintedForRef.current = null;
42743
42786
  return;
42744
42787
  }
42788
+ // Everything below is measured off the DOM (offsets force a layout, the
42789
+ // colours a style resolve), and what it depends on is exactly two facts:
42790
+ // which tab is current and which one the travel leans toward. A travel
42791
+ // announces its state several times between those two changing — painted
42792
+ // each time, the same measurement is paid again on frames the travel is
42793
+ // drawn on. The size observer clears the memo, since the same pair on a
42794
+ // resized row measures differently.
42795
+ const towardAreaNow = containerElement.getAttribute("data-slide-travel-toward");
42796
+ const paintedFor = indicatorPaintedForRef.current;
42797
+ if (paintedFor && paintedFor.currentTabElement === tabElements[currentIndex] && paintedFor.towardArea === towardAreaNow && paintedFor.tabCount === tabElements.length && paintedFor.vertical === vertical) {
42798
+ return;
42799
+ }
42800
+ indicatorPaintedForRef.current = {
42801
+ currentTabElement: tabElements[currentIndex],
42802
+ towardArea: towardAreaNow,
42803
+ tabCount: tabElements.length,
42804
+ vertical
42805
+ };
42745
42806
  const measure = tabElement => vertical ? {
42746
42807
  position: tabElement.offsetTop,
42747
42808
  length: tabElement.offsetHeight
@@ -42763,7 +42824,7 @@ const Nav = ({
42763
42824
  }
42764
42825
  };
42765
42826
  const currentMeasure = measure(tabElements[currentIndex]);
42766
- const towardArea = containerElement.getAttribute("data-slide-travel-toward");
42827
+ const towardArea = towardAreaNow;
42767
42828
  const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
42768
42829
  let positionDelta = 0;
42769
42830
  let lengthDelta = 0;
@@ -42799,6 +42860,9 @@ const Nav = ({
42799
42860
  // about the row as it is now.
42800
42861
  const paintIndicatorGeometryRef = useRef(null);
42801
42862
  paintIndicatorGeometryRef.current = paintIndicatorGeometry;
42863
+ // What the last paint measured for — see the memo at the top of
42864
+ // paintIndicatorGeometry.
42865
+ const indicatorPaintedForRef = useRef(null);
42802
42866
 
42803
42867
  // A row of tabs wider than the box holding it scrolls (the caller asks for
42804
42868
  // that with overflowX) — and then saying which tab one is on is not enough,
@@ -42870,6 +42934,20 @@ const Nav = ({
42870
42934
  }
42871
42935
  let travel = travelScrollRef.current;
42872
42936
  if (!travel || travel.from !== currentTabElement || travel.toward !== towardTabElement) {
42937
+ // Everything that holds for the whole travel is read here and only here
42938
+ // — the frame loop below runs sixty times a second and every layout or
42939
+ // style read in it is paid on the very frames the travel is drawn on.
42940
+ // A row that cannot scroll has nowhere to ride the travel to: remembered
42941
+ // on the travel, and the loop is not even armed for it.
42942
+ const rideable = navElement.scrollWidth > navElement.clientWidth + 1 || navElement.scrollHeight > navElement.clientHeight + 1;
42943
+ if (!rideable) {
42944
+ travelScrollRef.current = {
42945
+ from: currentTabElement,
42946
+ toward: towardTabElement,
42947
+ rideable
42948
+ };
42949
+ return;
42950
+ }
42873
42951
  // The two ends, measured once for this travel: measuring them again on a
42874
42952
  // row that has since moved would be measuring from the answer.
42875
42953
  const fromOffsets = getScrollIntoViewScopedOffsets(currentTabElement, {
@@ -42885,14 +42963,46 @@ const Nav = ({
42885
42963
  travel = {
42886
42964
  from: currentTabElement,
42887
42965
  toward: towardTabElement,
42966
+ rideable,
42888
42967
  left: fromOffsets.left,
42889
42968
  top: fromOffsets.top,
42890
42969
  leftDelta: (towardOffsets.left - fromOffsets.left) * sign,
42891
42970
  topDelta: (towardOffsets.top - fromOffsets.top) * sign
42892
42971
  };
42972
+ rememberProgressAnimation(navElement, travel);
42893
42973
  travelScrollRef.current = travel;
42894
42974
  }
42895
- const progress = parseFloat(getComputedStyle(navElement).getPropertyValue("--slide-travel-progress")) || 0;
42975
+ if (!travel.rideable) {
42976
+ return;
42977
+ }
42978
+ // How far between the two slides the picture stands, read WITHOUT
42979
+ // getComputedStyle: computing the style of an element whose registered
42980
+ // property is animating is a full style resolve, and per frame it is what
42981
+ // ate the travel's frame budget on a throttled phone. The number has two
42982
+ // cheap sources, and the container guarantees one of them on this very
42983
+ // element (see paintTravelProgress / animateTravelProgress in
42984
+ // slide_container.jsx): a travel that plays on its own ANIMATES the
42985
+ // property here — its eased progress is on the animation itself
42986
+ // (getComputedTiming().progress is the transformed, post-easing progress)
42987
+ // — and a finger writes it as INLINE style per frame, readable as a plain
42988
+ // string.
42989
+ let progress;
42990
+ let eased = travel.progressAnimation ? travel.progressAnimation.effect.getComputedTiming().progress : null;
42991
+ if (eased === null) {
42992
+ // No animation, or one that is over: the travel may have been
42993
+ // re-animated since (a chained travel between the same two tabs), so
42994
+ // look again before falling back to the finger's inline value.
42995
+ rememberProgressAnimation(navElement, travel);
42996
+ eased = travel.progressAnimation ? travel.progressAnimation.effect.getComputedTiming().progress : null;
42997
+ }
42998
+ if (eased === null) {
42999
+ progress = parseFloat(navElement.style.getPropertyValue("--slide-travel-progress")) || 0;
43000
+ } else {
43001
+ // The animation walks the property from `progressFrom` to 0 (see
43002
+ // animateTravelProgress); its value at this instant is the same
43003
+ // interpolation, done here in arithmetic instead of in style.
43004
+ progress = travel.progressFrom * (1 - eased);
43005
+ }
42896
43006
  navElement.scrollTo({
42897
43007
  left: travel.left + progress * travel.leftDelta,
42898
43008
  top: travel.top + progress * travel.topDelta,
@@ -42956,6 +43066,8 @@ const Nav = ({
42956
43066
  // being pushed around by hand.
42957
43067
  useLayoutEffect(() => {
42958
43068
  const sizeObserver = new ResizeObserver(() => {
43069
+ // The same pair of tabs measures differently on a row of another size.
43070
+ indicatorPaintedForRef.current = null;
42959
43071
  paintIndicatorGeometryRef.current();
42960
43072
  // What was brought into sight was brought there for another row.
42961
43073
  tabInSightRef.current = null;
@@ -46061,7 +46173,12 @@ const usePopupContentMount = (
46061
46173
  const anchorElement =
46062
46174
  typeof anchor === "string"
46063
46175
  ? document.getElementById(anchor)
46064
- : (anchor.current ?? anchor);
46176
+ : // A ref is unwrapped even when it holds nothing: an expandable with
46177
+ // no UI part hands an empty ref over, and the ref object itself is
46178
+ // truthy — it would reach addEventListener below and throw.
46179
+ "current" in anchor
46180
+ ? anchor.current
46181
+ : anchor;
46065
46182
  if (!anchorElement) {
46066
46183
  return undefined;
46067
46184
  }
@@ -46979,7 +47096,9 @@ const useNextResolver = () => useContext(NextResolverContext);
46979
47096
  * ResolverIndexContext tracks which resolver is next so that when a resolver
46980
47097
  * re-renders and calls Next, the chain resumes from the correct position.
46981
47098
  */
46982
- const createComponentResolver = resolvers => {
47099
+ const createComponentResolver = (resolvers, {
47100
+ pure
47101
+ } = {}) => {
46983
47102
  const ResolverIndexContext = createContext(0);
46984
47103
  const ChainRunner = props => {
46985
47104
  const index = useContext(ResolverIndexContext);
@@ -47019,7 +47138,45 @@ const createComponentResolver = resolvers => {
47019
47138
  })
47020
47139
  });
47021
47140
  };
47022
- return renderComponent;
47141
+ if (!pure) {
47142
+ return renderComponent;
47143
+ }
47144
+ // The chain does not run again for props that say the same thing. The
47145
+ // signals integration already promises this for any component that READS a
47146
+ // signal ("props all === the previous ones → no render", relied on across
47147
+ // list.jsx) — but a chain head reads none, so without this a parent
47148
+ // re-rendering walks every resolver of every instance to conclude nothing
47149
+ // changed. For a component rendered by the hundred (a list's items), that
47150
+ // walk IS the cost of the parent's render. What still updates through the
47151
+ // bail: context (Preact re-renders subscribers directly) and signals (their
47152
+ // own subscription) — which is every way navi hands data down other than
47153
+ // props.
47154
+ // A FUNCTION component assigning shouldComponentUpdate on its own instance
47155
+ // (the pattern preact/compat's memo uses), and deliberately not a class: a
47156
+ // ref given to a class component is attached to the class INSTANCE, while a
47157
+ // function component receives it as a plain prop — and the chain forwards
47158
+ // it to the element the caller was reaching for.
47159
+ function PureRenderComponent(props) {
47160
+ this.shouldComponentUpdate = pureShouldComponentUpdate;
47161
+ return renderComponent(props);
47162
+ }
47163
+ return PureRenderComponent;
47164
+ };
47165
+ function pureShouldComponentUpdate(nextProps) {
47166
+ return shallowDiffers(this.props, nextProps);
47167
+ }
47168
+ const shallowDiffers = (a, b) => {
47169
+ for (const key in a) {
47170
+ if (!(key in b)) {
47171
+ return true;
47172
+ }
47173
+ }
47174
+ for (const key in b) {
47175
+ if (a[key] !== b[key]) {
47176
+ return true;
47177
+ }
47178
+ }
47179
+ return false;
47023
47180
  };
47024
47181
 
47025
47182
  const ButtonRouteResolver = props => {
@@ -53220,9 +53377,17 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
53220
53377
  const trackOffsetPx = (track, containerElement) => {
53221
53378
  const trackRect = track.getBoundingClientRect();
53222
53379
  const containerRect = containerElement.getBoundingClientRect();
53380
+ // Against the box the track RESTS in — the content box, past the border
53381
+ // (clientLeft/clientTop are the border widths; the box never has a
53382
+ // scrollbar nor padding, see the top-of-file comment on padding). Measured
53383
+ // against the border box, a border reads as that many pixels of translate
53384
+ // nothing ever asked for — and it COMPOUNDS: every travel interrupted
53385
+ // mid-flight re-measures the track and carries the error into its "from",
53386
+ // so rapid back-and-forth gestures stack a 1px border into a visible
53387
+ // vertical drift of the whole track.
53223
53388
  return {
53224
- x: trackRect.x - containerRect.x,
53225
- y: trackRect.y - containerRect.y
53389
+ x: trackRect.x - containerRect.x - containerElement.clientLeft,
53390
+ y: trackRect.y - containerRect.y - containerElement.clientTop
53226
53391
  };
53227
53392
  };
53228
53393
  const offsetToPx = (offset, box) => {
@@ -58520,8 +58685,10 @@ const useDialogProps = props => {
58520
58685
  }
58521
58686
  } else if (anchor) {
58522
58687
  // anchor prop is a ref or a DOM element — always a real anchor,
58523
- // regardless of anchorCustomEventDetail.
58524
- anchorElement = anchor.current ?? anchor;
58688
+ // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
58689
+ // holds nothing: falling back to the ref object itself would pass the
58690
+ // "is there an anchor?" test below with something that has no box.
58691
+ anchorElement = "current" in anchor ? anchor.current : anchor;
58525
58692
  } else if (anchorCustomEventDetail === "override") {
58526
58693
  // e.g. the button that triggered a --navi-toggle/--navi-open command,
58527
58694
  // already resolved from detail.anchor/detail.source by the caller
@@ -59931,8 +60098,10 @@ const usePopoverProps = props => {
59931
60098
  }
59932
60099
  } else if (anchor) {
59933
60100
  // anchor prop is a ref or a DOM element — always a real anchor,
59934
- // regardless of anchorCustomEventDetail.
59935
- anchorElement = anchor.current ?? anchor;
60101
+ // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
60102
+ // holds nothing: falling back to the ref object itself would pass the
60103
+ // "is there an anchor?" test below with something that has no box.
60104
+ anchorElement = "current" in anchor ? anchor.current : anchor;
59936
60105
  } else if (anchorCustomEventDetail === "override") {
59937
60106
  anchorElement = e.detail.anchor;
59938
60107
  }
@@ -66786,7 +66955,13 @@ const LIST_ITEM_STYLE_CSS_VARS = {
66786
66955
  * search-driven scroll-to-top-match behavior. `matchRanges` are [start, end]
66787
66956
  * ranges highlighted via the CSS Highlight API.
66788
66957
  */
66789
- const ListItem = createComponentResolver([ListItemFirstResolver, ListItemRowResolver, ListItemSkeletonResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
66958
+ const ListItem = createComponentResolver([ListItemFirstResolver, ListItemRowResolver, ListItemSkeletonResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI],
66959
+ // Rendered by the hundred, and almost always with the same primitive props:
66960
+ // a selection change re-renders the list, and every row but two has nothing
66961
+ // to change.
66962
+ {
66963
+ pure: true
66964
+ });
66790
66965
  List.Item = ListItem;
66791
66966
 
66792
66967
  // Everything the list knows about the collection while its children are being