@jsenv/navi 0.27.60 → 0.27.61

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.
@@ -14696,28 +14696,37 @@ const setupBrowserIntegrationViaHistory = ({
14696
14696
  };
14697
14697
 
14698
14698
  let abortController = null;
14699
- const handleRoutingTask = (
14700
- url,
14701
- {
14699
+ const handleRoutingTask = (url, options) => {
14700
+ const isSameUrl = url === window.location.href;
14701
+ const {
14702
14702
  reason,
14703
14703
  navigationType, // "load", "reload", "replace", "push", "traverse"
14704
14704
  state,
14705
- },
14706
- ) => {
14707
- const isSameUrl = url === window.location.href;
14705
+ } = options;
14708
14706
 
14709
14707
  if (navigationType === "push" || navigationType === "replace") {
14710
- // Merge onto the current state so existing useNavState entries (e.g. an
14711
- // open dialog/popover) survive URL changes triggered by signal updates.
14712
- // "traverse" is intentionally excluded: it restores the exact historical
14713
- // state from the history entry, which is the source of truth for back/forward.
14714
- const currentState = getDocumentState() || {};
14715
14708
  markUrlAsVisited(url);
14716
- const effectiveState = {
14717
- ...currentState,
14718
- ...(state || {}),
14709
+ // undefined inherit current state (link click, neutral navigation)
14710
+ // null → explicit reset (no nav-state keys carried over)
14711
+ // {...} → explicit state from enter()/leave(), already built from currentState
14712
+ // When state is given it's responsability of the caller to ensure it inherits document state (or not, you want it 99% of the time)
14713
+ let effectiveState;
14714
+ const sharedState = {
14719
14715
  jsenv_visited_urls: Array.from(visitedUrlSet),
14720
14716
  };
14717
+ if (state === undefined) {
14718
+ effectiveState = {
14719
+ ...(getDocumentState() || {}),
14720
+ ...sharedState,
14721
+ };
14722
+ } else if (state === null) {
14723
+ effectiveState = sharedState;
14724
+ } else if (state) {
14725
+ effectiveState = {
14726
+ ...state,
14727
+ ...sharedState,
14728
+ };
14729
+ }
14721
14730
  if (navigationType === "push") {
14722
14731
  window.history.pushState(effectiveState, null, url);
14723
14732
  } else {
@@ -14803,7 +14812,6 @@ const setupBrowserIntegrationViaHistory = ({
14803
14812
  handleRoutingTask(href, {
14804
14813
  reason: `"click" on a[href="${href}"]`,
14805
14814
  navigationType: "push",
14806
- state: null,
14807
14815
  });
14808
14816
  },
14809
14817
  { capture: true },
@@ -14828,7 +14836,7 @@ const setupBrowserIntegrationViaHistory = ({
14828
14836
  });
14829
14837
  });
14830
14838
 
14831
- const navTo = async (url, { replace, state = null } = {}) => {
14839
+ const navTo = async (url, { replace, state } = {}) => {
14832
14840
  handleRoutingTask(url, {
14833
14841
  reason: `navTo called with "${url}"`,
14834
14842
  navigationType: replace ? "replace" : "push",
@@ -14999,41 +15007,33 @@ const NO_OP = () => {};
14999
15007
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
15000
15008
  const useNavStateBasic = (
15001
15009
  id,
15002
- initialValue,
15003
- { debug, type = "replace", onLeave } = {},
15010
+ { debug, type = "replace", onLeave, defaultValue } = {},
15004
15011
  ) => {
15005
15012
  // Hooks must be called unconditionally — before the !id early return.
15006
15013
  const state = documentStateSignal.value;
15007
- const valueInState = id
15008
- ? state !== null
15009
- ? state[id]
15010
- : undefined
15011
- : undefined;
15014
+ // Key presence is the flag — the value may be anything, including undefined.
15015
+ const keyInState = Boolean(id && state && Object.hasOwn(state, id));
15012
15016
  const onLeaveRef = useRef(onLeave);
15013
15017
  onLeaveRef.current = onLeave;
15014
- const prevValueInStateRef = useRef(valueInState);
15018
+ const prevKeyInStateRef = useRef(keyInState);
15015
15019
  // enteredRef tracks whether enter() was called without a matching leave() yet.
15016
15020
  // It lets the effect distinguish an external disappearance (back button → fire onLeave)
15017
15021
  // from a programmatic one (leave() already set it to false before the state updates).
15018
15022
  const enteredRef = useRef(false);
15019
15023
  useEffect(() => {
15020
- const prevValue = prevValueInStateRef.current;
15021
- prevValueInStateRef.current = valueInState;
15022
- if (
15023
- prevValue !== undefined &&
15024
- valueInState === undefined &&
15025
- enteredRef.current
15026
- ) {
15024
+ const prevKeyInState = prevKeyInStateRef.current;
15025
+ prevKeyInStateRef.current = keyInState;
15026
+ if (prevKeyInState && !keyInState && enteredRef.current) {
15027
15027
  enteredRef.current = false;
15028
15028
  onLeaveRef.current?.();
15029
15029
  }
15030
- }, [valueInState]);
15030
+ }, [keyInState]);
15031
15031
 
15032
15032
  if (!id) {
15033
15033
  return NO_ID_GIVEN;
15034
15034
  }
15035
15035
 
15036
- const currentValue = valueInState !== undefined ? valueInState : initialValue;
15036
+ const currentValue = keyInState ? state[id] : defaultValue;
15037
15037
 
15038
15038
  if (debug) {
15039
15039
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -15045,30 +15045,36 @@ const useNavStateBasic = (
15045
15045
  // extra data with the entry when needed.
15046
15046
  const enter = (value = "on") => {
15047
15047
  enteredRef.current = true;
15048
- const currentState = browserIntegration.getDocumentState() || {};
15049
- if (currentState[id] === value) {
15048
+ const currentStateCopy = browserIntegration.getDocumentState() || {};
15049
+ if (Object.hasOwn(currentStateCopy, id) && currentStateCopy[id] === value) {
15050
15050
  return;
15051
15051
  }
15052
- const newState = { ...currentState, [id]: value };
15052
+ currentStateCopy[id] = value;
15053
15053
  navTo(window.location.href, {
15054
15054
  replace: type !== "push",
15055
- state: newState,
15055
+ state: currentStateCopy,
15056
15056
  });
15057
15057
  };
15058
15058
 
15059
15059
  // leave(): navigate AWAY FROM this state (navBack in push mode, replace in replace mode).
15060
- const leave = () => {
15060
+ // isBack: when true (cancel close in push mode), call history.back() to restore the
15061
+ // pre-open state — discards any in-progress edits.
15062
+ // When false (confirmed close), replace the pushed entry instead: preserves the
15063
+ // current URL state (e.g. a new picker value) while removing the popup key.
15064
+ const leave = ({ isBack } = {}) => {
15061
15065
  enteredRef.current = false;
15062
- const currentState = browserIntegration.getDocumentState() || {};
15063
- if (!Object.hasOwn(currentState, id)) {
15066
+ const currentStateCopy = browserIntegration.getDocumentState() || {};
15067
+ if (!Object.hasOwn(currentStateCopy, id)) {
15064
15068
  return;
15065
15069
  }
15066
- if (type === "push") {
15070
+ if (type === "push" && isBack) {
15067
15071
  browserIntegration.navBack();
15068
15072
  } else {
15069
- const newState = { ...currentState };
15070
- delete newState[id];
15071
- navTo(window.location.href, { replace: true, state: newState });
15073
+ delete currentStateCopy[id];
15074
+ navTo(window.location.href, {
15075
+ replace: true,
15076
+ state: currentStateCopy,
15077
+ });
15072
15078
  }
15073
15079
  };
15074
15080
 
@@ -15082,9 +15088,6 @@ const useNavStateBasic = (
15082
15088
  * @param {string} id
15083
15089
  * Unique key used to store the value in document state. Must be stable across renders.
15084
15090
  *
15085
- * @param {*} initialValue
15086
- * Value returned when `id` is absent from document state (e.g. before enter() is called).
15087
- *
15088
15091
  * @param {object} [options]
15089
15092
  * @param {"push"|"replace"} [options.type="replace"]
15090
15093
  * Controls how enter() adds the state to browser history.
@@ -15093,9 +15096,11 @@ const useNavStateBasic = (
15093
15096
  * @param {() => void} [options.onLeave]
15094
15097
  * Called when the state key disappears **externally** — e.g. the user presses the browser
15095
15098
  * back button. Not called when leave() is invoked programmatically.
15099
+ * @param {*} [options.defaultValue]
15100
+ * Value returned when `id` is absent from document state. Defaults to `undefined`.
15096
15101
  *
15097
15102
  * @returns {[value, enter, leave]}
15098
- * - `value`: current value from document state, or `initialValue` when the key is absent.
15103
+ * - `value`: current value from document state, or `defaultValue` when the key is absent.
15099
15104
  * - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
15100
15105
  * Calling without an argument stores `"on"` — the presence of the key is enough to match;
15101
15106
  * the value allows associating extra data when needed.
@@ -22710,6 +22715,23 @@ const useUIStateController = (
22710
22715
  if (e.type === "facade_propagate_up") {
22711
22716
  notifyParentAboutChildUIAction(e, { stateChanged: true });
22712
22717
  }
22718
+ // Exception: state_prop_change can only fire on a control with its own
22719
+ // controlled state/value prop (see hasStateProp above) — groups never
22720
+ // cascade state down into such children (they're explicitly skipped,
22721
+ // see shouldPropagateStateToChild/hasStateProp checks), so this change
22722
+ // can never be an echo of the parent's own cascade. The loop risk this
22723
+ // suppression exists for only applies when the parent itself just pushed
22724
+ // this value down, which requires the parent to be controlled (have its
22725
+ // own state/value prop). When the parent is "stateless" (uncontrolled),
22726
+ // notifying it is always safe and necessary — otherwise its aggregated
22727
+ // state silently drifts out of sync with this child.
22728
+ if (
22729
+ e.type === "state_prop_change" &&
22730
+ parentUIStateController &&
22731
+ !parentUIStateController.hasStateProp
22732
+ ) {
22733
+ notifyParentAboutChildUIAction(e, { stateChanged: true });
22734
+ }
22713
22735
  return true;
22714
22736
  }
22715
22737
  notifyParentAboutChildUIAction(e, { stateChanged: true });
@@ -35557,7 +35579,8 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
35557
35579
  const Dialog = props => {
35558
35580
  import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
35559
35581
  const {
35560
- open: openProp = false,
35582
+ // requestOpen,
35583
+ requestClose,
35561
35584
  anchorRef,
35562
35585
  children,
35563
35586
  scrollTrap,
@@ -35635,7 +35658,7 @@ const Dialog = props => {
35635
35658
  focusedBeforeOpen
35636
35659
  });
35637
35660
  };
35638
- const close = (e, detail = {}) => {
35661
+ const close = e => {
35639
35662
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
35640
35663
  const dialogEl = ref.current;
35641
35664
  markAutofocusRestoreOnClose(dialogEl);
@@ -35643,70 +35666,9 @@ const Dialog = props => {
35643
35666
  cleanup();
35644
35667
  openedRef.current = false;
35645
35668
  dispatchCustomEvent(dialogEl, "navi_close", {
35646
- event: e,
35647
- ...detail
35669
+ event: e
35648
35670
  });
35649
35671
  };
35650
- const onRequestOpen = e => {
35651
- const dialogEl = ref.current;
35652
- if (!dialogEl) {
35653
- return;
35654
- }
35655
- if (openedRef.current) {
35656
- return;
35657
- }
35658
- open(e);
35659
- };
35660
- const onRequestClose = (e, detail = {}) => {
35661
- const dialogEl = ref.current;
35662
- if (!dialogEl) {
35663
- return;
35664
- }
35665
- if (!openedRef.current) {
35666
- return;
35667
- }
35668
- if (closeRequestHandler) {
35669
- let denied = false;
35670
- const closePermission = {
35671
- deny: () => {
35672
- denied = true;
35673
- },
35674
- allow: () => {
35675
- denied = false;
35676
- }
35677
- };
35678
- closeRequestHandler(e, closePermission, detail);
35679
- if (denied) {
35680
- if (e.type === "cancel") {
35681
- e.preventDefault();
35682
- }
35683
- closePermission.allow = () => {
35684
- close(e, detail);
35685
- };
35686
- return;
35687
- }
35688
- }
35689
- close(e, detail);
35690
- };
35691
- useLayoutEffect(() => {
35692
- if (openProp === undefined) {
35693
- return;
35694
- }
35695
- // Skip when the popover is already in the desired state.
35696
- // This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
35697
- // the parent updates the prop, and the effect would fire again for a change we
35698
- // already handled. Comparing against openedRef is the authoritative check because
35699
- // it reflects the actual DOM state, not the React render cycle.
35700
- if (openProp === openedRef.current) {
35701
- return;
35702
- }
35703
- const e = new CustomEvent("open_prop_change");
35704
- if (openProp) {
35705
- onRequestOpen(e);
35706
- } else {
35707
- onRequestClose(e);
35708
- }
35709
- }, [openProp]);
35710
35672
  return jsx(Box, {
35711
35673
  ...rest,
35712
35674
  ...autoFocusProps,
@@ -35723,20 +35685,57 @@ const Dialog = props => {
35723
35685
  const rect = ref.current.getBoundingClientRect();
35724
35686
  const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
35725
35687
  if (isBackdrop) {
35726
- onRequestClose(e, {
35727
- isClickOutside: true
35688
+ requestClose(e, {
35689
+ isCancel: true
35728
35690
  });
35729
35691
  }
35730
35692
  }
35731
35693
  },
35732
35694
  onCancel: e => {
35733
- onRequestClose(e);
35695
+ requestClose(e, {
35696
+ isCancel: true
35697
+ });
35734
35698
  },
35735
35699
  onnavi_request_open: e => {
35736
- onRequestOpen(e);
35700
+ const dialogEl = ref.current;
35701
+ if (!dialogEl) {
35702
+ return;
35703
+ }
35704
+ if (openedRef.current) {
35705
+ return;
35706
+ }
35707
+ open(e);
35737
35708
  },
35738
35709
  onnavi_request_close: e => {
35739
- onRequestClose(e);
35710
+ const dialogEl = ref.current;
35711
+ if (!dialogEl) {
35712
+ return;
35713
+ }
35714
+ if (!openedRef.current) {
35715
+ return;
35716
+ }
35717
+ if (closeRequestHandler) {
35718
+ let denied = false;
35719
+ const closePermission = {
35720
+ deny: () => {
35721
+ denied = true;
35722
+ },
35723
+ allow: () => {
35724
+ denied = false;
35725
+ }
35726
+ };
35727
+ closeRequestHandler(e, closePermission);
35728
+ if (denied) {
35729
+ if (e.type === "cancel") {
35730
+ e.preventDefault();
35731
+ }
35732
+ closePermission.allow = () => {
35733
+ close(e);
35734
+ };
35735
+ return;
35736
+ }
35737
+ }
35738
+ close(e);
35740
35739
  },
35741
35740
  children: children
35742
35741
  });
@@ -35768,7 +35767,7 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
35768
35767
  const Popover = props => {
35769
35768
  import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
35770
35769
  const {
35771
- open: openProp = false,
35770
+ requestClose,
35772
35771
  anchorRef,
35773
35772
  scrollTrap,
35774
35773
  pointerTrap,
@@ -35880,7 +35879,7 @@ const Popover = props => {
35880
35879
  focusedBeforeOpen
35881
35880
  });
35882
35881
  };
35883
- const close = (e, detail = {}) => {
35882
+ const close = e => {
35884
35883
  debugPopup(e, `closePopover()`);
35885
35884
  const popoverEl = ref.current;
35886
35885
  markAutofocusRestoreOnClose(popoverEl);
@@ -35888,69 +35887,9 @@ const Popover = props => {
35888
35887
  cleanup();
35889
35888
  openedRef.current = false;
35890
35889
  dispatchCustomEvent(popoverEl, "navi_close", {
35891
- event: e,
35892
- ...detail
35890
+ event: e
35893
35891
  });
35894
35892
  };
35895
- const onRequestOpen = e => {
35896
- const popoverEl = ref.current;
35897
- if (!popoverEl) {
35898
- return;
35899
- }
35900
- if (openedRef.current) {
35901
- return;
35902
- }
35903
- open(e);
35904
- };
35905
- const onRequestClose = (e, detail = {}) => {
35906
- const popoverEl = ref.current;
35907
- if (!popoverEl) {
35908
- return;
35909
- }
35910
- if (!openedRef.current) {
35911
- return;
35912
- }
35913
- if (closeRequestHandler) {
35914
- let denied = false;
35915
- const closePermission = {
35916
- deny: () => {
35917
- denied = true;
35918
- },
35919
- allow: () => {
35920
- denied = false;
35921
- }
35922
- };
35923
- closeRequestHandler(e, closePermission, detail);
35924
- if (denied) {
35925
- closePermission.allow = () => {
35926
- close(e, detail);
35927
- };
35928
- return;
35929
- }
35930
- }
35931
- close(e, detail);
35932
- };
35933
- useLayoutEffect(() => {
35934
- if (openProp === undefined) {
35935
- return;
35936
- }
35937
- // Skip when the popover is already in the desired state.
35938
- // This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
35939
- // the parent updates the prop, and the effect would fire again for a change we
35940
- // already handled. Comparing against openedRef is the authoritative check because
35941
- // it reflects the actual DOM state, not the React render cycle.
35942
- if (openProp === openedRef.current) {
35943
- return;
35944
- }
35945
- const e = new CustomEvent("open_prop_change", {
35946
- detail: {}
35947
- });
35948
- if (openProp) {
35949
- onRequestOpen(e);
35950
- } else {
35951
- onRequestClose(e);
35952
- }
35953
- }, [openProp]);
35954
35893
  return jsxs(Box, {
35955
35894
  id: id,
35956
35895
  popover: "manual",
@@ -35960,10 +35899,42 @@ const Popover = props => {
35960
35899
  baseClassName: "navi_popover",
35961
35900
  pseudoClasses: POPOVER_PSEUDO_CLASSES,
35962
35901
  onnavi_request_open: e => {
35963
- onRequestOpen(e);
35902
+ const popoverEl = ref.current;
35903
+ if (!popoverEl) {
35904
+ return;
35905
+ }
35906
+ if (openedRef.current) {
35907
+ return;
35908
+ }
35909
+ open(e);
35964
35910
  },
35965
35911
  onnavi_request_close: e => {
35966
- onRequestClose(e);
35912
+ const popoverEl = ref.current;
35913
+ if (!popoverEl) {
35914
+ return;
35915
+ }
35916
+ if (!openedRef.current) {
35917
+ return;
35918
+ }
35919
+ if (closeRequestHandler) {
35920
+ let denied = false;
35921
+ const closePermission = {
35922
+ deny: () => {
35923
+ denied = true;
35924
+ },
35925
+ allow: () => {
35926
+ denied = false;
35927
+ }
35928
+ };
35929
+ closeRequestHandler(e, closePermission);
35930
+ if (denied) {
35931
+ closePermission.allow = () => {
35932
+ close(e);
35933
+ };
35934
+ return;
35935
+ }
35936
+ }
35937
+ close(e);
35967
35938
  },
35968
35939
  children: [jsx("div", {
35969
35940
  className: "navi_popover_backdrop",
@@ -35976,8 +35947,8 @@ const Popover = props => {
35976
35947
  e.preventDefault();
35977
35948
  return;
35978
35949
  }
35979
- onRequestClose(e, {
35980
- isClickOutside: true
35950
+ requestClose(e, {
35951
+ isCancel: true
35981
35952
  });
35982
35953
  }
35983
35954
  }), children]
@@ -36266,8 +36237,7 @@ const PickerCustom = props => {
36266
36237
  const popupRef = useRef(null);
36267
36238
  popupProps.ref = popupRef;
36268
36239
  // aria-controls + id
36269
- const autoId = useId();
36270
- const popupId = `picker_popup_${autoId}`;
36240
+ const popupId = `${props.id}_picker_popup`;
36271
36241
  {
36272
36242
  Object.assign(pickerProps, {
36273
36243
  "aria-controls": popupId
@@ -36284,7 +36254,7 @@ const PickerCustom = props => {
36284
36254
  // In "popover" mode, it replaces the current history state (no history entry added).
36285
36255
  const pickerNavType = mode === "dialog" ? "push" : "replace";
36286
36256
  const expandedRef = useRef(false);
36287
- const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, false, {
36257
+ const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, {
36288
36258
  type: pickerNavType,
36289
36259
  // onLeave fires only when the state key disappears externally (back button/gesture most of the time).
36290
36260
  onLeave: () => {
@@ -36295,7 +36265,8 @@ const PickerCustom = props => {
36295
36265
  });
36296
36266
  }
36297
36267
  });
36298
- expandedRef.current = Boolean(expanded);
36268
+ // expandedRef tracks actual open/close state, updated only by onOpen/onClose.
36269
+ // NOT synced to expanded on every render so the useLayoutEffect guard below works.
36299
36270
  const valueAtOpenRef = useRef(null);
36300
36271
  const activeElementAtOpenRef = useRef(null);
36301
36272
  const onOpen = e => {
@@ -36343,7 +36314,9 @@ const PickerCustom = props => {
36343
36314
  }
36344
36315
  }
36345
36316
  expandedRef.current = false;
36346
- leaveExpanded();
36317
+ leaveExpanded({
36318
+ isBack: e.detail.isCancel
36319
+ });
36347
36320
  // Reset so the next opening re-evaluates screen size
36348
36321
  defaultModeRef.current = null;
36349
36322
  restoreFocus(e);
@@ -36351,7 +36324,7 @@ const PickerCustom = props => {
36351
36324
  const disableClickFor = useIgnoreClickForMousedown(ref, e => {
36352
36325
  debugPopup(e, `click ignored`);
36353
36326
  });
36354
- const requestOpen = e => {
36327
+ const requestOpen = (e, detail) => {
36355
36328
  // scroll <button> of the picker into view when opening it
36356
36329
  const pickerEl = ref.current;
36357
36330
  pickerEl.scrollIntoView({
@@ -36359,18 +36332,47 @@ const PickerCustom = props => {
36359
36332
  });
36360
36333
  const popupEl = popupRef.current;
36361
36334
  return dispatchCustomEvent(popupEl, "navi_request_open", {
36362
- event: e
36335
+ event: e,
36336
+ ...detail
36363
36337
  });
36364
36338
  };
36365
- const requestClose = (e = new CustomEvent("programmatic"), {
36366
- isCancel = false
36367
- } = {}) => {
36339
+ const requestClose = (e = new CustomEvent("programmatic", {
36340
+ detail: {}
36341
+ }), detail) => {
36368
36342
  const popupEl = popupRef.current;
36369
36343
  return dispatchCustomEvent(popupEl, "navi_request_close", {
36370
36344
  event: e,
36371
- isCancel
36345
+ ...detail
36372
36346
  });
36373
36347
  };
36348
+ const open = Boolean(expanded);
36349
+ useLayoutEffect(() => {
36350
+ if (open === undefined) {
36351
+ return;
36352
+ }
36353
+ // Skip when the popup is already in the desired state.
36354
+ // expandedRef tracks actual open/close (updated by onOpen/onClose, not by renders)
36355
+ // so it is the authoritative check against feedback loops.
36356
+ if (open === expandedRef.current) {
36357
+ return;
36358
+ }
36359
+ // open_prop_change means the parent is driving the open state directly
36360
+ // (e.g. back-button navigation flipped openProp to false before onLeave fires).
36361
+ // Always treat it as cancel — the user's in-progress edit should be discarded.
36362
+ if (open) {
36363
+ requestOpen(new CustomEvent("open_by_prop", {
36364
+ detail: {}
36365
+ }), {
36366
+ isCancel: true
36367
+ });
36368
+ } else {
36369
+ requestClose(new CustomEvent("close_by_prop", {
36370
+ detail: {}
36371
+ }), {
36372
+ isCancel: true
36373
+ });
36374
+ }
36375
+ }, [open]);
36374
36376
  const requestInteraction = options => {
36375
36377
  dispatchRequestInteraction(ref.current, options);
36376
36378
  };
@@ -36412,16 +36414,9 @@ const PickerCustom = props => {
36412
36414
  });
36413
36415
  Object.assign(popupProps, {
36414
36416
  anchorRef: props.ref,
36415
- open: Boolean(expanded),
36416
- closeRequestHandler: (requestCloseEvent, closePermission, {
36417
- isClickOutside
36418
- } = {}) => {
36419
- const cancelEvent = findEvent(requestCloseEvent, eInChain => eInChain.type === "navi_request_close" && eInChain.detail.isCancel);
36420
- // open_prop_change means the parent is driving the open state directly
36421
- // (e.g. back-button navigation flipped openProp to false before onLeave fires).
36422
- // Always treat it as cancel — the user's in-progress edit should be discarded.
36423
- const isPropDrivenClose = requestCloseEvent.type === "open_prop_change";
36424
- const isCancel = isClickOutside || Boolean(cancelEvent) || isPropDrivenClose;
36417
+ requestClose,
36418
+ closeRequestHandler: (requestCloseEvent, closePermission) => {
36419
+ const isCancel = requestCloseEvent.detail.isCancel;
36425
36420
  if (isCancel) {
36426
36421
  const pickerEl = ref.current;
36427
36422
  const inputEl = getPickerInput(pickerEl);