@jsenv/navi 0.29.140 → 0.29.142

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.
@@ -5,7 +5,7 @@
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
7
  import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
- export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
8
+ export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
11
11
  import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
@@ -30465,7 +30465,16 @@ const anyMatchingRouteSignal = (routes) => {
30465
30465
  * @param {Element} element The element asking — the command's source, and the
30466
30466
  * anchor a popup opens on unless `anchor` says otherwise.
30467
30467
  * @param {string} command
30468
- * @param {Event} event What the user did.
30468
+ * @param {Event} event What caused this. Mandatory: it is what makes a command
30469
+ * traceable back to its origin — the debug panel groups everything a gesture
30470
+ * set off under it, and the gates below read it to know whether the default
30471
+ * was already prevented and which mouse button was pressed. When nothing was
30472
+ * handed over — a timer firing, an action settling, a signal changing — build
30473
+ * a `CustomEvent` that names what happened and chain it to whatever preceded
30474
+ * it, rather than leaving the origin unsaid:
30475
+ * const expiredEvent = new CustomEvent("session_expired");
30476
+ * chainEvent(expiredEvent, causeEvent); // when something did precede it
30477
+ * triggerNaviCommand(dialogEl, "--navi-open", expiredEvent);
30469
30478
  * @param {object} [options]
30470
30479
  * @param {boolean} [options.optional] No suitable target is not a warning.
30471
30480
  * @param {any} [options.value] What the command is about, carried to whoever
@@ -30482,6 +30491,11 @@ const triggerNaviCommand = (
30482
30491
  event,
30483
30492
  { optional, value, anchor } = {},
30484
30493
  ) => {
30494
+ if (!event) {
30495
+ throw new Error(
30496
+ `"${command}" triggered without an event: it is mandatory, a command must say what caused it. Pass the gesture, or a CustomEvent naming the cause when no gesture did — see triggerNaviCommand's jsdoc.`,
30497
+ );
30498
+ }
30485
30499
  const naviCommand =
30486
30500
  NAVI_COMMANDS[command] || NAVI_COMMANDS[commandName(command)];
30487
30501
  if (!naviCommand) {
@@ -33597,6 +33611,12 @@ const GROUP_DEFAULTS = {
33597
33611
  *
33598
33612
  * **Filtering**: `childControlFilter` can exclude certain child types from aggregation
33599
33613
  * (e.g. ignoring buttons inside a selectable list).
33614
+ *
33615
+ * **Aggregating**: `aggregateChildStates(children, fallbackState, stateNow)` — what the
33616
+ * children add up to. `fallbackState` is the empty of the declared `stateType`;
33617
+ * `stateNow` is what the group is worth as it is asked, which an aggregate reads to
33618
+ * answer for children that are not there (a selectable list whose selected row is not
33619
+ * drawn).
33600
33620
  */
33601
33621
  const useUIGroupStateController = (
33602
33622
  props,
@@ -33753,16 +33773,22 @@ const useUIGroupStateController = (
33753
33773
  // taking that for an answer is how a value handed to it evaporates on
33754
33774
  // the way in, and how that emptiness then travels back up to whoever
33755
33775
  // handed it (a picker showing its row as unanswered).
33756
- const aggregateGroupUIState = (whenNobodyCanAnswer) => {
33776
+ //
33777
+ // `stateNow` is what the group is worth as it is asked: what it keeps
33778
+ // when there is nobody to ask, and what an aggregate reads to answer for
33779
+ // what its children do not say — a selection whose row is not drawn (see
33780
+ // ListSelectable) lives there and nowhere else.
33781
+ const aggregateGroupUIState = (stateNow) => {
33757
33782
  const someChildCanAnswer = childUIStateControllerArray.some(
33758
33783
  shouldPropagateStateToChild,
33759
33784
  );
33760
33785
  if (!someChildCanAnswer) {
33761
- return whenNobodyCanAnswer;
33786
+ return stateNow;
33762
33787
  }
33763
33788
  const aggChildState = resolvedAggregateChildStates(
33764
33789
  childUIStateControllerArray,
33765
33790
  fallbackState,
33791
+ stateNow,
33766
33792
  );
33767
33793
  if (aggChildState !== undefined) {
33768
33794
  return aggChildState;
@@ -34286,8 +34312,6 @@ const useUIGroupStateController = (
34286
34312
  // ── update: runs every render after the first ─────────────────────────
34287
34313
  (s) => {
34288
34314
  const { controller } = s;
34289
- const prevValue = controller.value;
34290
- const prevHasValueProp = controller.hasValueProp;
34291
34315
  const prevDefaultValue = controller.defaultValue;
34292
34316
  controller.props = props;
34293
34317
  controller.ref = ref;
@@ -34305,10 +34329,14 @@ const useUIGroupStateController = (
34305
34329
  controller.placeChildrenUIState(groupUIState, propagateDownEvent);
34306
34330
  controller.syncInternalState(groupUIState);
34307
34331
  };
34308
- if (
34309
- hasValueProp &&
34310
- (!prevHasValueProp || !compareTwoJsValues(value, prevValue))
34311
- ) {
34332
+ // A controlled group goes on showing the value it is given. A child
34333
+ // answering for itself moves what the group is worth — that is what
34334
+ // `uiAction` reports — but the value stays the owner's, so the test is
34335
+ // against what the children are showing, not against the value handed
34336
+ // down last time. A popup reopened on another subject hands down the very
34337
+ // same empty value it did before, and the selection left inside it from
34338
+ // the previous opening is what has to go.
34339
+ if (hasValueProp && !compareTwoJsValues(value, controller.uiState)) {
34312
34340
  placeChildrenFrom(value);
34313
34341
  }
34314
34342
  if (
@@ -61346,23 +61374,22 @@ const ListSelectable = props => {
61346
61374
  focusGroupDirection,
61347
61375
  focusGroupWrap
61348
61376
  } = props;
61349
- // What the list holds, which is not the same as what its rows say. A list
61350
- // draws the rows it needs and no more: the selected one may be scrolled out
61351
- // of the window, or filtered out of the view. Aggregating over the rows that
61352
- // happen to be mounted would then lose the selection — a row that is not
61353
- // there cannot say it is not selected.
61354
- const selectionRef = useRef(undefined);
61355
- if (selectionRef.current === undefined) {
61356
- selectionRef.current = Object.hasOwn(props, "value") ? props.value : props.defaultValue;
61357
- }
61377
+ // `kept` is what the list holds as it is asked, which is not the same as what
61378
+ // its rows say: a list draws the rows it needs and no more, so the selected
61379
+ // one may be scrolled out of the window or filtered out of the view, and a
61380
+ // row that is not there cannot say it is not selected. Reading it off the
61381
+ // group rather than remembering it here is what makes a value put ON the list
61382
+ // (a `value` prop, a signal, a reopened popup) replace the whole selection —
61383
+ // a private memory of its own would go on holding the rows it could not see
61384
+ // being unselected.
61385
+ //
61358
61386
  // `fallbackState` is the empty of the shape the list declared below
61359
61387
  // (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
61360
61388
  // it is what lets an emptied list say "empty" — `undefined` is the word for
61361
61389
  // "unset", and a bound stateSignal reads that as "nothing decided here, go
61362
61390
  // back to the default" (see docs/control_value.md), which is how a list
61363
61391
  // emptied down to its last row puts that row back on reload.
61364
- const aggregateChildStates = (children, fallbackState) => {
61365
- const kept = selectionRef.current;
61392
+ const aggregateChildStates = (children, fallbackState, kept) => {
61366
61393
  if (multiple) {
61367
61394
  const drawnValues = new Set(children.map(child => child.props.value));
61368
61395
  const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
@@ -61371,13 +61398,10 @@ const ListSelectable = props => {
61371
61398
  stillSelected.push(child.uiState);
61372
61399
  }
61373
61400
  }
61374
- const values = stillSelected.length === 0 ? fallbackState : stillSelected;
61375
- selectionRef.current = values;
61376
- return values;
61401
+ return stillSelected.length === 0 ? fallbackState : stillSelected;
61377
61402
  }
61378
61403
  for (const child of children) {
61379
61404
  if (child.uiState !== undefined) {
61380
- selectionRef.current = child.uiState;
61381
61405
  return child.uiState;
61382
61406
  }
61383
61407
  }
@@ -61385,7 +61409,6 @@ const ListSelectable = props => {
61385
61409
  // deselected; if it is not, the list keeps what it holds.
61386
61410
  const keptIsDrawn = children.some(child => child.props.value === kept);
61387
61411
  if (keptIsDrawn) {
61388
- selectionRef.current = undefined;
61389
61412
  return undefined;
61390
61413
  }
61391
61414
  return kept;
@@ -61984,12 +62007,9 @@ const ListVirtualContext = createContext(null);
61984
62007
  // that returning a component of one's own — instead of a bare <List.Item> —
61985
62008
  // works the same way.
61986
62009
  const ListRowContext = createContext(null);
61987
- // The slot a child of the list stands in — which one it is, and where it sits
61988
- // among its siblings (see ListDeclaredChildren). A row takes its place in the
61989
- // collection by slot, and hears through this context that its slot moved: a
61990
- // context subscriber is re-rendered even when preact skipped the component
61991
- // that holds it, which is what lets a row nobody rebuilt follow the list all
61992
- // the same.
62010
+ // The slot a child of the list stands in, by id (see ListDeclaredChildren). A
62011
+ // row takes its place in the collection by slot: the place is then the list's
62012
+ // to move, and the row's to follow — see createListVirtual.
61993
62013
  const ListSlotContext = createContext(null);
61994
62014
  const css$x = /* css */`
61995
62015
  @layer navi {
@@ -64947,9 +64967,7 @@ const ListItemUI = props => {
64947
64967
  // registered the row, decided it is inside the render window, and placed its
64948
64968
  // separator. All that is left here is to draw it.
64949
64969
  const row = useContext(ListRowContext);
64950
- // Where this row was declared, when it was declared one by one (see
64951
- // ListDeclaredChildren).
64952
- const slot = useContext(ListSlotContext);
64970
+ const slotId = useContext(ListSlotContext);
64953
64971
  // There is no standalone match/matchScore/highlight prop — participation
64954
64972
  // in a matching system (search, filter…) only goes through `matchInfo`
64955
64973
  // (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
@@ -64975,18 +64993,20 @@ const ListItemUI = props => {
64975
64993
  // Where a row sits is where it was declared, full stop: a list is written in
64976
64994
  // the order it reads. Its slot is what says that — a search that reorders
64977
64995
  // rows reorders the rows it declares, and the slots follow. A row drawn by a
64978
- // run already knows its place; the run gave it.
64996
+ // run already knows its place; the run gave it. The place is taken in the
64997
+ // name of this very component (idDefault, not the row's id): two components
64998
+ // may stand for the same row for a moment, one leaving as the other arrives,
64999
+ // and the one leaving must give back its own place, not the newcomer's.
64979
65000
  if (!row) {
64980
65001
  if (props.filtered) {
64981
- virtual.drop(props.id);
65002
+ virtual.drop(idDefault);
64982
65003
  } else {
64983
- props.index = virtual.take(props.id, 1, slot ? slot.id : props.id);
65004
+ props.index = virtual.take(idDefault, 1, slotId);
64984
65005
  }
64985
65006
  }
64986
- const rowId = props.id;
64987
65007
  useLayoutEffect(() => {
64988
65008
  return () => {
64989
- virtual.drop(rowId);
65009
+ virtual.drop(idDefault);
64990
65010
  };
64991
65011
  }, []);
64992
65012
  // Every row that is drawn registers itself, whether it was declared one by
@@ -65355,9 +65375,10 @@ List.Item = ListItem;
65355
65375
  // So the places are read off the walk instead of off the renders: the list
65356
65376
  // names a slot for each of its children and declares them here, in order,
65357
65377
  // before any of them renders (see ListDeclaredChildren). A child then takes
65358
- // its place BY SLOT and keeps it for as long as its slot stands — whether it
65359
- // renders again or not — while a child whose slot the walk no longer names
65360
- // loses its place in that same render, so nothing removed is ever counted.
65378
+ // its place BY SLOT, and the place is a signal: it moves when what stands
65379
+ // before it changes — a row filtered out, a run taking in rows, a slot added
65380
+ // or moved — and the child follows, rendered again for it whether or not
65381
+ // anything else would have rendered it.
65361
65382
  const createListVirtual = () => {
65362
65383
  const totalSignal = signal(0);
65363
65384
  // Bumped whenever a run takes in rows. The list itself has to hear about it:
@@ -65369,100 +65390,108 @@ const createListVirtual = () => {
65369
65390
  // an attribute: what is drawn is from before, and the app may want to say so
65370
65391
  // without taking anything away.
65371
65392
  const refreshingSignal = signal(0);
65372
- // Where the walk put each slot: the slot it lives in (a group walks its own
65373
- // rows, inside the slot the list gave the group) and its rank among the
65374
- // slots declared with it.
65375
- const slotById = new Map();
65376
- // What stands in a slot once it has rendered: the row, or the run of rows,
65377
- // that took the place, and how many rows of the collection it stands for.
65393
+ // The slots each walk declared, in order, by the slot the walk stands in
65394
+ // (null for the list's own children). Together they are a tree: a group's
65395
+ // rows live inside the group's slot.
65396
+ const slotIdsByParent = new Map();
65397
+ // Who took a place — a row, or a run of rows — and how many rows of the
65398
+ // collection it stands for. The place itself is a signal, see take.
65378
65399
  const ownerById = new Map();
65379
- const placeByOwner = new Map();
65380
- // The owners in the order their slots put them, and how many rows they stand
65381
- // for together: kept so that a child declared after all the others — a whole
65382
- // first render, and anything the list gains at its end — is placed without
65383
- // going over the others again.
65384
- const orderedOwnerIds = [];
65385
- let rowTotal = 0;
65400
+ // The owners standing in each slot, in the order they took their place.
65401
+ // One, as a rule; a child that renders several rows keeps them in the order
65402
+ // they first rendered, which is all it can be told.
65403
+ const ownerIdsBySlot = new Map();
65386
65404
  const locatorByOwner = new Map();
65387
- const pathCache = new Map();
65388
- // Tells two owners sharing a slot apart (several rows inside one child of
65389
- // the list), in the order they first took a place.
65390
- let sequence = 0;
65405
+ // The slots as the tree reads, first to last, and where each stands in it.
65406
+ // Rebuilt once a walk has changed the tree, read to place the owners.
65407
+ const slotWalk = [];
65408
+ const rankBySlot = new Map();
65409
+ let rowTotal = 0;
65410
+ // Owners have left and the others have not been moved up yet. Done on the
65411
+ // next ask rather than on the spot: rows leave many at a time (a search, a
65412
+ // list unmounting), and moving the others up once is enough.
65391
65413
  let placesStale = false;
65392
-
65393
- // Whatever the slot held is gone with it, and so is whatever its own walk
65394
- // declared inside it.
65395
- const dropSlot = slotId => {
65396
- slotById.delete(slotId);
65397
- for (const [ownerId, owner] of ownerById) {
65398
- if (owner.slotId === slotId) {
65399
- ownerById.delete(ownerId);
65400
- placeByOwner.delete(ownerId);
65414
+ // Where the last slot holding an owner stands: an owner arriving at or after
65415
+ // it is placed at the end without going over the others — a whole first
65416
+ // render, rows arriving in order, costs each row nothing but itself.
65417
+ let rankOwnedLast = -1;
65418
+ const rebuildWalk = () => {
65419
+ slotWalk.length = 0;
65420
+ rankBySlot.clear();
65421
+ const visit = parentSlotId => {
65422
+ const slotIds = slotIdsByParent.get(parentSlotId);
65423
+ if (!slotIds) {
65424
+ return;
65401
65425
  }
65402
- }
65403
- for (const [otherSlotId, slot] of slotById) {
65404
- if (slot.parentId === slotId) {
65405
- dropSlot(otherSlotId);
65426
+ for (const slotId of slotIds) {
65427
+ rankBySlot.set(slotId, slotWalk.length);
65428
+ slotWalk.push(slotId);
65429
+ visit(slotId);
65406
65430
  }
65407
- }
65408
- };
65409
- // Where a slot sits, said as the ranks of the slots it lives in, outermost
65410
- // first: comparing two of them keeps a group's rows inside their group and
65411
- // the groups where the list declared them.
65412
- const pathOf = slotId => {
65413
- const pathCached = pathCache.get(slotId);
65414
- if (pathCached) {
65415
- return pathCached;
65416
- }
65417
- const slot = slotById.get(slotId);
65418
- if (!slot) {
65419
- return null;
65420
- }
65421
- let path;
65422
- if (slot.parentId === null) {
65423
- path = [slot.order];
65424
- } else {
65425
- const parentPath = pathOf(slot.parentId);
65426
- path = parentPath === null ? [slot.order] : [...parentPath, slot.order];
65427
- }
65428
- pathCache.set(slotId, path);
65429
- return path;
65431
+ };
65432
+ visit(null);
65430
65433
  };
65431
- // The places, all of them, from the order the slots are in. Done in one go
65432
- // rather than owner by owner: a place is a sum of what stands before it, so
65433
- // there is nothing to hand out one at a time — and nothing changes it but a
65434
- // slot moving or a child taking a different number of rows.
65434
+ // Every place, in one go: a place is the sum of what stands before it, so
65435
+ // there is nothing to hand out one at a time. Writing a place that did not
65436
+ // change wakes nobody — a signal ignores a value equal to its own.
65435
65437
  const refreshPlaces = () => {
65436
65438
  placesStale = false;
65437
- const owners = [...ownerById.entries()];
65438
- owners.sort(([, leftOwner], [, rightOwner]) => {
65439
- const comparison = comparePath(pathOf(leftOwner.slotId), pathOf(rightOwner.slotId));
65440
- if (comparison !== 0) {
65441
- return comparison;
65442
- }
65443
- return leftOwner.sequence - rightOwner.sequence;
65444
- });
65445
- orderedOwnerIds.length = 0;
65446
65439
  let index = 0;
65447
- for (const [ownerId, owner] of owners) {
65448
- orderedOwnerIds.push(ownerId);
65449
- placeByOwner.set(ownerId, index);
65450
- index += owner.rowCount;
65440
+ let rank = 0;
65441
+ rankOwnedLast = -1;
65442
+ while (rank < slotWalk.length) {
65443
+ const ownerIds = ownerIdsBySlot.get(slotWalk[rank]);
65444
+ if (ownerIds) {
65445
+ for (const ownerId of ownerIds) {
65446
+ const owner = ownerById.get(ownerId);
65447
+ owner.placeSignal.value = index;
65448
+ index += owner.rowCount;
65449
+ }
65450
+ rankOwnedLast = rank;
65451
+ }
65452
+ rank++;
65451
65453
  }
65452
65454
  rowTotal = index;
65453
- totalSignal.value = rowTotal;
65455
+ totalSignal.value = index;
65454
65456
  };
65455
- // Whether nothing that has taken a place stands after this one.
65456
- const standsLast = owner => {
65457
- if (orderedOwnerIds.length === 0) {
65458
- return true;
65457
+ const addToSlot = (slotId, ownerId) => {
65458
+ const ownerIds = ownerIdsBySlot.get(slotId);
65459
+ if (ownerIds) {
65460
+ ownerIds.push(ownerId);
65461
+ } else {
65462
+ ownerIdsBySlot.set(slotId, [ownerId]);
65463
+ }
65464
+ };
65465
+ const removeFromSlot = (slotId, ownerId) => {
65466
+ const ownerIds = ownerIdsBySlot.get(slotId);
65467
+ if (!ownerIds) {
65468
+ return;
65459
65469
  }
65460
- const lastOwner = ownerById.get(orderedOwnerIds[orderedOwnerIds.length - 1]);
65461
- const comparison = comparePath(pathOf(lastOwner.slotId), pathOf(owner.slotId));
65462
- if (comparison !== 0) {
65463
- return comparison < 0;
65470
+ const index = ownerIds.indexOf(ownerId);
65471
+ if (index !== -1) {
65472
+ ownerIds.splice(index, 1);
65473
+ }
65474
+ if (ownerIds.length === 0) {
65475
+ ownerIdsBySlot.delete(slotId);
65476
+ }
65477
+ };
65478
+ // A slot the walk no longer names: whatever stood in it is gone, and so is
65479
+ // whatever a walk inside it had declared.
65480
+ const dropSlot = slotId => {
65481
+ const ownerIds = ownerIdsBySlot.get(slotId);
65482
+ if (ownerIds) {
65483
+ for (const ownerId of ownerIds) {
65484
+ ownerById.delete(ownerId);
65485
+ }
65486
+ ownerIdsBySlot.delete(slotId);
65487
+ }
65488
+ const childSlotIds = slotIdsByParent.get(slotId);
65489
+ if (childSlotIds) {
65490
+ slotIdsByParent.delete(slotId);
65491
+ for (const childSlotId of childSlotIds) {
65492
+ dropSlot(childSlotId);
65493
+ }
65464
65494
  }
65465
- return lastOwner.sequence < owner.sequence;
65466
65495
  };
65467
65496
  const virtual = {
65468
65497
  totalSignal,
@@ -65486,40 +65515,30 @@ const createListVirtual = () => {
65486
65515
  renderSkeleton: undefined,
65487
65516
  // The children a walk stands over, in order — said in one call, before any
65488
65517
  // of them renders, so that what a child asks next is answered against the
65489
- // whole picture and not against the children that happened to render first.
65518
+ // whole picture and not against the children that happened to render
65519
+ // first. Said again on every render of the walk, and heard only when
65520
+ // something moved.
65490
65521
  declareSlots: (parentSlotId, slotIds) => {
65491
- const slotIdSet = new Set(slotIds);
65492
- for (const [slotId, slot] of slotById) {
65493
- if (slot.parentId === parentSlotId && !slotIdSet.has(slotId)) {
65494
- dropSlot(slotId);
65495
- pathCache.clear();
65496
- placesStale = true;
65497
- }
65522
+ const slotIdsPrevious = slotIdsByParent.get(parentSlotId);
65523
+ if (slotIdsPrevious && sameSlotIds(slotIdsPrevious, slotIds)) {
65524
+ return;
65498
65525
  }
65499
- let order = 0;
65500
- for (const slotId of slotIds) {
65501
- const slot = slotById.get(slotId);
65502
- if (!slot) {
65503
- // A slot nothing stands in yet moves nobody: the child that takes it
65504
- // says so itself, when it renders.
65505
- slotById.set(slotId, {
65506
- parentId: parentSlotId,
65507
- order
65508
- });
65509
- } else if (slot.order !== order) {
65510
- slot.order = order;
65511
- pathCache.clear();
65512
- placesStale = true;
65526
+ if (slotIdsPrevious) {
65527
+ const slotIdSet = new Set(slotIds);
65528
+ for (const slotId of slotIdsPrevious) {
65529
+ if (!slotIdSet.has(slotId)) {
65530
+ dropSlot(slotId);
65531
+ }
65513
65532
  }
65514
- order++;
65515
- }
65516
- // Now, rather than when the first child asks: the walk has just said
65517
- // where everything stands, so every place can be settled once instead of
65518
- // once per child.
65519
- if (placesStale) {
65520
- refreshPlaces();
65521
65533
  }
65534
+ slotIdsByParent.set(parentSlotId, slotIds);
65535
+ rebuildWalk();
65536
+ refreshPlaces();
65522
65537
  },
65538
+ // Whether something has taken this slot for its own: what it renders
65539
+ // inside is then its to place (a run draws its groups with their rows
65540
+ // already placed), and no walk inside it has anything to declare.
65541
+ slotHasOwner: slotId => ownerIdsBySlot.has(slotId),
65523
65542
  // Whether any run of rows lives in this list: what makes a render window
65524
65543
  // mean anything (see List's renderBudget).
65525
65544
  hasRuns: () => locatorByOwner.size > 0,
@@ -65539,127 +65558,129 @@ const createListVirtual = () => {
65539
65558
  }
65540
65559
  return null;
65541
65560
  },
65542
- // The place the slot's rows start at. Asked again on every render of the
65543
- // child and answered with the same number for as long as nothing moved —
65544
- // and answered at all for a child that does not ask again, which is the
65545
- // point of taking by slot.
65561
+ // The place the owner's rows start at — read from a signal, so that the
65562
+ // owner is rendered again when it moves (see createListVirtual). Asked on
65563
+ // every render, and answered without a second look for as long as the
65564
+ // owner stands in the same slot for the same number of rows.
65546
65565
  take: (ownerId, rowCount, slotId) => {
65547
- const previous = ownerById.get(ownerId);
65548
- if (previous && previous.slotId === slotId && previous.rowCount === rowCount) {
65566
+ let owner = ownerById.get(ownerId);
65567
+ if (owner) {
65568
+ if (owner.slotId !== slotId || owner.rowCount !== rowCount) {
65569
+ removeFromSlot(owner.slotId, ownerId);
65570
+ addToSlot(slotId, ownerId);
65571
+ owner.slotId = slotId;
65572
+ owner.rowCount = rowCount;
65573
+ placesStale = true;
65574
+ }
65549
65575
  if (placesStale) {
65550
65576
  refreshPlaces();
65551
65577
  }
65552
- return placeByOwner.get(ownerId);
65578
+ return owner.placeSignal.value;
65579
+ }
65580
+ if (placesStale) {
65581
+ refreshPlaces();
65582
+ }
65583
+ const rank = rankBySlot.get(slotId);
65584
+ addToSlot(slotId, ownerId);
65585
+ if (rank !== undefined && rank >= rankOwnedLast) {
65586
+ owner = {
65587
+ slotId,
65588
+ rowCount,
65589
+ placeSignal: signal(rowTotal)
65590
+ };
65591
+ ownerById.set(ownerId, owner);
65592
+ rowTotal += rowCount;
65593
+ rankOwnedLast = rank;
65594
+ totalSignal.value = rowTotal;
65595
+ return owner.placeSignal.value;
65553
65596
  }
65554
- const owner = {
65597
+ owner = {
65555
65598
  slotId,
65556
65599
  rowCount,
65557
- sequence: previous ? previous.sequence : sequence++
65600
+ placeSignal: signal(0)
65558
65601
  };
65559
65602
  ownerById.set(ownerId, owner);
65560
- if (!previous && !placesStale && standsLast(owner)) {
65561
- orderedOwnerIds.push(ownerId);
65562
- placeByOwner.set(ownerId, rowTotal);
65563
- rowTotal += rowCount;
65564
- totalSignal.value = rowTotal;
65565
- return placeByOwner.get(ownerId);
65566
- }
65567
- placesStale = true;
65568
65603
  refreshPlaces();
65569
- return placeByOwner.get(ownerId);
65604
+ return owner.placeSignal.value;
65570
65605
  },
65571
- // The child stands for no row of the collection any more: it was filtered
65572
- // out by a search, or it is gone.
65606
+ // The owner stands for no row of the collection: it was filtered out by a
65607
+ // search, or it is gone.
65573
65608
  drop: ownerId => {
65574
- if (!ownerById.delete(ownerId)) {
65609
+ const owner = ownerById.get(ownerId);
65610
+ if (!owner) {
65575
65611
  return;
65576
65612
  }
65577
- placeByOwner.delete(ownerId);
65578
- refreshPlaces();
65613
+ ownerById.delete(ownerId);
65614
+ removeFromSlot(owner.slotId, ownerId);
65615
+ if (placesStale) {
65616
+ return;
65617
+ }
65618
+ placesStale = true;
65619
+ queueMicrotask(() => {
65620
+ if (placesStale) {
65621
+ refreshPlaces();
65622
+ }
65623
+ });
65579
65624
  }
65580
65625
  };
65581
65626
  return virtual;
65582
65627
  };
65583
- // The walk that gives the list's children their places. It names a slot for
65584
- // each of them — after the child's key when it has one, after its position
65585
- // otherwise, which is how preact itself tells unkeyed children apart — and says
65586
- // the whole set to the list's virtual before any of them renders.
65628
+ const sameSlotIds = (left, right) => {
65629
+ if (left.length !== right.length) {
65630
+ return false;
65631
+ }
65632
+ let index = 0;
65633
+ while (index < left.length) {
65634
+ if (left[index] !== right[index]) {
65635
+ return false;
65636
+ }
65637
+ index++;
65638
+ }
65639
+ return true;
65640
+ };
65641
+
65642
+ // The walk that gives the list's children their places: a slot for each of
65643
+ // them, declared to the list's virtual all at once before any child renders,
65644
+ // and handed to the child through a provider of its own — which is what lets
65645
+ // the row reach it however deep the caller buried it in components of theirs.
65587
65646
  //
65588
- // Wrapping each child in its own provider is what makes a slot reach the row
65589
- // inside it, however deep the caller buried it, and what wakes a row whose slot
65590
- // moved: the value it holds is the same object for as long as the child stays
65591
- // where it was.
65647
+ // A slot is named the way preact tells the child apart: by key when it has
65648
+ // one, by position otherwise, and inside the array it was given in — a nested
65649
+ // array is one child to preact, so what follows the array keeps its name
65650
+ // however many rows the array holds. A child preact would not render (null,
65651
+ // a boolean) has no slot: it is not there.
65592
65652
  const ListDeclaredChildren = ({
65593
65653
  children
65594
65654
  }) => {
65595
65655
  const virtual = useContext(ListVirtualContext);
65596
- const parentSlot = useContext(ListSlotContext);
65597
- const parentSlotId = parentSlot ? parentSlot.id : null;
65598
- const slotsRef = useRef(new Map());
65599
- const slotsPrevious = slotsRef.current;
65600
- const childArray = flattenChildren(children);
65601
- const slots = new Map();
65602
- const slotIds = [];
65603
- let index = 0;
65604
- for (const child of childArray) {
65605
- const childKey = child && child.key !== undefined && child.key !== null ? `k${child.key}` : `i${index}`;
65606
- let slotId = parentSlotId === null ? childKey : `${parentSlotId}/${childKey}`;
65607
- if (slots.has(slotId)) {
65608
- // Two children answering to the same key: preact cannot tell them apart
65609
- // either, but a slot each is what keeps them both on screen.
65610
- slotId = `${slotId}#${index}`;
65611
- }
65612
- const slotPrevious = slotsPrevious.get(slotId);
65613
- slots.set(slotId, slotPrevious && slotPrevious.order === index ? slotPrevious : {
65614
- id: slotId,
65615
- order: index
65616
- });
65617
- slotIds.push(slotId);
65618
- index++;
65656
+ const parentSlotId = useContext(ListSlotContext);
65657
+ if (parentSlotId !== null && virtual.slotHasOwner(parentSlotId)) {
65658
+ return children;
65619
65659
  }
65620
- slotsRef.current = slots;
65660
+ const slotIds = [];
65661
+ const declared = [];
65662
+ declareChildren(children, parentSlotId === null ? "" : `${parentSlotId}/`, slotIds, declared);
65621
65663
  virtual.declareSlots(parentSlotId, slotIds);
65622
65664
  return jsx(Fragment$1, {
65623
- children: childArray.map((child, childIndex) => {
65624
- const slotId = slotIds[childIndex];
65625
- return jsx(ListSlotContext.Provider, {
65626
- value: slots.get(slotId),
65627
- children: child
65628
- }, slotId);
65629
- })
65665
+ children: declared
65630
65666
  });
65631
65667
  };
65632
- // The children as preact will diff them: nested arrays flattened, and a child
65633
- // that renders nothing still holding its position — a conditional child must
65634
- // not move the ones after it as it comes and goes.
65635
- const flattenChildren = (children, flat = []) => {
65636
- if (Array.isArray(children)) {
65637
- for (const child of children) {
65638
- flattenChildren(child, flat);
65639
- }
65640
- return flat;
65641
- }
65642
- flat.push(children);
65643
- return flat;
65644
- };
65645
-
65646
- // Two slots compared by where they sit (see pathOf). A slot no walk declared
65647
- // sorts after them all: it stands nowhere the list knows of.
65648
- const comparePath = (leftPath, rightPath) => {
65649
- if (leftPath === null) {
65650
- return rightPath === null ? 0 : 1;
65651
- }
65652
- if (rightPath === null) {
65653
- return -1;
65654
- }
65655
- let depth = 0;
65656
- while (depth < leftPath.length && depth < rightPath.length) {
65657
- if (leftPath[depth] !== rightPath[depth]) {
65658
- return leftPath[depth] - rightPath[depth];
65668
+ const declareChildren = (children, prefix, slotIds, declared) => {
65669
+ const childArray = Array.isArray(children) ? children : [children];
65670
+ let index = 0;
65671
+ for (const child of childArray) {
65672
+ if (Array.isArray(child)) {
65673
+ declareChildren(child, `${prefix}${index}/`, slotIds, declared);
65674
+ } else if (child !== null && child !== undefined && child !== false && child !== true) {
65675
+ const slotId = child.key === undefined || child.key === null ? `${prefix}i${index}` : `${prefix}k${child.key}`;
65676
+ slotIds.push(slotId);
65677
+ declared.push(jsx(ListSlotContext.Provider, {
65678
+ value: slotId,
65679
+ children: child
65680
+ }, slotId));
65659
65681
  }
65660
- depth++;
65682
+ index++;
65661
65683
  }
65662
- return leftPath.length - rightPath.length;
65663
65684
  };
65664
65685
  const VISIBILITY_HIDDEN_STYLE = {
65665
65686
  visibility: "hidden"
@@ -65795,7 +65816,7 @@ const ListItems = ({
65795
65816
  }) => {
65796
65817
  const ownerId = useId();
65797
65818
  const virtual = useContext(ListVirtualContext);
65798
- const slot = useContext(ListSlotContext);
65819
+ const slotId = useContext(ListSlotContext);
65799
65820
  const renderWindow = useContext(RenderWindowContext);
65800
65821
  const separator = useContext(SeparatorContext);
65801
65822
  const store = useItemStore({
@@ -65817,7 +65838,7 @@ const ListItems = ({
65817
65838
  skeletonRow.rowMinHeight = `${virtualItemSize}px`;
65818
65839
  }
65819
65840
  }
65820
- const runStart = virtual.take(ownerId, store.rowCount, slot ? slot.id : ownerId);
65841
+ const runStart = virtual.take(ownerId, store.rowCount, slotId);
65821
65842
  const runEnd = runStart + store.rowCount;
65822
65843
  const getItemAt = index => store.getItem(index);
65823
65844
  const windowFrom = renderWindow.start > runStart ? renderWindow.start : runStart;