@jsenv/navi 0.29.318 → 0.29.320

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.
@@ -27017,14 +27017,35 @@ browserIntegration.handleActionTask;
27017
27017
 
27018
27018
  const NO_OP = () => {};
27019
27019
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
27020
+ // What the computed below answers for a key the document state does not hold:
27021
+ // key presence is the flag, and the value under a present key may be anything,
27022
+ // undefined included.
27023
+ const KEY_NOT_IN_STATE = Symbol("key_not_in_state");
27020
27024
  const useNavStateBasic = (
27021
27025
  id,
27022
27026
  { debug, type = "replace", onLeave, defaultValue } = {},
27023
27027
  ) => {
27024
27028
  // Hooks must be called unconditionally — before the !id early return.
27025
- const state = documentStateSignal.value;
27026
- // Key presence is the flag — the value may be anything, including undefined.
27027
- const keyInState = Boolean(id && state && Object.hasOwn(state, id));
27029
+ //
27030
+ // Subscribed to this one key, through a computed, rather than to the state
27031
+ // as a whole: every popup with a nav state reads here, and a document state
27032
+ // is written whenever any of them opens — a list of forty names each with a
27033
+ // tooltip would re-render forty pickers, chains included, for the sheet
27034
+ // around them opening. The computed only wakes this component when what
27035
+ // sits under its own key changes.
27036
+ const keyValueComputed = useMemo(
27037
+ () =>
27038
+ computed(() => {
27039
+ const state = documentStateSignal.value;
27040
+ if (id && state && Object.hasOwn(state, id)) {
27041
+ return state[id];
27042
+ }
27043
+ return KEY_NOT_IN_STATE;
27044
+ }),
27045
+ [id],
27046
+ );
27047
+ const keyValue = keyValueComputed.value;
27048
+ const keyInState = keyValue !== KEY_NOT_IN_STATE;
27028
27049
  const onLeaveRef = useRef(onLeave);
27029
27050
  onLeaveRef.current = onLeave;
27030
27051
  const prevKeyInStateRef = useRef(keyInState);
@@ -27054,7 +27075,7 @@ const useNavStateBasic = (
27054
27075
  effectiveType = "replace";
27055
27076
  }
27056
27077
 
27057
- const currentValue = keyInState ? state[id] : defaultValue;
27078
+ const currentValue = keyInState ? keyValue : defaultValue;
27058
27079
 
27059
27080
  if (debug) {
27060
27081
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -34565,6 +34586,18 @@ const MOUNT_DEFAULT = "from-first-open";
34565
34586
  const popupsMountingContentForOpen = new Set();
34566
34587
  const isMountingContentForOpen = (popupElement) =>
34567
34588
  popupsMountingContentForOpen.has(popupElement);
34589
+ // The popup being built that holds `element` somewhere below it, or null. For
34590
+ // an element whose own openable ancestor is something else (a closed trigger
34591
+ // inside the popup): what the layout would say about it is what it says about
34592
+ // the whole popup — nothing is on screen there yet.
34593
+ const findPopupMountingContentAround = (element) => {
34594
+ for (const popupElement of popupsMountingContentForOpen) {
34595
+ if (popupElement !== element && popupElement.contains(element)) {
34596
+ return popupElement;
34597
+ }
34598
+ }
34599
+ return null;
34600
+ };
34568
34601
 
34569
34602
  // requestIdleCallback is missing from Safari; a timeout is close enough there.
34570
34603
  const requestIdle = (callback) =>
@@ -34734,6 +34767,9 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34734
34767
  // Set by the mount effect below for an element that lives in its openable
34735
34768
  // ancestor's façade rather than in what that ancestor opens.
34736
34769
  const displayedWhileAncestorClosedRef = useRef(false);
34770
+ // The popup whose opening will answer what the mount could not — see the
34771
+ // mount effect below.
34772
+ const decisionDeferredToRef = useRef(null);
34737
34773
 
34738
34774
  // Run on mount (or when deps change) — but only if the element is visible.
34739
34775
  useLayoutEffect(() => {
@@ -34747,14 +34783,26 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34747
34783
  return;
34748
34784
  }
34749
34785
  if (!isAncestorOpen(ancestor)) {
34750
- if (
34786
+ if (isMountingContentForOpen(ancestor)) {
34751
34787
  // The popup building its content for its own opening: nothing in it
34752
34788
  // is on screen, and the open about to follow reveals all of it. Known
34753
34789
  // without asking the layout, which a synchronous mount would have to
34754
- // bring up to date once per element asking.
34755
- isMountingContentForOpen(ancestor) ||
34756
- !isDisplayedDespiteClosedAncestor(el, ancestor)
34757
- ) {
34790
+ // bring up to date once per element asking; the observer below fires
34791
+ // once it opens.
34792
+ return;
34793
+ }
34794
+ const popupMounting = findPopupMountingContentAround(el);
34795
+ if (popupMounting) {
34796
+ // Under a closed trigger — a picker's façade, an expandable's header —
34797
+ // inside a popup being built. Whether this element is on screen once
34798
+ // that popup is open is exactly what the layout cannot say while the
34799
+ // popup, and everything in it, is display:none: it would answer "hidden"
34800
+ // for a façade that the opening is about to show. Asked again when the
34801
+ // popup opens (see the observer effect below).
34802
+ decisionDeferredToRef.current = popupMounting;
34803
+ return;
34804
+ }
34805
+ if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
34758
34806
  // Ancestor is closed and took this element off screen with it — skip
34759
34807
  // now; the observeAncestorOpenState call below will fire once it
34760
34808
  // opens.
@@ -34778,19 +34826,63 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34778
34826
  if (!ancestor) {
34779
34827
  return undefined;
34780
34828
  }
34781
- return observeAncestorOpenState(ancestor, ({ isOpen }) => {
34782
- if (!isOpen) {
34783
- return;
34784
- }
34785
- if (displayedWhileAncestorClosedRef.current) {
34786
- // Façade content: on screen the whole time, so this opening reveals
34787
- // nothing here — and `becauseAncestorOpened: true` about it would be
34788
- // false in a way consumers act on (see use_auto_focus.js).
34789
- return;
34790
- }
34791
- const lastEl = ref.current;
34792
- callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
34793
- });
34829
+ const stopObservingAncestor = observeAncestorOpenState(
34830
+ ancestor,
34831
+ ({ isOpen }) => {
34832
+ if (!isOpen) {
34833
+ return;
34834
+ }
34835
+ if (displayedWhileAncestorClosedRef.current) {
34836
+ // Façade content: on screen the whole time, so this opening reveals
34837
+ // nothing here — and `becauseAncestorOpened: true` about it would be
34838
+ // false in a way consumers act on (see use_auto_focus.js).
34839
+ return;
34840
+ }
34841
+ const lastEl = ref.current;
34842
+ callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
34843
+ },
34844
+ );
34845
+ const popupMounting = decisionDeferredToRef.current;
34846
+ if (!popupMounting) {
34847
+ return stopObservingAncestor;
34848
+ }
34849
+ // The mount deferred its decision to this popup's opening (see above):
34850
+ // once it is open, the layout answers for what the closed ancestor keeps
34851
+ // on screen. A façade element is then displayed, by that opening — which
34852
+ // owns it, the way it owns everything the popup reveals. An element the
34853
+ // closed ancestor genuinely hides stays with the observer above.
34854
+ const stopObservingPopup = observeAncestorOpenState(
34855
+ popupMounting,
34856
+ ({ isOpen }) => {
34857
+ if (!isOpen) {
34858
+ return;
34859
+ }
34860
+ stopObservingPopup();
34861
+ decisionDeferredToRef.current = null;
34862
+ const lastEl = ref.current;
34863
+ if (!lastEl) {
34864
+ return;
34865
+ }
34866
+ if (isAncestorOpen(ancestor)) {
34867
+ callbackRef.current(
34868
+ lastEl,
34869
+ createDisplayedEvent(popupMounting, true),
34870
+ );
34871
+ return;
34872
+ }
34873
+ if (isDisplayedDespiteClosedAncestor(lastEl, ancestor)) {
34874
+ displayedWhileAncestorClosedRef.current = true;
34875
+ callbackRef.current(
34876
+ lastEl,
34877
+ createDisplayedEvent(popupMounting, true),
34878
+ );
34879
+ }
34880
+ },
34881
+ );
34882
+ return () => {
34883
+ stopObservingAncestor();
34884
+ stopObservingPopup();
34885
+ };
34794
34886
  }, []);
34795
34887
  };
34796
34888
 
@@ -60017,24 +60109,18 @@ const useDialogProps = props => {
60017
60109
  // comment): this opening has its own content to be measured against, and
60018
60110
  // measuring it inside last time's box would answer with last time's size.
60019
60111
  unfreezeSize(dialogEl);
60020
- positionDialog();
60021
- if (sizing === "frozen") {
60022
- // After positionDialog: the caps it writes
60023
- // (--container-position-remaining-*) are part of what decides the size
60024
- // being taken, so measuring before it would freeze a box the dialog
60025
- // never actually had.
60026
- freezeSize(dialogEl);
60027
- }
60028
60112
 
60029
- // Reposition on the same triggers Popover's own visibleRectEffect
60030
- // already reacts to generically — window resize/scroll/visual-viewport
60031
- // changes for layer="top" (positionedAncestor is already
60113
+ // Placed by the effect's own first check, synchronously, on the opening
60114
+ // event — and repositioned on the same triggers Popover's own
60115
+ // visibleRectEffect reacts to generically: window resize/scroll/
60116
+ // visual-viewport changes for layer="top" (positionedAncestor is already
60032
60117
  // document.documentElement there, see its own computation above;
60033
- // visibleRectEffect already debounces visualViewport resize by 100ms
60034
- // to avoid the mobile tap-to-tap-input keyboard flicker, so no
60035
- // separate mechanism is needed here for that), or the positioned
60036
- // ancestor's own resize for layer="local" — see this file's top
60037
- // comment.
60118
+ // visibleRectEffect already debounces visualViewport resize by 100ms to
60119
+ // avoid the mobile tap-to-tap-input keyboard flicker, so no separate
60120
+ // mechanism is needed here for that), or the positioned ancestor's own
60121
+ // resize for layer="local" — see this file's top comment. One placement
60122
+ // for the opening: a positionDialog() of our own before the effect would
60123
+ // be a second pick, and a second layout between two reads of the viewport.
60038
60124
  const rectEffect = visibleRectEffect(positionedAncestor, (visibleRect, {
60039
60125
  event
60040
60126
  }) => {
@@ -60049,6 +60135,13 @@ const useDialogProps = props => {
60049
60135
  event: e,
60050
60136
  skipElementResize: true
60051
60137
  });
60138
+ if (sizing === "frozen") {
60139
+ // After the placement: the caps it writes
60140
+ // (--container-position-remaining-*) are part of what decides the size
60141
+ // being taken, so measuring before it would freeze a box the dialog
60142
+ // never actually had.
60143
+ freezeSize(dialogEl);
60144
+ }
60052
60145
  rectEffect.observeSize(dialogEl);
60053
60146
  // Exposed for the placement-props effect below, which needs to re-place an
60054
60147
  // already-open dialog.
@@ -62379,6 +62472,13 @@ const PickerCustomResolver = props => {
62379
62472
  if (props.calloutIcon === undefined) {
62380
62473
  props.calloutIcon = props.variant !== "text";
62381
62474
  }
62475
+ // A door's content is never where its value comes from (see `standalone`
62476
+ // above), so nothing needs it before the first open: built then, like a
62477
+ // popup's. A name in a list wrapped in a tooltip is otherwise a card in
62478
+ // the DOM per name, and each one asks the layout whether it is on screen.
62479
+ if (props.mount === undefined) {
62480
+ props.mount = MOUNT_DEFAULT;
62481
+ }
62382
62482
  if (props.rightSlotIcon === undefined) {
62383
62483
  props.rightSlotIcon = jsx(CalloutStatusIcon, {
62384
62484
  status: props.calloutStatus,
@@ -63230,13 +63330,15 @@ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
63230
63330
  * The content is rendered into an element this component owns, handed to the
63231
63331
  * callout as its message (a Node, appended as-is) and taken back when the
63232
63332
  * callout closes. Between two opens it is docked, hidden, in the span below:
63233
- * in the document the whole time, the way a popup's `mount="always"` keeps its
63234
- * content — so what it holds survives a close, and what it measures of itself
63235
- * at mount (a computed color, a light-dark() pair) resolves against a real
63236
- * ancestry. An element with no document has no computed style, and a badge
63237
- * reading its own background there would see nothing at all. The element
63238
- * carries data-picker-content: the callout is appended inside the picker root,
63239
- * and a press in there must read as inside the popup, not on the trigger.
63333
+ * in the document the whole time once built, so what it holds survives a
63334
+ * close, and what it measures of itself at mount (a computed color, a
63335
+ * light-dark() pair) resolves against a real ancestry. An element with no
63336
+ * document has no computed style, and a badge reading its own background
63337
+ * there would see nothing at all. When it is built is the popup's own `mount`
63338
+ * rule (see popup_content_mount.js) — the first open, for a callout. The
63339
+ * element carries data-picker-content: the callout is appended inside the
63340
+ * picker root, and a press in there must read as inside the popup, not on the
63341
+ * trigger.
63240
63342
  */
63241
63343
  const PickerCalloutPopup = ({
63242
63344
  ref,
@@ -63251,9 +63353,18 @@ const PickerCalloutPopup = ({
63251
63353
  onnavi_request_open,
63252
63354
  onnavi_request_close,
63253
63355
  onnavi_request_confirm,
63254
- children
63356
+ mount,
63357
+ children: childrenProp
63255
63358
  }) => {
63256
63359
  const hostRef = useRef(null);
63360
+ // The dock is what the content finds as its openable ancestor (the
63361
+ // aria-expanded below), which is what the mount marks as being built for
63362
+ // its own opening.
63363
+ const contentMounted = usePopupContentMount(openController, ref, {
63364
+ mount,
63365
+ anchor
63366
+ });
63367
+ const children = contentMounted ? childrenProp : null;
63257
63368
  // Reassigned on every render, like Popover's own, so it closes over the
63258
63369
  // latest props.
63259
63370
  openController.getElement = () => pickerRef.current;
@@ -66134,7 +66245,8 @@ const useListScrollSync = ({
66134
66245
  const debugScroll = useDebugScroll();
66135
66246
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
66136
66247
  virtual,
66137
- renderBudget
66248
+ renderBudget,
66249
+ scrolledWanted: scrolled ?? defaultScrolled
66138
66250
  });
66139
66251
  const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
66140
66252
  const getListEl = () => ref.current.querySelector(".navi_list");
@@ -67488,7 +67600,8 @@ const measureItemSize = (listEl, horizontal) => {
67488
67600
  };
67489
67601
  const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
67490
67602
  virtual,
67491
- renderBudget
67603
+ renderBudget,
67604
+ scrolledWanted
67492
67605
  }) => {
67493
67606
  const virtualSizeSignalRef = useRef(null);
67494
67607
  if (!virtualSizeSignalRef.current) {
@@ -67558,6 +67671,14 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
67558
67671
  if (virtualSizeSignal.peek() !== 0) {
67559
67672
  return undefined;
67560
67673
  }
67674
+ // Measured only for what reads the size: the fillers of rows held off
67675
+ // screen, and a list held somewhere (placeWhereHeld) before it knows where
67676
+ // that is. A list drawing every row it has, opening at its start, would
67677
+ // pay a layout in every commit for a number nobody reads.
67678
+ const sizeRead = virtual.totalSignal.peek() > renderBudget || scrolledWanted !== undefined && scrolledWanted !== "start";
67679
+ if (!sizeRead) {
67680
+ return undefined;
67681
+ }
67561
67682
  const listEl = ref.current?.querySelector(".navi_list");
67562
67683
  if (!listEl) {
67563
67684
  return undefined;
@@ -68774,6 +68895,7 @@ const ListItems = ({
68774
68895
  const virtual = useContext(ListVirtualContext);
68775
68896
  const slotId = useContext(ListSlotContext);
68776
68897
  const renderWindow = useContext(RenderWindowContext);
68898
+ const separator = useContext(SeparatorContext);
68777
68899
  // The vnode drawn for a row, kept by item: a run rendering again (its window
68778
68900
  // moving, its first paint's budget giving way to the full one) hands preact
68779
68901
  // the same vnode for a row that has not changed, and preact leaves that
@@ -68782,14 +68904,14 @@ const ListItems = ({
68782
68904
  // row at the same index, in the same refreshing state: everything the
68783
68905
  // function is given.
68784
68906
  const rowVnodesRef = useRef(null);
68785
- if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
68907
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem || rowVnodesRef.current.separator !== separator) {
68786
68908
  rowVnodesRef.current = {
68787
68909
  renderItem,
68910
+ separator,
68788
68911
  byItem: new Map()
68789
68912
  };
68790
68913
  }
68791
68914
  const rowVnodesByItem = rowVnodesRef.current.byItem;
68792
- const separator = useContext(SeparatorContext);
68793
68915
  const store = useItemStore({
68794
68916
  items,
68795
68917
  count,
@@ -69019,17 +69141,33 @@ const ListItems = ({
69019
69141
  const item = getItemAt(rowIndex);
69020
69142
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
69021
69143
  let rowVnode;
69144
+ let rowContextValue;
69145
+ let rowKept = null;
69022
69146
  if (item !== undefined) {
69023
69147
  const rowVnodeKept = rowVnodesByItem.get(item);
69024
69148
  if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
69025
69149
  rowVnode = rowVnodeKept.vnode;
69150
+ rowContextValue = rowVnodeKept.rowContextValue;
69151
+ rowKept = rowVnodeKept;
69026
69152
  } else {
69027
69153
  rowVnode = renderItem(item, rowIndex, renderItemState);
69028
- rowVnodesByItem.set(item, {
69154
+ // Kept with the vnode, for the same reason: a context value that is a
69155
+ // fresh object on every render forces every consumer of it to render,
69156
+ // which is the row's own chain — the vnode handed back unchanged would
69157
+ // then buy nothing.
69158
+ rowContextValue = {
69159
+ id: key,
69160
+ index: rowIndex,
69161
+ item
69162
+ };
69163
+ rowKept = {
69029
69164
  vnode: rowVnode,
69165
+ rowContextValue,
69030
69166
  rowIndex,
69031
- refreshing: renderItemState.refreshing
69032
- });
69167
+ refreshing: renderItemState.refreshing,
69168
+ separatorVnode: null
69169
+ };
69170
+ rowVnodesByItem.set(item, rowKept);
69033
69171
  }
69034
69172
  } else if (renderRowSkeleton === false) {
69035
69173
  // The row must still take its room: without it the rows below would
@@ -69053,20 +69191,25 @@ const ListItems = ({
69053
69191
  // as a hairline under the label.
69054
69192
  const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
69055
69193
  if (drawSeparator) {
69056
- pushRow(cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
69057
- key: `${key}_separator`
69058
- }), item, rowIndex);
69194
+ // Kept with the row too: a separator built again is a separator
69195
+ // rendered again.
69196
+ let separatorVnode = rowKept ? rowKept.separatorVnode : null;
69197
+ if (!separatorVnode) {
69198
+ separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
69199
+ key: `${key}_separator`
69200
+ });
69201
+ if (rowKept) {
69202
+ rowKept.separatorVnode = separatorVnode;
69203
+ }
69204
+ }
69205
+ pushRow(separatorVnode, item, rowIndex);
69059
69206
  }
69060
69207
  pushRow(jsx(ListRowContext.Provider, {
69061
69208
  value: item === undefined ? {
69062
69209
  id: key,
69063
69210
  index: rowIndex,
69064
69211
  ...getSkeletonRow()
69065
- } : {
69066
- id: key,
69067
- index: rowIndex,
69068
- item
69069
- },
69212
+ } : rowContextValue,
69070
69213
  children: rowVnode
69071
69214
  }, key), item, rowIndex);
69072
69215
  }
@@ -75264,9 +75407,13 @@ const useSearchText = (searchText, items, matchFn = applySearch) => {
75264
75407
  return { orderedItems, matchInfoMap };
75265
75408
  }, [items, searchText, matchFn]);
75266
75409
 
75267
- const getItemMatchInfo = (item) => {
75268
- return matchInfoMap.get(item);
75269
- };
75410
+ // The same function for as long as the map is the same: a `renderItem`
75411
+ // reading it is stable only if this is, and a run keeps the rows it drew
75412
+ // only for a stable `renderItem` (see List.Items).
75413
+ const getItemMatchInfo = useCallback(
75414
+ (item) => matchInfoMap.get(item),
75415
+ [matchInfoMap],
75416
+ );
75270
75417
 
75271
75418
  return [orderedItems, getItemMatchInfo];
75272
75419
  };