@jsenv/navi 0.27.62 → 0.27.64

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.
@@ -6494,9 +6494,6 @@ const DIMENSION_PROPS = {
6494
6494
  if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
6495
6495
  return {
6496
6496
  alignSelf: "stretch",
6497
- // ensure we override any with: 'fit-content' for instance
6498
- // also useful for ".navi_text_sizer" to inherit the full size
6499
- width: "100%",
6500
6497
  };
6501
6498
  }
6502
6499
  // Can't use flexGrow — parent is not flex-x
@@ -6515,9 +6512,6 @@ const DIMENSION_PROPS = {
6515
6512
  if (parentBoxFlow === "flex-x" || parentBoxFlow === "inline-flex-x") {
6516
6513
  return {
6517
6514
  alignSelf: "stretch",
6518
- // ensure we override any with: 'fit-content' for instance
6519
- // also useful for ".navi_text_sizer" to inherit the full size
6520
- height: "100%",
6521
6515
  };
6522
6516
  }
6523
6517
  // Can't use flexGrow — parent is not flex-y
@@ -28315,6 +28309,7 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
28315
28309
  --button-color-disabled: var(--button-color-readonly);
28316
28310
 
28317
28311
  /* Here to be easy to override */
28312
+ display: inline-block; /* So box css can override when wanting to put button inline flex */
28318
28313
  font-size: var(--button-font-size);
28319
28314
  font-family: var(--button-font-family);
28320
28315
  }
@@ -28347,7 +28342,6 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
28347
28342
  touch-action: manipulation;
28348
28343
  user-select: none;
28349
28344
  -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
28350
- display: inline-flex;
28351
28345
 
28352
28346
  .navi_button_content {
28353
28347
  position: relative;
@@ -28535,6 +28529,7 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
28535
28529
  }
28536
28530
  &[data-icon] {
28537
28531
  --button-padding: 0;
28532
+ display: inline-flex;
28538
28533
  }
28539
28534
  /* cta: call-to-action — special background, border matches background */
28540
28535
  &[data-cta] {
@@ -28623,7 +28618,7 @@ const ButtonUI = props => {
28623
28618
  });
28624
28619
  return jsxs(Box, {
28625
28620
  inline: true,
28626
- flex: true,
28621
+ block: true,
28627
28622
  ...buttonControlRootProps,
28628
28623
  ...buttonControlHostProps,
28629
28624
  // eslint-disable-next-line react/no-children-prop
@@ -33658,13 +33653,16 @@ const css$x = /* css */`
33658
33653
  &::placeholder {
33659
33654
  color: var(--x-placeholder-color);
33660
33655
  }
33656
+ /* Webkit is putting a slight blue bckground on autofilled input */
33657
+ /* For now we override with out custom background color */
33658
+ /* Ideally we'll later provide a custom data attribute with ability to see styles when autofilled */
33661
33659
  &:-webkit-autofill {
33662
33660
  -webkit-box-shadow: 0 0 0 1000px var(--x-background-color) inset;
33663
33661
  }
33662
+ /* Webkit is putting some nasty styles after automplete that look as follow */
33663
+ /* input:-internal-autofill-selected { color: FieldText !important; } */
33664
+ /* Fortunately we can override it as follow */
33664
33665
  &:-internal-autofill-selected {
33665
- /* Webkit is putting some nasty styles after automplete that look as follow */
33666
- /* input:-internal-autofill-selected { color: FieldText !important; } */
33667
- /* Fortunately we can override it as follow */
33668
33666
  -webkit-text-fill-color: var(--x-color) !important;
33669
33667
  }
33670
33668
 
@@ -35123,7 +35121,10 @@ const useClipboardProps = groupRef => {
35123
35121
  if (!target.matches) {
35124
35122
  return false;
35125
35123
  }
35126
- if (!target.matches(".navi_duration_part")) {
35124
+ if (!target.matches(`[navi-control-host="input"`)) {
35125
+ return false;
35126
+ }
35127
+ if (!target.closest(".navi_duration_part")) {
35127
35128
  return false;
35128
35129
  }
35129
35130
  return true;
@@ -35541,29 +35542,6 @@ const transferFocus = (containerEl, debugFocus, e, fallback) => {
35541
35542
  }
35542
35543
  };
35543
35544
 
35544
- const useCleanup = () => {
35545
- const cleanupMethodsRef = useRef(null);
35546
- let cleanupMethods = cleanupMethodsRef.current;
35547
- if (!cleanupMethods) {
35548
- const [publish, subscribe, clear] = createPubSub();
35549
- const cleanup = () => {
35550
- publish();
35551
- clear();
35552
- };
35553
- const registerCleanup = (cb) => {
35554
- subscribe(cb);
35555
- };
35556
- cleanupMethodsRef.current = cleanupMethods = [registerCleanup, cleanup];
35557
- }
35558
- useLayoutEffect(() => {
35559
- return () => {
35560
- const [, cleanup] = cleanupMethods;
35561
- cleanup();
35562
- };
35563
- }, []);
35564
- return cleanupMethods;
35565
- };
35566
-
35567
35545
  installImportMetaCssBuild(import.meta);const css$r = /* css */`
35568
35546
  .navi_dialog {
35569
35547
  &[open] {
@@ -35585,14 +35563,12 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
35585
35563
  const Dialog = props => {
35586
35564
  import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
35587
35565
  const {
35588
- // requestOpen,
35589
- requestClose,
35566
+ openController,
35590
35567
  anchorRef,
35591
35568
  children,
35592
35569
  scrollTrap,
35593
35570
  pointerTrap,
35594
35571
  centerInVisualViewport: centerInVisualViewportProp,
35595
- closeRequestHandler,
35596
35572
  ...rest
35597
35573
  } = props;
35598
35574
  const defaultRef = useRef();
@@ -35601,16 +35577,20 @@ const Dialog = props => {
35601
35577
  const debugFocus = useDebugFocus();
35602
35578
  const autoFocusProps = useAutoFocus(ref, props.autoFocus);
35603
35579
 
35604
- // Tracks actual DOM state (open/closed), updated only by open() and close().
35605
- // Intentionally NOT synced to openProp on every render so the useEffect guard
35606
- // below can detect whether a prop change requires action.
35607
- const openedRef = useRef(false);
35608
- const [addCleanup, cleanup] = useCleanup();
35609
- const open = e => {
35580
+ // Sync the DOM open and return how to sync it back closed, fresh on every
35581
+ // render so it closes over the latest props (scrollTrap, etc.). The
35582
+ // controller (owned by picker_custom.jsx) decides *when* this runs.
35583
+ // openEffect runs outside of render (triggered by openController.open()), so
35584
+ // it cannot call hooks — cleanup is a plain pub/sub.
35585
+ openController.openEffect = e => {
35586
+ const dialogEl = ref.current;
35587
+ if (!dialogEl) {
35588
+ return undefined;
35589
+ }
35590
+ const [cleanup, addCleanup] = createPubSub(true);
35610
35591
  const anchor = anchorRef?.current ?? null;
35611
35592
  const effectiveAnchor = anchor || document.documentElement;
35612
35593
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
35613
- const dialogEl = ref.current;
35614
35594
  const {
35615
35595
  width,
35616
35596
  height
@@ -35658,22 +35638,15 @@ const Dialog = props => {
35658
35638
  dialogEl.style.removeProperty("--dialog-top-inset");
35659
35639
  });
35660
35640
  }
35661
- openedRef.current = true;
35662
- dispatchCustomEvent(dialogEl, "navi_open", {
35663
- event: e,
35664
- focusedBeforeOpen
35665
- });
35666
- };
35667
- const close = e => {
35668
- debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
35669
- const dialogEl = ref.current;
35670
- markAutofocusRestoreOnClose(dialogEl);
35671
- dialogEl.close();
35672
- cleanup();
35673
- openedRef.current = false;
35674
- dispatchCustomEvent(dialogEl, "navi_close", {
35675
- event: e
35676
- });
35641
+ // Picker's openController.open() reads this back synchronously right
35642
+ // after openEffect() returns (see picker_custom.jsx useOpenController).
35643
+ e.detail.focusedBeforeOpen = focusedBeforeOpen;
35644
+ return () => {
35645
+ debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
35646
+ markAutofocusRestoreOnClose(dialogEl);
35647
+ dialogEl.close();
35648
+ cleanup();
35649
+ };
35677
35650
  };
35678
35651
  return jsx(Box, {
35679
35652
  ...rest,
@@ -35691,58 +35664,17 @@ const Dialog = props => {
35691
35664
  const rect = ref.current.getBoundingClientRect();
35692
35665
  const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
35693
35666
  if (isBackdrop) {
35694
- requestClose(e, {
35667
+ openController.requestClose(e, {
35695
35668
  isCancel: true
35696
35669
  });
35697
35670
  }
35698
35671
  }
35699
35672
  },
35700
35673
  onCancel: e => {
35701
- requestClose(e, {
35674
+ openController.requestClose(e, {
35702
35675
  isCancel: true
35703
35676
  });
35704
35677
  },
35705
- onnavi_request_open: e => {
35706
- const dialogEl = ref.current;
35707
- if (!dialogEl) {
35708
- return;
35709
- }
35710
- if (openedRef.current) {
35711
- return;
35712
- }
35713
- open(e);
35714
- },
35715
- onnavi_request_close: e => {
35716
- const dialogEl = ref.current;
35717
- if (!dialogEl) {
35718
- return;
35719
- }
35720
- if (!openedRef.current) {
35721
- return;
35722
- }
35723
- if (closeRequestHandler) {
35724
- let denied = false;
35725
- const closePermission = {
35726
- deny: () => {
35727
- denied = true;
35728
- },
35729
- allow: () => {
35730
- denied = false;
35731
- }
35732
- };
35733
- closeRequestHandler(e, closePermission);
35734
- if (denied) {
35735
- if (e.type === "cancel") {
35736
- e.preventDefault();
35737
- }
35738
- closePermission.allow = () => {
35739
- close(e);
35740
- };
35741
- return;
35742
- }
35743
- }
35744
- close(e);
35745
- },
35746
35678
  children: children
35747
35679
  });
35748
35680
  };
@@ -35773,7 +35705,7 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
35773
35705
  const Popover = props => {
35774
35706
  import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
35775
35707
  const {
35776
- requestClose,
35708
+ openController,
35777
35709
  anchorRef,
35778
35710
  scrollTrap,
35779
35711
  pointerTrap,
@@ -35785,7 +35717,6 @@ const Popover = props => {
35785
35717
  positionYFixed,
35786
35718
  spacing = 0,
35787
35719
  viewportSpacing = 0,
35788
- closeRequestHandler,
35789
35720
  ...rest
35790
35721
  } = props;
35791
35722
  const defaultRef = useRef();
@@ -35796,14 +35727,18 @@ const Popover = props => {
35796
35727
  const debugFocus = useDebugFocus();
35797
35728
  const autoFocusProps = useAutoFocus(ref, props.autoFocus);
35798
35729
 
35799
- // Tracks actual DOM state (open/closed), updated only by open() and close().
35800
- // Intentionally NOT synced to openProp on every render so the useEffect guard
35801
- // below can detect whether a prop change requires action.
35802
- const openedRef = useRef(false);
35803
- const [addCleanup, cleanup] = useCleanup();
35804
- const open = e => {
35805
- debugPopup(e, `openPopover()`);
35730
+ // Sync the DOM open and return how to sync it back closed, fresh on every
35731
+ // render so it closes over the latest props (scrollTrap, etc.). The
35732
+ // controller (owned by picker_custom.jsx) decides *when* this runs.
35733
+ // openEffect runs outside of render (triggered by openController.open()), so
35734
+ // it cannot call hooks — cleanup is a plain pub/sub.
35735
+ openController.openEffect = e => {
35806
35736
  const popoverEl = ref.current;
35737
+ if (!popoverEl) {
35738
+ return undefined;
35739
+ }
35740
+ const [cleanup, addCleanup] = createPubSub(true);
35741
+ debugPopup(e, `openPopover()`);
35807
35742
  const focusedBeforeOpen = getFocusedBeforeTransfer(e);
35808
35743
  popoverEl.showPopover();
35809
35744
  transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
@@ -35879,22 +35814,15 @@ const Popover = props => {
35879
35814
  addCleanup(() => {
35880
35815
  rectEffect.disconnect();
35881
35816
  });
35882
- openedRef.current = true;
35883
- dispatchCustomEvent(popoverEl, "navi_open", {
35884
- event: e,
35885
- focusedBeforeOpen
35886
- });
35887
- };
35888
- const close = e => {
35889
- debugPopup(e, `closePopover()`);
35890
- const popoverEl = ref.current;
35891
- markAutofocusRestoreOnClose(popoverEl);
35892
- popoverEl.hidePopover();
35893
- cleanup();
35894
- openedRef.current = false;
35895
- dispatchCustomEvent(popoverEl, "navi_close", {
35896
- event: e
35897
- });
35817
+ // Picker's openController.open() reads this back synchronously right
35818
+ // after openEffect() returns (see picker_custom.jsx useOpenController).
35819
+ e.detail.focusedBeforeOpen = focusedBeforeOpen;
35820
+ return () => {
35821
+ debugPopup(e, `closePopover()`);
35822
+ markAutofocusRestoreOnClose(popoverEl);
35823
+ popoverEl.hidePopover();
35824
+ cleanup();
35825
+ };
35898
35826
  };
35899
35827
  return jsxs(Box, {
35900
35828
  id: id,
@@ -35904,44 +35832,6 @@ const Popover = props => {
35904
35832
  ref: ref,
35905
35833
  baseClassName: "navi_popover",
35906
35834
  pseudoClasses: POPOVER_PSEUDO_CLASSES,
35907
- onnavi_request_open: e => {
35908
- const popoverEl = ref.current;
35909
- if (!popoverEl) {
35910
- return;
35911
- }
35912
- if (openedRef.current) {
35913
- return;
35914
- }
35915
- open(e);
35916
- },
35917
- onnavi_request_close: e => {
35918
- const popoverEl = ref.current;
35919
- if (!popoverEl) {
35920
- return;
35921
- }
35922
- if (!openedRef.current) {
35923
- return;
35924
- }
35925
- if (closeRequestHandler) {
35926
- let denied = false;
35927
- const closePermission = {
35928
- deny: () => {
35929
- denied = true;
35930
- },
35931
- allow: () => {
35932
- denied = false;
35933
- }
35934
- };
35935
- closeRequestHandler(e, closePermission);
35936
- if (denied) {
35937
- closePermission.allow = () => {
35938
- close(e);
35939
- };
35940
- return;
35941
- }
35942
- }
35943
- close(e);
35944
- },
35945
35835
  children: [jsx("div", {
35946
35836
  className: "navi_popover_backdrop",
35947
35837
  "aria-hidden": "true",
@@ -35953,7 +35843,7 @@ const Popover = props => {
35953
35843
  e.preventDefault();
35954
35844
  return;
35955
35845
  }
35956
- requestClose(e, {
35846
+ openController.requestClose(e, {
35957
35847
  isCancel: true
35958
35848
  });
35959
35849
  }
@@ -36220,6 +36110,11 @@ const PickerCustom = props => {
36220
36110
  ref,
36221
36111
  mode: modeProp
36222
36112
  } = props;
36113
+ // Resolve the id the same way useControlProps does (own id > Field's id > generated id)
36114
+ // before computing popupId below, so two Pickers without an explicit id never collide.
36115
+ const idDefault = useId();
36116
+ const controlId = useContext(ControlIdContext);
36117
+ props.id = props.id || controlId || idDefault;
36223
36118
  // Freeze the mode for the lifetime of an opening: compute it when closed,
36224
36119
  // keep it stable while open so a screen resize mid-session doesn't switch
36225
36120
  // between Popover and Dialog.
@@ -36259,7 +36154,6 @@ const PickerCustom = props => {
36259
36154
  // In "dialog" mode, enterExpanded() pushes a history entry so the back button closes.
36260
36155
  // In "popover" mode, it replaces the current history state (no history entry added).
36261
36156
  const pickerNavType = mode === "dialog" ? "push" : "replace";
36262
- const expandedRef = useRef(false);
36263
36157
  const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, {
36264
36158
  type: pickerNavType,
36265
36159
  // onLeave fires only when the state key disappears externally (back button/gesture most of the time).
@@ -36271,95 +36165,108 @@ const PickerCustom = props => {
36271
36165
  });
36272
36166
  }
36273
36167
  });
36274
- // expandedRef tracks actual open/close state, updated only by onOpen/onClose.
36275
- // NOT synced to expanded on every render so the useLayoutEffect guard below works.
36276
- const valueAtOpenRef = useRef(null);
36277
- const activeElementAtOpenRef = useRef(null);
36278
- const onOpen = e => {
36279
- expandedRef.current = true;
36280
- enterExpanded();
36281
- const focusedBeforeOpen = e.detail.focusedBeforeOpen;
36282
- activeElementAtOpenRef.current = focusedBeforeOpen;
36283
- debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
36284
- const valueAtOpen = getPickerInputUIState(ref.current);
36285
- valueAtOpenRef.current = valueAtOpen;
36286
- debugPopup(e, `picker opened, store value at open`, valueAtOpen);
36287
- };
36288
- const restoreFocus = e => {
36289
- const activeElementAtOpen = activeElementAtOpenRef.current;
36290
- activeElementAtOpenRef.current = null;
36291
- const focusoutEvent = findEvent(e, "focusout");
36292
- if (focusoutEvent) {
36293
- debugFocus(e, `closed by focusout -> let focus go away`);
36294
- return;
36295
- }
36296
- const mousedownEvent = findEvent(e, "mousedown");
36297
- if (mousedownEvent) {
36298
- debugFocus(e, "closed by mousedown -> prevent browser focus (mousedown.preventDefault())");
36299
- mousedownEvent.preventDefault();
36300
- }
36301
- debugFocus(e, `restore focus to previously focused element`, activeElementAtOpen);
36302
- activeElementAtOpen.focus({
36303
- preventScroll: true
36304
- });
36305
- return;
36306
- };
36307
- const onClose = e => {
36308
- const mousedownEvent = findEvent(e, "mousedown");
36309
- if (mousedownEvent) {
36310
- debugPopup(e, `closed by mousedown -> disable next click`);
36311
- disableClickFor();
36312
- } else {
36313
- const spaceEvent = findEvent(e, e => e.type === "keydown" && e.key === " ");
36314
- if (spaceEvent) {
36315
- // space would trigger a click on the picker button causing it to re-open immediatly after closing
36316
- debugPopup(e, `closed by space key -> prevent browser click`);
36317
- // browser won't try to dispatch click
36318
- // and our "space_to_open" will see e.defaultPrevented too and won't try to open picker
36319
- spaceEvent.preventDefault();
36320
- }
36321
- }
36322
- expandedRef.current = false;
36323
- leaveExpanded({
36324
- isBack: e.detail.isCancel
36325
- });
36326
- // Reset so the next opening re-evaluates screen size
36327
- defaultModeRef.current = null;
36328
- restoreFocus(e);
36329
- };
36330
36168
  const disableClickFor = useIgnoreClickForMousedown(ref, e => {
36331
36169
  debugPopup(e, `click ignored`);
36332
36170
  });
36171
+ // openController centralizes open/close decision-making (validation,
36172
+ // focus and value bookkeeping) for the picker. The returned
36173
+ // { onRequestClose, onClose } pair is the picker's reaction to close
36174
+ // requests — see createOpenController below for the full contract.
36175
+ const openController = useOpenController(openEvent => {
36176
+ enterExpanded();
36177
+ const focusedBeforeOpen = openEvent.detail.focusedBeforeOpen;
36178
+ debugFocus(openEvent, "picked opened, store element focused", focusedBeforeOpen);
36179
+ const valueAtOpen = getPickerInputUIState(ref.current);
36180
+ debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen);
36181
+ return {
36182
+ onRequestClose: requestCloseEvent => {
36183
+ if (requestCloseEvent.detail.isCancel) {
36184
+ // Cancelling always succeeds — nothing to validate.
36185
+ return;
36186
+ }
36187
+ const pickerEl = ref.current;
36188
+ const inputEl = getPickerInput(pickerEl);
36189
+ dispatchRequestAction(inputEl, {
36190
+ event: requestCloseEvent,
36191
+ name: "picker request close",
36192
+ prevented: () => {
36193
+ requestCloseEvent.preventDefault();
36194
+ },
36195
+ // Always report validation when the picker tries to close so the
36196
+ // user sees what is wrong, even if the picker has no action prop.
36197
+ reportOnInvalid: true,
36198
+ onInvalid: () => {
36199
+ requestCloseEvent.preventDefault();
36200
+ }
36201
+ });
36202
+ },
36203
+ onClose: closeEvent => {
36204
+ if (closeEvent.detail.isCancel) {
36205
+ const pickerEl = ref.current;
36206
+ const inputEl = getPickerInput(pickerEl);
36207
+ debugPopup(closeEvent, `picker cancel, restoring value at open ${JSON.stringify(valueAtOpen)}`);
36208
+ dispatchRequestSetUIState(inputEl, valueAtOpen, {
36209
+ event: closeEvent
36210
+ });
36211
+ }
36212
+ {
36213
+ const mousedownEvent = findEvent(closeEvent, "mousedown");
36214
+ if (mousedownEvent) {
36215
+ debugPopup(closeEvent, `closed by mousedown -> disable next click`);
36216
+ disableClickFor();
36217
+ } else {
36218
+ const spaceEvent = findEvent(closeEvent, e => e.type === "keydown" && e.key === " ");
36219
+ if (spaceEvent) {
36220
+ // space would trigger a click on the picker button causing it to re-open immediatly after closing
36221
+ debugPopup(closeEvent, `closed by space key -> prevent browser click`);
36222
+ // browser won't try to dispatch click
36223
+ // and our "space_to_open" will see e.defaultPrevented too and won't try to open picker
36224
+ spaceEvent.preventDefault();
36225
+ }
36226
+ }
36227
+ }
36228
+ {
36229
+ const focusoutEvent = findEvent(closeEvent, "focusout");
36230
+ if (focusoutEvent) {
36231
+ debugFocus(closeEvent, `closed by focusout -> let focus go away`);
36232
+ } else {
36233
+ const mousedownEvent = findEvent(closeEvent, "mousedown");
36234
+ if (mousedownEvent) {
36235
+ debugFocus(closeEvent, "closed by mousedown -> prevent browser focus (mousedown.preventDefault())");
36236
+ mousedownEvent.preventDefault();
36237
+ }
36238
+ debugFocus(closeEvent, `restore focus to previously focused element`, focusedBeforeOpen);
36239
+ focusedBeforeOpen.focus({
36240
+ preventScroll: true
36241
+ });
36242
+ }
36243
+ }
36244
+ leaveExpanded({
36245
+ isBack: closeEvent.detail.isCancel
36246
+ });
36247
+ // Reset so the next opening re-evaluates screen size
36248
+ defaultModeRef.current = null;
36249
+ }
36250
+ };
36251
+ });
36333
36252
  const requestOpen = (e, detail) => {
36334
36253
  // scroll <button> of the picker into view when opening it
36335
36254
  const pickerEl = ref.current;
36336
36255
  pickerEl.scrollIntoView({
36337
36256
  block: "nearest"
36338
36257
  });
36339
- const popupEl = popupRef.current;
36340
- return dispatchCustomEvent(popupEl, "navi_request_open", {
36341
- event: e,
36342
- ...detail
36343
- });
36344
- };
36345
- const requestClose = (e = new CustomEvent("programmatic", {
36346
- detail: {}
36347
- }), detail) => {
36348
- const popupEl = popupRef.current;
36349
- return dispatchCustomEvent(popupEl, "navi_request_close", {
36350
- event: e,
36351
- ...detail
36352
- });
36258
+ openController.open(e, detail);
36353
36259
  };
36260
+ const requestClose = openController.requestClose;
36354
36261
  const open = Boolean(expanded);
36355
36262
  useLayoutEffect(() => {
36356
36263
  if (open === undefined) {
36357
36264
  return;
36358
36265
  }
36359
36266
  // Skip when the popup is already in the desired state.
36360
- // expandedRef tracks actual open/close (updated by onOpen/onClose, not by renders)
36361
- // so it is the authoritative check against feedback loops.
36362
- if (open === expandedRef.current) {
36267
+ // openController.opened tracks actual open/close (updated by onopen/onclose,
36268
+ // not by renders) so it is the authoritative check against feedback loops.
36269
+ if (open === openController.opened) {
36363
36270
  return;
36364
36271
  }
36365
36272
  // open_prop_change means the parent is driving the open state directly
@@ -36394,7 +36301,7 @@ const PickerCustom = props => {
36394
36301
  // requestClose(e);
36395
36302
  },
36396
36303
  "onnavi_request_open": e => {
36397
- if (expandedRef.current) {
36304
+ if (openController.opened) {
36398
36305
  return;
36399
36306
  }
36400
36307
  requestInteraction({
@@ -36409,7 +36316,9 @@ const PickerCustom = props => {
36409
36316
  requestInteraction({
36410
36317
  event: e,
36411
36318
  allowed: () => {
36412
- requestClose(e);
36319
+ requestClose(e, {
36320
+ isCancel: e.detail.isCancel
36321
+ });
36413
36322
  }
36414
36323
  });
36415
36324
  },
@@ -36420,41 +36329,7 @@ const PickerCustom = props => {
36420
36329
  });
36421
36330
  Object.assign(popupProps, {
36422
36331
  anchorRef: props.ref,
36423
- requestClose,
36424
- closeRequestHandler: (requestCloseEvent, closePermission) => {
36425
- const isCancel = requestCloseEvent.detail.isCancel;
36426
- if (isCancel) {
36427
- const pickerEl = ref.current;
36428
- const inputEl = getPickerInput(pickerEl);
36429
- const valueAtOpen = valueAtOpenRef.current;
36430
- debugPopup(requestCloseEvent, `picker cancel, restoring value at open ${JSON.stringify(valueAtOpen)}`);
36431
- dispatchRequestSetUIState(inputEl, valueAtOpen, {
36432
- event: requestCloseEvent
36433
- });
36434
- return;
36435
- }
36436
- const pickerEl = ref.current;
36437
- const inputEl = getPickerInput(pickerEl);
36438
- dispatchRequestAction(inputEl, {
36439
- event: requestCloseEvent,
36440
- name: "picker close",
36441
- prevented: () => {
36442
- closePermission.deny();
36443
- },
36444
- // Always report validation when the picker tries to close so the
36445
- // user sees what is wrong, even if the picker has no action prop.
36446
- reportOnInvalid: true,
36447
- onInvalid: () => {
36448
- closePermission.deny();
36449
- }
36450
- });
36451
- },
36452
- onnavi_open: e => {
36453
- onOpen(e);
36454
- },
36455
- onnavi_close: e => {
36456
- onClose(e);
36457
- }
36332
+ openController
36458
36333
  });
36459
36334
  {
36460
36335
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
@@ -36511,7 +36386,7 @@ const PickerCustom = props => {
36511
36386
  };
36512
36387
  },
36513
36388
  "escape": e => {
36514
- if (!expandedRef.current) {
36389
+ if (!openController.opened) {
36515
36390
  return null;
36516
36391
  }
36517
36392
  return {
@@ -36535,7 +36410,7 @@ const PickerCustom = props => {
36535
36410
  if (isWithinPopup(e.target)) {
36536
36411
  return null;
36537
36412
  }
36538
- if (expandedRef.current) {
36413
+ if (openController.opened) {
36539
36414
  return {
36540
36415
  name: "mousedown to close picker",
36541
36416
  allowed: () => requestClose(e, {
@@ -36631,8 +36506,7 @@ const PickerContentInsidePopover = props => {
36631
36506
  name: "blur",
36632
36507
  category: "interaction",
36633
36508
  allowed: () => {
36634
- dispatchCustomEvent(popoverEl, "navi_request_close", {
36635
- event: e,
36509
+ popupProps.openController.requestClose(e, {
36636
36510
  isCancel: true
36637
36511
  });
36638
36512
  }
@@ -36657,9 +36531,7 @@ const PickerContentInsidePopover = props => {
36657
36531
  if (e.button !== 0) {
36658
36532
  return;
36659
36533
  }
36660
- const popupEl = popupProps.ref.current;
36661
- dispatchCustomEvent(popupEl, "navi_request_close", {
36662
- event: e,
36534
+ popupProps.openController.requestClose(e, {
36663
36535
  isCancel: true
36664
36536
  });
36665
36537
  },
@@ -36744,6 +36616,142 @@ const useIgnoreClickForMousedown = (elementRef, onIgnore) => {
36744
36616
  return disableClickFor;
36745
36617
  };
36746
36618
 
36619
+ // Created once per picker instance: openHandler is wrapped in a stable callback
36620
+ // so the controller identity never changes across renders, even though
36621
+ // Dialog/Popover read fresh closures (scrollTrap, etc.) via
36622
+ // openController.openEffect on every render.
36623
+ const useOpenController = openHandler => {
36624
+ const stableOpenHandler = useStableCallback(openHandler);
36625
+ const controllerRef = useRef(null);
36626
+ if (!controllerRef.current) {
36627
+ controllerRef.current = createOpenController(stableOpenHandler);
36628
+ }
36629
+ // Unmount safety net: if Dialog/Popover unmounts while still open (parent
36630
+ // removes it from the tree without going through requestClose()), there is
36631
+ // no choice to leave open — close it for real.
36632
+ useLayoutEffect(() => {
36633
+ return () => {
36634
+ controllerRef.current.close();
36635
+ };
36636
+ }, []);
36637
+ return controllerRef.current;
36638
+ };
36639
+
36640
+ /**
36641
+ * Owns open/close decision-making for a picker popup (Dialog or Popover):
36642
+ * guards against duplicate requests and notifies the picker's own reactions.
36643
+ *
36644
+ * `controller.openEffect` is implemented by the controlled element (Dialog or
36645
+ * Popover), reassigned on every render so it always closes over the latest
36646
+ * props (scrollTrap, anchorRef, etc.). It performs whatever DOM side effects
36647
+ * are needed to make the element actually open (`showModal()`/`showPopover()`,
36648
+ * focus transfer, positioning, traps...) and returns its cleanup —
36649
+ * the matching side effects to sync back to closed (`close()`/
36650
+ * `hidePopover()`, releasing traps...). That cleanup is kept private to the
36651
+ * controller (not exposed as a property) and invoked when the popup actually
36652
+ * closes, however that happens.
36653
+ *
36654
+ * Dialog/Popover also call `openController.requestClose(e, { isCancel })` for
36655
+ * their own internal triggers (backdrop click, Escape).
36656
+ *
36657
+ * `openHandler` is the picker's own business logic, passed once to
36658
+ * `createOpenController`. Its return value is `{ onRequestClose, onClose }`,
36659
+ * in the spirit of CloseWatcher
36660
+ * (https://developer.mozilla.org/en-US/docs/Web/API/CloseWatcher) but with
36661
+ * clearer naming than its cancel/close pair:
36662
+ * - `onRequestClose(e)`: about to close — call `e.preventDefault()` to stay
36663
+ * open. Validation lives here.
36664
+ * - `onClose(e)`: actually closing, not preventable — final reactions live here.
36665
+ *
36666
+ * The controller exposes matching action methods:
36667
+ * - `open()`: requests opening — runs `openEffect`, then `openHandler`.
36668
+ * - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
36669
+ * stopping after the first if denied. The popup may choose to stay open.
36670
+ * - `close()`: closes for real — calls only `onClose`, skipping
36671
+ * `onRequestClose` entirely. Used when there really is no choice (e.g. the
36672
+ * popup unmounting).
36673
+ */
36674
+ const createOpenController = openHandler => {
36675
+ let closeHandlers = null; // { onRequestClose, onClose } returned by openHandler
36676
+ let openEffectCleanup = null; // function returned by openEffect, undoes its DOM side effects
36677
+ const performClose = closeEvent => {
36678
+ controller.opened = false;
36679
+ // Sync the DOM closed first (releasing the focus trap) — only then run
36680
+ // the picker's own reaction (onClose may restore focus to an element
36681
+ // outside the popup, which the focus trap would otherwise fight while
36682
+ // still active).
36683
+ openEffectCleanup?.(closeEvent);
36684
+ openEffectCleanup = null;
36685
+ closeHandlers?.onClose?.(closeEvent);
36686
+ closeHandlers = null;
36687
+ };
36688
+ const controller = {
36689
+ opened: false,
36690
+ openEffect: null,
36691
+ open: (e, detail) => {
36692
+ if (controller.opened || !controller.openEffect) {
36693
+ return;
36694
+ }
36695
+ const requestOpenEvent = new CustomEvent("navi_request_open", {
36696
+ detail: {
36697
+ event: e,
36698
+ ...detail
36699
+ },
36700
+ cancelable: true
36701
+ });
36702
+ chainEvent(requestOpenEvent, e);
36703
+ controller.opened = true;
36704
+ // openEffect may populate requestOpenEvent.detail (e.g. focusedBeforeOpen)
36705
+ // by mutating it — openHandler reads it right after, synchronously.
36706
+ openEffectCleanup = controller.openEffect(requestOpenEvent) || null;
36707
+ closeHandlers = openHandler(requestOpenEvent) || null;
36708
+ },
36709
+ requestClose: (e = new CustomEvent("programmatic", {
36710
+ detail: {}
36711
+ }), detail) => {
36712
+ if (!controller.opened) {
36713
+ return;
36714
+ }
36715
+ const requestCloseEvent = new CustomEvent("navi_request_close", {
36716
+ detail: {
36717
+ event: e,
36718
+ ...detail
36719
+ },
36720
+ cancelable: true
36721
+ });
36722
+ chainEvent(requestCloseEvent, e);
36723
+ closeHandlers?.onRequestClose?.(requestCloseEvent);
36724
+ if (requestCloseEvent.defaultPrevented) {
36725
+ // The native <dialog> "cancel" event (Escape key) closes the dialog
36726
+ // by default; prevent that default so denial actually keeps it open.
36727
+ const nativeCancelEvent = findEvent(requestCloseEvent, "cancel");
36728
+ if (nativeCancelEvent) {
36729
+ nativeCancelEvent.preventDefault();
36730
+ }
36731
+ return;
36732
+ }
36733
+ performClose(requestCloseEvent);
36734
+ },
36735
+ close: (e = new CustomEvent("programmatic", {
36736
+ detail: {}
36737
+ }), detail) => {
36738
+ if (!controller.opened) {
36739
+ return;
36740
+ }
36741
+ const closeEvent = new CustomEvent("navi_close", {
36742
+ detail: {
36743
+ event: e,
36744
+ ...detail
36745
+ }
36746
+ });
36747
+ chainEvent(closeEvent, e);
36748
+ // Skips onRequestClose entirely — there is no choice here.
36749
+ performClose(closeEvent);
36750
+ }
36751
+ };
36752
+ return controller;
36753
+ };
36754
+
36747
36755
  const PickerNaviMinute = props => {
36748
36756
  const Next = useNextResolver();
36749
36757
  const {
@@ -39954,7 +39962,7 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
39954
39962
  --x-color: var(--color, white);
39955
39963
 
39956
39964
  position: relative;
39957
- display: inline-flex;
39965
+ display: inline;
39958
39966
  max-width: 200px;
39959
39967
  padding-top: var(--padding-y);
39960
39968
  padding-right: var(--padding-x);
@@ -39963,7 +39971,6 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
39963
39971
  align-items: stretch;
39964
39972
  color: var(--x-color);
39965
39973
  font-size: var(--font-size);
39966
- line-height: normal;
39967
39974
  background: var(--x-background);
39968
39975
  background-color: var(--x-background-color);
39969
39976
  border-radius: 1em;
@@ -39974,7 +39981,7 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
39974
39981
  }
39975
39982
 
39976
39983
  &[data-text-overflow] {
39977
- display: flex;
39984
+ display: inline;
39978
39985
 
39979
39986
  .navi_text_overflow_wrapper {
39980
39987
  /* Keep badge text and button together */
@@ -39986,10 +39993,13 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
39986
39993
  display: inline-flex;
39987
39994
  margin-top: calc(-1 * var(--padding-y));
39988
39995
  margin-bottom: calc(-1 * var(--padding-y));
39996
+ padding-top: var(--padding-y);
39989
39997
  padding-right: calc(var(--padding-x) / 2);
39998
+ padding-bottom: var(--padding-y);
39990
39999
  padding-left: calc(var(--padding-x) / 2);
39991
40000
  align-items: center;
39992
40001
  cursor: pointer;
40002
+ pointer-events: auto;
39993
40003
  user-select: none;
39994
40004
 
39995
40005
  &:first-child {
@@ -40101,6 +40111,11 @@ const BadgeList = ({
40101
40111
  const measure = () => {
40102
40112
  visibleEl.style.width = "";
40103
40113
  if (shrinkWrap) {
40114
+ // Clone the already-rendered DOM nodes instead of letting React/Preact
40115
+ // render the children a second time into the ghost: re-rendering would
40116
+ // instantiate Badge/Badge.Button a second time, double-registering
40117
+ // their controllers (and any other mount side effect) under the same id.
40118
+ measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
40104
40119
  const optimalWidth = measureWidestChildRow(measureEl);
40105
40120
  if (optimalWidth !== null) {
40106
40121
  visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
@@ -40142,8 +40157,7 @@ const BadgeList = ({
40142
40157
  ...sharedProps,
40143
40158
  ref: measureRef,
40144
40159
  "aria-hidden": "true",
40145
- "navi-badge-list-clone": "",
40146
- children: childArray
40160
+ "navi-badge-list-clone": ""
40147
40161
  }), jsxs(Box, {
40148
40162
  baseClassName: "navi_badge_list",
40149
40163
  ...sharedProps,