@jsenv/navi 0.24.0 → 0.24.2

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.
@@ -7226,6 +7226,12 @@ const PSEUDO_CLASSES = {
7226
7226
  attribute: "data-invalid",
7227
7227
  test: (el) => el.matches(":invalid"),
7228
7228
  },
7229
+ ":-navi-highlighted": {
7230
+ attribute: "data-highlighted",
7231
+ },
7232
+ ":-navi-selected": {
7233
+ attribute: "data-selected",
7234
+ },
7229
7235
  ":-navi-loading": {
7230
7236
  attribute: "data-loading",
7231
7237
  },
@@ -15782,7 +15788,7 @@ installImportMetaCssBuild(import.meta);
15782
15788
  * - Centers in viewport when no anchor element provided or anchor is too big
15783
15789
  */
15784
15790
 
15785
- import.meta.css = [/* css */`
15791
+ const css$w = /* css */`
15786
15792
  @layer navi {
15787
15793
  .navi_callout {
15788
15794
  --callout-success-color: #4caf50;
@@ -15924,7 +15930,7 @@ import.meta.css = [/* css */`
15924
15930
  }
15925
15931
  }
15926
15932
  }
15927
- `, "@jsenv/navi/src/field/validation/callout/callout.js"];
15933
+ `;
15928
15934
 
15929
15935
  /**
15930
15936
  * Shows a callout attached to the specified element
@@ -15956,6 +15962,7 @@ const openCallout = (message, {
15956
15962
  showErrorStack,
15957
15963
  debug = false
15958
15964
  } = {}) => {
15965
+ import.meta.css = [css$w, "@jsenv/navi/src/field/validation/callout/callout.js"];
15959
15966
  const callout = {
15960
15967
  opened: true,
15961
15968
  close: null,
@@ -16115,8 +16122,14 @@ const openCallout = (message, {
16115
16122
  }
16116
16123
  allowWheelThrough(calloutElement, anchorElement);
16117
16124
  anchorElement.setAttribute("data-callout", calloutId);
16125
+ dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_open", {
16126
+ bubbles: true
16127
+ }));
16118
16128
  addTeardown(() => {
16119
16129
  anchorElement.removeAttribute("data-callout");
16130
+ dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_close", {
16131
+ bubbles: true
16132
+ }));
16120
16133
  });
16121
16134
  addStatusEffect(status => {
16122
16135
  if (!status) {
@@ -16747,6 +16760,20 @@ const generateSvgWithoutArrow = (width, height) => {
16747
16760
  />
16748
16761
  </svg>`;
16749
16762
  };
16763
+ const dispatchCalloutCustomElement = (anchorElement, customEvent) => {
16764
+ let targetElement;
16765
+ const visualSelector = anchorElement.getAttribute("data-visual-selector");
16766
+ if (visualSelector) {
16767
+ const visualElement = anchorElement.querySelector(visualSelector);
16768
+ if (visualElement) {
16769
+ targetElement = visualElement;
16770
+ }
16771
+ } else {
16772
+ targetElement = anchorElement;
16773
+ }
16774
+ console.log("dispatch on", targetElement, "event", customEvent);
16775
+ targetElement.dispatchEvent(customEvent);
16776
+ };
16750
16777
 
16751
16778
  /**
16752
16779
  * Creates a live mirror of a source DOM element that automatically stays in sync.
@@ -17122,6 +17149,56 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-special-charset");
17122
17149
  CONSTRAINT_ATTRIBUTE_SET.add("data-min-special-char");
17123
17150
  CONSTRAINT_ATTRIBUTE_SET.add("data-min-special-char-message");
17124
17151
 
17152
+ const ONE_OF_CONSTRAINT = {
17153
+ name: "one_of",
17154
+ messageAttribute: "data-one-of-message",
17155
+ check: (field) => {
17156
+ const oneOf = field.getAttribute("data-one-of");
17157
+ if (!oneOf) {
17158
+ return null;
17159
+ }
17160
+ const fieldValue = field.value;
17161
+ if (!fieldValue) {
17162
+ return null;
17163
+ }
17164
+ const listEl = document.querySelector(oneOf);
17165
+ if (!listEl) {
17166
+ console.warn(
17167
+ `One of constraint: could not find element for selector "${oneOf}"`,
17168
+ );
17169
+ return null;
17170
+ }
17171
+ const allowedValues = collectAllowedValues(listEl);
17172
+ if (allowedValues.size === 0) {
17173
+ return null;
17174
+ }
17175
+ if (allowedValues.has(fieldValue)) {
17176
+ return null;
17177
+ }
17178
+ const message = field.getAttribute("data-one-of-message");
17179
+ if (message) {
17180
+ return message;
17181
+ }
17182
+ return `Veuillez choisir une valeur parmi les suggestions.`;
17183
+ },
17184
+ };
17185
+ CONSTRAINT_ATTRIBUTE_SET.add("data-one-of");
17186
+ CONSTRAINT_ATTRIBUTE_SET.add("data-one-of-message");
17187
+
17188
+ const collectAllowedValues = (listEl) => {
17189
+ const values = new Set();
17190
+ for (const optionEl of listEl.querySelectorAll("[role='option']")) {
17191
+ const value =
17192
+ optionEl.dataset.value ??
17193
+ optionEl.getAttribute("value") ??
17194
+ optionEl.textContent.trim();
17195
+ if (value) {
17196
+ values.add(value);
17197
+ }
17198
+ }
17199
+ return values;
17200
+ };
17201
+
17125
17202
  const READONLY_CONSTRAINT = {
17126
17203
  name: "readonly",
17127
17204
  messageAttribute: "data-readonly-message",
@@ -17874,6 +17951,7 @@ const NAVI_CONSTRAINT_SET = new Set([
17874
17951
  MIN_UPPER_LETTER_CONSTRAINT,
17875
17952
  MIN_LOWER_LETTER_CONSTRAINT,
17876
17953
  SAME_AS_CONSTRAINT,
17954
+ ONE_OF_CONSTRAINT,
17877
17955
  READONLY_CONSTRAINT,
17878
17956
  ]);
17879
17957
  const DEFAULT_CONSTRAINT_SET = new Set([
@@ -20292,7 +20370,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
20292
20370
  };
20293
20371
 
20294
20372
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
20295
- const css$u = /* css */`
20373
+ const css$v = /* css */`
20296
20374
  @layer navi {
20297
20375
  .navi_text {
20298
20376
  &[data-skeleton] {
@@ -20601,7 +20679,7 @@ const shouldInjectSpacingBetween = (left, right) => {
20601
20679
  };
20602
20680
  const OverflowPinnedElementContext = createContext(null);
20603
20681
  const Text = props => {
20604
- import.meta.css = [css$u, "@jsenv/navi/src/text/text.jsx"];
20682
+ import.meta.css = [css$v, "@jsenv/navi/src/text/text.jsx"];
20605
20683
  if (props.loading || props.skeleton) {
20606
20684
  return jsx(TextSkeleton, {
20607
20685
  ...props
@@ -20797,7 +20875,7 @@ const TextBasic = ({
20797
20875
  });
20798
20876
  };
20799
20877
 
20800
- installImportMetaCssBuild(import.meta);const css$t = /* css */`
20878
+ installImportMetaCssBuild(import.meta);const css$u = /* css */`
20801
20879
  .navi_text_anchor {
20802
20880
  vertical-align: baseline;
20803
20881
  user-select: none;
@@ -20832,7 +20910,7 @@ const TextAnchor = ({
20832
20910
  textSize,
20833
20911
  lineLayout
20834
20912
  }) => {
20835
- import.meta.css = [css$t, "@jsenv/navi/src/text/text_anchor.jsx"];
20913
+ import.meta.css = [css$u, "@jsenv/navi/src/text/text_anchor.jsx"];
20836
20914
  const anchorRef = useRef();
20837
20915
  useLayoutEffect(() => {
20838
20916
  const anchorEl = anchorRef.current;
@@ -20927,7 +21005,7 @@ const computeTopOffset = ({
20927
21005
  };
20928
21006
  const charTopCanvas = document.createElement("canvas");
20929
21007
 
20930
- installImportMetaCssBuild(import.meta);const css$s = /* css */`
21008
+ installImportMetaCssBuild(import.meta);const css$t = /* css */`
20931
21009
  @layer navi {
20932
21010
  /* Ensure data attributes from box.jsx can win to update display */
20933
21011
  .navi_icon {
@@ -21000,7 +21078,7 @@ const Icon = ({
21000
21078
  lineLayout,
21001
21079
  ...props
21002
21080
  }) => {
21003
- import.meta.css = [css$s, "@jsenv/navi/src/text/icon.jsx"];
21081
+ import.meta.css = [css$t, "@jsenv/navi/src/text/icon.jsx"];
21004
21082
  const innerChildren = href ? jsx("svg", {
21005
21083
  width: "100%",
21006
21084
  height: "100%",
@@ -21706,7 +21784,7 @@ const useUIState = (uiStateController) => {
21706
21784
  };
21707
21785
 
21708
21786
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
21709
- const css$r = /* css */`
21787
+ const css$s = /* css */`
21710
21788
  @layer navi {
21711
21789
  .navi_button {
21712
21790
  --button-outline-width: 1px;
@@ -21972,7 +22050,7 @@ const css$r = /* css */`
21972
22050
  }
21973
22051
  `;
21974
22052
  const Button = props => {
21975
- import.meta.css = [css$r, "@jsenv/navi/src/field/button.jsx"];
22053
+ import.meta.css = [css$s, "@jsenv/navi/src/field/button.jsx"];
21976
22054
  return renderActionableComponent(props, {
21977
22055
  Basic: ButtonBasicDispatch,
21978
22056
  WithAction: ButtonWithAction,
@@ -22504,7 +22582,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
22504
22582
  };
22505
22583
 
22506
22584
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
22507
- const css$q = /* css */`
22585
+ const css$r = /* css */`
22508
22586
  @layer navi {
22509
22587
  .navi_link {
22510
22588
  --link-border-radius: unset;
@@ -22576,10 +22654,22 @@ const css$q = /* css */`
22576
22654
 
22577
22655
  position: relative;
22578
22656
  aspect-ratio: inherit;
22579
- padding-top: max(var(--x-link-padding-top), var(--link-loading-outline-size));
22580
- padding-right: max(var(--x-link-padding-right), var(--link-loading-outline-size));
22581
- padding-bottom: max(var(--x-link-padding-bottom), var(--link-loading-outline-size));
22582
- padding-left: max(var(--x-link-padding-left), var(--link-loading-outline-size));
22657
+ padding-top: max(
22658
+ var(--x-link-padding-top),
22659
+ var(--link-loading-outline-size)
22660
+ );
22661
+ padding-right: max(
22662
+ var(--x-link-padding-right),
22663
+ var(--link-loading-outline-size)
22664
+ );
22665
+ padding-bottom: max(
22666
+ var(--x-link-padding-bottom),
22667
+ var(--link-loading-outline-size)
22668
+ );
22669
+ padding-left: max(
22670
+ var(--x-link-padding-left),
22671
+ var(--link-loading-outline-size)
22672
+ );
22583
22673
  color: var(--x-link-color);
22584
22674
  text-decoration: var(--x-link-text-decoration);
22585
22675
  background: var(--x-link-background);
@@ -22846,13 +22936,10 @@ Object.assign(PSEUDO_CLASSES, {
22846
22936
  },
22847
22937
  ":-navi-href-current": {
22848
22938
  attribute: "data-href-current"
22849
- },
22850
- ":-navi-selected": {
22851
- attribute: "data-selected"
22852
22939
  }
22853
22940
  });
22854
22941
  const Link = props => {
22855
- import.meta.css = [css$q, "@jsenv/navi/src/nav/link/link.jsx"];
22942
+ import.meta.css = [css$r, "@jsenv/navi/src/nav/link/link.jsx"];
22856
22943
  return renderActionableComponent(props, {
22857
22944
  Basic: LinkBasic,
22858
22945
  WithAction: LinkWithAction
@@ -23114,7 +23201,7 @@ installImportMetaCssBuild(import.meta);/**
23114
23201
  * TabList component with support for horizontal and vertical layouts
23115
23202
  * https://dribbble.com/search/tabs
23116
23203
  */
23117
- const css$p = /* css */`
23204
+ const css$q = /* css */`
23118
23205
  @layer navi {
23119
23206
  .navi_nav {
23120
23207
  --nav-border: none;
@@ -23250,7 +23337,7 @@ const Nav = ({
23250
23337
  panelBorderConnection,
23251
23338
  ...props
23252
23339
  }) => {
23253
- import.meta.css = [css$p, "@jsenv/navi/src/nav/link/nav.jsx"];
23340
+ import.meta.css = [css$q, "@jsenv/navi/src/nav/link/nav.jsx"];
23254
23341
  children = toChildArray(children);
23255
23342
  return jsx(Box, {
23256
23343
  as: "nav",
@@ -23298,7 +23385,7 @@ const useFocusGroup = (
23298
23385
 
23299
23386
  installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
23300
23387
  const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
23301
- const css$o = /* css */`
23388
+ const css$p = /* css */`
23302
23389
  .navi_summary_marker {
23303
23390
  width: 1em;
23304
23391
  height: 1em;
@@ -23383,7 +23470,7 @@ const SummaryMarker = ({
23383
23470
  open,
23384
23471
  loading
23385
23472
  }) => {
23386
- import.meta.css = [css$o, "@jsenv/navi/src/field/details/summary_marker.jsx"];
23473
+ import.meta.css = [css$p, "@jsenv/navi/src/field/details/summary_marker.jsx"];
23387
23474
  const showLoading = useDebounceTrue(loading, 300);
23388
23475
  const mountedRef = useRef(false);
23389
23476
  const prevOpenRef = useRef(open);
@@ -23437,7 +23524,7 @@ const SummaryMarker = ({
23437
23524
  });
23438
23525
  };
23439
23526
 
23440
- installImportMetaCssBuild(import.meta);const css$n = /* css */`
23527
+ installImportMetaCssBuild(import.meta);const css$o = /* css */`
23441
23528
  .navi_details {
23442
23529
  position: relative;
23443
23530
  z-index: 1;
@@ -23474,7 +23561,7 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
23474
23561
  }
23475
23562
  `;
23476
23563
  const Details = props => {
23477
- import.meta.css = [css$n, "@jsenv/navi/src/field/details/details.jsx"];
23564
+ import.meta.css = [css$o, "@jsenv/navi/src/field/details/details.jsx"];
23478
23565
  const {
23479
23566
  value = "on",
23480
23567
  persists
@@ -23789,7 +23876,7 @@ const fieldPropSet = new Set([
23789
23876
  "data-testid",
23790
23877
  ]);
23791
23878
 
23792
- installImportMetaCssBuild(import.meta);const css$m = /* css */`
23879
+ installImportMetaCssBuild(import.meta);const css$n = /* css */`
23793
23880
  @layer navi {
23794
23881
  label {
23795
23882
  &[data-interactive] {
@@ -23821,7 +23908,7 @@ const reportDisabledToLabel = value => {
23821
23908
  };
23822
23909
  const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
23823
23910
  const Label = props => {
23824
- import.meta.css = [css$m, "@jsenv/navi/src/field/label.jsx"];
23911
+ import.meta.css = [css$n, "@jsenv/navi/src/field/label.jsx"];
23825
23912
  const {
23826
23913
  readOnly,
23827
23914
  disabled,
@@ -23855,7 +23942,7 @@ const Label = props => {
23855
23942
  });
23856
23943
  };
23857
23944
 
23858
- installImportMetaCssBuild(import.meta);const css$l = /* css */`
23945
+ installImportMetaCssBuild(import.meta);const css$m = /* css */`
23859
23946
  @layer navi {
23860
23947
  .navi_checkbox {
23861
23948
  --margin: 3px 3px 3px 4px;
@@ -24182,7 +24269,7 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
24182
24269
  }
24183
24270
  `;
24184
24271
  const InputCheckbox = props => {
24185
- import.meta.css = [css$l, "@jsenv/navi/src/field/input_checkbox.jsx"];
24272
+ import.meta.css = [css$m, "@jsenv/navi/src/field/input_checkbox.jsx"];
24186
24273
  const {
24187
24274
  value = "on"
24188
24275
  } = props;
@@ -24596,7 +24683,7 @@ forwardRef((props, ref) => {
24596
24683
  });
24597
24684
  });
24598
24685
 
24599
- installImportMetaCssBuild(import.meta);const css$k = /* css */`
24686
+ installImportMetaCssBuild(import.meta);const css$l = /* css */`
24600
24687
  @layer navi {
24601
24688
  .navi_radio {
24602
24689
  --margin: 3px 3px 0 5px;
@@ -24889,7 +24976,7 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
24889
24976
  }
24890
24977
  `;
24891
24978
  const InputRadio = props => {
24892
- import.meta.css = [css$k, "@jsenv/navi/src/field/input_radio.jsx"];
24979
+ import.meta.css = [css$l, "@jsenv/navi/src/field/input_radio.jsx"];
24893
24980
  const {
24894
24981
  value = "on"
24895
24982
  } = props;
@@ -25135,7 +25222,7 @@ const InputRadioWithAction = () => {
25135
25222
  throw new Error(`<Input type="radio" /> with an action make no sense. Use <RadioList action={something} /> instead`);
25136
25223
  };
25137
25224
 
25138
- installImportMetaCssBuild(import.meta);const css$j = /* css */`
25225
+ installImportMetaCssBuild(import.meta);const css$k = /* css */`
25139
25226
  @layer navi {
25140
25227
  .navi_input_range {
25141
25228
  --border-radius: 6px;
@@ -25344,7 +25431,7 @@ installImportMetaCssBuild(import.meta);const css$j = /* css */`
25344
25431
  }
25345
25432
  `;
25346
25433
  const InputRange = props => {
25347
- import.meta.css = [css$j, "@jsenv/navi/src/field/input_range.jsx"];
25434
+ import.meta.css = [css$k, "@jsenv/navi/src/field/input_range.jsx"];
25348
25435
  const uiStateController = useUIStateController(props, "input");
25349
25436
  const uiState = useUIState(uiStateController);
25350
25437
  const input = renderActionableComponent(props, {
@@ -25614,24 +25701,8 @@ const SearchSvg = () => jsx("svg", {
25614
25701
  })
25615
25702
  });
25616
25703
 
25617
- installImportMetaCssBuild(import.meta);/**
25618
- * Input component for all textual input types.
25619
- *
25620
- * Supports:
25621
- * - text (default)
25622
- * - password
25623
- * - hidden
25624
- * - email
25625
- * - url
25626
- * - search
25627
- * - tel
25628
- * - etc.
25629
- *
25630
- * For non-textual inputs, specialized components will be used:
25631
- * - <InputCheckbox /> for type="checkbox"
25632
- * - <InputRadio /> for type="radio"
25633
- */
25634
- const css$i = /* css */`
25704
+ installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
25705
+ const css$j = /* css */`
25635
25706
  @layer navi {
25636
25707
  .navi_input {
25637
25708
  --border-radius: 2px;
@@ -25843,7 +25914,7 @@ const css$i = /* css */`
25843
25914
  }
25844
25915
  `;
25845
25916
  const InputTextual = props => {
25846
- import.meta.css = [css$i, "@jsenv/navi/src/field/input_textual.jsx"];
25917
+ import.meta.css = [css$j, "@jsenv/navi/src/field/input_textual.jsx"];
25847
25918
  const uiStateController = useUIStateController(props, "input");
25848
25919
  const uiState = useUIState(uiStateController);
25849
25920
  const input = renderActionableComponent(props, {
@@ -25916,6 +25987,197 @@ Object.assign(PSEUDO_CLASSES, {
25916
25987
  const InputPseudoElements = ["::-navi-loader"];
25917
25988
  const InputChildPropSet = new Set([...fieldPropSet]);
25918
25989
  const InputTextualBasic = props => {
25990
+ if (props.combobox) {
25991
+ return jsx(InputTextualCombobox, {
25992
+ ...props
25993
+ });
25994
+ }
25995
+ return jsx(InputTextualPlain, {
25996
+ ...props
25997
+ });
25998
+ };
25999
+ const InputTextualCombobox = ({
26000
+ combobox,
26001
+ onInput,
26002
+ onFocus,
26003
+ onBlur,
26004
+ ...rest
26005
+ }) => {
26006
+ const defaultRef = useRef();
26007
+ const ref = rest.ref || defaultRef;
26008
+ const [comboboxOpen, setComboboxOpen] = useState(false);
26009
+ const comboboxOpenRef = useRef(false);
26010
+ comboboxOpenRef.current = comboboxOpen;
26011
+ const showPopover = e => {
26012
+ if (comboboxOpenRef.current) {
26013
+ return;
26014
+ }
26015
+ console.debug(`showPopover (e.type:${e.type})`);
26016
+ const popoverEl = document.getElementById(combobox);
26017
+ positionPopover();
26018
+ popoverEl.showPopover();
26019
+ comboboxOpenRef.current = true;
26020
+ setComboboxOpen(true);
26021
+ window.addEventListener("scroll", positionPopover, {
26022
+ capture: true,
26023
+ passive: true
26024
+ });
26025
+ };
26026
+ const hidePopover = e => {
26027
+ if (!comboboxOpenRef.current) {
26028
+ return;
26029
+ }
26030
+ console.debug(`hidePopover (e.type:${e.type})`);
26031
+ comboboxOpenRef.current = false;
26032
+ setComboboxOpen(false);
26033
+ window.removeEventListener("scroll", positionPopover, {
26034
+ capture: true
26035
+ });
26036
+ const popoverEl = document.getElementById(combobox);
26037
+ if (popoverEl) {
26038
+ popoverEl.dispatchEvent(new CustomEvent("combobox-clear"));
26039
+ popoverEl.hidePopover();
26040
+ }
26041
+ setComboboxOpen(false);
26042
+ };
26043
+ const positionPopover = () => {
26044
+ const input = ref.current;
26045
+ const rect = input.getBoundingClientRect();
26046
+ const popoverEl = document.getElementById(combobox);
26047
+ if (popoverEl) {
26048
+ popoverEl.style.top = `${rect.bottom + 2}px`;
26049
+ popoverEl.style.left = `${rect.left}px`;
26050
+ popoverEl.style.width = `${rect.width}px`;
26051
+ }
26052
+ };
26053
+ const dispatchToOptionList = customEvent => {
26054
+ const popoverEl = document.getElementById(combobox);
26055
+ if (!popoverEl) {
26056
+ return false;
26057
+ }
26058
+ popoverEl.dispatchEvent(customEvent);
26059
+ return customEvent.defaultPrevented;
26060
+ };
26061
+ useKeyboardShortcuts(ref, [{
26062
+ key: "arrowdown",
26063
+ description: "Open popover and highlight next option",
26064
+ handler: e => {
26065
+ showPopover(e);
26066
+ const popoverEl = document.getElementById(combobox);
26067
+ if (!popoverEl) {
26068
+ return false;
26069
+ }
26070
+ popoverEl.dispatchEvent(new CustomEvent("combobox-navigate", {
26071
+ detail: {
26072
+ direction: "down"
26073
+ }
26074
+ }));
26075
+ return true;
26076
+ }
26077
+ }, {
26078
+ key: "arrowup",
26079
+ description: "Open popover and highlight previous option",
26080
+ handler: e => {
26081
+ showPopover(e);
26082
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26083
+ detail: {
26084
+ direction: "up"
26085
+ }
26086
+ }));
26087
+ }
26088
+ }, {
26089
+ key: "home",
26090
+ description: "Highlight first option",
26091
+ handler: () => {
26092
+ if (!comboboxOpenRef.current) {
26093
+ return false;
26094
+ }
26095
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26096
+ detail: {
26097
+ direction: "first"
26098
+ }
26099
+ }));
26100
+ }
26101
+ }, {
26102
+ key: "end",
26103
+ description: "Highlight last option",
26104
+ handler: () => {
26105
+ if (!comboboxOpenRef.current) {
26106
+ return false;
26107
+ }
26108
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26109
+ detail: {
26110
+ direction: "last"
26111
+ }
26112
+ }));
26113
+ }
26114
+ }, {
26115
+ key: "enter",
26116
+ description: "Confirm highlighted option",
26117
+ handler: () => {
26118
+ if (!comboboxOpenRef.current) {
26119
+ return false;
26120
+ }
26121
+ return dispatchToOptionList(new CustomEvent("combobox-confirm", {
26122
+ cancelable: true
26123
+ }));
26124
+ }
26125
+ }, {
26126
+ key: "escape",
26127
+ description: "Close popover",
26128
+ handler: e => {
26129
+ if (!comboboxOpenRef.current) {
26130
+ return false;
26131
+ }
26132
+ hidePopover(e);
26133
+ return true;
26134
+ }
26135
+ }]);
26136
+ useEffect(() => {
26137
+ const inputEl = ref.current;
26138
+ const popoverEl = document.getElementById(combobox);
26139
+ if (!popoverEl) {
26140
+ return undefined;
26141
+ }
26142
+ const onSelected = e => {
26143
+ inputEl.value = e.detail.value;
26144
+ inputEl.dispatchEvent(new Event("input", {
26145
+ bubbles: true
26146
+ }));
26147
+ hidePopover(e);
26148
+ };
26149
+ popoverEl.addEventListener("combobox-selected", onSelected);
26150
+ return () => {
26151
+ popoverEl.removeEventListener("combobox-selected", onSelected);
26152
+ };
26153
+ }, [combobox]);
26154
+ return jsx(InputTextualPlain, {
26155
+ ref: ref,
26156
+ role: "combobox",
26157
+ autoComplete: "off",
26158
+ "aria-controls": combobox,
26159
+ "aria-haspopup": "listbox",
26160
+ "aria-expanded": comboboxOpen,
26161
+ "aria-autocomplete": "list",
26162
+ onnavi_callout_open: e => {
26163
+ hidePopover(e);
26164
+ },
26165
+ onFocus: e => {
26166
+ onFocus?.(e);
26167
+ showPopover(e);
26168
+ },
26169
+ onBlur: e => {
26170
+ onBlur?.(e);
26171
+ hidePopover(e);
26172
+ },
26173
+ onInput: e => {
26174
+ onInput?.(e);
26175
+ showPopover(e);
26176
+ },
26177
+ ...rest
26178
+ });
26179
+ };
26180
+ const InputTextualPlain = props => {
25919
26181
  const contextReadOnly = useContext(ReadOnlyContext);
25920
26182
  const contextDisabled = useContext(DisabledContext);
25921
26183
  const contextLoading = useContext(LoadingContext);
@@ -26213,7 +26475,7 @@ installImportMetaCssBuild(import.meta);/**
26213
26475
  * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
26214
26476
  *
26215
26477
  */
26216
- const css$h = /* css */`
26478
+ const css$i = /* css */`
26217
26479
  .navi_editable_wrapper {
26218
26480
  --inset-top: 0px;
26219
26481
  --inset-right: 0px;
@@ -26262,7 +26524,7 @@ const useEditionController = () => {
26262
26524
  };
26263
26525
  };
26264
26526
  const Editable = props => {
26265
- import.meta.css = [css$h, "@jsenv/navi/src/field/edition/editable.jsx"];
26527
+ import.meta.css = [css$i, "@jsenv/navi/src/field/edition/editable.jsx"];
26266
26528
  let {
26267
26529
  children,
26268
26530
  action,
@@ -26673,7 +26935,7 @@ const FormWithAction = props => {
26673
26935
  // form.dispatchEvent(customEvent);
26674
26936
  // };
26675
26937
 
26676
- installImportMetaCssBuild(import.meta);const css$g = /* css */`
26938
+ installImportMetaCssBuild(import.meta);const css$h = /* css */`
26677
26939
  .navi_group {
26678
26940
  --border-width: 1px;
26679
26941
 
@@ -26770,7 +27032,7 @@ const Group = ({
26770
27032
  vertical = row,
26771
27033
  ...props
26772
27034
  }) => {
26773
- import.meta.css = [css$g, "@jsenv/navi/src/field/group.jsx"];
27035
+ import.meta.css = [css$h, "@jsenv/navi/src/field/group.jsx"];
26774
27036
  if (typeof borderWidth === "string") {
26775
27037
  borderWidth = parseFloat(borderWidth);
26776
27038
  }
@@ -26788,57 +27050,437 @@ const Group = ({
26788
27050
  });
26789
27051
  };
26790
27052
 
26791
- const RadioList = props => {
26792
- const uiStateController = useUIGroupStateController(props, "radio_list", {
26793
- childComponentType: "radio",
26794
- aggregateChildStates: childUIStateControllers => {
26795
- let activeValue;
26796
- for (const childUIStateController of childUIStateControllers) {
26797
- if (childUIStateController.uiState) {
26798
- activeValue = childUIStateController.uiState;
26799
- break;
27053
+ const createItemTracker = () => {
27054
+ const ItemTrackerContext = createContext();
27055
+ const useItemTrackerProvider = () => {
27056
+ const itemsRef = useRef([]);
27057
+ const items = itemsRef.current;
27058
+ const itemCountRef = useRef(0);
27059
+ const tracker = useMemo(() => {
27060
+ const ItemTrackerProvider = ({
27061
+ children
27062
+ }) => {
27063
+ // Reset on each render to start fresh
27064
+ tracker.reset();
27065
+ return jsx(ItemTrackerContext.Provider, {
27066
+ value: tracker,
27067
+ children: children
27068
+ });
27069
+ };
27070
+ ItemTrackerProvider.items = items;
27071
+ return {
27072
+ ItemTrackerProvider,
27073
+ items,
27074
+ registerItem: data => {
27075
+ const index = itemCountRef.current++;
27076
+ items[index] = data;
27077
+ return index;
27078
+ },
27079
+ getItem: index => {
27080
+ return items[index];
27081
+ },
27082
+ getAllItems: () => {
27083
+ return items;
27084
+ },
27085
+ reset: () => {
27086
+ items.length = 0;
27087
+ itemCountRef.current = 0;
26800
27088
  }
26801
- }
26802
- return activeValue;
27089
+ };
27090
+ }, []);
27091
+ return tracker.ItemTrackerProvider;
27092
+ };
27093
+ const useTrackItem = data => {
27094
+ const tracker = useContext(ItemTrackerContext);
27095
+ if (!tracker) {
27096
+ throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
26803
27097
  }
26804
- });
26805
- const uiState = useUIState(uiStateController);
26806
- const radioList = renderActionableComponent(props, {
26807
- Basic: RadioListBasic,
26808
- WithAction: RadioListWithAction
26809
- });
26810
- return jsx(UIStateControllerContext.Provider, {
26811
- value: uiStateController,
26812
- children: jsx(UIStateContext.Provider, {
26813
- value: uiState,
26814
- children: radioList
26815
- })
26816
- });
27098
+ return tracker.registerItem(data);
27099
+ };
27100
+ const useTrackedItem = index => {
27101
+ const trackedItems = useTrackedItems();
27102
+ const item = trackedItems[index];
27103
+ return item;
27104
+ };
27105
+ const useTrackedItems = () => {
27106
+ const tracker = useContext(ItemTrackerContext);
27107
+ if (!tracker) {
27108
+ throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
27109
+ }
27110
+ return tracker.items;
27111
+ };
27112
+ return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
26817
27113
  };
26818
- const Radio = InputRadio;
26819
- const RadioListBasic = props => {
26820
- const contextReadOnly = useContext(ReadOnlyContext);
26821
- const contextDisabled = useContext(DisabledContext);
26822
- const contextLoading = useContext(LoadingContext);
26823
- const uiStateController = useContext(UIStateControllerContext);
26824
- const {
26825
- name,
26826
- loading,
26827
- disabled,
26828
- readOnly,
26829
- children,
26830
- required,
26831
- ...rest
26832
- } = props;
26833
- const innerLoading = loading || contextLoading;
26834
- const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
26835
- const innerDisabled = disabled || contextDisabled;
26836
- return jsx(Box, {
26837
- "data-action": rest["data-action"],
26838
- flex: "y",
26839
- ...rest,
26840
- baseClassName: "navi_radio_list",
26841
- "data-radio-list": "",
27114
+
27115
+ installImportMetaCssBuild(import.meta);const [useOptionItemTrackerProvider, useTrackOption] = createItemTracker();
27116
+
27117
+ /**
27118
+ * OptionList + Option: a composable accessible listbox.
27119
+ *
27120
+ * Usage:
27121
+ * <OptionList id="my-list" value={selected} onChange={setSelected}>
27122
+ * <Option value="a">Option A</Option>
27123
+ * <Option value="b">Option B</Option>
27124
+ * </OptionList>
27125
+ *
27126
+ * CSS vars on .navi_option_list:
27127
+ * --border-radius, --border-width, --border-color, --background-color, --max-height
27128
+ *
27129
+ * CSS vars on .navi_option:
27130
+ * --padding, --color, --background-color, --font-weight
27131
+ * --color-hover, --background-color-hover
27132
+ * --color-highlighted, --background-color-highlighted
27133
+ * --color-selected, --background-color-selected, --font-weight-selected
27134
+ * --color-highlighted-selected, --background-color-highlighted-selected
27135
+ */
27136
+
27137
+ const css$g = /* css */`
27138
+ @layer navi {
27139
+ .navi_option_list {
27140
+ --border-radius: 4px;
27141
+ --border-width: 1px;
27142
+ --border-color: light-dark(#ccc, #555);
27143
+ --background-color: light-dark(#fff, #1e1e1e);
27144
+ --max-height: 220px;
27145
+ }
27146
+ .navi_option {
27147
+ --padding: 8px 12px;
27148
+ --color: inherit;
27149
+ --background-color: transparent;
27150
+ --font-weight: inherit;
27151
+
27152
+ /* Hover (mouse) */
27153
+ --color-hover: var(--color);
27154
+ --background-color-hover: light-dark(#f5f5f5, #2a2a2a);
27155
+
27156
+ /* Highlighted (keyboard navigation cursor) */
27157
+ --color-highlighted: var(--color);
27158
+ --background-color-highlighted: light-dark(#e8f0fe, #1c3a6e);
27159
+
27160
+ /* Selected */
27161
+ --color-selected: light-dark(#1a73e8, #7baaf7);
27162
+ --background-color-selected: light-dark(#e8f0fe, #1c3a6e);
27163
+ --font-weight-selected: 500;
27164
+
27165
+ /* Highlighted + selected */
27166
+ --color-highlighted-selected: var(--color-selected);
27167
+ --background-color-highlighted-selected: light-dark(#d2e3fc, #174ea6);
27168
+ }
27169
+ }
27170
+
27171
+ .navi_option_list {
27172
+ --x-border-radius: var(--border-radius);
27173
+ --x-border-width: var(--border-width);
27174
+ --x-border-color: var(--border-color);
27175
+ --x-background-color: var(--background-color);
27176
+ box-sizing: border-box;
27177
+ max-height: var(--max-height);
27178
+
27179
+ margin: 0;
27180
+ padding: 0;
27181
+ list-style: none;
27182
+ background-color: var(--x-background-color);
27183
+ border: var(--x-border-width) solid var(--x-border-color);
27184
+ border-radius: var(--x-border-radius);
27185
+ outline: none;
27186
+ overflow-y: auto;
27187
+
27188
+ /* Popover reset — browser adds border, background, padding, margin by default */
27189
+ &[popover] {
27190
+ position: fixed;
27191
+ inset: unset;
27192
+ margin: 0;
27193
+ padding: 0;
27194
+ border: none;
27195
+ }
27196
+ }
27197
+ .navi_option {
27198
+ --x-color: var(--color);
27199
+ --x-background-color: var(--background-color);
27200
+ --x-font-weight: var(--font-weight);
27201
+
27202
+ padding: var(--padding);
27203
+ color: var(--x-color);
27204
+ font-weight: var(--x-font-weight);
27205
+ background-color: var(--x-background-color);
27206
+ cursor: pointer;
27207
+ user-select: none;
27208
+
27209
+ &:hover {
27210
+ --x-color: var(--color-hover);
27211
+ --x-background-color: var(--background-color-hover);
27212
+ }
27213
+
27214
+ &[data-highlighted] {
27215
+ --x-color: var(--color-highlighted);
27216
+ --x-background-color: var(--background-color-highlighted);
27217
+ }
27218
+
27219
+ &[data-selected] {
27220
+ --x-color: var(--color-selected);
27221
+ --x-background-color: var(--background-color-selected);
27222
+ --x-font-weight: var(--font-weight-selected);
27223
+ }
27224
+
27225
+ &[data-highlighted][data-selected] {
27226
+ --x-color: var(--color-highlighted-selected);
27227
+ --x-background-color: var(--background-color-highlighted-selected);
27228
+ }
27229
+ }
27230
+ `;
27231
+
27232
+ /**
27233
+ * Context OptionList provides downward to its Option children.
27234
+ */
27235
+ const OptionListContext = createContext(null);
27236
+ const OptionList = ({
27237
+ popover,
27238
+ onChange: onChangeProp,
27239
+ children,
27240
+ ...rest
27241
+ }) => {
27242
+ import.meta.css = [css$g, "@jsenv/navi/src/field/option_list.jsx"];
27243
+ const ItemTrackerProvider = useOptionItemTrackerProvider();
27244
+ const [highlightedValue, setHighlightedValue] = useState(null);
27245
+ const highlightedValueRef = useRef(null);
27246
+ highlightedValueRef.current = highlightedValue;
27247
+ const ownId = useId();
27248
+ const id = rest.id ?? ownId;
27249
+ const listRef = useRef(null);
27250
+ const effectiveOnChange = popover ? value => {
27251
+ onChangeProp?.(value);
27252
+ listRef.current?.dispatchEvent(new CustomEvent("combobox-selected", {
27253
+ detail: {
27254
+ value
27255
+ },
27256
+ bubbles: true
27257
+ }));
27258
+ } : onChangeProp;
27259
+ const onChangeRef = useRef(effectiveOnChange);
27260
+ onChangeRef.current = effectiveOnChange;
27261
+ const navigate = direction => {
27262
+ const values = ItemTrackerProvider.items.filter(item => !item.hidden).map(item => item.value);
27263
+ if (values.length === 0) {
27264
+ return false;
27265
+ }
27266
+ const current = highlightedValueRef.current;
27267
+ if (direction === "down") {
27268
+ const idx = current === null ? -1 : values.indexOf(current);
27269
+ setHighlightedValue(values[idx < values.length - 1 ? idx + 1 : idx]);
27270
+ } else if (direction === "up") {
27271
+ const idx = current === null ? -1 : values.indexOf(current);
27272
+ setHighlightedValue(values[idx > 0 ? idx - 1 : 0]);
27273
+ } else if (direction === "first") {
27274
+ setHighlightedValue(values[0]);
27275
+ } else if (direction === "last") {
27276
+ setHighlightedValue(values[values.length - 1]);
27277
+ }
27278
+ return true;
27279
+ };
27280
+
27281
+ // Listen for commands dispatched by a linked Input (combobox mode)
27282
+ const noopRef = useRef(null);
27283
+ useEffect(() => {
27284
+ if (!popover || !listRef.current) {
27285
+ return undefined;
27286
+ }
27287
+ const el = listRef.current;
27288
+ const onNavigate = e => {
27289
+ navigate(e.detail.direction);
27290
+ };
27291
+ const onConfirm = e => {
27292
+ const current = highlightedValueRef.current;
27293
+ if (current !== null) {
27294
+ onChangeRef.current?.(current);
27295
+ e.preventDefault();
27296
+ }
27297
+ };
27298
+ const onClear = () => {
27299
+ setHighlightedValue(null);
27300
+ };
27301
+ el.addEventListener("combobox-navigate", onNavigate);
27302
+ el.addEventListener("combobox-confirm", onConfirm);
27303
+ el.addEventListener("combobox-clear", onClear);
27304
+ return () => {
27305
+ el.removeEventListener("combobox-navigate", onNavigate);
27306
+ el.removeEventListener("combobox-confirm", onConfirm);
27307
+ el.removeEventListener("combobox-clear", onClear);
27308
+ };
27309
+ }, [popover]);
27310
+ useKeyboardShortcuts(popover ? noopRef : listRef, [{
27311
+ key: "arrowdown",
27312
+ description: "Highlight next option",
27313
+ handler: () => navigate("down")
27314
+ }, {
27315
+ key: "arrowup",
27316
+ description: "Highlight previous option",
27317
+ handler: () => navigate("up")
27318
+ }, {
27319
+ key: "home",
27320
+ description: "Highlight first option",
27321
+ handler: () => navigate("first")
27322
+ }, {
27323
+ key: "end",
27324
+ description: "Highlight last option",
27325
+ handler: () => navigate("last")
27326
+ }, {
27327
+ key: "enter",
27328
+ description: "Select highlighted option",
27329
+ handler: () => {
27330
+ const current = highlightedValueRef.current;
27331
+ if (current === null) {
27332
+ return false;
27333
+ }
27334
+ onChangeRef.current?.(current);
27335
+ return true;
27336
+ }
27337
+ }, {
27338
+ key: "escape",
27339
+ description: "Clear highlighted option",
27340
+ handler: () => {
27341
+ setHighlightedValue(null);
27342
+ return true;
27343
+ }
27344
+ }]);
27345
+ const optionListContext = {
27346
+ highlightedValue,
27347
+ setHighlightedValue,
27348
+ onSelect: effectiveOnChange
27349
+ };
27350
+ return jsx(Box, {
27351
+ as: "ul",
27352
+ ref: listRef,
27353
+ id: id,
27354
+ role: "listbox",
27355
+ tabIndex: popover ? -1 : 0,
27356
+ popover: popover ? "manual" : undefined,
27357
+ ...rest,
27358
+ baseClassName: "navi_option_list",
27359
+ children: jsx(OptionListContext.Provider, {
27360
+ value: optionListContext,
27361
+ children: jsx(ItemTrackerProvider, {
27362
+ children: children
27363
+ })
27364
+ })
27365
+ });
27366
+ };
27367
+ const OPTION_PSEUDO_CLASSES = [":-navi-highlighted", ":-navi-selected"];
27368
+ const Option = ({
27369
+ value,
27370
+ selected,
27371
+ hidden,
27372
+ children,
27373
+ ...rest
27374
+ }) => {
27375
+ import.meta.css = [css$g, "@jsenv/navi/src/field/option_list.jsx"];
27376
+ const optionId = useId();
27377
+ const id = rest.id || optionId;
27378
+ useTrackOption({
27379
+ value,
27380
+ optionId: id,
27381
+ hidden
27382
+ });
27383
+ const {
27384
+ highlightedValue,
27385
+ setHighlightedValue,
27386
+ onSelect
27387
+ } = useContext(OptionListContext);
27388
+ const isHighlighted = highlightedValue === value;
27389
+ const optionRef = useRef(null);
27390
+ useEffect(() => {
27391
+ const optionEl = optionRef.current;
27392
+ if (isHighlighted && optionEl) {
27393
+ optionEl.scrollIntoView({
27394
+ block: "nearest"
27395
+ });
27396
+ }
27397
+ }, [isHighlighted]);
27398
+ return jsx(Box, {
27399
+ as: "li",
27400
+ ref: optionRef,
27401
+ baseClassName: "navi_option",
27402
+ id: optionId,
27403
+ role: "option",
27404
+ "aria-selected": selected,
27405
+ "aria-hidden": hidden ? true : undefined,
27406
+ hidden: hidden,
27407
+ basePseudoState: {
27408
+ ":-navi-highlighted": isHighlighted,
27409
+ ":-navi-selected": selected
27410
+ },
27411
+ pseudoClasses: OPTION_PSEUDO_CLASSES,
27412
+ onMouseEnter: () => {
27413
+ if (!hidden) {
27414
+ setHighlightedValue(value);
27415
+ }
27416
+ },
27417
+ onMouseLeave: () => {
27418
+ if (!hidden) {
27419
+ setHighlightedValue(null);
27420
+ }
27421
+ },
27422
+ onMouseDown: e => {
27423
+ if (hidden || e.button !== 0) {
27424
+ return;
27425
+ }
27426
+ onSelect?.(value);
27427
+ },
27428
+ ...rest,
27429
+ children: children
27430
+ });
27431
+ };
27432
+
27433
+ const RadioList = props => {
27434
+ const uiStateController = useUIGroupStateController(props, "radio_list", {
27435
+ childComponentType: "radio",
27436
+ aggregateChildStates: childUIStateControllers => {
27437
+ let activeValue;
27438
+ for (const childUIStateController of childUIStateControllers) {
27439
+ if (childUIStateController.uiState) {
27440
+ activeValue = childUIStateController.uiState;
27441
+ break;
27442
+ }
27443
+ }
27444
+ return activeValue;
27445
+ }
27446
+ });
27447
+ const uiState = useUIState(uiStateController);
27448
+ const radioList = renderActionableComponent(props, {
27449
+ Basic: RadioListBasic,
27450
+ WithAction: RadioListWithAction
27451
+ });
27452
+ return jsx(UIStateControllerContext.Provider, {
27453
+ value: uiStateController,
27454
+ children: jsx(UIStateContext.Provider, {
27455
+ value: uiState,
27456
+ children: radioList
27457
+ })
27458
+ });
27459
+ };
27460
+ const Radio = InputRadio;
27461
+ const RadioListBasic = props => {
27462
+ const contextReadOnly = useContext(ReadOnlyContext);
27463
+ const contextDisabled = useContext(DisabledContext);
27464
+ const contextLoading = useContext(LoadingContext);
27465
+ const uiStateController = useContext(UIStateControllerContext);
27466
+ const {
27467
+ name,
27468
+ loading,
27469
+ disabled,
27470
+ readOnly,
27471
+ children,
27472
+ required,
27473
+ ...rest
27474
+ } = props;
27475
+ const innerLoading = loading || contextLoading;
27476
+ const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
27477
+ const innerDisabled = disabled || contextDisabled;
27478
+ return jsx(Box, {
27479
+ "data-action": rest["data-action"],
27480
+ flex: "y",
27481
+ ...rest,
27482
+ baseClassName: "navi_radio_list",
27483
+ "data-radio-list": "",
26842
27484
  onresetuistate: e => {
26843
27485
  uiStateController.resetUIState(e);
26844
27486
  },
@@ -27258,6 +27900,131 @@ const filterTableSelection = (selection, predicate) => {
27258
27900
  return matching;
27259
27901
  };
27260
27902
 
27903
+ // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
27904
+ // https://github.com/pacocoursey/use-descendants/tree/master
27905
+
27906
+ const createIsolatedItemTracker = () => {
27907
+ // Producer contexts (ref-based, no re-renders)
27908
+ const ProducerTrackerContext = createContext();
27909
+ const ProducerItemCountRefContext = createContext();
27910
+ const ProducerListRenderIdContext = createContext();
27911
+
27912
+ // Consumer contexts (state-based, re-renders)
27913
+ const ConsumerItemsContext = createContext();
27914
+ const useIsolatedItemTrackerProvider = () => {
27915
+ const itemsRef = useRef([]);
27916
+ const items = itemsRef.current;
27917
+ const itemCountRef = useRef();
27918
+ const itemTracker = useMemo(() => {
27919
+ // Snapshot taken by FlushSentinel after all producer children rendered.
27920
+ // Consumers read from this — always up-to-date within the same render pass.
27921
+ const itemsSnapshotRef = {
27922
+ current: items
27923
+ };
27924
+ const registerItem = (index, value) => {
27925
+ const hasValue = index in items;
27926
+ if (hasValue) {
27927
+ const currentValue = items[index];
27928
+ if (compareTwoJsValues(currentValue, value)) {
27929
+ return;
27930
+ }
27931
+ }
27932
+ items[index] = value;
27933
+ };
27934
+ const getProducerItem = itemIndex => {
27935
+ return items[itemIndex];
27936
+ };
27937
+ const ItemProducerProvider = ({
27938
+ children
27939
+ }) => {
27940
+ items.length = 0;
27941
+ itemCountRef.current = 0;
27942
+ const listRenderId = {};
27943
+ return jsx(ProducerItemCountRefContext.Provider, {
27944
+ value: itemCountRef,
27945
+ children: jsx(ProducerListRenderIdContext.Provider, {
27946
+ value: listRenderId,
27947
+ children: jsxs(ProducerTrackerContext.Provider, {
27948
+ value: itemTracker,
27949
+ children: [children, jsx(FlushSentinel, {})]
27950
+ })
27951
+ })
27952
+ });
27953
+ };
27954
+
27955
+ // Renders after all producer children (e.g. <Col>) have registered their
27956
+ // items. Taking a snapshot here guarantees the consumer sees the correct
27957
+ // item list within the same render pass, without any heuristic.
27958
+ const FlushSentinel = () => {
27959
+ itemsSnapshotRef.current = items;
27960
+ return null;
27961
+ };
27962
+ const ItemConsumerProvider = ({
27963
+ children
27964
+ }) => {
27965
+ // FlushSentinel (last child of ItemProducerProvider) already set
27966
+ // itemsSnapshotRef.current to the up-to-date items array before any
27967
+ // consumer rendered. Reading from the snapshot is always correct.
27968
+ return jsx(ConsumerItemsContext.Provider, {
27969
+ value: itemsSnapshotRef.current,
27970
+ children: children
27971
+ });
27972
+ };
27973
+ return {
27974
+ registerItem,
27975
+ getProducerItem,
27976
+ ItemProducerProvider,
27977
+ ItemConsumerProvider
27978
+ };
27979
+ }, []);
27980
+ const {
27981
+ ItemProducerProvider,
27982
+ ItemConsumerProvider
27983
+ } = itemTracker;
27984
+ return [ItemProducerProvider, ItemConsumerProvider, items];
27985
+ };
27986
+
27987
+ // Hook for producers to register items (ref-based, no re-renders)
27988
+ const useTrackIsolatedItem = data => {
27989
+ const listRenderId = useContext(ProducerListRenderIdContext);
27990
+ const itemCountRef = useContext(ProducerItemCountRefContext);
27991
+ const itemTracker = useContext(ProducerTrackerContext);
27992
+ const listRenderIdRef = useRef();
27993
+ const itemIndexRef = useRef();
27994
+ const dataRef = useRef();
27995
+ const prevListRenderId = listRenderIdRef.current;
27996
+ if (prevListRenderId === listRenderId) {
27997
+ const itemIndex = itemIndexRef.current;
27998
+ itemTracker.registerItem(itemIndex, data);
27999
+ dataRef.current = data;
28000
+ return itemIndex;
28001
+ }
28002
+ listRenderIdRef.current = listRenderId;
28003
+ const itemCount = itemCountRef.current;
28004
+ const itemIndex = itemCount;
28005
+ itemCountRef.current = itemIndex + 1;
28006
+ itemIndexRef.current = itemIndex;
28007
+ dataRef.current = data;
28008
+ itemTracker.registerItem(itemIndex, data);
28009
+ return itemIndex;
28010
+ };
28011
+ const useTrackedIsolatedItem = itemIndex => {
28012
+ const items = useTrackedIsolatedItems();
28013
+ const item = items[itemIndex];
28014
+ return item;
28015
+ };
28016
+
28017
+ // Hooks for consumers to read items (state-based, re-renders)
28018
+ const useTrackedIsolatedItems = () => {
28019
+ const consumerItems = useContext(ConsumerItemsContext);
28020
+ if (!consumerItems) {
28021
+ throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
28022
+ }
28023
+ return consumerItems;
28024
+ };
28025
+ return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
28026
+ };
28027
+
27261
28028
  const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
27262
28029
 
27263
28030
  /* needed because cell uses position:relative, sticky must win even if before in DOM order */
@@ -27714,193 +28481,6 @@ const createTableAttributeSync = (table, tableClone) => {
27714
28481
  return observer;
27715
28482
  };
27716
28483
 
27717
- // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
27718
- // https://github.com/pacocoursey/use-descendants/tree/master
27719
-
27720
- const createIsolatedItemTracker = () => {
27721
- // Producer contexts (ref-based, no re-renders)
27722
- const ProducerTrackerContext = createContext();
27723
- const ProducerItemCountRefContext = createContext();
27724
- const ProducerListRenderIdContext = createContext();
27725
-
27726
- // Consumer contexts (state-based, re-renders)
27727
- const ConsumerItemsContext = createContext();
27728
- const useIsolatedItemTrackerProvider = () => {
27729
- const itemsRef = useRef([]);
27730
- const items = itemsRef.current;
27731
- const itemCountRef = useRef();
27732
- const itemTracker = useMemo(() => {
27733
- // Snapshot taken by FlushSentinel after all producer children rendered.
27734
- // Consumers read from this — always up-to-date within the same render pass.
27735
- const itemsSnapshotRef = {
27736
- current: items
27737
- };
27738
- const registerItem = (index, value) => {
27739
- const hasValue = index in items;
27740
- if (hasValue) {
27741
- const currentValue = items[index];
27742
- if (compareTwoJsValues(currentValue, value)) {
27743
- return;
27744
- }
27745
- }
27746
- items[index] = value;
27747
- };
27748
- const getProducerItem = itemIndex => {
27749
- return items[itemIndex];
27750
- };
27751
- const ItemProducerProvider = ({
27752
- children
27753
- }) => {
27754
- items.length = 0;
27755
- itemCountRef.current = 0;
27756
- const listRenderId = {};
27757
- return jsx(ProducerItemCountRefContext.Provider, {
27758
- value: itemCountRef,
27759
- children: jsx(ProducerListRenderIdContext.Provider, {
27760
- value: listRenderId,
27761
- children: jsxs(ProducerTrackerContext.Provider, {
27762
- value: itemTracker,
27763
- children: [children, jsx(FlushSentinel, {})]
27764
- })
27765
- })
27766
- });
27767
- };
27768
-
27769
- // Renders after all producer children (e.g. <Col>) have registered their
27770
- // items. Taking a snapshot here guarantees the consumer sees the correct
27771
- // item list within the same render pass, without any heuristic.
27772
- const FlushSentinel = () => {
27773
- itemsSnapshotRef.current = items;
27774
- return null;
27775
- };
27776
- const ItemConsumerProvider = ({
27777
- children
27778
- }) => {
27779
- // FlushSentinel (last child of ItemProducerProvider) already set
27780
- // itemsSnapshotRef.current to the up-to-date items array before any
27781
- // consumer rendered. Reading from the snapshot is always correct.
27782
- return jsx(ConsumerItemsContext.Provider, {
27783
- value: itemsSnapshotRef.current,
27784
- children: children
27785
- });
27786
- };
27787
- return {
27788
- registerItem,
27789
- getProducerItem,
27790
- ItemProducerProvider,
27791
- ItemConsumerProvider
27792
- };
27793
- }, []);
27794
- const {
27795
- ItemProducerProvider,
27796
- ItemConsumerProvider
27797
- } = itemTracker;
27798
- return [ItemProducerProvider, ItemConsumerProvider, items];
27799
- };
27800
-
27801
- // Hook for producers to register items (ref-based, no re-renders)
27802
- const useTrackIsolatedItem = data => {
27803
- const listRenderId = useContext(ProducerListRenderIdContext);
27804
- const itemCountRef = useContext(ProducerItemCountRefContext);
27805
- const itemTracker = useContext(ProducerTrackerContext);
27806
- const listRenderIdRef = useRef();
27807
- const itemIndexRef = useRef();
27808
- const dataRef = useRef();
27809
- const prevListRenderId = listRenderIdRef.current;
27810
- if (prevListRenderId === listRenderId) {
27811
- const itemIndex = itemIndexRef.current;
27812
- itemTracker.registerItem(itemIndex, data);
27813
- dataRef.current = data;
27814
- return itemIndex;
27815
- }
27816
- listRenderIdRef.current = listRenderId;
27817
- const itemCount = itemCountRef.current;
27818
- const itemIndex = itemCount;
27819
- itemCountRef.current = itemIndex + 1;
27820
- itemIndexRef.current = itemIndex;
27821
- dataRef.current = data;
27822
- itemTracker.registerItem(itemIndex, data);
27823
- return itemIndex;
27824
- };
27825
- const useTrackedIsolatedItem = itemIndex => {
27826
- const items = useTrackedIsolatedItems();
27827
- const item = items[itemIndex];
27828
- return item;
27829
- };
27830
-
27831
- // Hooks for consumers to read items (state-based, re-renders)
27832
- const useTrackedIsolatedItems = () => {
27833
- const consumerItems = useContext(ConsumerItemsContext);
27834
- if (!consumerItems) {
27835
- throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
27836
- }
27837
- return consumerItems;
27838
- };
27839
- return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
27840
- };
27841
-
27842
- const createItemTracker = () => {
27843
- const ItemTrackerContext = createContext();
27844
- const useItemTrackerProvider = () => {
27845
- const itemsRef = useRef([]);
27846
- const items = itemsRef.current;
27847
- const itemCountRef = useRef(0);
27848
- const tracker = useMemo(() => {
27849
- const ItemTrackerProvider = ({
27850
- children
27851
- }) => {
27852
- // Reset on each render to start fresh
27853
- tracker.reset();
27854
- return jsx(ItemTrackerContext.Provider, {
27855
- value: tracker,
27856
- children: children
27857
- });
27858
- };
27859
- ItemTrackerProvider.items = items;
27860
- return {
27861
- ItemTrackerProvider,
27862
- items,
27863
- registerItem: data => {
27864
- const index = itemCountRef.current++;
27865
- items[index] = data;
27866
- return index;
27867
- },
27868
- getItem: index => {
27869
- return items[index];
27870
- },
27871
- getAllItems: () => {
27872
- return items;
27873
- },
27874
- reset: () => {
27875
- items.length = 0;
27876
- itemCountRef.current = 0;
27877
- }
27878
- };
27879
- }, []);
27880
- return tracker.ItemTrackerProvider;
27881
- };
27882
- const useTrackItem = data => {
27883
- const tracker = useContext(ItemTrackerContext);
27884
- if (!tracker) {
27885
- throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
27886
- }
27887
- return tracker.registerItem(data);
27888
- };
27889
- const useTrackedItem = index => {
27890
- const trackedItems = useTrackedItems();
27891
- const item = trackedItems[index];
27892
- return item;
27893
- };
27894
- const useTrackedItems = () => {
27895
- const tracker = useContext(ItemTrackerContext);
27896
- if (!tracker) {
27897
- throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
27898
- }
27899
- return tracker.items;
27900
- };
27901
- return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
27902
- };
27903
-
27904
28484
  const TableSizeContext = createContext();
27905
28485
 
27906
28486
  const useTableSizeContextValue = ({
@@ -32819,5 +33399,5 @@ const UserSvg = () => jsx("svg", {
32819
33399
  })
32820
33400
  });
32821
33401
 
32822
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, Loading, MessageBox, Meter, Nav, Paragraph, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
33402
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, Loading, MessageBox, Meter, Nav, Option, OptionList, Paragraph, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
32823
33403
  //# sourceMappingURL=jsenv_navi.js.map