@jsenv/navi 0.27.54 → 0.27.56

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.
@@ -1,4 +1,4 @@
1
- import { installImportMetaCssBuild } from "./jsenv_navi_side_effects.js";
1
+ import { installImportMetaCssBuild, windowWidthSignal } from "./jsenv_navi_side_effects.js";
2
2
  import { isValidElement, createContext, h, toChildArray, render, Fragment, cloneElement } from "preact";
3
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useId } from "preact/hooks";
4
4
  import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
@@ -14530,6 +14530,14 @@ computed(() => {
14530
14530
  return reasonArray;
14531
14531
  });
14532
14532
 
14533
+ const documentStateSignal = signal(null);
14534
+ const useDocumentState = () => {
14535
+ return documentStateSignal.value;
14536
+ };
14537
+ const updateDocumentState = (value) => {
14538
+ documentStateSignal.value = value;
14539
+ };
14540
+
14533
14541
  const documentUrlSignal = signal(
14534
14542
  typeof window === "undefined" ? "http://localhost" : window.location.href,
14535
14543
  );
@@ -14574,14 +14582,6 @@ const urlToScheme = (url) => {
14574
14582
  return scheme;
14575
14583
  };
14576
14584
 
14577
- const documentStateSignal = signal(null);
14578
- const useDocumentState = () => {
14579
- return documentStateSignal.value;
14580
- };
14581
- const updateDocumentState = (value) => {
14582
- documentStateSignal.value = value;
14583
- };
14584
-
14585
14585
  const getHrefTargetInfo = (href) => {
14586
14586
  href = String(href);
14587
14587
 
@@ -14664,18 +14664,6 @@ const setupBrowserIntegrationViaHistory = ({
14664
14664
  return window.history.state ? { ...window.history.state } : null;
14665
14665
  };
14666
14666
 
14667
- const replaceDocumentState = (
14668
- newState,
14669
- { reason = "replaceDocumentState called" } = {},
14670
- ) => {
14671
- const url = window.location.href;
14672
- handleRoutingTask(url, {
14673
- reason,
14674
- navigationType: "replace",
14675
- state: newState,
14676
- });
14677
- };
14678
-
14679
14667
  const historyStartAtStart = getDocumentState();
14680
14668
  const visitedUrlSet = historyStartAtStart
14681
14669
  ? new Set(historyStartAtStart.jsenv_visited_urls || [])
@@ -14695,19 +14683,7 @@ const setupBrowserIntegrationViaHistory = ({
14695
14683
  return;
14696
14684
  }
14697
14685
  visitedUrlSet.add(url);
14698
- visitedUrlsSignal.value++; // Increment signal to notify subscribers that visited URLs changed
14699
-
14700
- const historyState = getDocumentState() || {};
14701
- const historyStateWithVisitedUrls = {
14702
- ...historyState,
14703
- jsenv_visited_urls: Array.from(visitedUrlSet),
14704
- };
14705
- window.history.replaceState(
14706
- historyStateWithVisitedUrls,
14707
- null,
14708
- window.location.href,
14709
- );
14710
- updateDocumentState(historyStateWithVisitedUrls);
14686
+ visitedUrlsSignal.value++;
14711
14687
  };
14712
14688
 
14713
14689
  let abortController = null;
@@ -14719,15 +14695,34 @@ const setupBrowserIntegrationViaHistory = ({
14719
14695
  state,
14720
14696
  },
14721
14697
  ) => {
14722
- if (navigationType === "push") {
14723
- window.history.pushState(state, null, url);
14724
- } else if (navigationType === "replace") {
14725
- window.history.replaceState(state, null, url);
14698
+ const isSameUrl = url === window.location.href;
14699
+
14700
+ if (navigationType === "push" || navigationType === "replace") {
14701
+ // Pre-merge visited URLs so push/replaceState is called only once.
14702
+ markUrlAsVisited(url);
14703
+ const effectiveState = {
14704
+ ...(state || {}),
14705
+ jsenv_visited_urls: Array.from(visitedUrlSet),
14706
+ };
14707
+ if (navigationType === "push") {
14708
+ window.history.pushState(effectiveState, null, url);
14709
+ } else {
14710
+ window.history.replaceState(effectiveState, null, url);
14711
+ }
14712
+ updateDocumentUrl(url);
14713
+ updateDocumentState(effectiveState);
14714
+ } else {
14715
+ // traverse / reload: state comes from the history entry, no push/replace needed.
14716
+ markUrlAsVisited(url);
14717
+ updateDocumentUrl(url);
14718
+ updateDocumentState(state);
14719
+ }
14720
+
14721
+ // Routes only match on URL — skip route matching for state-only changes.
14722
+ if (isSameUrl && navigationType !== "reload") {
14723
+ return undefined;
14726
14724
  }
14727
14725
 
14728
- updateDocumentUrl(url);
14729
- updateDocumentState(state);
14730
- markUrlAsVisited(url);
14731
14726
  if (abortController) {
14732
14727
  abortController.abort(`navigating to ${url}`);
14733
14728
  }
@@ -14861,7 +14856,6 @@ const setupBrowserIntegrationViaHistory = ({
14861
14856
  navBack,
14862
14857
  navForward,
14863
14858
  getDocumentState,
14864
- replaceDocumentState,
14865
14859
  isVisited,
14866
14860
  visitedUrlsSignal,
14867
14861
  };
@@ -14947,12 +14941,19 @@ setOnAllRouteReady((v) => {
14947
14941
  });
14948
14942
  setRouteIntegration(browserIntegration);
14949
14943
 
14950
- const actionIntegratedVia = browserIntegration.integration;
14944
+ const navIntegratedVia = browserIntegration.integration;
14951
14945
  const navTo = (target, options) => {
14952
14946
  const url = new URL(target, window.location.href).href;
14953
14947
  const currentUrl = documentUrlSignal.peek();
14954
14948
  if (url === currentUrl) {
14955
- return null;
14949
+ if (options?.state === undefined) {
14950
+ return null;
14951
+ }
14952
+ // State-only update on same URL: skip if state is identical to current.
14953
+ const currentState = browserIntegration.getDocumentState();
14954
+ if (compareTwoJsValues(options.state, currentState)) {
14955
+ return null;
14956
+ }
14956
14957
  }
14957
14958
  return browserIntegration.navTo(url, options);
14958
14959
  };
@@ -14973,78 +14974,113 @@ const isVisited = browserIntegration.isVisited;
14973
14974
  const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
14974
14975
  browserIntegration.handleActionTask;
14975
14976
 
14976
- const NOT_SET = {};
14977
14977
  const NO_OP = () => {};
14978
14978
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
14979
- const useNavStateBasic = (id, initialValue, { debug } = {}) => {
14980
- const navStateRef = useRef(NOT_SET);
14979
+ const useNavStateBasic = (
14980
+ id,
14981
+ initialValue,
14982
+ { debug, type = "replace", onLeave } = {},
14983
+ ) => {
14984
+ // Hooks must be called unconditionally — before the !id early return.
14985
+ const state = documentStateSignal.value;
14986
+ const valueInState = id
14987
+ ? state !== null
14988
+ ? state[id]
14989
+ : undefined
14990
+ : undefined;
14991
+ const onLeaveRef = useRef(onLeave);
14992
+ onLeaveRef.current = onLeave;
14993
+ const prevValueInStateRef = useRef(valueInState);
14994
+ // enteredRef tracks whether enter() was called without a matching leave() yet.
14995
+ // It lets the effect distinguish an external disappearance (back button → fire onLeave)
14996
+ // from a programmatic one (leave() already set it to false before the state updates).
14997
+ const enteredRef = useRef(false);
14998
+ useEffect(() => {
14999
+ const prevValue = prevValueInStateRef.current;
15000
+ prevValueInStateRef.current = valueInState;
15001
+ if (
15002
+ prevValue !== undefined &&
15003
+ valueInState === undefined &&
15004
+ enteredRef.current
15005
+ ) {
15006
+ enteredRef.current = false;
15007
+ onLeaveRef.current?.();
15008
+ }
15009
+ }, [valueInState]);
15010
+
14981
15011
  if (!id) {
14982
15012
  return NO_ID_GIVEN;
14983
15013
  }
14984
15014
 
14985
- if (navStateRef.current === NOT_SET) {
14986
- const documentState = browserIntegration.getDocumentState();
14987
- const valueInDocumentState = documentState ? documentState[id] : undefined;
14988
- if (valueInDocumentState === undefined) {
14989
- navStateRef.current = initialValue;
14990
- if (initialValue !== undefined) {
14991
- console.debug(
14992
- `useNavState(${id}) initial value is ${initialValue} (from initialValue passed in as argument)`,
14993
- );
14994
- }
14995
- } else {
14996
- navStateRef.current = valueInDocumentState;
14997
- if (debug) {
14998
- console.debug(
14999
- `useNavState(${id}) initial value is ${initialValue} (from nav state)`,
15000
- );
15001
- }
15002
- }
15003
- }
15015
+ const currentValue = valueInState !== undefined ? valueInState : initialValue;
15004
15016
 
15005
- const set = (value) => {
15006
- const currentValue = navStateRef.current;
15007
- if (typeof value === "function") {
15008
- value = value(currentValue);
15009
- }
15010
- if (debug) {
15011
- console.debug(
15012
- `useNavState(${id}) set ${value} (previous was ${currentValue})`,
15013
- );
15014
- }
15017
+ if (debug) {
15018
+ console.debug(`useNavState(${id}) current value is ${currentValue}`);
15019
+ }
15015
15020
 
15021
+ // enter(value): navigate TO this state (push or replace depending on type).
15022
+ // Calling enter() without a value stores "on" — the mere presence of the key
15023
+ // in the document state is enough to match; the value just allows associating
15024
+ // extra data with the entry when needed.
15025
+ const enter = (value = "on") => {
15026
+ enteredRef.current = true;
15016
15027
  const currentState = browserIntegration.getDocumentState() || {};
15017
-
15018
- if (value === undefined) {
15019
- if (!Object.hasOwn(currentState, id)) {
15020
- return;
15021
- }
15022
- delete currentState[id];
15023
- browserIntegration.replaceDocumentState(currentState, {
15024
- reason: `delete "${id}" from browser state`,
15025
- });
15028
+ if (currentState[id] === value) {
15026
15029
  return;
15027
15030
  }
15031
+ const newState = { ...currentState, [id]: value };
15032
+ navTo(window.location.href, {
15033
+ replace: type !== "push",
15034
+ state: newState,
15035
+ });
15036
+ };
15028
15037
 
15029
- const valueInBrowserState = currentState[id];
15030
- if (valueInBrowserState === value) {
15038
+ // leave(): navigate AWAY FROM this state (navBack in push mode, replace in replace mode).
15039
+ const leave = () => {
15040
+ enteredRef.current = false;
15041
+ const currentState = browserIntegration.getDocumentState() || {};
15042
+ if (!Object.hasOwn(currentState, id)) {
15031
15043
  return;
15032
15044
  }
15033
- currentState[id] = value;
15034
- browserIntegration.replaceDocumentState(currentState, {
15035
- reason: `set { ${id}: ${value} } in browser state`,
15036
- });
15045
+ if (type === "push") {
15046
+ browserIntegration.navBack();
15047
+ } else {
15048
+ const newState = { ...currentState };
15049
+ delete newState[id];
15050
+ navTo(window.location.href, { replace: true, state: newState });
15051
+ }
15037
15052
  };
15038
15053
 
15039
- return [
15040
- navStateRef.current,
15041
- set,
15042
- () => {
15043
- set(undefined);
15044
- },
15045
- ];
15054
+ return [currentValue, enter, leave];
15046
15055
  };
15047
15056
 
15057
+ /**
15058
+ * Stores a named value in the browser's document state and returns it reactively.
15059
+ * The component re-renders whenever the value changes (navigation, back/forward button).
15060
+ *
15061
+ * @param {string} id
15062
+ * Unique key used to store the value in document state. Must be stable across renders.
15063
+ *
15064
+ * @param {*} initialValue
15065
+ * Value returned when `id` is absent from document state (e.g. before enter() is called).
15066
+ *
15067
+ * @param {object} [options]
15068
+ * @param {"push"|"replace"} [options.type="replace"]
15069
+ * Controls how enter() adds the state to browser history.
15070
+ * - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
15071
+ * - "replace": updates the current history entry — no extra history entry is created.
15072
+ * @param {() => void} [options.onLeave]
15073
+ * Called when the state key disappears **externally** — e.g. the user presses the browser
15074
+ * back button. Not called when leave() is invoked programmatically.
15075
+ *
15076
+ * @returns {[value, enter, leave]}
15077
+ * - `value`: current value from document state, or `initialValue` when the key is absent.
15078
+ * - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
15079
+ * Calling without an argument stores `"on"` — the presence of the key is enough to match;
15080
+ * the value allows associating extra data when needed.
15081
+ * - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
15082
+ * or goes back in history when `type` is "push").
15083
+ */
15048
15084
  const useNavState = useNavStateBasic;
15049
15085
 
15050
15086
  const NEVER_SET = {};
@@ -35356,12 +35392,6 @@ const renderSafe = (value) => {
35356
35392
 
35357
35393
  const PickerContext = createContext();
35358
35394
 
35359
- const windowWidthSignal = signal(window.innerWidth);
35360
-
35361
- window.addEventListener("resize", () => {
35362
- windowWidthSignal.value = window.innerWidth;
35363
- });
35364
-
35365
35395
  /**
35366
35396
  * Mirrors what browsers do when navigating to a page:
35367
35397
  * 1. Focus the first element with [navi-autofocus] (but not [navi-autofocus="fallback"]) inside the container
@@ -35388,14 +35418,17 @@ const markAutofocusRestoreOnClose = (containerEl) => {
35388
35418
  // meaning the focus did not yet reach the element receiving the mousedown
35389
35419
  // as a result document.activeElement is not up-to-date (can be document.body for instance)
35390
35420
  const getFocusedBeforeTransfer = (e) => {
35391
- const initiator = e.detail.eventChain[0];
35392
- if (initiator.type === "mousedown") {
35393
- // if we we had let browser give focus, the element would be the one that would be focused
35394
- return initiator.currentTarget;
35395
- }
35396
- if (initiator.type === "click") {
35397
- // label use case
35398
- return initiator.currentTarget;
35421
+ const initiator =
35422
+ e.detail && e.detail.eventChain ? e.detail.eventChain[0] : null;
35423
+ if (initiator) {
35424
+ if (initiator.type === "mousedown") {
35425
+ // if we we had let browser give focus, the element would be the one that would be focused
35426
+ return initiator.currentTarget;
35427
+ }
35428
+ if (initiator.type === "click") {
35429
+ // label use case
35430
+ return initiator.currentTarget;
35431
+ }
35399
35432
  }
35400
35433
  return document.activeElement;
35401
35434
  };
@@ -35492,6 +35525,7 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
35492
35525
  margin-top: var(--dialog-top-inset, auto);
35493
35526
  margin-bottom: auto;
35494
35527
  flex-direction: column;
35528
+ transition: margin-top 0.1s ease-in-out;
35495
35529
  }
35496
35530
 
35497
35531
  &::backdrop {
@@ -35502,6 +35536,8 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
35502
35536
  const Dialog = props => {
35503
35537
  import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
35504
35538
  const {
35539
+ open: openProp = false,
35540
+ anchorRef,
35505
35541
  children,
35506
35542
  scrollTrap,
35507
35543
  pointerTrap,
@@ -35514,11 +35550,14 @@ const Dialog = props => {
35514
35550
  const debugPopup = useDebugPopup();
35515
35551
  const debugFocus = useDebugFocus();
35516
35552
  const autoFocusProps = useAutoFocus(ref, props.autoFocus);
35553
+
35554
+ // Tracks actual DOM state (open/closed), updated only by open() and close().
35555
+ // Intentionally NOT synced to openProp on every render so the useEffect guard
35556
+ // below can detect whether a prop change requires action.
35517
35557
  const openedRef = useRef(false);
35518
35558
  const [addCleanup, cleanup] = useCleanup();
35519
- const open = (e, {
35520
- anchor
35521
- }) => {
35559
+ const open = e => {
35560
+ const anchor = anchorRef?.current ?? null;
35522
35561
  const effectiveAnchor = anchor || document.documentElement;
35523
35562
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
35524
35563
  const dialogEl = ref.current;
@@ -35587,9 +35626,7 @@ const Dialog = props => {
35587
35626
  ...detail
35588
35627
  });
35589
35628
  };
35590
- const onRequestOpen = (e, {
35591
- anchor
35592
- }) => {
35629
+ const onRequestOpen = e => {
35593
35630
  const dialogEl = ref.current;
35594
35631
  if (!dialogEl) {
35595
35632
  return;
@@ -35597,9 +35634,7 @@ const Dialog = props => {
35597
35634
  if (openedRef.current) {
35598
35635
  return;
35599
35636
  }
35600
- open(e, {
35601
- anchor
35602
- });
35637
+ open(e);
35603
35638
  };
35604
35639
  const onRequestClose = (e, detail = {}) => {
35605
35640
  const dialogEl = ref.current;
@@ -35629,6 +35664,25 @@ const Dialog = props => {
35629
35664
  }
35630
35665
  close(e, detail);
35631
35666
  };
35667
+ useLayoutEffect(() => {
35668
+ if (openProp === undefined) {
35669
+ return;
35670
+ }
35671
+ // Skip when the popover is already in the desired state.
35672
+ // This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
35673
+ // the parent updates the prop, and the effect would fire again for a change we
35674
+ // already handled. Comparing against openedRef is the authoritative check because
35675
+ // it reflects the actual DOM state, not the React render cycle.
35676
+ if (openProp === openedRef.current) {
35677
+ return;
35678
+ }
35679
+ const e = new CustomEvent("open_prop_change");
35680
+ if (openProp) {
35681
+ onRequestOpen(e);
35682
+ } else {
35683
+ onRequestClose(e);
35684
+ }
35685
+ }, [openProp]);
35632
35686
  return jsx(Box, {
35633
35687
  ...rest,
35634
35688
  ...autoFocusProps,
@@ -35638,12 +35692,17 @@ const Dialog = props => {
35638
35692
  pseudoClasses: DIALOG_PSEUDO_CLASSES,
35639
35693
  onMouseDown: e => {
35640
35694
  rest.onMouseDown?.(e);
35641
- // The <dialog> element covers the full viewport; clicking the backdrop
35642
- // hits the dialog itself (not any child). Close when that happens.
35695
+ // Detect backdrop click: the click must land outside the dialog's
35696
+ // bounding rect. Checking coordinates is necessary because clicking
35697
+ // on the dialog's own padding also sets e.target === ref.current.
35643
35698
  if (!pointerTrap && e.button === 0 && e.target === ref.current) {
35644
- onRequestClose(e, {
35645
- isClickOutside: true
35646
- });
35699
+ const rect = ref.current.getBoundingClientRect();
35700
+ const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
35701
+ if (isBackdrop) {
35702
+ onRequestClose(e, {
35703
+ isClickOutside: true
35704
+ });
35705
+ }
35647
35706
  }
35648
35707
  },
35649
35708
  onCancel: e => {
@@ -35653,12 +35712,7 @@ const Dialog = props => {
35653
35712
  onRequestClose(e);
35654
35713
  },
35655
35714
  onnavi_request_open: e => {
35656
- const {
35657
- anchor
35658
- } = e.detail;
35659
- onRequestOpen(e, {
35660
- anchor
35661
- });
35715
+ onRequestOpen(e);
35662
35716
  },
35663
35717
  onnavi_request_close: e => {
35664
35718
  onRequestClose(e);
@@ -35693,6 +35747,8 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
35693
35747
  const Popover = props => {
35694
35748
  import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
35695
35749
  const {
35750
+ open: openProp = false,
35751
+ anchorRef,
35696
35752
  scrollTrap,
35697
35753
  pointerTrap,
35698
35754
  focusTrap,
@@ -35713,18 +35769,19 @@ const Popover = props => {
35713
35769
  const debugPopup = useDebugPopup();
35714
35770
  const debugFocus = useDebugFocus();
35715
35771
  const autoFocusProps = useAutoFocus(ref, props.autoFocus);
35716
- const [opened, setOpened] = useState(false);
35717
- const openedRef = useRef(opened);
35718
- openedRef.current = opened;
35772
+
35773
+ // Tracks actual DOM state (open/closed), updated only by open() and close().
35774
+ // Intentionally NOT synced to openProp on every render so the useEffect guard
35775
+ // below can detect whether a prop change requires action.
35776
+ const openedRef = useRef(false);
35719
35777
  const [addCleanup, cleanup] = useCleanup();
35720
- const open = (e, {
35721
- anchor
35722
- }) => {
35778
+ const open = e => {
35723
35779
  debugPopup(e, `openPopover()`);
35724
35780
  const popoverEl = ref.current;
35725
35781
  const focusedBeforeOpen = getFocusedBeforeTransfer(e);
35726
35782
  popoverEl.showPopover();
35727
35783
  transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
35784
+ const anchor = anchorRef?.current ?? null;
35728
35785
  const effectiveAnchor = anchor || document.documentElement;
35729
35786
  const positionPopover = positionEvent => {
35730
35787
  const {
@@ -35797,7 +35854,6 @@ const Popover = props => {
35797
35854
  rectEffect.disconnect();
35798
35855
  });
35799
35856
  openedRef.current = true;
35800
- setOpened(true);
35801
35857
  dispatchCustomEvent(popoverEl, "navi_open", {
35802
35858
  event: e,
35803
35859
  focusedBeforeOpen
@@ -35810,15 +35866,12 @@ const Popover = props => {
35810
35866
  popoverEl.hidePopover();
35811
35867
  cleanup();
35812
35868
  openedRef.current = false;
35813
- setOpened(false);
35814
35869
  dispatchCustomEvent(popoverEl, "navi_close", {
35815
35870
  event: e,
35816
35871
  ...detail
35817
35872
  });
35818
35873
  };
35819
- const onRequestOpen = (e, {
35820
- anchor
35821
- }) => {
35874
+ const onRequestOpen = e => {
35822
35875
  const popoverEl = ref.current;
35823
35876
  if (!popoverEl) {
35824
35877
  return;
@@ -35826,9 +35879,7 @@ const Popover = props => {
35826
35879
  if (openedRef.current) {
35827
35880
  return;
35828
35881
  }
35829
- open(e, {
35830
- anchor
35831
- });
35882
+ open(e);
35832
35883
  };
35833
35884
  const onRequestClose = (e, detail = {}) => {
35834
35885
  const popoverEl = ref.current;
@@ -35858,6 +35909,27 @@ const Popover = props => {
35858
35909
  }
35859
35910
  close(e, detail);
35860
35911
  };
35912
+ useLayoutEffect(() => {
35913
+ if (openProp === undefined) {
35914
+ return;
35915
+ }
35916
+ // Skip when the popover is already in the desired state.
35917
+ // This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
35918
+ // the parent updates the prop, and the effect would fire again for a change we
35919
+ // already handled. Comparing against openedRef is the authoritative check because
35920
+ // it reflects the actual DOM state, not the React render cycle.
35921
+ if (openProp === openedRef.current) {
35922
+ return;
35923
+ }
35924
+ const e = new CustomEvent("open_prop_change", {
35925
+ detail: {}
35926
+ });
35927
+ if (openProp) {
35928
+ onRequestOpen(e);
35929
+ } else {
35930
+ onRequestClose(e);
35931
+ }
35932
+ }, [openProp]);
35861
35933
  return jsxs(Box, {
35862
35934
  id: id,
35863
35935
  popover: "manual",
@@ -35867,12 +35939,7 @@ const Popover = props => {
35867
35939
  baseClassName: "navi_popover",
35868
35940
  pseudoClasses: POPOVER_PSEUDO_CLASSES,
35869
35941
  onnavi_request_open: e => {
35870
- const {
35871
- anchor
35872
- } = e.detail;
35873
- onRequestOpen(e, {
35874
- anchor
35875
- });
35942
+ onRequestOpen(e);
35876
35943
  },
35877
35944
  onnavi_request_close: e => {
35878
35945
  onRequestClose(e);
@@ -35902,11 +35969,21 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
35902
35969
  /* Shared by popover and dialog */
35903
35970
  --picker-popup-background-color: var(--picker-background-color);
35904
35971
  --picker-popup-border-radius: var(--picker-border-radius);
35972
+ --picker-popup-border-width: var(--picker-border-width);
35905
35973
  /* Popover */
35906
- --picker-popover-max-height: 300px;
35974
+ --picker-popover-max-height: 300px; /* soft: user-configurable preferred max-height */
35975
+ --picker-popover-maxmax-height: calc(0.95 * var(--navi-vvh));
35976
+ --picker-popover-maxmax-width: calc(0.95 * var(--navi-vvw));
35977
+ /* --picker-popover-max-width: soft, leave unset to rely on maxmax */
35907
35978
  /* Dialog */
35908
- --picker-dialog-max-width: 95dvw;
35909
- --picker-dialog-max-height: 95dvh;
35979
+ --picker-dialog-margin: 3dvw; /* min gap between dialog edges and viewport */
35980
+ --picker-dialog-maxmax-width: calc(
35981
+ var(--navi-vvw) - 2 * var(--picker-dialog-margin)
35982
+ );
35983
+ --picker-dialog-maxmax-height: calc(
35984
+ var(--navi-vvh) - 2 * var(--picker-dialog-margin)
35985
+ );
35986
+ --picker-dialog-border-width: 0px; /* Dialog do not need border like popover (they stand out more) */
35910
35987
 
35911
35988
  /* popover */
35912
35989
  &[aria-haspopup="listbox"] {
@@ -35914,11 +35991,15 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
35914
35991
  position: absolute;
35915
35992
  inset: unset;
35916
35993
  min-width: var(--anchor-width, 0px);
35917
- max-width: 95vw;
35994
+ max-width: min(
35995
+ var(--picker-popover-max-width, var(--picker-popover-maxmax-width)),
35996
+ var(--picker-popover-maxmax-width)
35997
+ );
35918
35998
  /* max-height covers the placeholder + list; the list scrolls internally */
35919
35999
  max-height: min(
35920
36000
  var(--picker-popover-max-height),
35921
- var(--space-available, 95dvh)
36001
+ var(--space-available, var(--picker-popover-maxmax-height)),
36002
+ var(--picker-popover-maxmax-height)
35922
36003
  );
35923
36004
  margin: 0;
35924
36005
  padding: 0;
@@ -36012,11 +36093,18 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
36012
36093
  &[aria-haspopup="dialog"] {
36013
36094
  .navi_picker_dialog {
36014
36095
  min-width: var(--anchor-width, 0px);
36015
- max-width: var(--picker-dialog-max-width);
36016
- max-height: var(--picker-dialog-max-height);
36096
+ max-width: min(
36097
+ var(--picker-dialog-max-width, var(--picker-dialog-maxmax-width)),
36098
+ var(--picker-dialog-maxmax-width)
36099
+ );
36100
+ max-height: min(
36101
+ var(--picker-dialog-max-height, var(--picker-dialog-maxmax-height)),
36102
+ var(--picker-dialog-maxmax-height)
36103
+ );
36017
36104
  padding: 0;
36018
36105
  background: var(--picker-popup-background-color);
36019
- border: var(--picker-border-width) solid var(--x-picker-border-color);
36106
+ border: var(--picker-dialog-border-width) solid
36107
+ var(--x-picker-border-color);
36020
36108
  border-radius: var(--picker-popup-border-radius);
36021
36109
  outline-width: var(--picker-outline-width);
36022
36110
  outline-color: var(--picker-outline-color);
@@ -36028,10 +36116,10 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
36028
36116
  /* overscroll-behavior: contain; */
36029
36117
 
36030
36118
  &[data-expand-x] {
36031
- width: var(--picker-dialog-max-width);
36119
+ width: var(--picker-dialog-maxmax-width);
36032
36120
  }
36033
36121
  &[data-expand-y] {
36034
- height: var(--picker-dialog-max-height);
36122
+ height: var(--picker-dialog-maxmax-height);
36035
36123
  }
36036
36124
 
36037
36125
  &[open] {
@@ -36157,8 +36245,9 @@ const PickerCustom = props => {
36157
36245
  const popupRef = useRef(null);
36158
36246
  popupProps.ref = popupRef;
36159
36247
  // aria-controls + id
36248
+ const autoId = useId();
36249
+ const popupId = `picker_popup_${autoId}`;
36160
36250
  {
36161
- const popupId = useId();
36162
36251
  Object.assign(pickerProps, {
36163
36252
  "aria-controls": popupId
36164
36253
  });
@@ -36170,14 +36259,27 @@ const PickerCustom = props => {
36170
36259
  {
36171
36260
  const debugFocus = useDebugFocus();
36172
36261
  const debugPopup = useDebugPopup();
36173
- const [expanded, setExpanded] = useState(false);
36174
- const expandedRef = useRef(expanded);
36175
- expandedRef.current = expanded;
36262
+ // In "dialog" mode, enterExpanded() pushes a history entry so the back button closes.
36263
+ // In "popover" mode, it replaces the current history state (no history entry added).
36264
+ const pickerNavType = mode === "dialog" ? "push" : "replace";
36265
+ const expandedRef = useRef(false);
36266
+ const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, false, {
36267
+ type: pickerNavType,
36268
+ // onLeave fires only when the state key disappears externally (back button/gesture most of the time).
36269
+ onLeave: () => {
36270
+ requestClose(new CustomEvent("navi_nav_away", {
36271
+ detail: {}
36272
+ }), {
36273
+ isCancel: true
36274
+ });
36275
+ }
36276
+ });
36277
+ expandedRef.current = Boolean(expanded);
36176
36278
  const valueAtOpenRef = useRef(null);
36177
36279
  const activeElementAtOpenRef = useRef(null);
36178
36280
  const onOpen = e => {
36179
36281
  expandedRef.current = true;
36180
- setExpanded(true);
36282
+ enterExpanded();
36181
36283
  const focusedBeforeOpen = e.detail.focusedBeforeOpen;
36182
36284
  activeElementAtOpenRef.current = focusedBeforeOpen;
36183
36285
  debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
@@ -36220,7 +36322,7 @@ const PickerCustom = props => {
36220
36322
  }
36221
36323
  }
36222
36324
  expandedRef.current = false;
36223
- setExpanded(false);
36325
+ leaveExpanded();
36224
36326
  // Reset so the next opening re-evaluates screen size
36225
36327
  defaultModeRef.current = null;
36226
36328
  restoreFocus(e);
@@ -36236,8 +36338,7 @@ const PickerCustom = props => {
36236
36338
  });
36237
36339
  const popupEl = popupRef.current;
36238
36340
  return dispatchCustomEvent(popupEl, "navi_request_open", {
36239
- event: e,
36240
- anchor: pickerEl
36341
+ event: e
36241
36342
  });
36242
36343
  };
36243
36344
  const requestClose = (e = new CustomEvent("programmatic"), {
@@ -36258,7 +36359,7 @@ const PickerCustom = props => {
36258
36359
  uiAction: uiActionProp
36259
36360
  } = props;
36260
36361
  Object.assign(pickerProps, {
36261
- "aria-expanded": expanded,
36362
+ "aria-expanded": Boolean(expanded),
36262
36363
  "onActionStart": e => {
36263
36364
  onActionStart?.(e);
36264
36365
  // requestClose(e);
@@ -36289,6 +36390,8 @@ const PickerCustom = props => {
36289
36390
  children
36290
36391
  });
36291
36392
  Object.assign(popupProps, {
36393
+ anchorRef: props.ref,
36394
+ open: Boolean(expanded),
36292
36395
  closeRequestHandler: (requestCloseEvent, closePermission, {
36293
36396
  isClickOutside
36294
36397
  } = {}) => {
@@ -36396,11 +36499,14 @@ const PickerCustom = props => {
36396
36499
  };
36397
36500
  }
36398
36501
  });
36502
+ const isWithinPopup = el => {
36503
+ const popupEl = popupRef.current;
36504
+ return el === popupEl || popupEl.contains(el);
36505
+ };
36399
36506
  Object.assign(pickerProps, {
36400
36507
  eventReactionDefinitions: {
36401
36508
  mouseDown: e => {
36402
- const popupEl = popupRef.current;
36403
- if (popupEl && popupEl.contains(e.target)) {
36509
+ if (isWithinPopup(e.target)) {
36404
36510
  return null;
36405
36511
  }
36406
36512
  if (expandedRef.current) {
@@ -36421,8 +36527,7 @@ const PickerCustom = props => {
36421
36527
  };
36422
36528
  },
36423
36529
  click: e => {
36424
- const popupEl = popupRef.current;
36425
- if (popupEl && popupEl.contains(e.target)) {
36530
+ if (isWithinPopup(e.target)) {
36426
36531
  return null;
36427
36532
  }
36428
36533
  // When a label is clicked it transfers focus to the select
@@ -40979,6 +41084,7 @@ const PickerStyleCSSVars = {
40979
41084
  "popoverMaxHeight": "--picker-popover-max-height",
40980
41085
  "popupBackgroundColor": "--picker-popup-background-color",
40981
41086
  "popupBorderRadius": "--picker-popup-border-radius",
41087
+ "dialogBorderWidth": "--picker-dialog-border-width",
40982
41088
  "padding": "--picker-padding",
40983
41089
  "paddingX": "--picker-padding-x",
40984
41090
  "paddingY": "--picker-padding-y",
@@ -47066,5 +47172,5 @@ const UserSvg = () => jsx("svg", {
47066
47172
  })
47067
47173
  });
47068
47174
 
47069
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
47175
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
47070
47176
  //# sourceMappingURL=jsenv_navi.js.map