@jsenv/navi 0.29.319 → 0.29.321

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.
@@ -4,7 +4,7 @@
4
4
  */
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
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, refuseDragTo, startDragTo, installPanZoom, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
@@ -10544,7 +10544,17 @@ defineInteractionDetector({
10544
10544
  },
10545
10545
  });
10546
10546
  }
10547
- if (hasLongPress) {
10547
+ // A press on something INSIDE this element that is picked up and
10548
+ // carried is that thing's, the same nearer word as the inner hold below:
10549
+ // a finger held still on a piece is the piece's own hold when it drags
10550
+ // by holding, and when it drags by distance it has decided nothing yet
10551
+ // at the delay — a hold answered there is a hold on a piece, and a carry
10552
+ // out of the same press the moment the hand moves. A surface that pans
10553
+ // steps back from the same source at the press (see pan_zoom.js).
10554
+ if (
10555
+ hasLongPress &&
10556
+ !isPressOnDragSourceInside(pointerDownEvent, element)
10557
+ ) {
10548
10558
  // A hold declared INSIDE this element, on this same press: the nearer
10549
10559
  // one answers, the way a click is the innermost target's, and this
10550
10560
  // wait is given up — two sheets opening from one hold is nobody's
@@ -10711,6 +10721,23 @@ const startSwipe = (
10711
10721
  });
10712
10722
  };
10713
10723
 
10724
+ // Whether the press landed on a drag source strictly inside the element — the
10725
+ // element itself being one is the other arrangement, where the grab answers
10726
+ // the hold (see refuseDragTo in @jsenv/dom). A source that stood down from this
10727
+ // press carries nothing, so it is no reason to hold back, and the walk goes on
10728
+ // above it the way a surface's does.
10729
+ const isPressOnDragSourceInside = (pointerDownEvent, element) => {
10730
+ const target = pointerDownEvent.target;
10731
+ if (!target || typeof target.closest !== "function") {
10732
+ return false;
10733
+ }
10734
+ let source = target.closest("[data-drag-source]");
10735
+ if (source && source === dragSourceThatStoodDown(pointerDownEvent)) {
10736
+ source = source.parentElement?.closest("[data-drag-source]") || null;
10737
+ }
10738
+ return Boolean(source) && source !== element && element.contains(source);
10739
+ };
10740
+
10714
10741
  // Whether a press landing now completes the tap before it: the same hand, soon
10715
10742
  // enough, and near enough. Axis by axis rather than as a distance, the way every
10716
10743
  // other slop in this family is read.
@@ -27017,14 +27044,35 @@ browserIntegration.handleActionTask;
27017
27044
 
27018
27045
  const NO_OP = () => {};
27019
27046
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
27047
+ // What the computed below answers for a key the document state does not hold:
27048
+ // key presence is the flag, and the value under a present key may be anything,
27049
+ // undefined included.
27050
+ const KEY_NOT_IN_STATE = Symbol("key_not_in_state");
27020
27051
  const useNavStateBasic = (
27021
27052
  id,
27022
27053
  { debug, type = "replace", onLeave, defaultValue } = {},
27023
27054
  ) => {
27024
27055
  // 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));
27056
+ //
27057
+ // Subscribed to this one key, through a computed, rather than to the state
27058
+ // as a whole: every popup with a nav state reads here, and a document state
27059
+ // is written whenever any of them opens — a list of forty names each with a
27060
+ // tooltip would re-render forty pickers, chains included, for the sheet
27061
+ // around them opening. The computed only wakes this component when what
27062
+ // sits under its own key changes.
27063
+ const keyValueComputed = useMemo(
27064
+ () =>
27065
+ computed(() => {
27066
+ const state = documentStateSignal.value;
27067
+ if (id && state && Object.hasOwn(state, id)) {
27068
+ return state[id];
27069
+ }
27070
+ return KEY_NOT_IN_STATE;
27071
+ }),
27072
+ [id],
27073
+ );
27074
+ const keyValue = keyValueComputed.value;
27075
+ const keyInState = keyValue !== KEY_NOT_IN_STATE;
27028
27076
  const onLeaveRef = useRef(onLeave);
27029
27077
  onLeaveRef.current = onLeave;
27030
27078
  const prevKeyInStateRef = useRef(keyInState);
@@ -27054,7 +27102,7 @@ const useNavStateBasic = (
27054
27102
  effectiveType = "replace";
27055
27103
  }
27056
27104
 
27057
- const currentValue = keyInState ? state[id] : defaultValue;
27105
+ const currentValue = keyInState ? keyValue : defaultValue;
27058
27106
 
27059
27107
  if (debug) {
27060
27108
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -34565,6 +34613,18 @@ const MOUNT_DEFAULT = "from-first-open";
34565
34613
  const popupsMountingContentForOpen = new Set();
34566
34614
  const isMountingContentForOpen = (popupElement) =>
34567
34615
  popupsMountingContentForOpen.has(popupElement);
34616
+ // The popup being built that holds `element` somewhere below it, or null. For
34617
+ // an element whose own openable ancestor is something else (a closed trigger
34618
+ // inside the popup): what the layout would say about it is what it says about
34619
+ // the whole popup — nothing is on screen there yet.
34620
+ const findPopupMountingContentAround = (element) => {
34621
+ for (const popupElement of popupsMountingContentForOpen) {
34622
+ if (popupElement !== element && popupElement.contains(element)) {
34623
+ return popupElement;
34624
+ }
34625
+ }
34626
+ return null;
34627
+ };
34568
34628
 
34569
34629
  // requestIdleCallback is missing from Safari; a timeout is close enough there.
34570
34630
  const requestIdle = (callback) =>
@@ -34734,6 +34794,9 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34734
34794
  // Set by the mount effect below for an element that lives in its openable
34735
34795
  // ancestor's façade rather than in what that ancestor opens.
34736
34796
  const displayedWhileAncestorClosedRef = useRef(false);
34797
+ // The popup whose opening will answer what the mount could not — see the
34798
+ // mount effect below.
34799
+ const decisionDeferredToRef = useRef(null);
34737
34800
 
34738
34801
  // Run on mount (or when deps change) — but only if the element is visible.
34739
34802
  useLayoutEffect(() => {
@@ -34747,14 +34810,26 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34747
34810
  return;
34748
34811
  }
34749
34812
  if (!isAncestorOpen(ancestor)) {
34750
- if (
34813
+ if (isMountingContentForOpen(ancestor)) {
34751
34814
  // The popup building its content for its own opening: nothing in it
34752
34815
  // is on screen, and the open about to follow reveals all of it. Known
34753
34816
  // 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
- ) {
34817
+ // bring up to date once per element asking; the observer below fires
34818
+ // once it opens.
34819
+ return;
34820
+ }
34821
+ const popupMounting = findPopupMountingContentAround(el);
34822
+ if (popupMounting) {
34823
+ // Under a closed trigger — a picker's façade, an expandable's header —
34824
+ // inside a popup being built. Whether this element is on screen once
34825
+ // that popup is open is exactly what the layout cannot say while the
34826
+ // popup, and everything in it, is display:none: it would answer "hidden"
34827
+ // for a façade that the opening is about to show. Asked again when the
34828
+ // popup opens (see the observer effect below).
34829
+ decisionDeferredToRef.current = popupMounting;
34830
+ return;
34831
+ }
34832
+ if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
34758
34833
  // Ancestor is closed and took this element off screen with it — skip
34759
34834
  // now; the observeAncestorOpenState call below will fire once it
34760
34835
  // opens.
@@ -34778,19 +34853,63 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
34778
34853
  if (!ancestor) {
34779
34854
  return undefined;
34780
34855
  }
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
- });
34856
+ const stopObservingAncestor = observeAncestorOpenState(
34857
+ ancestor,
34858
+ ({ isOpen }) => {
34859
+ if (!isOpen) {
34860
+ return;
34861
+ }
34862
+ if (displayedWhileAncestorClosedRef.current) {
34863
+ // Façade content: on screen the whole time, so this opening reveals
34864
+ // nothing here — and `becauseAncestorOpened: true` about it would be
34865
+ // false in a way consumers act on (see use_auto_focus.js).
34866
+ return;
34867
+ }
34868
+ const lastEl = ref.current;
34869
+ callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
34870
+ },
34871
+ );
34872
+ const popupMounting = decisionDeferredToRef.current;
34873
+ if (!popupMounting) {
34874
+ return stopObservingAncestor;
34875
+ }
34876
+ // The mount deferred its decision to this popup's opening (see above):
34877
+ // once it is open, the layout answers for what the closed ancestor keeps
34878
+ // on screen. A façade element is then displayed, by that opening — which
34879
+ // owns it, the way it owns everything the popup reveals. An element the
34880
+ // closed ancestor genuinely hides stays with the observer above.
34881
+ const stopObservingPopup = observeAncestorOpenState(
34882
+ popupMounting,
34883
+ ({ isOpen }) => {
34884
+ if (!isOpen) {
34885
+ return;
34886
+ }
34887
+ stopObservingPopup();
34888
+ decisionDeferredToRef.current = null;
34889
+ const lastEl = ref.current;
34890
+ if (!lastEl) {
34891
+ return;
34892
+ }
34893
+ if (isAncestorOpen(ancestor)) {
34894
+ callbackRef.current(
34895
+ lastEl,
34896
+ createDisplayedEvent(popupMounting, true),
34897
+ );
34898
+ return;
34899
+ }
34900
+ if (isDisplayedDespiteClosedAncestor(lastEl, ancestor)) {
34901
+ displayedWhileAncestorClosedRef.current = true;
34902
+ callbackRef.current(
34903
+ lastEl,
34904
+ createDisplayedEvent(popupMounting, true),
34905
+ );
34906
+ }
34907
+ },
34908
+ );
34909
+ return () => {
34910
+ stopObservingAncestor();
34911
+ stopObservingPopup();
34912
+ };
34794
34913
  }, []);
34795
34914
  };
34796
34915
 
@@ -68803,6 +68922,7 @@ const ListItems = ({
68803
68922
  const virtual = useContext(ListVirtualContext);
68804
68923
  const slotId = useContext(ListSlotContext);
68805
68924
  const renderWindow = useContext(RenderWindowContext);
68925
+ const separator = useContext(SeparatorContext);
68806
68926
  // The vnode drawn for a row, kept by item: a run rendering again (its window
68807
68927
  // moving, its first paint's budget giving way to the full one) hands preact
68808
68928
  // the same vnode for a row that has not changed, and preact leaves that
@@ -68811,14 +68931,14 @@ const ListItems = ({
68811
68931
  // row at the same index, in the same refreshing state: everything the
68812
68932
  // function is given.
68813
68933
  const rowVnodesRef = useRef(null);
68814
- if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
68934
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem || rowVnodesRef.current.separator !== separator) {
68815
68935
  rowVnodesRef.current = {
68816
68936
  renderItem,
68937
+ separator,
68817
68938
  byItem: new Map()
68818
68939
  };
68819
68940
  }
68820
68941
  const rowVnodesByItem = rowVnodesRef.current.byItem;
68821
- const separator = useContext(SeparatorContext);
68822
68942
  const store = useItemStore({
68823
68943
  items,
68824
68944
  count,
@@ -69048,17 +69168,33 @@ const ListItems = ({
69048
69168
  const item = getItemAt(rowIndex);
69049
69169
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
69050
69170
  let rowVnode;
69171
+ let rowContextValue;
69172
+ let rowKept = null;
69051
69173
  if (item !== undefined) {
69052
69174
  const rowVnodeKept = rowVnodesByItem.get(item);
69053
69175
  if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
69054
69176
  rowVnode = rowVnodeKept.vnode;
69177
+ rowContextValue = rowVnodeKept.rowContextValue;
69178
+ rowKept = rowVnodeKept;
69055
69179
  } else {
69056
69180
  rowVnode = renderItem(item, rowIndex, renderItemState);
69057
- rowVnodesByItem.set(item, {
69181
+ // Kept with the vnode, for the same reason: a context value that is a
69182
+ // fresh object on every render forces every consumer of it to render,
69183
+ // which is the row's own chain — the vnode handed back unchanged would
69184
+ // then buy nothing.
69185
+ rowContextValue = {
69186
+ id: key,
69187
+ index: rowIndex,
69188
+ item
69189
+ };
69190
+ rowKept = {
69058
69191
  vnode: rowVnode,
69192
+ rowContextValue,
69059
69193
  rowIndex,
69060
- refreshing: renderItemState.refreshing
69061
- });
69194
+ refreshing: renderItemState.refreshing,
69195
+ separatorVnode: null
69196
+ };
69197
+ rowVnodesByItem.set(item, rowKept);
69062
69198
  }
69063
69199
  } else if (renderRowSkeleton === false) {
69064
69200
  // The row must still take its room: without it the rows below would
@@ -69082,20 +69218,25 @@ const ListItems = ({
69082
69218
  // as a hairline under the label.
69083
69219
  const drawSeparator = separator && rowIndex > 0 && !opensGroup(groupKeyOf(item, rowIndex));
69084
69220
  if (drawSeparator) {
69085
- pushRow(cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
69086
- key: `${key}_separator`
69087
- }), item, rowIndex);
69221
+ // Kept with the row too: a separator built again is a separator
69222
+ // rendered again.
69223
+ let separatorVnode = rowKept ? rowKept.separatorVnode : null;
69224
+ if (!separatorVnode) {
69225
+ separatorVnode = cloneElement(resolveSeparatorVnode(separator, rowIndex - 1), {
69226
+ key: `${key}_separator`
69227
+ });
69228
+ if (rowKept) {
69229
+ rowKept.separatorVnode = separatorVnode;
69230
+ }
69231
+ }
69232
+ pushRow(separatorVnode, item, rowIndex);
69088
69233
  }
69089
69234
  pushRow(jsx(ListRowContext.Provider, {
69090
69235
  value: item === undefined ? {
69091
69236
  id: key,
69092
69237
  index: rowIndex,
69093
69238
  ...getSkeletonRow()
69094
- } : {
69095
- id: key,
69096
- index: rowIndex,
69097
- item
69098
- },
69239
+ } : rowContextValue,
69099
69240
  children: rowVnode
69100
69241
  }, key), item, rowIndex);
69101
69242
  }