@jsenv/navi 0.29.407 → 0.29.409

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.
@@ -13122,7 +13122,8 @@ const useActionBoundToOneParam = (action, paramsSignal) => {
13122
13122
  }, []);
13123
13123
  return [boundAction, getValue(), setValue];
13124
13124
  };
13125
- // used by <details> to just call their action
13125
+ // The action bound to the signal, without reading it: the caller is not
13126
+ // re-rendered by the params (a control, a <details>).
13126
13127
  const useAction = (action, paramsSignal) => {
13127
13128
  return useBoundAction(action, paramsSignal);
13128
13129
  };
@@ -13144,12 +13145,13 @@ const useBoundAction = (action, actionParamsSignal) => {
13144
13145
  if (existingNoopAction) {
13145
13146
  return existingNoopAction;
13146
13147
  }
13148
+ // Not bound to the params signal: nothing reads what a noop runs with, and
13149
+ // a proxy following the signal would retarget (a new child action for each
13150
+ // new value) at every change — for every button of a form, at every change
13151
+ // of any of its fields.
13147
13152
  const noopAction = createAction(() => {}, { params: undefined });
13148
- const noopActionBound = actionParamsSignal
13149
- ? noopAction.bindParams(actionParamsSignal)
13150
- : noopAction;
13151
- noopActionRef.current = noopActionBound;
13152
- return noopActionBound;
13153
+ noopActionRef.current = noopAction;
13154
+ return noopAction;
13153
13155
  }
13154
13156
  const isFunction = typeof action === "function";
13155
13157
  if (!isFunction) {
@@ -23009,7 +23011,7 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
23009
23011
  }
23010
23012
  }
23011
23013
 
23012
- [hidden] {
23014
+ [hidden]:not([hidden="until-found" i]) {
23013
23015
  display: none !important;
23014
23016
  }
23015
23017
  `, "@jsenv/navi/src/box/box.jsx"];
@@ -42656,7 +42658,14 @@ const useControlProps = (props, {
42656
42658
  persists,
42657
42659
  uiActionInternal
42658
42660
  });
42659
- const [boundAction] = useActionBoundToOneParam(props.action, uiStateController.uiStateSignal);
42661
+ // What the control draws is what it holds by itself, and this read is what
42662
+ // re-renders it when that changes. A button inheriting the value of the
42663
+ // control around it (a submit in a form, see ownUIStateSignal) draws none of
42664
+ // that value, it only hands it to its action: subscribed to it, every button
42665
+ // of a form would re-render at each change of any of its fields.
42666
+ // eslint-disable-next-line no-unused-expressions
42667
+ uiStateController.ownUIStateSignal.value;
42668
+ const boundAction = useAction(props.action, uiStateController.uiStateSignal);
42660
42669
  const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
42661
42670
  uiStateController,
42662
42671
  boundAction,
@@ -43423,7 +43432,10 @@ const useControlProps = (props, {
43423
43432
  };
43424
43433
  }
43425
43434
  }
43426
- const uiState = uiStateController.uiStateSignal.peek();
43435
+
43436
+ // Its own, like syncDomState: a button's DOM value is not the value it
43437
+ // inherits from the control around it.
43438
+ const uiState = uiStateController.ownUIStateSignal.peek();
43427
43439
  const domProps = toDomProps(uiState);
43428
43440
  {
43429
43441
  // Same as syncDomState: a field being typed into keeps its own text, so a
@@ -43972,7 +43984,9 @@ const useInteractiveProps = (props, {
43972
43984
  // stop waiting on a run that is still going. What it waits on is the run
43973
43985
  // it started, from navi_action_start to its settlement.
43974
43986
  const runningAction = uiStateController.runningActionSignal.value;
43975
- const actionStatus = useActionStatus(runningAction || boundAction);
43987
+ // Only whether it runs: the whole status (useActionStatus) reads the params
43988
+ // too, which for a button inheriting its form's value are the form.
43989
+ const actionRunning = (runningAction || boundAction).runningStateSignal.value === RUNNING;
43976
43990
  const networkPolicyReason = useNetworkPolicyReason();
43977
43991
  const {
43978
43992
  disabled,
@@ -44020,7 +44034,7 @@ const useInteractiveProps = (props, {
44020
44034
  // just set stays visible and interactive while the action runs — no
44021
44035
  // loading, no readonly. On failure resetOnError rolls the state back and
44022
44036
  // the error callout says why.
44023
- const actionLoading = optimistic ? false : actionStatus.loading;
44037
+ const actionLoading = optimistic ? false : actionRunning;
44024
44038
  const loadingResolved = loadingBase || actionLoading;
44025
44039
  const readOnlyResolved = readOnlyBase || actionLoading;
44026
44040
  // Read-only, and what this control opens still opens: reading what is in
@@ -44215,7 +44229,11 @@ const useInteractiveProps = (props, {
44215
44229
  errorEffect: actionErrorEffect,
44216
44230
  errorMapping
44217
44231
  });
44218
- const dataAction = action === undefined ? undefined : boundAction.callSource;
44232
+ // What the attribute says is that an action is there (see
44233
+ // findClosestControlWithAction); its text is a label. Peeked (toString),
44234
+ // because `.callSource` follows the params — for a button inheriting its
44235
+ // form's value, a re-render at every change of any field.
44236
+ const dataAction = action === undefined ? undefined : String(boundAction);
44219
44237
  Object.assign(controlHostProps, {
44220
44238
  "data-action": dataAction
44221
44239
  });
@@ -53143,30 +53161,9 @@ const ButtonUI = props => {
53143
53161
  } = buttonControlHostProps;
53144
53162
  const loading = basePseudoState[":-navi-loading"];
53145
53163
 
53146
- // subscribe to document url to re-render and re-compute getHrefTargetInfo
53147
- useDocumentUrl();
53148
- const isLink = href !== undefined;
53149
- let as = "button";
53150
- let innerTarget;
53151
- let innerRel;
53152
- let innerCurrent;
53153
- if (isLink) {
53154
- as = "a";
53155
- const {
53156
- isSameSite,
53157
- isCurrent
53158
- } = getHrefTargetInfo(href);
53159
- innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
53160
- innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
53161
- innerCurrent = isCurrent;
53162
- }
53163
- // For a button that has only an href: nothing else knows it points at the
53164
- // page one is on. A route says so through `pseudoState`, which Box lays over
53165
- // this one.
53166
- buttonControlHostProps.basePseudoState = {
53167
- ...basePseudoState,
53168
- ":-navi-href-current": innerCurrent
53169
- };
53164
+ // Where the href leads next to the page one is on (the target, the rel, the
53165
+ // current state) is resolved above, by ButtonHrefResolver.
53166
+ const as = href === undefined ? "button" : "a";
53170
53167
 
53171
53168
  // Worn as an attribute, like a link's (see link_replace.js): read off the
53172
53169
  // anchor by the click handler, off the source by --navi-nav-to.
@@ -53215,8 +53212,8 @@ const ButtonUI = props => {
53215
53212
  ref: ref,
53216
53213
  as: as,
53217
53214
  href: href,
53218
- target: innerTarget,
53219
- rel: innerRel,
53215
+ target: target,
53216
+ rel: rel,
53220
53217
  replace: undefined,
53221
53218
  ...replaceRequest,
53222
53219
  document: undefined,
@@ -53516,6 +53513,49 @@ const ButtonConfirm = ({
53516
53513
  });
53517
53514
  };
53518
53515
 
53516
+ const ButtonHrefResolver = props => {
53517
+ const Next = useNextResolver();
53518
+ if (props.href !== undefined) {
53519
+ return renderResolver(ButtonWithHref, props);
53520
+ }
53521
+ return jsx(Next, {
53522
+ ...props
53523
+ });
53524
+ };
53525
+
53526
+ // What the href means next to the page one is on depends on the document url,
53527
+ // so a button with an href re-renders at every address write. One without
53528
+ // never reaches here: it has nothing to re-compute, and every button on screen
53529
+ // would otherwise pay for each write.
53530
+ const ButtonWithHref = props => {
53531
+ const Next = useNextResolver();
53532
+ const {
53533
+ href,
53534
+ target,
53535
+ rel,
53536
+ pseudoState
53537
+ } = props;
53538
+ // Read for the subscription: getHrefTargetInfo reads window.location.
53539
+ useDocumentUrl();
53540
+ const {
53541
+ isSameSite,
53542
+ isCurrent
53543
+ } = getHrefTargetInfo(href);
53544
+ return jsx(Next, {
53545
+ ...props,
53546
+ target: target === undefined ? isSameSite ? undefined : "_blank" : target,
53547
+ rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
53548
+ // Under whatever the caller already says: a route knows better whether
53549
+ // it is the page one is on (see button_route.jsx), and a demo can force
53550
+ // the state.
53551
+ ,
53552
+ pseudoState: {
53553
+ ":-navi-href-current": isCurrent,
53554
+ ...pseudoState
53555
+ }
53556
+ });
53557
+ };
53558
+
53519
53559
  const ButtonRouteResolver = props => {
53520
53560
  const Next = useNextResolver();
53521
53561
  if (props.route) {
@@ -53773,7 +53813,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
53773
53813
  * (it wears it itself) and loses the shrink under the finger, which has
53774
53814
  * nothing left to scale but the interactive area itself.
53775
53815
  */
53776
- const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
53816
+ const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
53777
53817
 
53778
53818
  installImportMetaCssBuild(import.meta);
53779
53819
  const css$O = /* css */`.navi_control_swap {
@@ -71269,12 +71309,28 @@ const css$x = /* css */`@layer navi {
71269
71309
  display: inline-block;
71270
71310
  }
71271
71311
 
71312
+ .navi_list_find_stand_in {
71313
+ height: calc(var(--x-find-line-count) * var(--x-find-line-size));
71314
+ color: #0000;
71315
+ line-height: var(--x-find-line-size);
71316
+ white-space: pre;
71317
+ overflow: clip;
71318
+ }
71319
+
71272
71320
  .navi_list_container[data-horizontal] {
71273
71321
  --list-max-height: none;
71274
71322
 
71275
71323
  & > .navi_list_scroll_container > .navi_list > .navi_list_virtual_filler {
71276
71324
  width: var(--size-to-fill, 0px);
71277
71325
  height: 100%;
71326
+ display: flex;
71327
+
71328
+ & > .navi_list_find_stand_in {
71329
+ width: calc(var(--x-find-line-count) * var(--x-find-line-size));
71330
+ writing-mode: vertical-lr;
71331
+ flex-shrink: 0;
71332
+ height: auto;
71333
+ }
71278
71334
  }
71279
71335
  }
71280
71336
 
@@ -73871,10 +73927,12 @@ const Fallback = ({
73871
73927
  // settled after the first commit.
73872
73928
  const VirtualFiller = ({
73873
73929
  edge,
73874
- itemCount
73930
+ itemCount,
73931
+ findChunks
73875
73932
  }) => {
73876
73933
  const listRows = useContext(ListRowsContext);
73877
- const sizeToFill = itemCount * listRows.virtualItemSizeSignal.value;
73934
+ const virtualItemSize = listRows.virtualItemSizeSignal.value;
73935
+ const sizeToFill = itemCount * virtualItemSize;
73878
73936
  if (!sizeToFill) {
73879
73937
  return null;
73880
73938
  }
@@ -73885,8 +73943,17 @@ const VirtualFiller = ({
73885
73943
  "navi-virtual-filler": edge,
73886
73944
  "aria-hidden": true,
73887
73945
  style: {
73888
- "--size-to-fill": `${sizeToFill}px`
73889
- }
73946
+ "--size-to-fill": `${sizeToFill}px`,
73947
+ "--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
73948
+ },
73949
+ children: findChunks && findChunks.map(chunk => jsx("div", {
73950
+ className: "navi_list_find_stand_in",
73951
+ hidden: "until-found",
73952
+ style: {
73953
+ "--x-find-line-count": chunk.to - chunk.from
73954
+ },
73955
+ children: chunk.text
73956
+ }, `${chunk.from}_${chunk.to}`))
73890
73957
  });
73891
73958
  };
73892
73959
 
@@ -74522,6 +74589,7 @@ const VISIBILITY_HIDDEN_STYLE = {
74522
74589
  *
74523
74590
  * @type {import("ignore:preact").FunctionComponent<{
74524
74591
  * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
74592
+ * findText?: (item: any, index: number) => string,
74525
74593
  * items?: any[],
74526
74594
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
74527
74595
  * count?: number,
@@ -74540,6 +74608,13 @@ const VISIBILITY_HIDDEN_STYLE = {
74540
74608
  * the run. `state.refreshing` says the rows drawn are the ones from before
74541
74609
  * while the run reads the collection again — the list carries
74542
74610
  * `navi-refreshing` for the same reason.
74611
+ * @param {(item: any, index: number) => string} [props.findText]
74612
+ * The text of a row as the browser's find in page (Cmd/Ctrl + F) sees it
74613
+ * while the row is not drawn — without it, find reaches only the rows in the
74614
+ * render window. The rows held off screen then carry that text where they
74615
+ * stand, hidden until the browser finds it there: it scrolls to the place,
74616
+ * and the list draws the row. One line per row. A row the run does not hold
74617
+ * yet has nothing to be found by.
74543
74618
  * @param {any[]} [props.items]
74544
74619
  * The collection, when it is held in memory: all of it, in order. Nothing is
74545
74620
  * ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
@@ -74636,12 +74711,19 @@ const createStoreHoldingNothing = rowCount => {
74636
74711
  };
74637
74712
  };
74638
74713
 
74714
+ // How many rows share one element of the text a filler carries for find in
74715
+ // page (see findText). One element per row would put back the DOM nodes the
74716
+ // window saves; one per filler would be revealed whole — every row of it laid
74717
+ // out — by the first match found in it.
74718
+ const FIND_CHUNK_ROW_COUNT = 64;
74719
+
74639
74720
  // What a run draws: the rows its window frames, a skeleton for each one the
74640
74721
  // store does not hold, and fillers holding the room of the rows outside it.
74641
74722
  // Which rows are held, and fetching the others, is the store's (see
74642
74723
  // useItemStore).
74643
74724
  const useRunRows = (store, {
74644
74725
  renderItem,
74726
+ findText,
74645
74727
  pageSize,
74646
74728
  groupBy,
74647
74729
  renderGroupLabel,
@@ -74849,6 +74931,37 @@ const useRunRows = (store, {
74849
74931
  }
74850
74932
  group.children.push(rowNode);
74851
74933
  };
74934
+ // The text of the rows a filler stands for, cut in chunks aligned on the
74935
+ // run's own ranks: the window sliding changes the chunk at its edge and
74936
+ // leaves the others as they are.
74937
+ const getFindChunks = (from, to) => {
74938
+ if (!findText) {
74939
+ return null;
74940
+ }
74941
+ const chunks = [];
74942
+ let chunkFrom = from;
74943
+ while (chunkFrom < to) {
74944
+ const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ROW_COUNT);
74945
+ const alignedTo = rowOf((chunkIndex + 1) * FIND_CHUNK_ROW_COUNT);
74946
+ const chunkTo = alignedTo < to ? alignedTo : to;
74947
+ const lines = [];
74948
+ let lineRowIndex = chunkFrom;
74949
+ while (lineRowIndex < chunkTo) {
74950
+ const item = getItemAt(lineRowIndex);
74951
+ // A row not held has nothing to find, but keeps its line: the rows
74952
+ // after it stay at their place.
74953
+ lines.push(item === undefined ? "" : toFindLine(findText(item, lineRowIndex)));
74954
+ lineRowIndex++;
74955
+ }
74956
+ chunks.push({
74957
+ from: chunkFrom,
74958
+ to: chunkTo,
74959
+ text: lines.join("\n")
74960
+ });
74961
+ chunkFrom = chunkTo;
74962
+ }
74963
+ return chunks;
74964
+ };
74852
74965
  // The room held for this run's own rows that the window leaves out. It
74853
74966
  // belongs to the run and not to the list: a list is not necessarily made of
74854
74967
  // one run, and what sits before or after it (a header, rows given one by
@@ -74856,7 +74969,8 @@ const useRunRows = (store, {
74856
74969
  if (windowFrom > runStart) {
74857
74970
  rows.push(jsx(VirtualFiller, {
74858
74971
  edge: "before",
74859
- itemCount: windowFrom - runStart
74972
+ itemCount: windowFrom - runStart,
74973
+ findChunks: getFindChunks(runStart, windowFrom)
74860
74974
  }, "navi-list-filler-before"));
74861
74975
  }
74862
74976
  const renderItemState = {
@@ -74958,11 +75072,20 @@ const useRunRows = (store, {
74958
75072
  if (runEnd > windowTo) {
74959
75073
  rows.push(jsx(VirtualFiller, {
74960
75074
  edge: "after",
74961
- itemCount: runEnd - windowTo
75075
+ itemCount: runEnd - windowTo,
75076
+ findChunks: getFindChunks(windowTo, runEnd)
74962
75077
  }, "navi-list-filler-after"));
74963
75078
  }
74964
75079
  return rows;
74965
75080
  };
75081
+ // One row, one line: a line break inside it would push every row below off
75082
+ // its place.
75083
+ const toFindLine = text => {
75084
+ if (text === undefined || text === null) {
75085
+ return "";
75086
+ }
75087
+ return String(text).replace(/[\r\n]+/g, " ");
75088
+ };
74966
75089
 
74967
75090
  // A run's row that has not arrived, standing where the real one will. It never
74968
75091
  // reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
@@ -83710,8 +83833,22 @@ function WheelUI(props) {
83710
83833
  ...style
83711
83834
  };
83712
83835
 
83713
- // Main-axis size of the viewport, from the orientation.
83714
- const viewportMain = vp => isHorizontal ? vp.clientWidth : vp.clientHeight;
83836
+ // Main-axis size of the viewport, from the orientation. Cached like the row
83837
+ // size: applyOffset runs right after each window commit, where reading the
83838
+ // box would force a layout at every row crossing. The observer below keeps it
83839
+ // current; 0 (no box yet) is never kept, so the next read measures again.
83840
+ const viewportSizeRef = useRef(0);
83841
+ const measureViewportMain = vp => isHorizontal ? vp.clientWidth : vp.clientHeight;
83842
+ const viewportMain = vp => {
83843
+ if (viewportSizeRef.current > 0) {
83844
+ return viewportSizeRef.current;
83845
+ }
83846
+ const size = measureViewportMain(vp);
83847
+ if (size > 0) {
83848
+ viewportSizeRef.current = size;
83849
+ }
83850
+ return size;
83851
+ };
83715
83852
 
83716
83853
  // Row size (px). Rows are uniform, so one measurement (cached) drives all the
83717
83854
  // geometry — positions are then computed from value indices, not read off the
@@ -83782,6 +83919,21 @@ function WheelUI(props) {
83782
83919
  applyOffset(vp);
83783
83920
  }
83784
83921
  };
83922
+ // Refreshes the cached viewport size (see viewportMain). Read in the observer
83923
+ // callback, where the layout is already clean. A new size moves the center
83924
+ // window, so the offset is re-applied with it.
83925
+ useLayoutEffect(() => {
83926
+ const vp = getViewport();
83927
+ viewportSizeRef.current = 0;
83928
+ const resizeObserver = new ResizeObserver(() => {
83929
+ viewportSizeRef.current = measureViewportMain(vp);
83930
+ applyOffset(vp);
83931
+ });
83932
+ resizeObserver.observe(vp);
83933
+ return () => {
83934
+ resizeObserver.disconnect();
83935
+ };
83936
+ }, [isHorizontal]);
83785
83937
 
83786
83938
  // The value index at the center for the current position.
83787
83939
  const centeredIndex = vp => {