@jsenv/navi 0.26.6 → 0.26.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.
@@ -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");
6782
+ };
6783
+ const stringifyTypoStyle = (size, property = "fontSize") => {
6784
+ return normalizeStyle(TYPO_SIZE_MAP[size] || size, property, "css");
6788
6785
  };
6789
- const normalizeRegularStyle = (
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
@@ -30731,6 +30728,8 @@ const Popover = props => {
30731
30728
  positionY,
30732
30729
  positionXFixed,
30733
30730
  positionYFixed,
30731
+ spacing = 0,
30732
+ viewportSpacing = 0,
30734
30733
  ...rest
30735
30734
  } = props;
30736
30735
  const defaultRef = useRef();
@@ -30762,20 +30761,39 @@ const Popover = props => {
30762
30761
  width,
30763
30762
  height
30764
30763
  } = effectiveAnchor.getBoundingClientRect();
30765
- popoverEl.style.setProperty("--anchor-width", `${width}px`);
30766
- popoverEl.style.setProperty("--anchor-height", `${height}px`);
30764
+ const {
30765
+ left: borderLeft,
30766
+ right: borderRight,
30767
+ top: borderTop,
30768
+ bottom: borderBottom
30769
+ } = getBorderSizes(effectiveAnchor);
30770
+ popoverEl.style.setProperty("--anchor-width", `${snapToPixel(width)}px`);
30771
+ popoverEl.style.setProperty("--anchor-height", `${snapToPixel(height)}px`);
30772
+ popoverEl.style.setProperty("--anchor-inner-width", `${snapToPixel(width - borderLeft - borderRight)}px`);
30773
+ popoverEl.style.setProperty("--anchor-inner-height", `${snapToPixel(height - borderTop - borderBottom)}px`);
30767
30774
  const minLeft = 1;
30768
30775
  const effectivePositionX = anchor ? positionX : "center";
30776
+ // Remove max-height constraint so pickPositionRelativeTo measures the natural
30777
+ // (unconstrained) height of the popover. This ensures the 60% flip threshold
30778
+ // compares against the real content height, not the already-truncated one.
30779
+ popoverEl.style.removeProperty("--space-available");
30769
30780
  const {
30770
30781
  left,
30771
- top
30782
+ top,
30783
+ positionY: finalPositionY,
30784
+ spaceAbove,
30785
+ spaceBelow
30772
30786
  } = pickPositionRelativeTo(popoverEl, effectiveAnchor, {
30773
30787
  positionX: effectivePositionX,
30774
30788
  positionY,
30775
30789
  positionXFixed,
30776
30790
  positionYFixed,
30791
+ spacing: resolveSpacingSize(spacing),
30792
+ viewportSpacing: resolveSpacingSize(viewportSpacing),
30777
30793
  minLeft
30778
30794
  });
30795
+ const spaceAvailable = finalPositionY === "above" || finalPositionY === "above-overlap" ? spaceAbove : spaceBelow;
30796
+ popoverEl.style.setProperty("--space-available", `${spaceAvailable}px`);
30779
30797
  debugPopup(`positionPopover("${positionEvent.type}") -> left: ${left}, top: ${top}`);
30780
30798
  popoverEl.style.top = `${top}px`;
30781
30799
  popoverEl.style.left = `${Math.max(left, minLeft)}px`;
@@ -31045,7 +31063,7 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31045
31063
  min-width: var(--anchor-width, 0px);
31046
31064
  max-width: 95vw;
31047
31065
  /* max-height covers the placeholder + list; the list scrolls internally */
31048
- max-height: 95dvh;
31066
+ max-height: var(--space-available, 95dvh);
31049
31067
  margin: 0;
31050
31068
  padding: 0;
31051
31069
  background: var(--select-background-color);
@@ -31067,7 +31085,10 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31067
31085
  when the popover is below the trigger, and after when above. */
31068
31086
  .navi_select_anchor_clone {
31069
31087
  display: flex;
31070
- min-height: calc(var(--anchor-height) - var(--select-border-width));
31088
+ /* To make clone same height as original we need to force it because context can impact height */
31089
+ /* Like siblings with a bigger height in a flex container */
31090
+ /* We subtract the border sizes as anchor-height includes borders in the dimensions */
31091
+ min-height: var(--anchor-inner-height);
31071
31092
  /* Mirror the trigger's padding so the clone looks identical */
31072
31093
  padding-top: var(--x-select-padding-top);
31073
31094
  padding-right: var(--x-select-padding-right);
@@ -31075,7 +31096,6 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31075
31096
  padding-left: var(--x-select-padding-left);
31076
31097
  flex-shrink: 0;
31077
31098
  flex-direction: column;
31078
- align-items: center;
31079
31099
  justify-content: center;
31080
31100
  gap: var(--navi-s);
31081
31101
  order: -1; /* before the list — popover is below the trigger */
@@ -31118,6 +31138,13 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31118
31138
  }
31119
31139
 
31120
31140
  &[aria-expanded="true"] {
31141
+ &[navi-popover-mode="overlay"],
31142
+ &[navi-popover-mode="attached"] {
31143
+ /* When sizes uses float AND the border uses border-radius it's possible it's possible to see some pixels
31144
+ of the underlying select borders. We hide them to ensure this cannot happen. */
31145
+ border-color: transparent;
31146
+ }
31147
+
31121
31148
  .navi_select_popover {
31122
31149
  display: flex;
31123
31150
  flex-direction: column;
@@ -31381,7 +31408,7 @@ const SelectTrigger = () => {
31381
31408
  });
31382
31409
  };
31383
31410
 
31384
- // SelectWithPopover — trigger + popover anchored below the trigger.
31411
+ // SelectWithPopover — trigger + popover anchored relative to the trigger.
31385
31412
  const SelectWithPopover = props => {
31386
31413
  const {
31387
31414
  ref,
@@ -31391,6 +31418,9 @@ const SelectWithPopover = props => {
31391
31418
  pointerTrap,
31392
31419
  scrollTrap = true,
31393
31420
  focusTrap = true,
31421
+ popoverMode = "nearby",
31422
+ popoverSpacing = popoverMode === "nearby" ? 5 : 0,
31423
+ viewportSpacing = 10,
31394
31424
  ...rest
31395
31425
  } = props;
31396
31426
  const debugFocus = useDebugFocus();
@@ -31429,8 +31459,13 @@ const SelectWithPopover = props => {
31429
31459
  });
31430
31460
  };
31431
31461
  const moveFocusToSelect = e => {
31462
+ if (e.type === "mousedown") {
31463
+ e.preventDefault();
31464
+ debugFocus(formatEventSideEffect(e, `preventDefault and move focus to select`));
31465
+ } else {
31466
+ debugFocus(formatEventSideEffect(e, `move focus to select`));
31467
+ }
31432
31468
  const select = ref.current;
31433
- debugFocus(`moveFocusToSelect("${e.type}")`);
31434
31469
  select.focus({
31435
31470
  preventScroll: true
31436
31471
  });
@@ -31440,6 +31475,7 @@ const SelectWithPopover = props => {
31440
31475
  "aria-haspopup": "listbox",
31441
31476
  "aria-expanded": expanded,
31442
31477
  "aria-controls": popoverId,
31478
+ "navi-popover-mode": popoverMode,
31443
31479
  onMouseDown: e => {
31444
31480
  if (e.button !== 0) {
31445
31481
  return;
@@ -31545,11 +31581,13 @@ const SelectWithPopover = props => {
31545
31581
  }
31546
31582
  },
31547
31583
  positionX: "left-aligned",
31548
- positionY: "below-overlap",
31584
+ positionY: popoverMode === "nearby" ? "below" : "below-overlap",
31585
+ spacing: popoverSpacing,
31586
+ viewportSpacing: viewportSpacing,
31549
31587
  scrollTrap: scrollTrap,
31550
31588
  pointerTrap: pointerTrap,
31551
31589
  focusTrap: focusTrap,
31552
- children: [jsx("div", {
31590
+ children: [popoverMode === "attached" ? jsx("div", {
31553
31591
  className: "navi_select_anchor_clone",
31554
31592
  onMouseDown: e => {
31555
31593
  if (e.button !== 0) {
@@ -31558,7 +31596,7 @@ const SelectWithPopover = props => {
31558
31596
  requestClose(e);
31559
31597
  },
31560
31598
  children: props.trigger
31561
- }), jsx(SelectRequestCloseContext.Provider, {
31599
+ }) : null, jsx(SelectRequestCloseContext.Provider, {
31562
31600
  value: requestClose,
31563
31601
  children: children
31564
31602
  })]
@@ -31607,8 +31645,14 @@ const SelectWithDialog = props => {
31607
31645
  });
31608
31646
  };
31609
31647
  const moveFocusToSelect = e => {
31610
- debugFocus(`moveFocusToSelect("${e.type}")`);
31611
- ref.current.focus({
31648
+ if (e.type === "mousedown") {
31649
+ e.preventDefault();
31650
+ debugFocus(formatEventSideEffect(e, `preventDefault and move focus to select`));
31651
+ } else {
31652
+ debugFocus(formatEventSideEffect(e, `move focus to select`));
31653
+ }
31654
+ const select = ref.current;
31655
+ select.focus({
31612
31656
  preventScroll: true
31613
31657
  });
31614
31658
  };
@@ -31826,9 +31870,11 @@ const applySearch = (searchText, value) => {
31826
31870
 
31827
31871
  // Multi-word OR: split on whitespace, any word matching contributes to the score.
31828
31872
  // Items where all words match rank higher than partial matches.
31829
- if (words.length < 2) {
31830
- return { match: false, matchScore: 0, matchRanges: [] };
31831
- }
31873
+ // Note: words always has at least 1 element here (searchText is non-empty and
31874
+ // foldedSearch.split filters empty strings). This path also handles the case
31875
+ // where searchText has trailing/leading spaces: the phrase match above tries
31876
+ // the literal (e.g. "tc " in "tc adapter"), and if that fails we fall through
31877
+ // here to try each word individually (e.g. "tc" matches "tca").
31832
31878
  const matchRanges = [];
31833
31879
  let matchedWordCount = 0;
31834
31880
  let anyWordAtStart = false;
@@ -31861,7 +31907,7 @@ const applySearch = (searchText, value) => {
31861
31907
  }
31862
31908
  }
31863
31909
  if (matchedWordCount === 0) {
31864
- return { match: false, matchScore: 0, matchRanges: [] };
31910
+ return tryAcronymMatch(foldedStr, str, searchText);
31865
31911
  }
31866
31912
  const wordRatio = matchedWordCount / words.length;
31867
31913
  let baseScore;
@@ -31903,8 +31949,53 @@ const SCORE_PHRASE_AT_START = 1;
31903
31949
  const SCORE_MULTI_WORD_AT_START = 0.75;
31904
31950
  const SCORE_AT_WORD_BOUNDARY = 0.625;
31905
31951
  const SCORE_MID_WORD = 0.5;
31952
+ const SCORE_ACRONYM = 0.4;
31906
31953
  const SCORE_BONUS_CASE_EXACT = 0.125;
31907
31954
 
31955
+ // Acronym match: each char of searchText (spaces stripped) must be the first
31956
+ // letter of a word in value, in order (greedy subsequence on word-starts).
31957
+ // e.g. "TC" matches "Total Count" highlighting the T and C.
31958
+ const tryAcronymMatch = (foldedStr, str, searchText) => {
31959
+ const acronymChars = foldAccents(searchText).toLowerCase().replace(/\s/g, "");
31960
+ if (acronymChars.length < 2) {
31961
+ // Single-char acronym is too ambiguous — skip.
31962
+ return { match: false, matchScore: 0, matchRanges: [] };
31963
+ }
31964
+ const wordStarts = [];
31965
+ for (let i = 0; i < foldedStr.length; i++) {
31966
+ if (isWordBoundary(foldedStr, i)) {
31967
+ wordStarts.push(i);
31968
+ }
31969
+ }
31970
+ const matchedPositions = [];
31971
+ let wordIdx = 0;
31972
+ const originalAcronym = searchText.replace(/\s/g, "");
31973
+ for (let si = 0; si < acronymChars.length; si++) {
31974
+ const ch = acronymChars[si];
31975
+ let found = false;
31976
+ while (wordIdx < wordStarts.length) {
31977
+ const pos = wordStarts[wordIdx];
31978
+ wordIdx++;
31979
+ if (foldedStr[pos] === ch) {
31980
+ matchedPositions.push(pos);
31981
+ found = true;
31982
+ break;
31983
+ }
31984
+ }
31985
+ if (!found) {
31986
+ return { match: false, matchScore: 0, matchRanges: [] };
31987
+ }
31988
+ }
31989
+ const atStart = matchedPositions[0] === 0;
31990
+ const caseExact = matchedPositions.every(
31991
+ (p, i) => str[p] === originalAcronym[i],
31992
+ );
31993
+ const baseScore = atStart ? SCORE_ACRONYM + 0.05 : SCORE_ACRONYM;
31994
+ const matchScore = baseScore + (caseExact ? SCORE_BONUS_CASE_EXACT : 0);
31995
+ const matchRanges = matchedPositions.map((p) => [p, p + 1]);
31996
+ return { match: true, matchScore, matchRanges };
31997
+ };
31998
+
31908
31999
  // LRU cache for pre-computed search info, avoids recomputing foldAccents/toLowerCase
31909
32000
  // for the same searchText across all items in a list render.
31910
32001
  const searchCache = new Map();