@jsenv/navi 0.26.3 → 0.26.4

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, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, 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, 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";
7
7
  export { contrastColor } from "@jsenv/dom";
8
8
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
9
9
  import { createValidity } from "@jsenv/validity";
@@ -2450,7 +2450,7 @@ const useRunOnMount = (action, Component) => {
2450
2450
 
2451
2451
  const DebugFocusContext = createContext(false);
2452
2452
  const DebugScrollContext = createContext(false);
2453
- const DebugPopoverContext = createContext(false);
2453
+ const DebugPopupContext = createContext(false);
2454
2454
  const debugNoop = () => {};
2455
2455
  const useDebugFocus = () => {
2456
2456
  const debug = useContext(DebugFocusContext);
@@ -2460,8 +2460,8 @@ const useDebugScroll = () => {
2460
2460
  const debug = useContext(DebugScrollContext);
2461
2461
  return debug || debugNoop;
2462
2462
  };
2463
- const useDebugPopover = () => {
2464
- const debug = useContext(DebugPopoverContext);
2463
+ const useDebugPopup = () => {
2464
+ const debug = useContext(DebugPopupContext);
2465
2465
  return debug || debugNoop;
2466
2466
  };
2467
2467
 
@@ -2471,14 +2471,14 @@ const useDebugPopover = () => {
2471
2471
  * Props:
2472
2472
  * debugFocus — log focus moves (autoFocus, restoring previous focus, etc.)
2473
2473
  * debugScroll — log virtual scroll window updates and scroll-to-item calls
2474
- * debugPopover — log popover open/close/positioning decisions
2474
+ * debugPopup — log popover open/close/positioning decisions
2475
2475
  *
2476
2476
  * Pass a boolean `true` to use `console.debug`, or pass a custom function.
2477
2477
  */
2478
2478
  const NaviDebug = ({
2479
2479
  debugFocus,
2480
2480
  debugScroll,
2481
- debugPopover,
2481
+ debugPopup,
2482
2482
  children
2483
2483
  }) => {
2484
2484
  if (debugFocus === true) {
@@ -2487,20 +2487,23 @@ const NaviDebug = ({
2487
2487
  if (debugScroll === true) {
2488
2488
  debugScroll = console.debug;
2489
2489
  }
2490
- if (debugPopover === true) {
2491
- debugPopover = console.debug;
2490
+ if (debugPopup === true) {
2491
+ debugPopup = console.debug;
2492
2492
  }
2493
2493
  return jsx(DebugFocusContext.Provider, {
2494
2494
  value: debugFocus,
2495
2495
  children: jsx(DebugScrollContext.Provider, {
2496
2496
  value: debugScroll,
2497
- children: jsx(DebugPopoverContext.Provider, {
2498
- value: debugPopover,
2497
+ children: jsx(DebugPopupContext.Provider, {
2498
+ value: debugPopup,
2499
2499
  children: children
2500
2500
  })
2501
2501
  })
2502
2502
  });
2503
2503
  };
2504
+ const formatEventSideEffect = (e, sideEffect) => {
2505
+ return `"${e.type}" on ${getElementSignature(e.target)} -> ${sideEffect}`;
2506
+ };
2504
2507
 
2505
2508
  const addIntoArray = (array, ...valuesToAdd) => {
2506
2509
  if (valuesToAdd.length === 1) {
@@ -7647,6 +7650,11 @@ definePseudoClass(":active", {
7647
7650
  if (isControlledByFocusedElement(el)) {
7648
7651
  return true;
7649
7652
  }
7653
+ if (el.contains(document.activeElement)) {
7654
+ // for some reason :focus-within sometimes is false while focus is within...
7655
+ // (popover with chrome for some reason)
7656
+ return true;
7657
+ }
7650
7658
  return false;
7651
7659
  },
7652
7660
  });
@@ -16599,14 +16607,22 @@ const openCallout = (message, {
16599
16607
  }
16600
16608
  allowWheelThrough(calloutElement, anchorElement);
16601
16609
  anchorElement.setAttribute("data-callout", calloutId);
16602
- dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_open", {
16603
- bubbles: true
16604
- }));
16605
16610
  addTeardown(() => {
16606
16611
  anchorElement.removeAttribute("data-callout");
16607
- dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_close", {
16608
- bubbles: true
16609
- }));
16612
+ });
16613
+ const visualElement = (() => {
16614
+ const visualSelector = anchorElement.getAttribute("data-visual-selector");
16615
+ if (visualSelector) {
16616
+ const visualElement = anchorElement.querySelector(visualSelector);
16617
+ if (visualElement) {
16618
+ return visualElement;
16619
+ }
16620
+ }
16621
+ return anchorElement;
16622
+ })();
16623
+ dispatchPublicCustomEvent(visualElement, "navi_callout_open");
16624
+ addTeardown(() => {
16625
+ dispatchPublicCustomEvent(visualElement, "navi_callout_close");
16610
16626
  });
16611
16627
  addStatusEffect(status => {
16612
16628
  if (!status) {
@@ -16869,7 +16885,7 @@ const stickCalloutToAnchor = (calloutElement, anchorElement) => {
16869
16885
  }) => {
16870
16886
  const calloutElementClone = cloneCalloutToMeasureNaturalSize(calloutElement);
16871
16887
  const {
16872
- position,
16888
+ positionY,
16873
16889
  left: calloutLeft,
16874
16890
  top: calloutTop,
16875
16891
  width: calloutWidth,
@@ -16877,20 +16893,19 @@ const stickCalloutToAnchor = (calloutElement, anchorElement) => {
16877
16893
  spaceAbove,
16878
16894
  spaceBelow
16879
16895
  } = pickPositionRelativeTo(calloutElementClone, anchorElement, {
16880
- alignToViewportEdgeWhenTargetNearEdge: 20,
16881
- // when fully to the left, the border color is collé to the browser window making it hard to see
16896
+ alignToViewportEdgeWhenAnchorNearEdge: 20,
16882
16897
  minLeft: 1,
16883
- // Check for preferred and forced position from anchor element
16884
- positionTry: anchorElement.getAttribute("data-callout-position-try") || "bottom",
16885
- position: anchorElement.getAttribute("data-callout-position")
16898
+ positionX: "center",
16899
+ positionY: anchorElement.getAttribute("data-callout-position") || "below",
16900
+ positionYFixed: anchorElement.getAttribute("data-callout-position-fixed")
16886
16901
  });
16887
- // data-position-current is written to the clone by pickPositionRelativeTo,
16902
+ // data-position-y-current is written to the clone by pickPositionRelativeTo,
16888
16903
  // copy it back to the real element so stickiness works on next call
16889
- const positionCurrent = calloutElementClone.getAttribute("data-position-current");
16890
- if (positionCurrent) {
16891
- calloutElement.setAttribute("data-position-current", positionCurrent);
16904
+ const positionYCurrent = calloutElementClone.getAttribute("data-position-y-current");
16905
+ if (positionYCurrent) {
16906
+ calloutElement.setAttribute("data-position-y-current", positionYCurrent);
16892
16907
  } else {
16893
- calloutElement.removeAttribute("data-position-current");
16908
+ calloutElement.removeAttribute("data-position-y-current");
16894
16909
  }
16895
16910
  calloutElementClone.remove();
16896
16911
 
@@ -16932,7 +16947,7 @@ const stickCalloutToAnchor = (calloutElement, anchorElement) => {
16932
16947
 
16933
16948
  // Force content overflow when there is not enough space to display
16934
16949
  // the entirety of the callout
16935
- const spaceAvailable = position === "bottom" ? spaceBelow : spaceAbove;
16950
+ const spaceAvailable = positionY === "above" || positionY === "above-overlap" ? spaceAbove : spaceBelow;
16936
16951
  const paddingSizes = getPaddingSizes(calloutBodyElement);
16937
16952
  const paddingY = paddingSizes.top + paddingSizes.bottom;
16938
16953
  const spaceNeededAroundContent = ARROW_HEIGHT + BORDER_WIDTH * 2 + paddingY;
@@ -16951,7 +16966,7 @@ const stickCalloutToAnchor = (calloutElement, anchorElement) => {
16951
16966
  width,
16952
16967
  height
16953
16968
  } = calloutElement.getBoundingClientRect();
16954
- if (position === "top") {
16969
+ if (positionY === "above" || positionY === "above-overlap") {
16955
16970
  // Position above target element
16956
16971
  calloutBoxElement.style.marginTop = "";
16957
16972
  calloutBoxElement.style.marginBottom = `${ARROW_HEIGHT}px`;
@@ -17238,21 +17253,6 @@ const generateSvgWithoutArrow = (width, height) => {
17238
17253
  />
17239
17254
  </svg>`;
17240
17255
  };
17241
- const dispatchCalloutCustomElement = (anchorElement, customEvent) => {
17242
- let targetElement;
17243
- const visualSelector = anchorElement.getAttribute("data-visual-selector");
17244
- if (visualSelector) {
17245
- const visualElement = anchorElement.querySelector(visualSelector);
17246
- if (visualElement) {
17247
- targetElement = visualElement;
17248
- }
17249
- } else {
17250
- targetElement = anchorElement;
17251
- }
17252
-
17253
- // console.log("dispatch on", targetElement, "event", customEvent);
17254
- targetElement.dispatchEvent(customEvent);
17255
- };
17256
17256
 
17257
17257
  /**
17258
17258
  * Creates a live mirror of a source DOM element that automatically stays in sync.
@@ -18709,6 +18709,9 @@ const installCustomConstraintValidation = (
18709
18709
  return element;
18710
18710
  })();
18711
18711
  const onmousedown = (e) => {
18712
+ if (e.button !== 0) {
18713
+ return;
18714
+ }
18712
18715
  if (!validationInterface.validationMessage) {
18713
18716
  return;
18714
18717
  }
@@ -22514,9 +22517,9 @@ const useUIState = (uiStateController) => {
22514
22517
  installImportMetaCssBuild(import.meta);const css$x = /* css */`
22515
22518
  @layer navi {
22516
22519
  .navi_button {
22520
+ --button-border-radius: 2px;
22517
22521
  --button-outline-width: 1px;
22518
22522
  --button-border-width: 1px;
22519
- --button-border-radius: 2px;
22520
22523
  /* Global padding defaults — override these to change all button paddings. */
22521
22524
  /* Use --button-padding, --button-padding-x, --button-padding-y for per-button overrides. */
22522
22525
  --button-padding-x-default: 6px;
@@ -22580,25 +22583,22 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
22580
22583
  }
22581
22584
 
22582
22585
  .navi_button {
22583
- /* Internal vars prefixed with --x- to signal they are private, do not use from outside */
22584
- --x-button-outline-width: var(--button-outline-width);
22585
- --x-button-border-radius: var(--button-border-radius);
22586
- --x-button-border-width: var(--button-border-width);
22587
- --x-button-outer-width: calc(
22588
- var(--x-button-border-width) + var(--x-button-outline-width)
22589
- );
22590
- --x-button-outline-color: var(--button-outline-color);
22586
+ /* outline will draw the border when visible */
22587
+ --x-button-outline-width: var(--button-outline-width) +
22588
+ var(--button-border-width);
22589
+ --x-button-outline-offset: calc(-1 * var(--button-border-width));
22591
22590
  --x-button-border-color: var(--button-border-color);
22592
22591
  --x-button-background: var(--button-background);
22593
22592
  --x-button-background-color: var(--button-background-color);
22594
22593
  --x-button-color: var(--button-color);
22595
22594
  --x-button-cursor: var(--button-cursor);
22595
+
22596
22596
  box-sizing: border-box;
22597
22597
  aspect-ratio: inherit;
22598
22598
  padding: 0;
22599
22599
  background: none;
22600
22600
  border: none;
22601
- border-radius: var(--x-button-border-radius);
22601
+ border-radius: var(--button-border-radius);
22602
22602
  outline: none;
22603
22603
  cursor: var(--x-button-cursor);
22604
22604
  -webkit-tap-highlight-color: transparent;
@@ -22658,15 +22658,13 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
22658
22658
  --x-button-background-color,
22659
22659
  var(--x-button-background)
22660
22660
  );
22661
-
22662
- border-width: var(--x-button-outer-width);
22661
+ border-width: var(--button-border-width);
22663
22662
  border-style: solid;
22664
- border-color: transparent;
22665
- border-radius: var(--x-button-border-radius);
22666
- outline-width: var(--x-button-border-width);
22667
- outline-style: solid;
22668
- outline-color: var(--x-button-border-color);
22669
- outline-offset: calc(-1 * (var(--x-button-border-width)));
22663
+ border-color: var(--x-button-border-color);
22664
+ border-radius: var(--button-border-radius);
22665
+ outline-width: var(--x-button-outline-width);
22666
+ outline-color: var(--button-outline-color);
22667
+ outline-offset: var(--x-button-outline-offset);
22670
22668
  transition-property: transform;
22671
22669
  transition-duration: 0.15s;
22672
22670
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -22727,12 +22725,10 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
22727
22725
  }
22728
22726
  /* Focus */
22729
22727
  &[data-focus-visible] {
22730
- --x-button-border-color: var(--x-button-outline-color);
22731
- }
22732
- &[data-focus-visible] {
22728
+ --x-button-border-color: transparent;
22729
+
22733
22730
  .navi_button_content {
22734
- outline-width: var(--x-button-outer-width);
22735
- outline-offset: calc(-1 * var(--x-button-outer-width));
22731
+ outline-style: solid;
22736
22732
  }
22737
22733
  }
22738
22734
  /* Disabled */
@@ -28152,7 +28148,6 @@ const ListWithPopover = props => {
28152
28148
  left,
28153
28149
  top
28154
28150
  } = pickPositionRelativeTo(listContainerEl, anchor, {
28155
- positionTry: "bottom",
28156
28151
  minLeft
28157
28152
  });
28158
28153
  listContainerEl.style.top = `${top}px`;
@@ -28942,7 +28937,6 @@ const css$k = /* css */`
28942
28937
  --border-radius: 2px;
28943
28938
  --border-width: 1px;
28944
28939
  --outline-width: 1px;
28945
- --outer-width: calc(var(--border-width) + var(--outline-width));
28946
28940
  --font-size: 14px;
28947
28941
 
28948
28942
  /* Default */
@@ -28986,6 +28980,16 @@ const css$k = /* css */`
28986
28980
  }
28987
28981
 
28988
28982
  .navi_input {
28983
+ /* outline will draw the border when visible */
28984
+ --x-outline-width: var(--outline-width) + var(--border-width);
28985
+ --x-outline-offset: calc(-1 * var(--border-width));
28986
+ --left-slot-size: 0px;
28987
+ --right-slot-size: 0px;
28988
+ --x-border-color: var(--border-color);
28989
+ --x-background-color: var(--background-color);
28990
+ --x-color: var(--color);
28991
+ --x-placeholder-color: var(--placeholder-color);
28992
+
28989
28993
  position: relative;
28990
28994
  box-sizing: border-box;
28991
28995
  width: fit-content;
@@ -28994,18 +28998,6 @@ const css$k = /* css */`
28994
28998
  border-radius: inherit;
28995
28999
  cursor: inherit;
28996
29000
 
28997
- --left-slot-size: 0px;
28998
- --right-slot-size: 0px;
28999
- --x-outline-width: var(--outline-width);
29000
- --x-border-radius: var(--border-radius);
29001
- --x-border-width: var(--border-width);
29002
- --x-outer-width: calc(var(--x-border-width) + var(--x-outline-width));
29003
- --x-outline-color: var(--outline-color);
29004
- --x-border-color: var(--border-color);
29005
- --x-background-color: var(--background-color);
29006
- --x-color: var(--color);
29007
- --x-placeholder-color: var(--placeholder-color);
29008
-
29009
29001
  --x-padding-top-base: var(
29010
29002
  --padding-top,
29011
29003
  var(--padding-y, var(--padding, 1px))
@@ -29033,15 +29025,13 @@ const css$k = /* css */`
29033
29025
  color: var(--x-color);
29034
29026
  font-size: var(--font-size);
29035
29027
  background-color: var(--x-background-color);
29036
- border-width: var(--x-outer-width);
29037
- border-width: var(--x-outer-width);
29028
+ border-width: var(--border-width);
29038
29029
  border-style: solid;
29039
- border-color: transparent;
29040
- border-radius: var(--x-border-radius);
29041
- outline-width: var(--x-border-width);
29042
- outline-style: solid;
29043
- outline-color: var(--x-border-color);
29044
- outline-offset: calc(-1 * (var(--x-border-width)));
29030
+ border-color: var(--x-border-color);
29031
+ border-radius: var(--border-radius);
29032
+ outline-width: var(--x-outline-width);
29033
+ outline-color: var(--outline-color);
29034
+ outline-offset: var(--x-outline-offset);
29045
29035
 
29046
29036
  &[type="search"] {
29047
29037
  -webkit-appearance: textfield;
@@ -29121,12 +29111,10 @@ const css$k = /* css */`
29121
29111
  &[data-focus],
29122
29112
  &[data-focus-visible] {
29123
29113
  --x-background-color: var(--background-color-focus);
29124
- --x-border-color: var(--border-color-focus);
29114
+ --x-border-color: transparent;
29125
29115
 
29126
29116
  .navi_native_input {
29127
- outline-width: var(--x-outer-width);
29128
- outline-offset: calc(-1 * var(--x-outer-width));
29129
- --x-border-color: var(--x-outline-color);
29117
+ outline-style: solid;
29130
29118
  }
29131
29119
  }
29132
29120
  /* Disabled */
@@ -29421,7 +29409,13 @@ const InputSlot = ({
29421
29409
  flex: true,
29422
29410
  alignY: "center",
29423
29411
  onMouseDown: e => {
29424
- e.preventDefault(); // keep focus in the input
29412
+ // Only prevent focus from leaving when the input already has focus.
29413
+ // If the input is not focused, let the mousedown proceed normally so
29414
+ // the slot element (e.g. a clear button) can receive focus itself.
29415
+ const inputEl = document.getElementById(id);
29416
+ if (inputEl && inputEl === document.activeElement) {
29417
+ e.preventDefault();
29418
+ }
29425
29419
  },
29426
29420
  onClick: e => {
29427
29421
  if (readOnly || disabled) {
@@ -30609,12 +30603,12 @@ const Dialog = props => {
30609
30603
  } = props;
30610
30604
  const defaultRef = useRef();
30611
30605
  const ref = rest.ref || defaultRef;
30612
- const debugPopover = useDebugPopover();
30606
+ const debugPopup = useDebugPopup();
30613
30607
  const debugFocus = useDebugFocus();
30614
30608
  const openedRef = useRef(false);
30615
30609
  const [addCleanup, cleanup] = useCleanup();
30616
30610
  const open = e => {
30617
- debugPopover(`openDialog("${e.type}")`);
30611
+ debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
30618
30612
  const dialogEl = ref.current;
30619
30613
  dialogEl.showModal();
30620
30614
  const firstFocusable = findFocusable(dialogEl);
@@ -30633,7 +30627,7 @@ const Dialog = props => {
30633
30627
  });
30634
30628
  };
30635
30629
  const close = e => {
30636
- debugPopover(`closeDialog("${e.type}")`);
30630
+ debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
30637
30631
  const dialogEl = ref.current;
30638
30632
  dialogEl.close();
30639
30633
  cleanup();
@@ -30733,14 +30727,17 @@ const Popover = props => {
30733
30727
  pointerTrap,
30734
30728
  focusTrap,
30735
30729
  children,
30736
- positionTry = "bottom",
30730
+ positionX,
30731
+ positionY,
30732
+ positionXFixed,
30733
+ positionYFixed,
30737
30734
  ...rest
30738
30735
  } = props;
30739
30736
  const defaultRef = useRef();
30740
30737
  const ref = rest.ref || defaultRef;
30741
30738
  const defaultId = useId();
30742
30739
  const id = rest.id || defaultId;
30743
- const debugPopover = useDebugPopover();
30740
+ const debugPopup = useDebugPopup();
30744
30741
  const debugFocus = useDebugFocus();
30745
30742
  const [opened, setOpened] = useState(false);
30746
30743
  const openedRef = useRef(opened);
@@ -30749,7 +30746,7 @@ const Popover = props => {
30749
30746
  const open = (e, {
30750
30747
  anchor
30751
30748
  }) => {
30752
- debugPopover(`openPopover("${e.type}")`);
30749
+ debugPopup(`openPopover("${e.type}")`);
30753
30750
  const popoverEl = ref.current;
30754
30751
  popoverEl.showPopover();
30755
30752
  const firstFocusable = findFocusable(popoverEl);
@@ -30761,27 +30758,27 @@ const Popover = props => {
30761
30758
  }
30762
30759
  const effectiveAnchor = anchor || document.documentElement;
30763
30760
  const positionPopover = positionEvent => {
30764
- debugPopover(`positionPopover("${positionEvent.type}")`);
30765
- popoverEl.style.setProperty("--anchor-width", `${effectiveAnchor.getBoundingClientRect().width}px`);
30761
+ const {
30762
+ width,
30763
+ height
30764
+ } = effectiveAnchor.getBoundingClientRect();
30765
+ popoverEl.style.setProperty("--anchor-width", `${width}px`);
30766
+ popoverEl.style.setProperty("--anchor-height", `${height}px`);
30766
30767
  const minLeft = 1;
30767
- const effectivePositionTry = anchor ? positionTry : "center";
30768
+ const effectivePositionX = anchor ? positionX : "center";
30768
30769
  const {
30769
30770
  left,
30770
30771
  top
30771
30772
  } = pickPositionRelativeTo(popoverEl, effectiveAnchor, {
30772
- positionTry: effectivePositionTry,
30773
+ positionX: effectivePositionX,
30774
+ positionY,
30775
+ positionXFixed,
30776
+ positionYFixed,
30773
30777
  minLeft
30774
30778
  });
30779
+ debugPopup(`positionPopover("${positionEvent.type}") -> left: ${left}, top: ${top}`);
30775
30780
  popoverEl.style.top = `${top}px`;
30776
- const popoverRect = popoverEl.getBoundingClientRect();
30777
- const maxWidth = parseFloat(getComputedStyle(popoverEl).maxWidth);
30778
- if (!isNaN(maxWidth) && popoverRect.width >= maxWidth - 1) {
30779
- const viewportWidth = document.documentElement.clientWidth;
30780
- const centeredLeft = (viewportWidth - popoverRect.width) / 2;
30781
- popoverEl.style.left = `${Math.max(centeredLeft, minLeft)}px`;
30782
- } else {
30783
- popoverEl.style.left = `${Math.max(left, minLeft)}px`;
30784
- }
30781
+ popoverEl.style.left = `${Math.max(left, minLeft)}px`;
30785
30782
  };
30786
30783
  if (scrollTrap) {
30787
30784
  addCleanup(trapScrollInside(popoverEl));
@@ -30813,7 +30810,7 @@ const Popover = props => {
30813
30810
  });
30814
30811
  };
30815
30812
  const close = e => {
30816
- debugPopover(`closePopover("${e.type}")`);
30813
+ debugPopup(`closePopover("${e.type}")`);
30817
30814
  const popoverEl = ref.current;
30818
30815
  popoverEl.hidePopover();
30819
30816
  cleanup();
@@ -30867,6 +30864,7 @@ const Popover = props => {
30867
30864
  ...rest,
30868
30865
  ref: ref,
30869
30866
  baseClassName: "navi_popover",
30867
+ pseudoClasses: PopoverPseudoClasses,
30870
30868
  onnavi_popover_request_open: e => {
30871
30869
  const {
30872
30870
  event = e,
@@ -30886,6 +30884,7 @@ const Popover = props => {
30886
30884
  })]
30887
30885
  });
30888
30886
  };
30887
+ const PopoverPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled"];
30889
30888
  const requestPopoverOpen = (popoverElement, {
30890
30889
  event,
30891
30890
  anchor
@@ -30907,11 +30906,12 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
30907
30906
  @layer navi {
30908
30907
  .navi_select {
30909
30908
  --select-border-radius: 2px;
30910
- --select-border-width: 1px;
30911
30909
  --select-outline-width: 1px;
30910
+ --select-border-width: 1px;
30912
30911
  --select-font-size: 14px;
30913
30912
  --select-padding-x-default: 8px;
30914
30913
  --select-padding-y-default: 5px;
30914
+ --select-outline-color: var(--navi-focus-outline-color);
30915
30915
  --select-border-color: light-dark(#767676, #8e8e93);
30916
30916
  --select-background-color: white;
30917
30917
  --select-color: currentColor;
@@ -30934,60 +30934,66 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
30934
30934
  }
30935
30935
 
30936
30936
  .navi_select {
30937
- position: relative;
30938
- box-sizing: border-box;
30939
- padding-top: var(
30937
+ --x-select-background-color: var(--select-background-color);
30938
+ --x-select-border-color: var(--select-border-color);
30939
+ /* outline will draw the border when visible */
30940
+ --x-select-outline-width: calc(
30941
+ var(--select-outline-width) + var(--select-border-width)
30942
+ );
30943
+ --x-select-outline-offset: calc(-1 * var(--select-border-width));
30944
+ --x-select-padding-top: var(
30940
30945
  --select-padding-top,
30941
30946
  var(--select-padding-y, var(--select-padding-y-default))
30942
30947
  );
30943
- padding-right: var(
30948
+ --x-select-padding-right: var(
30944
30949
  --select-padding-right,
30945
30950
  var(--select-padding-x, var(--select-padding-x-default))
30946
30951
  );
30947
- padding-bottom: var(
30948
- --select-padding-bottom,
30949
- var(--select-padding-y, var(--select-padding-y-default))
30950
- );
30951
- padding-left: var(
30952
+ --x-select-padding-left: var(
30952
30953
  --select-padding-left,
30953
30954
  var(--select-padding-x, var(--select-padding-x-default))
30954
30955
  );
30956
+ --x-select-padding-bottom: var(
30957
+ --select-padding-bottom,
30958
+ var(--select-padding-y, var(--select-padding-y-default))
30959
+ );
30960
+
30961
+ position: relative;
30962
+ box-sizing: border-box;
30963
+ padding-top: var(--x-select-padding-top);
30964
+ padding-right: var(--x-select-padding-right);
30965
+ padding-bottom: var(--x-select-padding-bottom);
30966
+ padding-left: var(--x-select-padding-left);
30955
30967
  color: var(--select-color);
30956
30968
  font-size: var(--select-font-size);
30957
30969
  text-align: inherit; /* override browser defaults on button which is center */
30958
30970
  white-space: nowrap; /* Prevent icon from going next line */
30959
- background-color: var(--select-background-color);
30960
- border: var(--select-border-width) solid transparent;
30971
+ background-color: var(--x-select-background-color);
30972
+ border-width: var(--select-border-width);
30973
+ border-style: solid;
30974
+ border-color: var(--x-select-border-color);
30961
30975
  border-radius: var(--select-border-radius);
30962
- outline: var(--select-outline-width) solid var(--select-border-color);
30963
- outline-offset: calc(-1 * var(--select-outline-width));
30976
+ outline-width: var(--x-select-outline-width);
30977
+ outline-color: var(--select-outline-color);
30978
+ outline-offset: var(--x-select-outline-offset);
30964
30979
  user-select: none;
30965
30980
 
30966
- --x-select-outline-width-focus-visible: calc(
30967
- var(--select-border-width) + var(--select-outline-width)
30968
- );
30969
- --x-select-outline-offset-focus-visible: calc(
30970
- -1 * (var(--select-border-width) + var(--select-outline-width))
30971
- );
30972
-
30973
30981
  &[data-hover] {
30974
- background-color: var(--select-background-color-hover);
30975
- outline-color: var(--select-border-color-hover);
30982
+ --x-select-background-color: var(--select-background-color-hover);
30983
+ --x-select-border-color: var(--select-border-color-hover);
30976
30984
  }
30977
30985
 
30978
30986
  &[data-focus-visible] {
30979
- outline-width: var(--x-select-outline-width-focus-visible);
30980
- outline-color: var(--navi-focus-outline-color);
30981
- outline-offset: var(--x-select-outline-offset-focus-visible);
30987
+ --x-select-border-color: transparent;
30988
+ outline-style: solid;
30982
30989
  }
30983
30990
 
30984
30991
  &[data-disabled] {
30985
30992
  opacity: 0.5;
30986
30993
  cursor: default;
30987
30994
  }
30988
-
30989
- .navi_list_container {
30990
- --list-border-radius: 0;
30995
+ &[data-callout] {
30996
+ --x-select-border-color: var(--callout-color);
30991
30997
  }
30992
30998
 
30993
30999
  .navi_select_trigger_text {
@@ -31015,19 +31021,21 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31015
31021
  }
31016
31022
  }
31017
31023
  .navi_select_trigger_icon {
31018
- margin-left: 6px;
31019
31024
  flex-shrink: 0;
31020
31025
  opacity: 0.6;
31021
31026
  }
31022
31027
 
31023
31028
  /* popover */
31024
31029
  &[aria-haspopup="listbox"] {
31025
- &:has(.navi_list_container[data-focus-visible]) {
31026
- outline-width: var(--x-select-outline-width-focus-visible);
31027
- outline-color: var(--navi-focus-outline-color);
31028
- outline-offset: var(--x-select-outline-offset-focus-visible);
31029
- .navi_list_container {
31030
- outline: none;
31030
+ .navi_list_container {
31031
+ width: 100%;
31032
+ /* Handled by the popover */
31033
+ border: none;
31034
+ border-radius: 0;
31035
+ outline: none;
31036
+
31037
+ .navi_list {
31038
+ width: 100%;
31031
31039
  }
31032
31040
  }
31033
31041
 
@@ -31036,18 +31044,79 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31036
31044
  inset: unset;
31037
31045
  min-width: var(--anchor-width, 0px);
31038
31046
  max-width: 95vw;
31047
+ /* max-height covers the placeholder + list; the list scrolls internally */
31039
31048
  max-height: 95dvh;
31040
31049
  margin: 0;
31041
31050
  padding: 0;
31042
- background: white;
31043
- border: none;
31044
- border-radius: 0;
31051
+ background: var(--select-background-color);
31052
+ border-width: var(--select-border-width);
31053
+ border-style: solid;
31054
+ border-color: var(--x-select-border-color);
31055
+ border-radius: var(--select-border-radius);
31056
+ outline-width: var(--x-select-outline-width);
31057
+ outline-color: var(--select-outline-color);
31058
+ outline-offset: var(--x-select-outline-offset);
31045
31059
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
31046
31060
  cursor: default; /* Reset pointer cursor within the select */
31047
- overflow: auto;
31061
+ overflow: hidden;
31048
31062
  overscroll-behavior: none;
31049
31063
 
31050
- &:popover-open {
31064
+ /* The anchor placeholder is a non-interactive visual clone of the
31065
+ trigger. It makes the popover wrap both the trigger area and the list
31066
+ under a single border/shadow. CSS order places it before the list
31067
+ when the popover is below the trigger, and after when above. */
31068
+ .navi_select_anchor_clone {
31069
+ /* Mirror the trigger's padding so the clone looks identical */
31070
+ padding-top: var(--x-select-padding-top);
31071
+ padding-right: var(--x-select-padding-right);
31072
+ padding-bottom: var(--x-select-padding-bottom);
31073
+ padding-left: var(--x-select-padding-left);
31074
+ flex-shrink: 0;
31075
+ order: -1; /* before the list — popover is below the trigger */
31076
+ background: var(--x-select-background-color);
31077
+ border-bottom: var(--select-border-width) solid
31078
+ var(--x-select-border-color);
31079
+
31080
+ &:hover {
31081
+ --x-select-background-color: var(--select-background-color-hover);
31082
+ --x-select-border-color: var(--select-border-color-hover);
31083
+ }
31084
+ }
31085
+
31086
+ &[data-position-y-current="above"],
31087
+ &[data-position-y-current="above-overlap"] {
31088
+ .navi_select_anchor_clone {
31089
+ order: 1; /* after the list — popover is above the trigger */
31090
+ border-top: var(--select-border-width) solid
31091
+ var(--x-select-border-color);
31092
+ border-bottom: none;
31093
+ }
31094
+ }
31095
+
31096
+ /* The list scrolls inside the popover */
31097
+ .navi_list_container {
31098
+ overflow: auto;
31099
+ overscroll-behavior: none;
31100
+ }
31101
+ }
31102
+
31103
+ &:has([data-hover]) {
31104
+ .navi_select_popover {
31105
+ --x-select-border-color: var(--select-border-color-hover);
31106
+ }
31107
+ }
31108
+ &:has([data-focus-visible]) {
31109
+ .navi_select_popover {
31110
+ outline-style: solid;
31111
+ }
31112
+ }
31113
+
31114
+ &[aria-expanded="true"] {
31115
+ border-top-color: var(--select-border-color);
31116
+ border-top-left-radius: 0;
31117
+ border-top-right-radius: 0;
31118
+
31119
+ .navi_select_popover {
31051
31120
  display: flex;
31052
31121
  flex-direction: column;
31053
31122
  }
@@ -31057,17 +31126,15 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31057
31126
  /* dialog */
31058
31127
  &[aria-haspopup="dialog"] {
31059
31128
  .navi_list_container {
31129
+ width: 100%;
31060
31130
  --list-max-height: none;
31061
- }
31131
+ /* Handled by the dialog */
31132
+ border: none;
31133
+ border-radius: 0;
31134
+ outline: none;
31062
31135
 
31063
- /* When the list inside the dialog has keyboard focus, show the focus ring
31064
- on the dialog instead */
31065
- &:has(.navi_list_container[data-focus-visible]) {
31066
- outline-width: var(--x-select-outline-width-focus-visible);
31067
- outline-color: var(--navi-focus-outline-color);
31068
- outline-offset: var(--x-select-outline-offset-focus-visible);
31069
- .navi_list_container {
31070
- outline: none;
31136
+ .navi_list {
31137
+ width: 100%;
31071
31138
  }
31072
31139
  }
31073
31140
 
@@ -31075,9 +31142,12 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31075
31142
  max-height: 95dvh;
31076
31143
  margin: auto;
31077
31144
  padding: 0;
31078
- background: white;
31079
- border: none;
31080
- border-radius: 8px;
31145
+ background: var(--select-background-color);
31146
+ border: var(--select-border-width) solid var(--x-select-border-color);
31147
+ border-radius: var(--select-border-radius);
31148
+ outline-width: var(--x-select-outline-width);
31149
+ outline-color: var(--select-outline-color);
31150
+ outline-offset: var(--x-select-outline-offset);
31081
31151
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
31082
31152
  cursor: default; /* Reset pointer cursor within the select */
31083
31153
 
@@ -31090,6 +31160,14 @@ installImportMetaCssBuild(import.meta);const css$f = /* css */`
31090
31160
  background: rgba(0, 0, 0, 0.4);
31091
31161
  }
31092
31162
  }
31163
+
31164
+ /* When the list inside the dialog has keyboard focus, show the focus ring
31165
+ on the dialog instead */
31166
+ &:has([data-focus-visible]) {
31167
+ .navi_select_dialog {
31168
+ outline-style: solid;
31169
+ }
31170
+ }
31093
31171
  }
31094
31172
  }
31095
31173
  `;
@@ -31133,6 +31211,7 @@ const Select = props => {
31133
31211
  return jsx(ParentUIStateControllerContext.Provider, {
31134
31212
  value: uiStateController,
31135
31213
  children: jsx(SelectDispatcher, {
31214
+ trigger: jsx(SelectTrigger, {}),
31136
31215
  ...props,
31137
31216
  ref: ref,
31138
31217
  value: value
@@ -31161,7 +31240,7 @@ const SelectDispatcher = props => {
31161
31240
  };
31162
31241
  const SelectUI = props => {
31163
31242
  import.meta.css = [css$f, "@jsenv/navi/src/field/select.jsx"];
31164
- let {
31243
+ const {
31165
31244
  placeholder = "Select…",
31166
31245
  trigger,
31167
31246
  name,
@@ -31192,9 +31271,6 @@ const SelectUI = props => {
31192
31271
  useAutoFocus(ref, autoFocus, {
31193
31272
  preventScroll: autoFocusPreventScroll
31194
31273
  });
31195
- if (trigger === undefined) {
31196
- trigger = jsx(SelectTrigger, {});
31197
- }
31198
31274
  return jsxs(Box, {
31199
31275
  as: "button",
31200
31276
  type: "button",
@@ -31237,6 +31313,7 @@ const SelectUI = props => {
31237
31313
  const SelectPlaceholderContext = createContext();
31238
31314
  const SelectValueContext = createContext(null);
31239
31315
  const SelectStyleCSSVars = {
31316
+ "outlineWidth": "--select-outline-width",
31240
31317
  "borderWidth": "--select-border-width",
31241
31318
  "borderRadius": "--select-border-radius",
31242
31319
  "paddingX": "--select-padding-x",
@@ -31274,14 +31351,16 @@ const SelectStyleCSSVars = {
31274
31351
  color: "--select-color-disabled"
31275
31352
  }
31276
31353
  };
31277
- const SelectPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-expanded"];
31354
+ const SelectPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-loading", ":-navi-expanded"];
31278
31355
  const SelectPseudoElements = ["::-navi-loader"];
31279
31356
  const SelectTrigger = () => {
31280
31357
  const placeholder = useContext(SelectPlaceholderContext);
31281
31358
  const value = useContext(SelectValueContext);
31282
31359
  const hasValue = value !== null && value !== undefined && value !== "";
31283
31360
  const isPlaceholder = !hasValue;
31284
- return jsxs(Fragment, {
31361
+ return jsxs(Box, {
31362
+ flex: true,
31363
+ spacing: "s",
31285
31364
  children: [jsxs("span", {
31286
31365
  className: "navi_select_trigger_text",
31287
31366
  children: [jsx("span", {
@@ -31307,13 +31386,13 @@ const SelectWithPopover = props => {
31307
31386
  disabled,
31308
31387
  onKeyDown,
31309
31388
  children,
31310
- positionTry,
31311
31389
  pointerTrap,
31312
31390
  scrollTrap = true,
31313
31391
  focusTrap = true,
31314
31392
  ...rest
31315
31393
  } = props;
31316
31394
  const debugFocus = useDebugFocus();
31395
+ const debugPopup = useDebugPopup();
31317
31396
  const popoverRef = useRef(null);
31318
31397
  const popoverId = useId();
31319
31398
  const [expanded, setExpanded] = useState(false);
@@ -31328,12 +31407,21 @@ const SelectWithPopover = props => {
31328
31407
  setExpanded(false);
31329
31408
  };
31330
31409
  const requestOpen = e => {
31410
+ // scroll select into view when opening it
31411
+ ref.current.scrollIntoView({
31412
+ block: "nearest"
31413
+ });
31331
31414
  return requestPopoverOpen(popoverRef.current, {
31332
31415
  event: e,
31333
31416
  anchor: ref.current
31334
31417
  });
31335
31418
  };
31419
+ const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
31336
31420
  const requestClose = (e = new CustomEvent("programmatic")) => {
31421
+ if (e.type === "mousedown") {
31422
+ debugPopup(formatEventSideEffect(e, `disable click`));
31423
+ disableClickFor(e);
31424
+ }
31337
31425
  return requestPopoverClose(popoverRef.current, {
31338
31426
  event: e
31339
31427
  });
@@ -31345,7 +31433,6 @@ const SelectWithPopover = props => {
31345
31433
  preventScroll: true
31346
31434
  });
31347
31435
  };
31348
- const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
31349
31436
  return jsx(SelectDispatcher, {
31350
31437
  disabled: disabled,
31351
31438
  "aria-haspopup": "listbox",
@@ -31372,6 +31459,7 @@ const SelectWithPopover = props => {
31372
31459
  return;
31373
31460
  }
31374
31461
  if (shouldIgnoreThatClick) {
31462
+ debugPopup(formatEventSideEffect(e, `ignore click`));
31375
31463
  return;
31376
31464
  }
31377
31465
  // When a label is clicked it transfers focus to the select
@@ -31393,8 +31481,8 @@ const SelectWithPopover = props => {
31393
31481
  // space can open the popover we don't want space to propagate to the select otherwise it would open it back immediatly
31394
31482
  event.stopPropagation();
31395
31483
  }
31396
- requestClose(e);
31397
- moveFocusToSelect(e);
31484
+ requestClose(event);
31485
+ moveFocusToSelect(event);
31398
31486
  },
31399
31487
  onFocusOut: e => {
31400
31488
  // Close when focus leaves the select entirely (not just moving between internal elements).
@@ -31432,7 +31520,7 @@ const SelectWithPopover = props => {
31432
31520
  }, onKeyDown),
31433
31521
  ref: ref,
31434
31522
  mode: "ui",
31435
- children: jsx(Popover, {
31523
+ children: jsxs(Popover, {
31436
31524
  ref: popoverRef,
31437
31525
  className: "navi_select_popover",
31438
31526
  onMouseDown: e => {
@@ -31441,7 +31529,6 @@ const SelectWithPopover = props => {
31441
31529
  }
31442
31530
  // mousedown inside popover should not bubble to the select (would re-open it if that mousedown closes it)
31443
31531
  e.stopPropagation();
31444
- disableClickFor(e);
31445
31532
  },
31446
31533
  onnavi_popover_open: e => {
31447
31534
  onOpen();
@@ -31452,17 +31539,27 @@ const SelectWithPopover = props => {
31452
31539
  event = e
31453
31540
  } = e.detail;
31454
31541
  if (event.type === "focusout") ; else {
31455
- moveFocusToSelect(e);
31542
+ moveFocusToSelect(event);
31456
31543
  }
31457
31544
  },
31458
- positionTry: positionTry,
31545
+ positionX: "left-aligned",
31546
+ positionY: "below-overlap",
31459
31547
  scrollTrap: scrollTrap,
31460
31548
  pointerTrap: pointerTrap,
31461
31549
  focusTrap: focusTrap,
31462
- children: jsx(SelectRequestCloseContext.Provider, {
31550
+ children: [jsx("div", {
31551
+ className: "navi_select_anchor_clone",
31552
+ onMouseDown: e => {
31553
+ if (e.button !== 0) {
31554
+ return;
31555
+ }
31556
+ requestClose(e);
31557
+ },
31558
+ children: props.trigger
31559
+ }), jsx(SelectRequestCloseContext.Provider, {
31463
31560
  value: requestClose,
31464
31561
  children: children
31465
- })
31562
+ })]
31466
31563
  })
31467
31564
  });
31468
31565
  };
@@ -31478,6 +31575,7 @@ const SelectWithDialog = props => {
31478
31575
  ...rest
31479
31576
  } = props;
31480
31577
  const debugFocus = useDebugFocus();
31578
+ const debugPopup = useDebugPopup();
31481
31579
  const dialogRef = useRef(null);
31482
31580
  const dialogId = useId();
31483
31581
  const [expanded, setExpanded] = useState(false);
@@ -31496,7 +31594,12 @@ const SelectWithDialog = props => {
31496
31594
  event: e
31497
31595
  });
31498
31596
  };
31597
+ const [shouldIgnore, disableClickFor] = useIgnoreClickForMousedown();
31499
31598
  const requestClose = (e = new CustomEvent("programmatic")) => {
31599
+ if (e.type === "mousedown") {
31600
+ debugPopup(formatEventSideEffect(e, `disable click`));
31601
+ disableClickFor(e);
31602
+ }
31500
31603
  return requestDialogClose(dialogRef.current, {
31501
31604
  event: e
31502
31605
  });
@@ -31507,7 +31610,6 @@ const SelectWithDialog = props => {
31507
31610
  preventScroll: true
31508
31611
  });
31509
31612
  };
31510
- const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
31511
31613
  return jsx(SelectDispatcher, {
31512
31614
  disabled: disabled,
31513
31615
  "aria-haspopup": "dialog",
@@ -31533,7 +31635,8 @@ const SelectWithDialog = props => {
31533
31635
  // click triggered by enter won't open the dialog
31534
31636
  return;
31535
31637
  }
31536
- if (shouldIgnoreThatClick) {
31638
+ if (shouldIgnore) {
31639
+ debugPopup(formatEventSideEffect(e, `ignore click`));
31537
31640
  // mousedown on the select already handled open/close; ignore this click
31538
31641
  // to avoid toggling the dialog again on mouseup
31539
31642
  return;
@@ -31549,7 +31652,7 @@ const SelectWithDialog = props => {
31549
31652
  // space can open the dialog, we don't want space to propagate to the select otherwise it would open it back immediately
31550
31653
  event.stopPropagation();
31551
31654
  }
31552
- requestClose(e);
31655
+ requestClose(event);
31553
31656
  },
31554
31657
  ...rest,
31555
31658
  onKeyDown: shortcutsViaOnKeyDown({
@@ -31593,7 +31696,6 @@ const SelectWithDialog = props => {
31593
31696
  }
31594
31697
  // mousedown inside dialog should not bubble to the select (would re-open it if that mousedown closes it)
31595
31698
  e.stopPropagation();
31596
- disableClickFor(e);
31597
31699
  },
31598
31700
  scrollTrap: scrollTrap,
31599
31701
  pointerTrap: pointerTrap,