@jsenv/navi 0.29.319 → 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.
@@ -27799,14 +27799,35 @@ Consider using unique IDs for each component instance.`,
27799
27799
 
27800
27800
  const NO_OP = () => {};
27801
27801
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
27802
+ // What the computed below answers for a key the document state does not hold:
27803
+ // key presence is the flag, and the value under a present key may be anything,
27804
+ // undefined included.
27805
+ const KEY_NOT_IN_STATE = Symbol("key_not_in_state");
27802
27806
  const useNavStateBasic = (
27803
27807
  id,
27804
27808
  { debug, type = "replace", onLeave, defaultValue } = {},
27805
27809
  ) => {
27806
27810
  // Hooks must be called unconditionally — before the !id early return.
27807
- const state = documentStateSignal.value;
27808
- // Key presence is the flag — the value may be anything, including undefined.
27809
- const keyInState = Boolean(id && state && Object.hasOwn(state, id));
27811
+ //
27812
+ // Subscribed to this one key, through a computed, rather than to the state
27813
+ // as a whole: every popup with a nav state reads here, and a document state
27814
+ // is written whenever any of them opens — a list of forty names each with a
27815
+ // tooltip would re-render forty pickers, chains included, for the sheet
27816
+ // around them opening. The computed only wakes this component when what
27817
+ // sits under its own key changes.
27818
+ const keyValueComputed = useMemo(
27819
+ () =>
27820
+ computed(() => {
27821
+ const state = documentStateSignal.value;
27822
+ if (id && state && Object.hasOwn(state, id)) {
27823
+ return state[id];
27824
+ }
27825
+ return KEY_NOT_IN_STATE;
27826
+ }),
27827
+ [id],
27828
+ );
27829
+ const keyValue = keyValueComputed.value;
27830
+ const keyInState = keyValue !== KEY_NOT_IN_STATE;
27810
27831
  const onLeaveRef = useRef(onLeave);
27811
27832
  onLeaveRef.current = onLeave;
27812
27833
  const prevKeyInStateRef = useRef(keyInState);
@@ -27841,7 +27862,7 @@ const useNavStateBasic = (
27841
27862
  effectiveType = "replace";
27842
27863
  }
27843
27864
 
27844
- const currentValue = keyInState ? state[id] : defaultValue;
27865
+ const currentValue = keyInState ? keyValue : defaultValue;
27845
27866
 
27846
27867
  if (debug) {
27847
27868
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -35465,6 +35486,18 @@ const MOUNT_DEFAULT = "from-first-open";
35465
35486
  const popupsMountingContentForOpen = new Set();
35466
35487
  const isMountingContentForOpen = (popupElement) =>
35467
35488
  popupsMountingContentForOpen.has(popupElement);
35489
+ // The popup being built that holds `element` somewhere below it, or null. For
35490
+ // an element whose own openable ancestor is something else (a closed trigger
35491
+ // inside the popup): what the layout would say about it is what it says about
35492
+ // the whole popup — nothing is on screen there yet.
35493
+ const findPopupMountingContentAround = (element) => {
35494
+ for (const popupElement of popupsMountingContentForOpen) {
35495
+ if (popupElement !== element && popupElement.contains(element)) {
35496
+ return popupElement;
35497
+ }
35498
+ }
35499
+ return null;
35500
+ };
35468
35501
 
35469
35502
  // requestIdleCallback is missing from Safari; a timeout is close enough there.
35470
35503
  const requestIdle = (callback) =>
@@ -35634,6 +35667,9 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
35634
35667
  // Set by the mount effect below for an element that lives in its openable
35635
35668
  // ancestor's façade rather than in what that ancestor opens.
35636
35669
  const displayedWhileAncestorClosedRef = useRef(false);
35670
+ // The popup whose opening will answer what the mount could not — see the
35671
+ // mount effect below.
35672
+ const decisionDeferredToRef = useRef(null);
35637
35673
 
35638
35674
  // Run on mount (or when deps change) — but only if the element is visible.
35639
35675
  useLayoutEffect(() => {
@@ -35647,14 +35683,26 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
35647
35683
  return;
35648
35684
  }
35649
35685
  if (!isAncestorOpen(ancestor)) {
35650
- if (
35686
+ if (isMountingContentForOpen(ancestor)) {
35651
35687
  // The popup building its content for its own opening: nothing in it
35652
35688
  // is on screen, and the open about to follow reveals all of it. Known
35653
35689
  // without asking the layout, which a synchronous mount would have to
35654
- // bring up to date once per element asking.
35655
- isMountingContentForOpen(ancestor) ||
35656
- !isDisplayedDespiteClosedAncestor(el, ancestor)
35657
- ) {
35690
+ // bring up to date once per element asking; the observer below fires
35691
+ // once it opens.
35692
+ return;
35693
+ }
35694
+ const popupMounting = findPopupMountingContentAround(el);
35695
+ if (popupMounting) {
35696
+ // Under a closed trigger — a picker's façade, an expandable's header —
35697
+ // inside a popup being built. Whether this element is on screen once
35698
+ // that popup is open is exactly what the layout cannot say while the
35699
+ // popup, and everything in it, is display:none: it would answer "hidden"
35700
+ // for a façade that the opening is about to show. Asked again when the
35701
+ // popup opens (see the observer effect below).
35702
+ decisionDeferredToRef.current = popupMounting;
35703
+ return;
35704
+ }
35705
+ if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
35658
35706
  // Ancestor is closed and took this element off screen with it — skip
35659
35707
  // now; the observeAncestorOpenState call below will fire once it
35660
35708
  // opens.
@@ -35678,19 +35726,63 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
35678
35726
  if (!ancestor) {
35679
35727
  return undefined;
35680
35728
  }
35681
- return observeAncestorOpenState(ancestor, ({ isOpen }) => {
35682
- if (!isOpen) {
35683
- return;
35684
- }
35685
- if (displayedWhileAncestorClosedRef.current) {
35686
- // Façade content: on screen the whole time, so this opening reveals
35687
- // nothing here — and `becauseAncestorOpened: true` about it would be
35688
- // false in a way consumers act on (see use_auto_focus.js).
35689
- return;
35690
- }
35691
- const lastEl = ref.current;
35692
- callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
35693
- });
35729
+ const stopObservingAncestor = observeAncestorOpenState(
35730
+ ancestor,
35731
+ ({ isOpen }) => {
35732
+ if (!isOpen) {
35733
+ return;
35734
+ }
35735
+ if (displayedWhileAncestorClosedRef.current) {
35736
+ // Façade content: on screen the whole time, so this opening reveals
35737
+ // nothing here — and `becauseAncestorOpened: true` about it would be
35738
+ // false in a way consumers act on (see use_auto_focus.js).
35739
+ return;
35740
+ }
35741
+ const lastEl = ref.current;
35742
+ callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
35743
+ },
35744
+ );
35745
+ const popupMounting = decisionDeferredToRef.current;
35746
+ if (!popupMounting) {
35747
+ return stopObservingAncestor;
35748
+ }
35749
+ // The mount deferred its decision to this popup's opening (see above):
35750
+ // once it is open, the layout answers for what the closed ancestor keeps
35751
+ // on screen. A façade element is then displayed, by that opening — which
35752
+ // owns it, the way it owns everything the popup reveals. An element the
35753
+ // closed ancestor genuinely hides stays with the observer above.
35754
+ const stopObservingPopup = observeAncestorOpenState(
35755
+ popupMounting,
35756
+ ({ isOpen }) => {
35757
+ if (!isOpen) {
35758
+ return;
35759
+ }
35760
+ stopObservingPopup();
35761
+ decisionDeferredToRef.current = null;
35762
+ const lastEl = ref.current;
35763
+ if (!lastEl) {
35764
+ return;
35765
+ }
35766
+ if (isAncestorOpen(ancestor)) {
35767
+ callbackRef.current(
35768
+ lastEl,
35769
+ createDisplayedEvent(popupMounting, true),
35770
+ );
35771
+ return;
35772
+ }
35773
+ if (isDisplayedDespiteClosedAncestor(lastEl, ancestor)) {
35774
+ displayedWhileAncestorClosedRef.current = true;
35775
+ callbackRef.current(
35776
+ lastEl,
35777
+ createDisplayedEvent(popupMounting, true),
35778
+ );
35779
+ }
35780
+ },
35781
+ );
35782
+ return () => {
35783
+ stopObservingAncestor();
35784
+ stopObservingPopup();
35785
+ };
35694
35786
  }, []);
35695
35787
  };
35696
35788
 
@@ -70059,6 +70151,7 @@ const ListItems = ({
70059
70151
  const virtual = useContext(ListVirtualContext);
70060
70152
  const slotId = useContext(ListSlotContext);
70061
70153
  const renderWindow = useContext(RenderWindowContext);
70154
+ const separator = useContext(SeparatorContext);
70062
70155
  // The vnode drawn for a row, kept by item: a run rendering again (its window
70063
70156
  // moving, its first paint's budget giving way to the full one) hands preact
70064
70157
  // the same vnode for a row that has not changed, and preact leaves that
@@ -70067,14 +70160,14 @@ const ListItems = ({
70067
70160
  // row at the same index, in the same refreshing state: everything the
70068
70161
  // function is given.
70069
70162
  const rowVnodesRef = useRef(null);
70070
- if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
70163
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem || rowVnodesRef.current.separator !== separator) {
70071
70164
  rowVnodesRef.current = {
70072
70165
  renderItem,
70166
+ separator,
70073
70167
  byItem: new Map()
70074
70168
  };
70075
70169
  }
70076
70170
  const rowVnodesByItem = rowVnodesRef.current.byItem;
70077
- const separator = useContext(SeparatorContext);
70078
70171
  const store = useItemStore({
70079
70172
  items,
70080
70173
  count,
@@ -70304,17 +70397,33 @@ const ListItems = ({
70304
70397
  const item = getItemAt(rowIndex);
70305
70398
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
70306
70399
  let rowVnode;
70400
+ let rowContextValue;
70401
+ let rowKept = null;
70307
70402
  if (item !== undefined) {
70308
70403
  const rowVnodeKept = rowVnodesByItem.get(item);
70309
70404
  if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
70310
70405
  rowVnode = rowVnodeKept.vnode;
70406
+ rowContextValue = rowVnodeKept.rowContextValue;
70407
+ rowKept = rowVnodeKept;
70311
70408
  } else {
70312
70409
  rowVnode = renderItem(item, rowIndex, renderItemState);
70313
- rowVnodesByItem.set(item, {
70410
+ // Kept with the vnode, for the same reason: a context value that is a
70411
+ // fresh object on every render forces every consumer of it to render,
70412
+ // which is the row's own chain — the vnode handed back unchanged would
70413
+ // then buy nothing.
70414
+ rowContextValue = {
70415
+ id: key,
70416
+ index: rowIndex,
70417
+ item
70418
+ };
70419
+ rowKept = {
70314
70420
  vnode: rowVnode,
70421
+ rowContextValue,
70315
70422
  rowIndex,
70316
- refreshing: renderItemState.refreshing
70317
- });
70423
+ refreshing: renderItemState.refreshing,
70424
+ separatorVnode: null
70425
+ };
70426
+ rowVnodesByItem.set(item, rowKept);
70318
70427
  }
70319
70428
  } else if (renderRowSkeleton === false) {
70320
70429
  // The row must still take its room: without it the rows below would
@@ -70338,20 +70447,25 @@ const ListItems = ({
70338
70447
  // as a hairline under the label.
70339
70448
  const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
70340
70449
  if (drawSeparator) {
70341
- pushRow(cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
70342
- key: `${key}_separator`
70343
- }), item, rowIndex);
70450
+ // Kept with the row too: a separator built again is a separator
70451
+ // rendered again.
70452
+ let separatorVnode = rowKept ? rowKept.separatorVnode : null;
70453
+ if (!separatorVnode) {
70454
+ separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
70455
+ key: `${key}_separator`
70456
+ });
70457
+ if (rowKept) {
70458
+ rowKept.separatorVnode = separatorVnode;
70459
+ }
70460
+ }
70461
+ pushRow(separatorVnode, item, rowIndex);
70344
70462
  }
70345
70463
  pushRow(jsx(ListRowContext.Provider, {
70346
70464
  value: item === undefined ? {
70347
70465
  id: key,
70348
70466
  index: rowIndex,
70349
70467
  ...getSkeletonRow()
70350
- } : {
70351
- id: key,
70352
- index: rowIndex,
70353
- item
70354
- },
70468
+ } : rowContextValue,
70355
70469
  children: rowVnode
70356
70470
  }, key), item, rowIndex);
70357
70471
  }