@jsenv/navi 0.29.343 → 0.29.344

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.
@@ -66045,10 +66045,11 @@ const ItemTransitionContext = createContext(false);
66045
66045
  // that must reserve room for what is not rendered.
66046
66046
  const ListVirtualContext = createContext(null);
66047
66047
  // Set around each row a run of items renders (see ListItems): which row of the
66048
- // collection it is, and where it stands among the rows the list holds. Carried
66049
- // by context rather than injected into whatever vnode renderItem returned, so
66050
- // that returning a component of one's own instead of a bare <List.Item> —
66051
- // works the same way.
66048
+ // collection it is, where it stands among the rows the list holds, and the
66049
+ // run's own account of which rows mount (see createRunRows) what the row
66050
+ // draws its separator from. Carried by context rather than injected into
66051
+ // whatever vnode renderItem returned, so that returning a component of one's
66052
+ // own — instead of a bare <List.Item> — works the same way.
66052
66053
  const ListRowContext = createContext(null);
66053
66054
  // The slot a child of the list stands in, by id (see ListDeclaredChildren). A
66054
66055
  // row takes its place in the collection by slot: the place is then the list's
@@ -68791,22 +68792,12 @@ const ListItemRowResolver = props => {
68791
68792
  ...props
68792
68793
  });
68793
68794
  }
68794
- // eslint-disable-next-line no-unused-vars
68795
- const {
68796
- id,
68797
- index,
68798
- item,
68799
- rowMinHeight,
68800
- rowMinWidth,
68801
- ...rowProps
68802
- } = row;
68803
68795
  return jsx(Next, {
68804
- ...rowProps,
68805
68796
  ...props,
68806
68797
  id: props.id || row.id,
68807
68798
  index: row.index,
68808
- minHeight: props.minHeight === undefined ? rowMinHeight : props.minHeight,
68809
- minWidth: props.minWidth === undefined ? rowMinWidth : props.minWidth
68799
+ minHeight: props.minHeight === undefined ? row.rowMinHeight : props.minHeight,
68800
+ minWidth: props.minWidth === undefined ? row.rowMinWidth : props.minWidth
68810
68801
  });
68811
68802
  };
68812
68803
  const ListItemPresentationResolver = props => {
@@ -68884,8 +68875,8 @@ const ListItemUI = props => {
68884
68875
  const virtual = useContext(ListVirtualContext);
68885
68876
  const searchNoMatchMode = useContext(SearchNoMatchModeContext);
68886
68877
  // The run this row belongs to, when it comes from one (see ListItems): it
68887
- // registered the row, decided it is inside the render window, and placed its
68888
- // separator. All that is left here is to draw it.
68878
+ // registered the row and decided it is inside the render window. Whether
68879
+ // the row mounts is decided here, and told back to the run.
68889
68880
  const row = useContext(ListRowContext);
68890
68881
  const slotId = useContext(ListSlotContext);
68891
68882
  // There is no standalone match/matchScore/highlight prop — participation
@@ -68917,16 +68908,24 @@ const ListItemUI = props => {
68917
68908
  // name of this very component (idDefault, not the row's id): two components
68918
68909
  // may stand for the same row for a moment, one leaving as the other arrives,
68919
68910
  // and the one leaving must give back its own place, not the newcomer's.
68920
- if (!row) {
68911
+ if (row) {
68921
68912
  if (props.filtered) {
68922
- virtual.drop(idDefault);
68913
+ row.run.unmount(idDefault);
68923
68914
  } else {
68924
- props.index = virtual.take(idDefault, 1, slotId);
68915
+ row.run.mount(idDefault, props.index, row.groupKey);
68925
68916
  }
68917
+ } else if (props.filtered) {
68918
+ virtual.drop(idDefault);
68919
+ } else {
68920
+ props.index = virtual.take(idDefault, 1, slotId);
68926
68921
  }
68927
68922
  useLayoutEffect(() => {
68928
68923
  return () => {
68929
- virtual.drop(idDefault);
68924
+ if (row) {
68925
+ row.run.unmount(idDefault);
68926
+ } else {
68927
+ virtual.drop(idDefault);
68928
+ }
68930
68929
  };
68931
68930
  }, []);
68932
68931
  // Every row that is drawn registers itself, whether it was declared one by
@@ -68940,47 +68939,43 @@ const ListItemUI = props => {
68940
68939
  if (props.filtered) {
68941
68940
  return null;
68942
68941
  }
68943
- // html-hidden items: excluded from virtual scroll accounting but always in DOM
68944
- if (props.hidden) {
68945
- // Its separator stays too, and stays invisible with it: the point of
68946
- // keeping a row that matches nothing is that nothing moves, and a divider
68947
- // that leaves takes its own height away.
68948
- if (!separator || props.index === 0) {
68949
- return jsx(ListItemReal, {
68950
- ...props
68951
- });
68952
- }
68953
- return jsxs(Fragment, {
68954
- children: [cloneElement(resolveSeparatorVnode(separator, props.index - 1), {
68955
- style: VISIBILITY_HIDDEN_STYLE
68956
- }), jsx(ListItemReal, {
68957
- ...props
68958
- })]
68959
- });
68960
- }
68961
- if (row) {
68962
- return jsx(ListItemReal, {
68963
- ...props
68964
- });
68965
- }
68966
- const index = props.index;
68967
68942
  const listItemVnode = jsx(ListItemReal, {
68968
68943
  ...props
68969
68944
  });
68970
- // "Am I the first visible item?" is answered by the place the list handed
68971
- // out (virtual.take above), not by the tracker's visibleIndex: during a
68972
- // reorder render pass (items resorted by search score) the other items still
68973
- // carry stale keyToExplicitOrder values, the binary search reads them, no
68974
- // item comes out at 0 and a spurious separator appears at the top. Inside a
68975
- // group, each group has its own tracker and its items do not reorder, so
68976
- // groupVisibleIndex is reliable.
68977
- const isFirstInList = groupVisibleIndex === null ? index === 0 : groupVisibleIndex === 0;
68978
- if (!separator || isFirstInList) {
68945
+ if (!separator) {
68979
68946
  return listItemVnode;
68980
68947
  }
68981
- // separatorIndex is only used as the function-form argument (gap index)
68982
- const separatorIndex = groupVisibleIndex === null ? index : groupVisibleIndex;
68983
- const separatorVnode = resolveSeparatorVnode(separator, separatorIndex - 1);
68948
+ // The separator a row wears is the one at the gap above it, so the first row
68949
+ // that mounts wears none. "Am I first?" is answered by whoever handed out
68950
+ // the place — the run for its rows, the list's virtual for a declared one
68951
+ // (virtual.take above) — not by the tracker's visibleIndex: during a reorder
68952
+ // render pass (items resorted by search score) the other items still carry
68953
+ // stale keyToExplicitOrder values, the binary search reads them, no item
68954
+ // comes out at 0 and a spurious separator appears at the top. Inside a
68955
+ // declared group, each group has its own tracker and its items do not
68956
+ // reorder, so groupVisibleIndex is reliable there.
68957
+ let isFirst;
68958
+ if (row) {
68959
+ isFirst = row.run.isFirst(props.index, row.groupKey);
68960
+ } else if (groupVisibleIndex === null || props.hidden) {
68961
+ isFirst = props.index === 0;
68962
+ } else {
68963
+ isFirst = groupVisibleIndex === 0;
68964
+ }
68965
+ if (isFirst) {
68966
+ return listItemVnode;
68967
+ }
68968
+ // The gap index, only used as the function-form argument.
68969
+ const gapIndex = row || groupVisibleIndex === null || props.hidden ? props.index - 1 : groupVisibleIndex - 1;
68970
+ let separatorVnode = resolveSeparatorVnode(separator, gapIndex);
68971
+ if (props.hidden) {
68972
+ // A row kept in the DOM but hidden keeps its separator, hidden with it:
68973
+ // the point of keeping a row that matches nothing is that nothing moves,
68974
+ // and a divider that leaves takes its own height away.
68975
+ separatorVnode = cloneElement(separatorVnode, {
68976
+ style: VISIBILITY_HIDDEN_STYLE
68977
+ });
68978
+ }
68984
68979
  return jsxs(Fragment, {
68985
68980
  children: [separatorVnode, listItemVnode]
68986
68981
  });
@@ -69602,6 +69597,101 @@ const sameSlotIds = (left, right) => {
69602
69597
  return true;
69603
69598
  };
69604
69599
 
69600
+ // Which of a run's rows mount, and which of them comes first. A run draws
69601
+ // every row of its window, and only the row itself knows, once it renders,
69602
+ // that it renders nothing (filtered out by a search, see ListItemUI). The
69603
+ // separator a row wears is the one at the gap above it, so the first row that
69604
+ // mounts wears none — and "first" is read off the rows that mount, not off
69605
+ // the collection. Rows say so as they render, in order, and the answer is a
69606
+ // signal: a row rendered from a kept vnode is rendered again when the row
69607
+ // before it leaves or comes back. Grouped rows are counted per group, the gap
69608
+ // above a group's first row being the group wrapper's own.
69609
+ const createRunRows = () => {
69610
+ // rowId → { index, groupKey }
69611
+ const rowById = new Map();
69612
+ // groupKey (undefined outside groups) → the index of the group's first
69613
+ // mounted row, -1 when none.
69614
+ const firstSignalByGroup = new Map();
69615
+ // Where the window starts: a run cut by the window has rows above its first
69616
+ // drawn one, and so does a run standing after declared rows.
69617
+ const windowFromSignal = signal(0);
69618
+ // Groups whose first row left: recounted on the next ask, or at the end of
69619
+ // the frame, whichever comes first.
69620
+ const staleGroupKeys = new Set();
69621
+ const firstSignalOf = groupKey => {
69622
+ let firstSignal = firstSignalByGroup.get(groupKey);
69623
+ if (!firstSignal) {
69624
+ firstSignal = signal(-1);
69625
+ firstSignalByGroup.set(groupKey, firstSignal);
69626
+ }
69627
+ return firstSignal;
69628
+ };
69629
+ const refresh = groupKey => {
69630
+ staleGroupKeys.delete(groupKey);
69631
+ let first = -1;
69632
+ for (const row of rowById.values()) {
69633
+ if (row.groupKey === groupKey && (first === -1 || row.index < first)) {
69634
+ first = row.index;
69635
+ }
69636
+ }
69637
+ firstSignalOf(groupKey).value = first;
69638
+ };
69639
+ const leave = row => {
69640
+ if (firstSignalOf(row.groupKey).peek() !== row.index) {
69641
+ return;
69642
+ }
69643
+ staleGroupKeys.add(row.groupKey);
69644
+ queueMicrotask(() => {
69645
+ if (staleGroupKeys.has(row.groupKey)) {
69646
+ refresh(row.groupKey);
69647
+ }
69648
+ });
69649
+ };
69650
+ return {
69651
+ setWindowFrom: windowFrom => {
69652
+ windowFromSignal.value = windowFrom;
69653
+ },
69654
+ mount: (rowId, index, groupKey) => {
69655
+ const row = rowById.get(rowId);
69656
+ if (row) {
69657
+ if (row.index === index && row.groupKey === groupKey) {
69658
+ return;
69659
+ }
69660
+ leave(row);
69661
+ row.index = index;
69662
+ row.groupKey = groupKey;
69663
+ } else {
69664
+ rowById.set(rowId, {
69665
+ index,
69666
+ groupKey
69667
+ });
69668
+ }
69669
+ const firstSignal = firstSignalOf(groupKey);
69670
+ const first = firstSignal.peek();
69671
+ if (first === -1 || index < first) {
69672
+ firstSignal.value = index;
69673
+ }
69674
+ },
69675
+ unmount: rowId => {
69676
+ const row = rowById.get(rowId);
69677
+ if (!row) {
69678
+ return;
69679
+ }
69680
+ rowById.delete(rowId);
69681
+ leave(row);
69682
+ },
69683
+ isFirst: (index, groupKey) => {
69684
+ if (staleGroupKeys.has(groupKey)) {
69685
+ refresh(groupKey);
69686
+ }
69687
+ if (firstSignalOf(groupKey).value !== index) {
69688
+ return false;
69689
+ }
69690
+ return groupKey !== undefined || windowFromSignal.value === 0;
69691
+ }
69692
+ };
69693
+ };
69694
+
69605
69695
  // The walk that gives the list's children their places: a slot for each of
69606
69696
  // them, declared to the list's virtual all at once before any child renders,
69607
69697
  // and handed to the child through a provider of its own — which is what lets
@@ -69786,6 +69876,11 @@ const ListItems = ({
69786
69876
  const slotId = useContext(ListSlotContext);
69787
69877
  const renderWindow = useContext(RenderWindowContext);
69788
69878
  const separator = useContext(SeparatorContext);
69879
+ const runRowsRef = useRef(null);
69880
+ if (!runRowsRef.current) {
69881
+ runRowsRef.current = createRunRows();
69882
+ }
69883
+ const runRows = runRowsRef.current;
69789
69884
  // The vnode drawn for a row, kept by item: a run rendering again (its window
69790
69885
  // moving, its first paint's budget giving way to the full one) hands preact
69791
69886
  // the same vnode for a row that has not changed, and preact leaves that
@@ -69794,10 +69889,9 @@ const ListItems = ({
69794
69889
  // row at the same index, in the same refreshing state: everything the
69795
69890
  // function is given.
69796
69891
  const rowVnodesRef = useRef(null);
69797
- if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem || rowVnodesRef.current.separator !== separator) {
69892
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
69798
69893
  rowVnodesRef.current = {
69799
69894
  renderItem,
69800
- separator,
69801
69895
  byItem: new Map()
69802
69896
  };
69803
69897
  }
@@ -69846,6 +69940,7 @@ const ListItems = ({
69846
69940
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
69847
69941
  const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
69848
69942
  store.forget(rankOf(windowFrom), rankOf(windowTo));
69943
+ runRows.setWindowFrom(windowFrom);
69849
69944
 
69850
69945
  // The row answers to its own id when the item carries one — that is what
69851
69946
  // addresses it from outside (--navi-select, --navi-scroll, startAt) — and
@@ -69969,13 +70064,9 @@ const ListItems = ({
69969
70064
  }, `${ownerId}_group_${group.key}`));
69970
70065
  group = null;
69971
70066
  };
69972
- // Which group a row belongs to, or undefined when it belongs to none. Asked
69973
- // before the row is pushed as well as while pushing it: a row opening a
69974
- // group is the one row that must not wear a separator (see below).
70067
+ // Which group a row belongs to, or undefined when it belongs to none.
69975
70068
  const groupKeyOf = (item, rowIndex) => groupBy && item !== undefined ? groupBy(item, rowIndex) : undefined;
69976
- const opensGroup = groupKey => groupKey !== undefined && (!group || group.key !== groupKey);
69977
- const pushRow = (rowNode, item, rowIndex) => {
69978
- const groupKey = groupKeyOf(item, rowIndex);
70069
+ const pushRow = (rowNode, item, rowIndex, groupKey) => {
69979
70070
  if (groupKey === undefined) {
69980
70071
  closeGroup();
69981
70072
  rows.push(rowNode);
@@ -70030,78 +70121,72 @@ const ListItems = ({
70030
70121
  }
70031
70122
  const item = getItemAt(rowIndex);
70032
70123
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
70124
+ const groupKey = groupKeyOf(item, rowIndex);
70125
+ if (item === undefined) {
70126
+ // A row on its way never reaches ListItemUI (see ListItemSkeletonResolver):
70127
+ // it is stood among the rows that mount, and given its separator, here.
70128
+ let rowVnode;
70129
+ if (renderRowSkeleton === false) {
70130
+ // The row must still take its room: without it the rows below would
70131
+ // climb up and slide back down as the answer arrives.
70132
+ rowVnode = jsx(ListItem, {
70133
+ skeleton: true,
70134
+ style: VISIBILITY_HIDDEN_STYLE
70135
+ });
70136
+ } else if (renderRowSkeleton) {
70137
+ rowVnode = renderRowSkeleton(rowIndex);
70138
+ } else {
70139
+ rowVnode = jsx(ListItem, {
70140
+ skeleton: true
70141
+ });
70142
+ }
70143
+ if (rowVnode) {
70144
+ pushRow(jsx(ListRunSkeletonRow, {
70145
+ run: runRows,
70146
+ row: {
70147
+ id: key,
70148
+ index: rowIndex,
70149
+ ...getSkeletonRow()
70150
+ },
70151
+ groupKey: groupKey,
70152
+ separator: separator,
70153
+ children: rowVnode
70154
+ }, key), item, rowIndex, groupKey);
70155
+ }
70156
+ rowIndex++;
70157
+ continue;
70158
+ }
70033
70159
  let rowVnode;
70034
70160
  let rowContextValue;
70035
- let rowKept = null;
70036
- if (item !== undefined) {
70037
- const rowVnodeKept = rowVnodesByItem.get(item);
70038
- if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
70039
- rowVnode = rowVnodeKept.vnode;
70040
- rowContextValue = rowVnodeKept.rowContextValue;
70041
- rowKept = rowVnodeKept;
70042
- } else {
70043
- rowVnode = renderItem(item, rowIndex, renderItemState);
70044
- // Kept with the vnode, for the same reason: a context value that is a
70045
- // fresh object on every render forces every consumer of it to render,
70046
- // which is the row's own chain — the vnode handed back unchanged would
70047
- // then buy nothing.
70048
- rowContextValue = {
70049
- id: key,
70050
- index: rowIndex,
70051
- item
70052
- };
70053
- rowKept = {
70054
- vnode: rowVnode,
70055
- rowContextValue,
70056
- rowIndex,
70057
- refreshing: renderItemState.refreshing,
70058
- separatorVnode: null
70059
- };
70060
- rowVnodesByItem.set(item, rowKept);
70061
- }
70062
- } else if (renderRowSkeleton === false) {
70063
- // The row must still take its room: without it the rows below would
70064
- // climb up and slide back down as the answer arrives.
70065
- rowVnode = jsx(ListItem, {
70066
- skeleton: true,
70067
- style: VISIBILITY_HIDDEN_STYLE
70068
- });
70069
- } else if (renderRowSkeleton) {
70070
- rowVnode = renderRowSkeleton(rowIndex);
70161
+ const rowVnodeKept = rowVnodesByItem.get(item);
70162
+ if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing && rowVnodeKept.rowContextValue.groupKey === groupKey) {
70163
+ rowVnode = rowVnodeKept.vnode;
70164
+ rowContextValue = rowVnodeKept.rowContextValue;
70071
70165
  } else {
70072
- rowVnode = jsx(ListItem, {
70073
- skeleton: true
70166
+ rowVnode = renderItem(item, rowIndex, renderItemState);
70167
+ // Kept with the vnode, for the same reason: a context value that is a
70168
+ // fresh object on every render forces every consumer of it to render,
70169
+ // which is the row's own chain — the vnode handed back unchanged would
70170
+ // then buy nothing.
70171
+ rowContextValue = {
70172
+ id: key,
70173
+ index: rowIndex,
70174
+ item,
70175
+ run: runRows,
70176
+ groupKey
70177
+ };
70178
+ rowVnodesByItem.set(item, {
70179
+ vnode: rowVnode,
70180
+ rowContextValue,
70181
+ rowIndex,
70182
+ refreshing: renderItemState.refreshing
70074
70183
  });
70075
70184
  }
70076
70185
  if (rowVnode) {
70077
- // The first row of a group wears no separator: the gap it sits at is the
70078
- // one between two groups, and that gap is the group wrapper's own — it
70079
- // is a row of the list like any other and draws its separator itself
70080
- // (see ListItemUI). Drawn here it would land inside the group instead,
70081
- // as a hairline under the label.
70082
- const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
70083
- if (drawSeparator) {
70084
- // Kept with the row too: a separator built again is a separator
70085
- // rendered again.
70086
- let separatorVnode = rowKept ? rowKept.separatorVnode : null;
70087
- if (!separatorVnode) {
70088
- separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
70089
- key: `${key}_separator`
70090
- });
70091
- if (rowKept) {
70092
- rowKept.separatorVnode = separatorVnode;
70093
- }
70094
- }
70095
- pushRow(separatorVnode, item, rowIndex);
70096
- }
70097
70186
  pushRow(jsx(ListRowContext.Provider, {
70098
- value: item === undefined ? {
70099
- id: key,
70100
- index: rowIndex,
70101
- ...getSkeletonRow()
70102
- } : rowContextValue,
70187
+ value: rowContextValue,
70103
70188
  children: rowVnode
70104
- }, key), item, rowIndex);
70189
+ }, key), item, rowIndex, groupKey);
70105
70190
  }
70106
70191
  rowIndex++;
70107
70192
  }
@@ -70115,6 +70200,35 @@ const ListItems = ({
70115
70200
  return rows;
70116
70201
  };
70117
70202
 
70203
+ // A run's row that has not arrived, standing where the real one will: it
70204
+ // mounts like any row (see createRunRows) and wears the separator of the gap
70205
+ // above it, the way a real row does in ListItemUI.
70206
+ const ListRunSkeletonRow = ({
70207
+ run,
70208
+ row,
70209
+ groupKey,
70210
+ separator,
70211
+ children
70212
+ }) => {
70213
+ const rowId = useId();
70214
+ run.mount(rowId, row.index, groupKey);
70215
+ useLayoutEffect(() => {
70216
+ return () => {
70217
+ run.unmount(rowId);
70218
+ };
70219
+ }, []);
70220
+ const rowVnode = jsx(ListRowContext.Provider, {
70221
+ value: row,
70222
+ children: children
70223
+ });
70224
+ if (!separator || run.isFirst(row.index, groupKey)) {
70225
+ return rowVnode;
70226
+ }
70227
+ return jsxs(Fragment, {
70228
+ children: [resolveSeparatorVnode(separator, row.index - 1), rowVnode]
70229
+ });
70230
+ };
70231
+
70118
70232
  // What is drawn where rows were asked for and never came: the sentence and the
70119
70233
  // way out, in the row itself — the rest of the list is fine, so replacing all
70120
70234
  // of it (List's own `error`) would be a lie.