@jsenv/navi 0.29.6 → 0.29.8

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.
@@ -7,7 +7,7 @@ import { createContext, isValidElement, h, Fragment, toChildArray, render, clone
7
7
  import { useContext, useLayoutEffect, useRef, useEffect, useCallback, useState, useMemo, useId, useErrorBoundary } from "preact/hooks";
8
8
  import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
9
9
  import { computed, signal, effect, batch, useSignal } from "@preact/signals";
10
- import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, dispatchCustomEvent, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, measureWidestChildRow, performTabNavigation, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
10
+ import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, findEvent, dispatchInternalCustomEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, dispatchCustomEvent, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterThreshold, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
11
11
  export { contrastColor, startDragToReorder } from "@jsenv/dom";
12
12
  import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
13
13
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
@@ -46952,10 +46952,10 @@ const css$u = /* css */`
46952
46952
  scrollbar-width: inherit;
46953
46953
  }
46954
46954
 
46955
- /* scroller="parent": the list does not scroll, the ancestor it lives in
46956
- does. Its own scroll box must then be transparent to layout — otherwise
46957
- it would cap the list at a height of its own and start a second, nested
46958
- scroll inside the page's. */
46955
+ /* Any scroller other than "self": the list does not scroll, something
46956
+ around it does. Its own scroll box must then be transparent to
46957
+ layout — otherwise it would cap the list at a height of its own and
46958
+ start a second, nested scroll inside the page's. */
46959
46959
  &[data-scroller="parent"] {
46960
46960
  max-height: none;
46961
46961
  overflow: visible;
@@ -47567,7 +47567,7 @@ const ListUI = props => {
47567
47567
  baseClassName: "navi_list_container",
47568
47568
  popover: popover,
47569
47569
  "data-horizontal": horizontal ? "" : undefined,
47570
- "data-scroller": scroller === "parent" ? "parent" : undefined,
47570
+ "data-scroller": scroller === "self" ? undefined : "parent",
47571
47571
  "data-expand-x": expandX || expand ? "" : undefined,
47572
47572
  "data-expand-y": expandY || expand ? "" : undefined,
47573
47573
  expandX: expandX,
@@ -47644,7 +47644,7 @@ const ListFirstResolver = props => {
47644
47644
  * scrolled?: "start" | "end" | number | {id: string, offset?: number},
47645
47645
  * defaultScrolled?: "start" | "end" | number | {id: string, offset?: number},
47646
47646
  * onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
47647
- * scroller?: "self" | "parent",
47647
+ * scroller?: "self" | "parent" | "document" | Element | {current: Element},
47648
47648
  * fallback?: import("ignore:preact").ComponentChildren,
47649
47649
  * searchFallback?: import("ignore:preact").ComponentChildren,
47650
47650
  * searchText?: string,
@@ -47705,11 +47705,20 @@ const ListFirstResolver = props => {
47705
47705
  * how far below the top of the view it starts. Keep it to come back to it
47706
47706
  * later through `scrolled`/`defaultScrolled` — an index would not do, since
47707
47707
  * rows get inserted while a list is being read.
47708
- * @param {"self"|"parent"} [props.scroller="self"]
47708
+ * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
47709
47709
  * Which box scrolls. `"self"` gives the list a scroll box of its own;
47710
47710
  * `"parent"` makes it virtualize against the scrollable ancestor it lives in
47711
47711
  * (the page, a panel) — no scroll box nested inside another one, no height
47712
47712
  * to compute.
47713
+ *
47714
+ * `"parent"` finds that ancestor by measuring: the nearest one whose content
47715
+ * actually overflows it, the page if none does. Declaring `overflow` is not
47716
+ * enough to be picked (a box with `overflow-x: auto` that grows with its
47717
+ * content computes `overflow-y: auto` without ever scrolling), and the
47718
+ * answer is taken again as the geometry moves, so an ancestor that starts to
47719
+ * scroll once it fills up is picked up then. When that is still not the box
47720
+ * you mean, say so: `"document"`, or the element itself (a ref works) —
47721
+ * nothing is guessed then.
47713
47722
  */
47714
47723
  const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
47715
47724
  const ListContent = ({
@@ -47803,6 +47812,20 @@ const useListScrollSync = ({
47803
47812
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal);
47804
47813
  const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
47805
47814
  const getListEl = () => ref.current.querySelector(".navi_list");
47815
+ // Which box scrolls is measured (see getScrollerEl), so the answer holds
47816
+ // only for the geometry it was taken on: an ancestor that is bounded but
47817
+ // still waiting for its first rows looks like it will never scroll. It is
47818
+ // taken again on every commit and every resize; the state exists so the
47819
+ // effects below reattach their listeners to whatever it lands on.
47820
+ const [scrollerElResolved, setScrollerElResolved] = useState(null);
47821
+ const resolveScroller = () => {
47822
+ if (!ref.current) {
47823
+ return;
47824
+ }
47825
+ const scrollerElNow = getScroller();
47826
+ setScrollerElResolved(current => current === scrollerElNow ? current : scrollerElNow);
47827
+ };
47828
+ useLayoutEffect(resolveScroller);
47806
47829
 
47807
47830
  // The row the scroll holds onto across a change of geometry, and where it
47808
47831
  // sat when that change was decided. Captured at the two moments the list
@@ -48310,7 +48333,7 @@ const useListScrollSync = ({
48310
48333
  scrollerEl.removeEventListener(type, takeOver);
48311
48334
  }
48312
48335
  };
48313
- }, [scrolledWanted, scroller]);
48336
+ }, [scrolledWanted, scrollerElResolved]);
48314
48337
 
48315
48338
  // Where the list is, said the way it can be given back to it: the row at the
48316
48339
  // top of what is on screen, and how far above the fold it sits. An index
@@ -48353,6 +48376,11 @@ const useListScrollSync = ({
48353
48376
  const scrollerEl = getScroller();
48354
48377
  const listEl = getListEl();
48355
48378
  const observer = new ResizeObserver(entries => {
48379
+ // A box that grows may be a box that starts to scroll — the scroller is
48380
+ // resolved again before anything is done about the resize, so that a
48381
+ // list which has outgrown a bounded ancestor stops holding on to the
48382
+ // page.
48383
+ resolveScroller();
48356
48384
  // Two things resize here, and they call for opposite answers. The LIST
48357
48385
  // growing is its own content settling: only a list holding itself
48358
48386
  // somewhere cares (the end it aims at has moved), and a list the user is
@@ -48394,7 +48422,7 @@ const useListScrollSync = ({
48394
48422
  return () => {
48395
48423
  observer.disconnect();
48396
48424
  };
48397
- }, [scroller]);
48425
+ }, [scrollerElResolved]);
48398
48426
 
48399
48427
  // Inserting rows above what the user is looking at must not move it by a
48400
48428
  // single pixel. The browser will not do it for us — overflow-anchor gives up
@@ -48547,7 +48575,7 @@ const useListScrollSync = ({
48547
48575
  return () => {
48548
48576
  scrollEventTarget.removeEventListener("scroll", onScroll);
48549
48577
  };
48550
- }, [renderBudget, scroller]);
48578
+ }, [renderBudget, scrollerElResolved]);
48551
48579
  holdWindow();
48552
48580
  return {
48553
48581
  virtualItemSizeSignal,
@@ -48576,21 +48604,43 @@ const getScrollerViewportRect = scrollerEl => {
48576
48604
  return scrollerEl.getBoundingClientRect();
48577
48605
  };
48578
48606
  // scroller="parent": the list virtualizes against the scroll box it lives in
48579
- // instead of one of its own.
48607
+ // instead of one of its own. Which box that is can only be measured, and a
48608
+ // measurement holds for the geometry it was taken on — see resolveScroller in
48609
+ // useListScrollSync, which takes it again whenever the geometry moves.
48580
48610
  const getScrollerEl = (listContainerEl, scroller, horizontal) => {
48611
+ if (scroller === "document") {
48612
+ return document.scrollingElement;
48613
+ }
48614
+ if (scroller && typeof scroller === "object") {
48615
+ // The caller names the scroller: an element, or a ref holding one.
48616
+ // Detection is a heuristic forever; being told is not.
48617
+ const el = scroller.nodeType === 1 ? scroller : scroller.current;
48618
+ return el || document.scrollingElement;
48619
+ }
48581
48620
  if (scroller !== "parent") {
48582
48621
  return listContainerEl.querySelector(`.navi_list_scroll_container`);
48583
48622
  }
48584
- let ancestor = listContainerEl.parentElement;
48585
- while (ancestor) {
48586
- const style = window.getComputedStyle(ancestor);
48587
- const overflow = horizontal ? style.overflowX : style.overflowY;
48588
- if (overflow === "auto" || overflow === "scroll") {
48589
- return ancestor;
48623
+ const axis = horizontal ? "x" : "y";
48624
+ let element = listContainerEl;
48625
+ let nearestScrollContainer = null;
48626
+ while (true) {
48627
+ const scrollContainer = getScrollContainer(element);
48628
+ if (!scrollContainer || scrollContainer === element || scrollContainer === document.documentElement || scrollContainer === document.scrollingElement) {
48629
+ break;
48630
+ }
48631
+ // A scroll container that declares an overflow it does not have is not the
48632
+ // one to listen to (see canScroll): the box that scrolls is above it.
48633
+ if (canScroll(scrollContainer, axis)) {
48634
+ return scrollContainer;
48590
48635
  }
48591
- ancestor = ancestor.parentElement;
48636
+ nearestScrollContainer = nearestScrollContainer || scrollContainer;
48637
+ element = scrollContainer;
48592
48638
  }
48593
- return document.scrollingElement;
48639
+ // Nothing scrolls yet. A bounded box waiting for its first rows is the one
48640
+ // that will, and until it does, holding it changes nothing — where holding
48641
+ // the page instead would drag the whole document to the end of a list that
48642
+ // is not even scrollable.
48643
+ return nearestScrollContainer || document.scrollingElement;
48594
48644
  };
48595
48645
  // A row must be worth looking at once put back where it was. The offset comes
48596
48646
  // from wherever the position was taken — another screen, another window size,
@@ -52581,7 +52631,7 @@ const renderDayDefault = (day, {
52581
52631
  // saying "you may wrap" there would undo the truncation instead.
52582
52632
  ,
52583
52633
 
52584
- noWrap: maxLines === 1 ? undefined : false,
52634
+ noWrap: maxLines === 1 || maxLines === "1" ? undefined : false,
52585
52635
  maxLines: maxLines,
52586
52636
  children: day
52587
52637
  });