@jsenv/navi 0.26.7 → 0.26.9

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.
@@ -3,7 +3,7 @@ import { isValidElement, createContext, h, options, toChildArray, render, cloneE
3
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useImperativeHandle, useId } from "preact/hooks";
4
4
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
5
5
  import { signal, effect, computed, batch, useSignal } from "@preact/signals";
6
- import { createIterableWeakSet, getElementSignature, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, resolveCSSSize, canInterceptKeys, activeElementSignal, hasCSSSizeUnit, resolveOklchLightness, contrastColor, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, findFocusable, trapScrollInside, trapFocusInside, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
6
+ import { createIterableWeakSet, getElementSignature, mergeOneStyle, normalizeStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, resolveCSSSize, canInterceptKeys, activeElementSignal, hasCSSSizeUnit, resolveOklchLightness, contrastColor, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, findFocusable, trapScrollInside, trapFocusInside, snapToPixel, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
7
  export { contrastColor } from "@jsenv/dom";
8
8
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
9
9
  import { createValidity } from "@jsenv/validity";
@@ -6222,15 +6222,6 @@ const withPropsClassName = (baseClassName, classNameFromProps) => {
6222
6222
 
6223
6223
  const BoxFlowContext = createContext();
6224
6224
 
6225
- const normalizeSpacingStyle = (value, property = "padding") => {
6226
- const cssValue = SIZE_MAP[value];
6227
- return cssValue || stringifyStyle(value, property);
6228
- };
6229
- const normalizeTypoStyle = (value, property = "fontSize") => {
6230
- const cssValue = TYPO_SIZE_MAP[value];
6231
- return cssValue || stringifyStyle(value, property);
6232
- };
6233
-
6234
6225
  const PASS_THROUGH = { name: "pass_through" };
6235
6226
  const applyOnCSSProp = (cssStyle) => {
6236
6227
  return (value) => {
@@ -6410,7 +6401,7 @@ const DIMENSION_PROPS = {
6410
6401
  return { transform: `scaleX(${stringifyStyle(value, "scaleX")})` };
6411
6402
  },
6412
6403
  scaleY: (value) => {
6413
- return { transform: `scaleY(${value})` };
6404
+ return { transform: `scaleY(${stringifyStyle(value, "scaleY")})` };
6414
6405
  },
6415
6406
  scale: (value) => {
6416
6407
  if (Array.isArray(value)) {
@@ -6655,7 +6646,7 @@ const CONTENT_PROPS = {
6655
6646
  spacing: (value, { boxFlow }) => {
6656
6647
  if (isSpacingHandledByFlow(boxFlow)) {
6657
6648
  return {
6658
- gap: resolveSpacingSize(value, "gap"),
6649
+ gap: stringifySpacingStyle(value, "gap"),
6659
6650
  };
6660
6651
  }
6661
6652
  return undefined;
@@ -6663,12 +6654,12 @@ const CONTENT_PROPS = {
6663
6654
  spacingX: (value, { boxFlow }) => {
6664
6655
  if (boxFlow === "flex-x" || boxFlow === "inline-flex-x") {
6665
6656
  return {
6666
- gap: resolveSpacingSize(value, "gap"),
6657
+ gap: stringifySpacingStyle(value, "gap"),
6667
6658
  };
6668
6659
  }
6669
6660
  if (boxFlow === "grid" || boxFlow === "inline-grid") {
6670
6661
  return {
6671
- columnGap: resolveSpacingSize(value, "columnGap"),
6662
+ columnGap: stringifySpacingStyle(value, "columnGap"),
6672
6663
  };
6673
6664
  }
6674
6665
  return undefined;
@@ -6676,12 +6667,12 @@ const CONTENT_PROPS = {
6676
6667
  spacingY: (value, { boxFlow }) => {
6677
6668
  if (boxFlow === "flex-y" || boxFlow === "inline-flex-y") {
6678
6669
  return {
6679
- gap: resolveSpacingSize(value, "gap"),
6670
+ gap: stringifySpacingStyle(value, "gap"),
6680
6671
  };
6681
6672
  }
6682
6673
  if (boxFlow === "grid" || boxFlow === "inline-grid") {
6683
6674
  return {
6684
- rowGap: resolveSpacingSize(value, "rowGap"),
6675
+ rowGap: stringifySpacingStyle(value, "rowGap"),
6685
6676
  };
6686
6677
  }
6687
6678
  return undefined;
@@ -6773,25 +6764,31 @@ const getStylePropGroup = (name) => {
6773
6764
  }
6774
6765
  return null;
6775
6766
  };
6776
- const getNormalizer = (key) => {
6767
+ const getStringifier = (key) => {
6777
6768
  if (key === "borderRadius") {
6778
- return normalizeSpacingStyle;
6769
+ return stringifySpacingStyle;
6779
6770
  }
6780
6771
  const group = getStylePropGroup(key);
6781
6772
  if (group === "margin" || group === "padding") {
6782
- return normalizeSpacingStyle;
6773
+ return stringifySpacingStyle;
6783
6774
  }
6784
6775
  if (group === "typo") {
6785
- return normalizeTypoStyle;
6776
+ return stringifyTypoStyle;
6786
6777
  }
6787
- return normalizeRegularStyle;
6778
+ return stringifyStyle;
6779
+ };
6780
+ const stringifySpacingStyle = (size, property = "padding") => {
6781
+ return normalizeStyle(SIZE_MAP[size] || size, property, "css");
6788
6782
  };
6789
- const normalizeRegularStyle = (
6783
+ const stringifyTypoStyle = (size, property = "fontSize") => {
6784
+ return normalizeStyle(TYPO_SIZE_MAP[size] || size, property, "css");
6785
+ };
6786
+ const stringifyStyle = (
6790
6787
  value,
6791
6788
  name,
6792
6789
  // styleContext, context
6793
6790
  ) => {
6794
- return stringifyStyle(value, name);
6791
+ return normalizeStyle(value, name, "css");
6795
6792
  };
6796
6793
  const getHowToHandleStyleProp = (name) => {
6797
6794
  const getStyle = All_PROPS[name];
@@ -6807,8 +6804,8 @@ const prepareStyleValue = (
6807
6804
  styleContext,
6808
6805
  context,
6809
6806
  ) => {
6810
- const normalizer = getNormalizer(name);
6811
- const cssValue = normalizer(value, name, styleContext, context);
6807
+ const stringifier = getStringifier(name);
6808
+ const cssValue = stringifier(value, name, styleContext, context);
6812
6809
  const mergedValue = mergeOneStyle(existingValue, cssValue, name, context);
6813
6810
  return mergedValue;
6814
6811
  };
@@ -6848,8 +6845,8 @@ const sizeSpacingKeySet = new Set(Object.keys(SIZE_MAP));
6848
6845
  const isSizeSpacingKey = (key) => {
6849
6846
  return sizeSpacingKeySet.has(key);
6850
6847
  };
6851
- const resolveSpacingSize = (size, property = "padding") => {
6852
- return stringifyStyle(SIZE_MAP[size] || size, property);
6848
+ const resolveSpacingSize = (size, element, property = "padding") => {
6849
+ return normalizeStyle(SIZE_MAP[size] || size, property, "js", element);
6853
6850
  };
6854
6851
 
6855
6852
  const COLOR_KEYWORD_MAP = {
@@ -21017,7 +21014,7 @@ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
21017
21014
  separator = defaultSpace;
21018
21015
  } else if (typeof spacing === "string") {
21019
21016
  if (isSizeSpacingKey(spacing)) {
21020
- const value = resolveSpacingSize(spacing);
21017
+ const value = stringifySpacingStyle(spacing);
21021
21018
  separator = jsx(CustomWidthSpace, {
21022
21019
  value: value,
21023
21020
  useRealSpaceChar: useRealSpaceChar
@@ -29448,33 +29445,6 @@ const InputControllingList = props => {
29448
29445
  const getListEl = () => {
29449
29446
  return document.getElementById(listId);
29450
29447
  };
29451
- useEffect(() => {
29452
- const inputEl = ref.current;
29453
- if (!inputEl) {
29454
- return undefined;
29455
- }
29456
- const listEl = getListEl();
29457
- if (!listEl) {
29458
- return undefined;
29459
- }
29460
- const onListSelect = e => {
29461
- const {
29462
- event
29463
- } = e.detail;
29464
- if (event.type === "mousedown") {
29465
- if (!inputEl.hidden) {
29466
- event.preventDefault();
29467
- inputEl.focus({
29468
- preventScroll: true
29469
- });
29470
- }
29471
- }
29472
- };
29473
- listEl.addEventListener("navi_list_select", onListSelect);
29474
- return () => {
29475
- listEl.removeEventListener("navi_list_select", onListSelect);
29476
- };
29477
- }, []);
29478
29448
  const onKeyDownWithShortcuts = shortcutsViaOnKeyDown({
29479
29449
  arrowdown: e => {
29480
29450
  const listEl = getListEl();
@@ -30731,6 +30701,8 @@ const Popover = props => {
30731
30701
  positionY,
30732
30702
  positionXFixed,
30733
30703
  positionYFixed,
30704
+ spacing = 0,
30705
+ viewportSpacing = 0,
30734
30706
  ...rest
30735
30707
  } = props;
30736
30708
  const defaultRef = useRef();
@@ -30762,21 +30734,39 @@ const Popover = props => {
30762
30734
  width,
30763
30735
  height
30764
30736
  } = effectiveAnchor.getBoundingClientRect();
30765
- const snap = v => Math.round(v * devicePixelRatio) / devicePixelRatio;
30766
- popoverEl.style.setProperty("--anchor-width", `${snap(width)}px`);
30767
- popoverEl.style.setProperty("--anchor-height", `${snap(height)}px`);
30737
+ const {
30738
+ left: borderLeft,
30739
+ right: borderRight,
30740
+ top: borderTop,
30741
+ bottom: borderBottom
30742
+ } = getBorderSizes(effectiveAnchor);
30743
+ popoverEl.style.setProperty("--anchor-width", `${snapToPixel(width)}px`);
30744
+ popoverEl.style.setProperty("--anchor-height", `${snapToPixel(height)}px`);
30745
+ popoverEl.style.setProperty("--anchor-inner-width", `${snapToPixel(width - borderLeft - borderRight)}px`);
30746
+ popoverEl.style.setProperty("--anchor-inner-height", `${snapToPixel(height - borderTop - borderBottom)}px`);
30768
30747
  const minLeft = 1;
30769
30748
  const effectivePositionX = anchor ? positionX : "center";
30749
+ // Remove max-height constraint so pickPositionRelativeTo measures the natural
30750
+ // (unconstrained) height of the popover. This ensures the 60% flip threshold
30751
+ // compares against the real content height, not the already-truncated one.
30752
+ popoverEl.style.removeProperty("--space-available");
30770
30753
  const {
30771
30754
  left,
30772
- top
30755
+ top,
30756
+ positionY: finalPositionY,
30757
+ spaceAbove,
30758
+ spaceBelow
30773
30759
  } = pickPositionRelativeTo(popoverEl, effectiveAnchor, {
30774
30760
  positionX: effectivePositionX,
30775
30761
  positionY,
30776
30762
  positionXFixed,
30777
30763
  positionYFixed,
30764
+ spacing: resolveSpacingSize(spacing),
30765
+ viewportSpacing: resolveSpacingSize(viewportSpacing),
30778
30766
  minLeft
30779
30767
  });
30768
+ const spaceAvailable = finalPositionY === "above" || finalPositionY === "above-overlap" ? spaceAbove : spaceBelow;
30769
+ popoverEl.style.setProperty("--space-available", `${spaceAvailable}px`);
30780
30770
  debugPopup(`positionPopover("${positionEvent.type}") -> left: ${left}, top: ${top}`);
30781
30771
  popoverEl.style.top = `${top}px`;
30782
30772
  popoverEl.style.left = `${Math.max(left, minLeft)}px`;
@@ -31046,7 +31036,7 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31046
31036
  min-width: var(--anchor-width, 0px);
31047
31037
  max-width: 95vw;
31048
31038
  /* max-height covers the placeholder + list; the list scrolls internally */
31049
- max-height: 95dvh;
31039
+ max-height: var(--space-available, 95dvh);
31050
31040
  margin: 0;
31051
31041
  padding: 0;
31052
31042
  background: var(--select-background-color);
@@ -31071,7 +31061,7 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31071
31061
  /* To make clone same height as original we need to force it because context can impact height */
31072
31062
  /* Like siblings with a bigger height in a flex container */
31073
31063
  /* We subtract the border sizes as anchor-height includes borders in the dimensions */
31074
- min-height: calc(var(--anchor-height) - var(--select-border-width));
31064
+ min-height: var(--anchor-inner-height);
31075
31065
  /* Mirror the trigger's padding so the clone looks identical */
31076
31066
  padding-top: var(--x-select-padding-top);
31077
31067
  padding-right: var(--x-select-padding-right);
@@ -31079,7 +31069,6 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31079
31069
  padding-left: var(--x-select-padding-left);
31080
31070
  flex-shrink: 0;
31081
31071
  flex-direction: column;
31082
- align-items: center;
31083
31072
  justify-content: center;
31084
31073
  gap: var(--navi-s);
31085
31074
  order: -1; /* before the list — popover is below the trigger */
@@ -31122,6 +31111,13 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31122
31111
  }
31123
31112
 
31124
31113
  &[aria-expanded="true"] {
31114
+ &[navi-popover-mode="overlay"],
31115
+ &[navi-popover-mode="attached"] {
31116
+ /* When sizes uses float AND the border uses border-radius it's possible it's possible to see some pixels
31117
+ of the underlying select borders. We hide them to ensure this cannot happen. */
31118
+ border-color: transparent;
31119
+ }
31120
+
31125
31121
  .navi_select_popover {
31126
31122
  display: flex;
31127
31123
  flex-direction: column;
@@ -31385,7 +31381,7 @@ const SelectTrigger = () => {
31385
31381
  });
31386
31382
  };
31387
31383
 
31388
- // SelectWithPopover — trigger + popover anchored below the trigger.
31384
+ // SelectWithPopover — trigger + popover anchored relative to the trigger.
31389
31385
  const SelectWithPopover = props => {
31390
31386
  const {
31391
31387
  ref,
@@ -31395,6 +31391,9 @@ const SelectWithPopover = props => {
31395
31391
  pointerTrap,
31396
31392
  scrollTrap = true,
31397
31393
  focusTrap = true,
31394
+ popoverMode = "nearby",
31395
+ popoverSpacing = popoverMode === "nearby" ? 5 : 0,
31396
+ viewportSpacing = 10,
31398
31397
  ...rest
31399
31398
  } = props;
31400
31399
  const debugFocus = useDebugFocus();
@@ -31433,8 +31432,13 @@ const SelectWithPopover = props => {
31433
31432
  });
31434
31433
  };
31435
31434
  const moveFocusToSelect = e => {
31435
+ if (e.type === "mousedown") {
31436
+ e.preventDefault();
31437
+ debugFocus(formatEventSideEffect(e, `preventDefault and move focus to select`));
31438
+ } else {
31439
+ debugFocus(formatEventSideEffect(e, `move focus to select`));
31440
+ }
31436
31441
  const select = ref.current;
31437
- debugFocus(`moveFocusToSelect("${e.type}")`);
31438
31442
  select.focus({
31439
31443
  preventScroll: true
31440
31444
  });
@@ -31444,6 +31448,7 @@ const SelectWithPopover = props => {
31444
31448
  "aria-haspopup": "listbox",
31445
31449
  "aria-expanded": expanded,
31446
31450
  "aria-controls": popoverId,
31451
+ "navi-popover-mode": popoverMode,
31447
31452
  onMouseDown: e => {
31448
31453
  if (e.button !== 0) {
31449
31454
  return;
@@ -31549,11 +31554,13 @@ const SelectWithPopover = props => {
31549
31554
  }
31550
31555
  },
31551
31556
  positionX: "left-aligned",
31552
- positionY: "below-overlap",
31557
+ positionY: popoverMode === "nearby" ? "below" : "below-overlap",
31558
+ spacing: popoverSpacing,
31559
+ viewportSpacing: viewportSpacing,
31553
31560
  scrollTrap: scrollTrap,
31554
31561
  pointerTrap: pointerTrap,
31555
31562
  focusTrap: focusTrap,
31556
- children: [jsx("div", {
31563
+ children: [popoverMode === "attached" ? jsx("div", {
31557
31564
  className: "navi_select_anchor_clone",
31558
31565
  onMouseDown: e => {
31559
31566
  if (e.button !== 0) {
@@ -31562,7 +31569,7 @@ const SelectWithPopover = props => {
31562
31569
  requestClose(e);
31563
31570
  },
31564
31571
  children: props.trigger
31565
- }), jsx(SelectRequestCloseContext.Provider, {
31572
+ }) : null, jsx(SelectRequestCloseContext.Provider, {
31566
31573
  value: requestClose,
31567
31574
  children: children
31568
31575
  })]
@@ -31611,8 +31618,14 @@ const SelectWithDialog = props => {
31611
31618
  });
31612
31619
  };
31613
31620
  const moveFocusToSelect = e => {
31614
- debugFocus(`moveFocusToSelect("${e.type}")`);
31615
- ref.current.focus({
31621
+ if (e.type === "mousedown") {
31622
+ e.preventDefault();
31623
+ debugFocus(formatEventSideEffect(e, `preventDefault and move focus to select`));
31624
+ } else {
31625
+ debugFocus(formatEventSideEffect(e, `move focus to select`));
31626
+ }
31627
+ const select = ref.current;
31628
+ select.focus({
31616
31629
  preventScroll: true
31617
31630
  });
31618
31631
  };
@@ -31830,9 +31843,11 @@ const applySearch = (searchText, value) => {
31830
31843
 
31831
31844
  // Multi-word OR: split on whitespace, any word matching contributes to the score.
31832
31845
  // Items where all words match rank higher than partial matches.
31833
- if (words.length < 2) {
31834
- return { match: false, matchScore: 0, matchRanges: [] };
31835
- }
31846
+ // Note: words always has at least 1 element here (searchText is non-empty and
31847
+ // foldedSearch.split filters empty strings). This path also handles the case
31848
+ // where searchText has trailing/leading spaces: the phrase match above tries
31849
+ // the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
31850
+ // here to try each word individually (e.g. "tc" matches "tca").
31836
31851
  const matchRanges = [];
31837
31852
  let matchedWordCount = 0;
31838
31853
  let anyWordAtStart = false;
@@ -31865,7 +31880,7 @@ const applySearch = (searchText, value) => {
31865
31880
  }
31866
31881
  }
31867
31882
  if (matchedWordCount === 0) {
31868
- return { match: false, matchScore: 0, matchRanges: [] };
31883
+ return tryAcronymMatch(foldedStr, str, searchText);
31869
31884
  }
31870
31885
  const wordRatio = matchedWordCount / words.length;
31871
31886
  let baseScore;
@@ -31907,8 +31922,53 @@ const SCORE_PHRASE_AT_START = 1;
31907
31922
  const SCORE_MULTI_WORD_AT_START = 0.75;
31908
31923
  const SCORE_AT_WORD_BOUNDARY = 0.625;
31909
31924
  const SCORE_MID_WORD = 0.5;
31925
+ const SCORE_ACRONYM = 0.4;
31910
31926
  const SCORE_BONUS_CASE_EXACT = 0.125;
31911
31927
 
31928
+ // Acronym match: each char of searchText (spaces stripped) must be the first
31929
+ // letter of a word in value, in order (greedy subsequence on word-starts).
31930
+ // e.g. "TC" matches "Total Count" highlighting the T and C.
31931
+ const tryAcronymMatch = (foldedStr, str, searchText) => {
31932
+ const acronymChars = foldAccents(searchText).toLowerCase().replace(/\s/g, "");
31933
+ if (acronymChars.length < 2) {
31934
+ // Single-char acronym is too ambiguous — skip.
31935
+ return { match: false, matchScore: 0, matchRanges: [] };
31936
+ }
31937
+ const wordStarts = [];
31938
+ for (let i = 0; i < foldedStr.length; i++) {
31939
+ if (isWordBoundary(foldedStr, i)) {
31940
+ wordStarts.push(i);
31941
+ }
31942
+ }
31943
+ const matchedPositions = [];
31944
+ let wordIdx = 0;
31945
+ const originalAcronym = searchText.replace(/\s/g, "");
31946
+ for (let si = 0; si < acronymChars.length; si++) {
31947
+ const ch = acronymChars[si];
31948
+ let found = false;
31949
+ while (wordIdx < wordStarts.length) {
31950
+ const pos = wordStarts[wordIdx];
31951
+ wordIdx++;
31952
+ if (foldedStr[pos] === ch) {
31953
+ matchedPositions.push(pos);
31954
+ found = true;
31955
+ break;
31956
+ }
31957
+ }
31958
+ if (!found) {
31959
+ return { match: false, matchScore: 0, matchRanges: [] };
31960
+ }
31961
+ }
31962
+ const atStart = matchedPositions[0] === 0;
31963
+ const caseExact = matchedPositions.every(
31964
+ (p, i) => str[p] === originalAcronym[i],
31965
+ );
31966
+ const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
31967
+ const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
31968
+ const matchRanges = matchedPositions.map((p) => [p, p + 1]);
31969
+ return { match: true, matchScore, matchRanges };
31970
+ };
31971
+
31912
31972
  // LRU cache for pre-computed search info, avoids recomputing foldAccents/toLowerCase
31913
31973
  // for the same searchText across all items in a list render.
31914
31974
  const searchCache = new Map();