@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.
@@ -67329,10 +67329,11 @@ const ItemTransitionContext = createContext(false);
67329
67329
  // that must reserve room for what is not rendered.
67330
67330
  const ListVirtualContext = createContext(null);
67331
67331
  // Set around each row a run of items renders (see ListItems): which row of the
67332
- // collection it is, and where it stands among the rows the list holds. Carried
67333
- // by context rather than injected into whatever vnode renderItem returned, so
67334
- // that returning a component of one's own instead of a bare <List.Item> —
67335
- // works the same way.
67332
+ // collection it is, where it stands among the rows the list holds, and the
67333
+ // run's own account of which rows mount (see createRunRows) what the row
67334
+ // draws its separator from. Carried by context rather than injected into
67335
+ // whatever vnode renderItem returned, so that returning a component of one's
67336
+ // own — instead of a bare <List.Item> — works the same way.
67336
67337
  const ListRowContext = createContext(null);
67337
67338
  // The slot a child of the list stands in, by id (see ListDeclaredChildren). A
67338
67339
  // row takes its place in the collection by slot: the place is then the list's
@@ -70161,22 +70162,12 @@ const ListItemRowResolver = props => {
70161
70162
  ...props
70162
70163
  });
70163
70164
  }
70164
- // eslint-disable-next-line no-unused-vars
70165
- const {
70166
- id,
70167
- index,
70168
- item,
70169
- rowMinHeight,
70170
- rowMinWidth,
70171
- ...rowProps
70172
- } = row;
70173
70165
  return jsx(Next, {
70174
- ...rowProps,
70175
70166
  ...props,
70176
70167
  id: props.id || row.id,
70177
70168
  index: row.index,
70178
- minHeight: props.minHeight === undefined ? rowMinHeight : props.minHeight,
70179
- minWidth: props.minWidth === undefined ? rowMinWidth : props.minWidth
70169
+ minHeight: props.minHeight === undefined ? row.rowMinHeight : props.minHeight,
70170
+ minWidth: props.minWidth === undefined ? row.rowMinWidth : props.minWidth
70180
70171
  });
70181
70172
  };
70182
70173
  const ListItemPresentationResolver = props => {
@@ -70254,8 +70245,8 @@ const ListItemUI = props => {
70254
70245
  const virtual = useContext(ListVirtualContext);
70255
70246
  const searchNoMatchMode = useContext(SearchNoMatchModeContext);
70256
70247
  // The run this row belongs to, when it comes from one (see ListItems): it
70257
- // registered the row, decided it is inside the render window, and placed its
70258
- // separator. All that is left here is to draw it.
70248
+ // registered the row and decided it is inside the render window. Whether
70249
+ // the row mounts is decided here, and told back to the run.
70259
70250
  const row = useContext(ListRowContext);
70260
70251
  const slotId = useContext(ListSlotContext);
70261
70252
  // There is no standalone match/matchScore/highlight prop — participation
@@ -70287,16 +70278,24 @@ const ListItemUI = props => {
70287
70278
  // name of this very component (idDefault, not the row's id): two components
70288
70279
  // may stand for the same row for a moment, one leaving as the other arrives,
70289
70280
  // and the one leaving must give back its own place, not the newcomer's.
70290
- if (!row) {
70281
+ if (row) {
70291
70282
  if (props.filtered) {
70292
- virtual.drop(idDefault);
70283
+ row.run.unmount(idDefault);
70293
70284
  } else {
70294
- props.index = virtual.take(idDefault, 1, slotId);
70285
+ row.run.mount(idDefault, props.index, row.groupKey);
70295
70286
  }
70287
+ } else if (props.filtered) {
70288
+ virtual.drop(idDefault);
70289
+ } else {
70290
+ props.index = virtual.take(idDefault, 1, slotId);
70296
70291
  }
70297
70292
  useLayoutEffect(() => {
70298
70293
  return () => {
70299
- virtual.drop(idDefault);
70294
+ if (row) {
70295
+ row.run.unmount(idDefault);
70296
+ } else {
70297
+ virtual.drop(idDefault);
70298
+ }
70300
70299
  };
70301
70300
  }, []);
70302
70301
  // Every row that is drawn registers itself, whether it was declared one by
@@ -70310,47 +70309,43 @@ const ListItemUI = props => {
70310
70309
  if (props.filtered) {
70311
70310
  return null;
70312
70311
  }
70313
- // html-hidden items: excluded from virtual scroll accounting but always in DOM
70314
- if (props.hidden) {
70315
- // Its separator stays too, and stays invisible with it: the point of
70316
- // keeping a row that matches nothing is that nothing moves, and a divider
70317
- // that leaves takes its own height away.
70318
- if (!separator || props.index === 0) {
70319
- return jsx(ListItemReal, {
70320
- ...props
70321
- });
70322
- }
70323
- return jsxs(Fragment, {
70324
- children: [cloneElement(resolveSeparatorVnode(separator, props.index - 1), {
70325
- style: VISIBILITY_HIDDEN_STYLE
70326
- }), jsx(ListItemReal, {
70327
- ...props
70328
- })]
70329
- });
70330
- }
70331
- if (row) {
70332
- return jsx(ListItemReal, {
70333
- ...props
70334
- });
70335
- }
70336
- const index = props.index;
70337
70312
  const listItemVnode = jsx(ListItemReal, {
70338
70313
  ...props
70339
70314
  });
70340
- // "Am I the first visible item?" is answered by the place the list handed
70341
- // out (virtual.take above), not by the tracker's visibleIndex: during a
70342
- // reorder render pass (items resorted by search score) the other items still
70343
- // carry stale keyToExplicitOrder values, the binary search reads them, no
70344
- // item comes out at 0 and a spurious separator appears at the top. Inside a
70345
- // group, each group has its own tracker and its items do not reorder, so
70346
- // groupVisibleIndex is reliable.
70347
- const isFirstInList = groupVisibleIndex === null ? index === 0 : groupVisibleIndex === 0;
70348
- if (!separator || isFirstInList) {
70315
+ if (!separator) {
70349
70316
  return listItemVnode;
70350
70317
  }
70351
- // separatorIndex is only used as the function-form argument (gap index)
70352
- const separatorIndex = groupVisibleIndex === null ? index : groupVisibleIndex;
70353
- const separatorVnode = resolveSeparatorVnode(separator, separatorIndex - 1);
70318
+ // The separator a row wears is the one at the gap above it, so the first row
70319
+ // that mounts wears none. "Am I first?" is answered by whoever handed out
70320
+ // the place — the run for its rows, the list's virtual for a declared one
70321
+ // (virtual.take above) — not by the tracker's visibleIndex: during a reorder
70322
+ // render pass (items resorted by search score) the other items still carry
70323
+ // stale keyToExplicitOrder values, the binary search reads them, no item
70324
+ // comes out at 0 and a spurious separator appears at the top. Inside a
70325
+ // declared group, each group has its own tracker and its items do not
70326
+ // reorder, so groupVisibleIndex is reliable there.
70327
+ let isFirst;
70328
+ if (row) {
70329
+ isFirst = row.run.isFirst(props.index, row.groupKey);
70330
+ } else if (groupVisibleIndex === null || props.hidden) {
70331
+ isFirst = props.index === 0;
70332
+ } else {
70333
+ isFirst = groupVisibleIndex === 0;
70334
+ }
70335
+ if (isFirst) {
70336
+ return listItemVnode;
70337
+ }
70338
+ // The gap index, only used as the function-form argument.
70339
+ const gapIndex = row || groupVisibleIndex === null || props.hidden ? props.index - 1 : groupVisibleIndex - 1;
70340
+ let separatorVnode = resolveSeparatorVnode(separator, gapIndex);
70341
+ if (props.hidden) {
70342
+ // A row kept in the DOM but hidden keeps its separator, hidden with it:
70343
+ // the point of keeping a row that matches nothing is that nothing moves,
70344
+ // and a divider that leaves takes its own height away.
70345
+ separatorVnode = cloneElement(separatorVnode, {
70346
+ style: VISIBILITY_HIDDEN_STYLE
70347
+ });
70348
+ }
70354
70349
  return jsxs(Fragment, {
70355
70350
  children: [separatorVnode, listItemVnode]
70356
70351
  });
@@ -70972,6 +70967,101 @@ const sameSlotIds = (left, right) => {
70972
70967
  return true;
70973
70968
  };
70974
70969
 
70970
+ // Which of a run's rows mount, and which of them comes first. A run draws
70971
+ // every row of its window, and only the row itself knows, once it renders,
70972
+ // that it renders nothing (filtered out by a search, see ListItemUI). The
70973
+ // separator a row wears is the one at the gap above it, so the first row that
70974
+ // mounts wears none — and "first" is read off the rows that mount, not off
70975
+ // the collection. Rows say so as they render, in order, and the answer is a
70976
+ // signal: a row rendered from a kept vnode is rendered again when the row
70977
+ // before it leaves or comes back. Grouped rows are counted per group, the gap
70978
+ // above a group's first row being the group wrapper's own.
70979
+ const createRunRows = () => {
70980
+ // rowId → { index, groupKey }
70981
+ const rowById = new Map();
70982
+ // groupKey (undefined outside groups) → the index of the group's first
70983
+ // mounted row, -1 when none.
70984
+ const firstSignalByGroup = new Map();
70985
+ // Where the window starts: a run cut by the window has rows above its first
70986
+ // drawn one, and so does a run standing after declared rows.
70987
+ const windowFromSignal = signal(0);
70988
+ // Groups whose first row left: recounted on the next ask, or at the end of
70989
+ // the frame, whichever comes first.
70990
+ const staleGroupKeys = new Set();
70991
+ const firstSignalOf = groupKey => {
70992
+ let firstSignal = firstSignalByGroup.get(groupKey);
70993
+ if (!firstSignal) {
70994
+ firstSignal = signal(-1);
70995
+ firstSignalByGroup.set(groupKey, firstSignal);
70996
+ }
70997
+ return firstSignal;
70998
+ };
70999
+ const refresh = groupKey => {
71000
+ staleGroupKeys.delete(groupKey);
71001
+ let first = -1;
71002
+ for (const row of rowById.values()) {
71003
+ if (row.groupKey === groupKey && (first === -1 || row.index < first)) {
71004
+ first = row.index;
71005
+ }
71006
+ }
71007
+ firstSignalOf(groupKey).value = first;
71008
+ };
71009
+ const leave = row => {
71010
+ if (firstSignalOf(row.groupKey).peek() !== row.index) {
71011
+ return;
71012
+ }
71013
+ staleGroupKeys.add(row.groupKey);
71014
+ queueMicrotask(() => {
71015
+ if (staleGroupKeys.has(row.groupKey)) {
71016
+ refresh(row.groupKey);
71017
+ }
71018
+ });
71019
+ };
71020
+ return {
71021
+ setWindowFrom: windowFrom => {
71022
+ windowFromSignal.value = windowFrom;
71023
+ },
71024
+ mount: (rowId, index, groupKey) => {
71025
+ const row = rowById.get(rowId);
71026
+ if (row) {
71027
+ if (row.index === index && row.groupKey === groupKey) {
71028
+ return;
71029
+ }
71030
+ leave(row);
71031
+ row.index = index;
71032
+ row.groupKey = groupKey;
71033
+ } else {
71034
+ rowById.set(rowId, {
71035
+ index,
71036
+ groupKey
71037
+ });
71038
+ }
71039
+ const firstSignal = firstSignalOf(groupKey);
71040
+ const first = firstSignal.peek();
71041
+ if (first === -1 || index < first) {
71042
+ firstSignal.value = index;
71043
+ }
71044
+ },
71045
+ unmount: rowId => {
71046
+ const row = rowById.get(rowId);
71047
+ if (!row) {
71048
+ return;
71049
+ }
71050
+ rowById.delete(rowId);
71051
+ leave(row);
71052
+ },
71053
+ isFirst: (index, groupKey) => {
71054
+ if (staleGroupKeys.has(groupKey)) {
71055
+ refresh(groupKey);
71056
+ }
71057
+ if (firstSignalOf(groupKey).value !== index) {
71058
+ return false;
71059
+ }
71060
+ return groupKey !== undefined || windowFromSignal.value === 0;
71061
+ }
71062
+ };
71063
+ };
71064
+
70975
71065
  // The walk that gives the list's children their places: a slot for each of
70976
71066
  // them, declared to the list's virtual all at once before any child renders,
70977
71067
  // and handed to the child through a provider of its own — which is what lets
@@ -71156,6 +71246,11 @@ const ListItems = ({
71156
71246
  const slotId = useContext(ListSlotContext);
71157
71247
  const renderWindow = useContext(RenderWindowContext);
71158
71248
  const separator = useContext(SeparatorContext);
71249
+ const runRowsRef = useRef(null);
71250
+ if (!runRowsRef.current) {
71251
+ runRowsRef.current = createRunRows();
71252
+ }
71253
+ const runRows = runRowsRef.current;
71159
71254
  // The vnode drawn for a row, kept by item: a run rendering again (its window
71160
71255
  // moving, its first paint's budget giving way to the full one) hands preact
71161
71256
  // the same vnode for a row that has not changed, and preact leaves that
@@ -71164,10 +71259,9 @@ const ListItems = ({
71164
71259
  // row at the same index, in the same refreshing state: everything the
71165
71260
  // function is given.
71166
71261
  const rowVnodesRef = useRef(null);
71167
- if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem || rowVnodesRef.current.separator !== separator) {
71262
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
71168
71263
  rowVnodesRef.current = {
71169
71264
  renderItem,
71170
- separator,
71171
71265
  byItem: new Map()
71172
71266
  };
71173
71267
  }
@@ -71216,6 +71310,7 @@ const ListItems = ({
71216
71310
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;
71217
71311
  const windowTo = renderWindow.end < runEnd ? renderWindow.end : runEnd;
71218
71312
  store.forget(rankOf(windowFrom), rankOf(windowTo));
71313
+ runRows.setWindowFrom(windowFrom);
71219
71314
 
71220
71315
  // The row answers to its own id when the item carries one — that is what
71221
71316
  // addresses it from outside (--navi-select, --navi-scroll, startAt) — and
@@ -71339,13 +71434,9 @@ const ListItems = ({
71339
71434
  }, `${ownerId}_group_${group.key}`));
71340
71435
  group = null;
71341
71436
  };
71342
- // Which group a row belongs to, or undefined when it belongs to none. Asked
71343
- // before the row is pushed as well as while pushing it: a row opening a
71344
- // group is the one row that must not wear a separator (see below).
71437
+ // Which group a row belongs to, or undefined when it belongs to none.
71345
71438
  const groupKeyOf = (item, rowIndex) => groupBy && item !== undefined ? groupBy(item, rowIndex) : undefined;
71346
- const opensGroup = groupKey => groupKey !== undefined && (!group || group.key !== groupKey);
71347
- const pushRow = (rowNode, item, rowIndex) => {
71348
- const groupKey = groupKeyOf(item, rowIndex);
71439
+ const pushRow = (rowNode, item, rowIndex, groupKey) => {
71349
71440
  if (groupKey === undefined) {
71350
71441
  closeGroup();
71351
71442
  rows.push(rowNode);
@@ -71400,78 +71491,72 @@ const ListItems = ({
71400
71491
  }
71401
71492
  const item = getItemAt(rowIndex);
71402
71493
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
71494
+ const groupKey = groupKeyOf(item, rowIndex);
71495
+ if (item === undefined) {
71496
+ // A row on its way never reaches ListItemUI (see ListItemSkeletonResolver):
71497
+ // it is stood among the rows that mount, and given its separator, here.
71498
+ let rowVnode;
71499
+ if (renderRowSkeleton === false) {
71500
+ // The row must still take its room: without it the rows below would
71501
+ // climb up and slide back down as the answer arrives.
71502
+ rowVnode = jsx(ListItem, {
71503
+ skeleton: true,
71504
+ style: VISIBILITY_HIDDEN_STYLE
71505
+ });
71506
+ } else if (renderRowSkeleton) {
71507
+ rowVnode = renderRowSkeleton(rowIndex);
71508
+ } else {
71509
+ rowVnode = jsx(ListItem, {
71510
+ skeleton: true
71511
+ });
71512
+ }
71513
+ if (rowVnode) {
71514
+ pushRow(jsx(ListRunSkeletonRow, {
71515
+ run: runRows,
71516
+ row: {
71517
+ id: key,
71518
+ index: rowIndex,
71519
+ ...getSkeletonRow()
71520
+ },
71521
+ groupKey: groupKey,
71522
+ separator: separator,
71523
+ children: rowVnode
71524
+ }, key), item, rowIndex, groupKey);
71525
+ }
71526
+ rowIndex++;
71527
+ continue;
71528
+ }
71403
71529
  let rowVnode;
71404
71530
  let rowContextValue;
71405
- let rowKept = null;
71406
- if (item !== undefined) {
71407
- const rowVnodeKept = rowVnodesByItem.get(item);
71408
- if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
71409
- rowVnode = rowVnodeKept.vnode;
71410
- rowContextValue = rowVnodeKept.rowContextValue;
71411
- rowKept = rowVnodeKept;
71412
- } else {
71413
- rowVnode = renderItem(item, rowIndex, renderItemState);
71414
- // Kept with the vnode, for the same reason: a context value that is a
71415
- // fresh object on every render forces every consumer of it to render,
71416
- // which is the row's own chain — the vnode handed back unchanged would
71417
- // then buy nothing.
71418
- rowContextValue = {
71419
- id: key,
71420
- index: rowIndex,
71421
- item
71422
- };
71423
- rowKept = {
71424
- vnode: rowVnode,
71425
- rowContextValue,
71426
- rowIndex,
71427
- refreshing: renderItemState.refreshing,
71428
- separatorVnode: null
71429
- };
71430
- rowVnodesByItem.set(item, rowKept);
71431
- }
71432
- } else if (renderRowSkeleton === false) {
71433
- // The row must still take its room: without it the rows below would
71434
- // climb up and slide back down as the answer arrives.
71435
- rowVnode = jsx(ListItem, {
71436
- skeleton: true,
71437
- style: VISIBILITY_HIDDEN_STYLE
71438
- });
71439
- } else if (renderRowSkeleton) {
71440
- rowVnode = renderRowSkeleton(rowIndex);
71531
+ const rowVnodeKept = rowVnodesByItem.get(item);
71532
+ if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing && rowVnodeKept.rowContextValue.groupKey === groupKey) {
71533
+ rowVnode = rowVnodeKept.vnode;
71534
+ rowContextValue = rowVnodeKept.rowContextValue;
71441
71535
  } else {
71442
- rowVnode = jsx(ListItem, {
71443
- skeleton: true
71536
+ rowVnode = renderItem(item, rowIndex, renderItemState);
71537
+ // Kept with the vnode, for the same reason: a context value that is a
71538
+ // fresh object on every render forces every consumer of it to render,
71539
+ // which is the row's own chain — the vnode handed back unchanged would
71540
+ // then buy nothing.
71541
+ rowContextValue = {
71542
+ id: key,
71543
+ index: rowIndex,
71544
+ item,
71545
+ run: runRows,
71546
+ groupKey
71547
+ };
71548
+ rowVnodesByItem.set(item, {
71549
+ vnode: rowVnode,
71550
+ rowContextValue,
71551
+ rowIndex,
71552
+ refreshing: renderItemState.refreshing
71444
71553
  });
71445
71554
  }
71446
71555
  if (rowVnode) {
71447
- // The first row of a group wears no separator: the gap it sits at is the
71448
- // one between two groups, and that gap is the group wrapper's own — it
71449
- // is a row of the list like any other and draws its separator itself
71450
- // (see ListItemUI). Drawn here it would land inside the group instead,
71451
- // as a hairline under the label.
71452
- const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
71453
- if (drawSeparator) {
71454
- // Kept with the row too: a separator built again is a separator
71455
- // rendered again.
71456
- let separatorVnode = rowKept ? rowKept.separatorVnode : null;
71457
- if (!separatorVnode) {
71458
- separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
71459
- key: `${key}_separator`
71460
- });
71461
- if (rowKept) {
71462
- rowKept.separatorVnode = separatorVnode;
71463
- }
71464
- }
71465
- pushRow(separatorVnode, item, rowIndex);
71466
- }
71467
71556
  pushRow(jsx(ListRowContext.Provider, {
71468
- value: item === undefined ? {
71469
- id: key,
71470
- index: rowIndex,
71471
- ...getSkeletonRow()
71472
- } : rowContextValue,
71557
+ value: rowContextValue,
71473
71558
  children: rowVnode
71474
- }, key), item, rowIndex);
71559
+ }, key), item, rowIndex, groupKey);
71475
71560
  }
71476
71561
  rowIndex++;
71477
71562
  }
@@ -71485,6 +71570,35 @@ const ListItems = ({
71485
71570
  return rows;
71486
71571
  };
71487
71572
 
71573
+ // A run's row that has not arrived, standing where the real one will: it
71574
+ // mounts like any row (see createRunRows) and wears the separator of the gap
71575
+ // above it, the way a real row does in ListItemUI.
71576
+ const ListRunSkeletonRow = ({
71577
+ run,
71578
+ row,
71579
+ groupKey,
71580
+ separator,
71581
+ children
71582
+ }) => {
71583
+ const rowId = useId();
71584
+ run.mount(rowId, row.index, groupKey);
71585
+ useLayoutEffect(() => {
71586
+ return () => {
71587
+ run.unmount(rowId);
71588
+ };
71589
+ }, []);
71590
+ const rowVnode = jsx(ListRowContext.Provider, {
71591
+ value: row,
71592
+ children: children
71593
+ });
71594
+ if (!separator || run.isFirst(row.index, groupKey)) {
71595
+ return rowVnode;
71596
+ }
71597
+ return jsxs(Fragment, {
71598
+ children: [resolveSeparatorVnode(separator, row.index - 1), rowVnode]
71599
+ });
71600
+ };
71601
+
71488
71602
  // What is drawn where rows were asked for and never came: the sentence and the
71489
71603
  // way out, in the row itself — the rest of the list is fine, so replacing all
71490
71604
  // of it (List's own `error`) would be a lie.