@lumx/react 4.18.0-next.1 → 4.18.0-next.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.
Files changed (3) hide show
  1. package/index.js +52 -11
  2. package/index.js.map +1 -1
  3. package/package.json +3 -3
package/index.js CHANGED
@@ -6460,6 +6460,12 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6460
6460
  /** Last notified input value, to re-fire `optionsChange` when the user keeps typing while empty. */
6461
6461
  let lastInputValue = '';
6462
6462
 
6463
+ /** Last notified loading state, used to replay current state to late subscribers. */
6464
+ let lastLoadingState = false;
6465
+
6466
+ /** Number of currently mounted skeleton placeholders. */
6467
+ let skeletonCount = 0;
6468
+
6463
6469
  /** Event subscribers managed by the handle. */
6464
6470
  const subscribers = {
6465
6471
  open: new Set(),
@@ -6474,6 +6480,20 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6474
6480
  subscribers[event].forEach(cb => cb(value));
6475
6481
  }
6476
6482
 
6483
+ /** Count visible (non-filtered) options. */
6484
+ function getVisibleOptionCount() {
6485
+ let count = 0;
6486
+ for (const reg of optionRegistrations.values()) {
6487
+ if (!reg.lastFiltered) count += 1;
6488
+ }
6489
+ return count;
6490
+ }
6491
+
6492
+ /** True when the popup has visible content (options or skeletons). */
6493
+ function hasVisibleContent() {
6494
+ return getVisibleOptionCount() > 0 || skeletonCount > 0;
6495
+ }
6496
+
6477
6497
  /**
6478
6498
  * Notify all registered sections and fire `optionsChange` if the visible option count changed
6479
6499
  * or if the input value changed while the list is empty (so `emptyMessage` callbacks get
@@ -6484,10 +6504,7 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6484
6504
  for (const [sectionElement] of sectionRegistrations) {
6485
6505
  notifySection(sectionElement, sectionRegistrations, optionRegistrations);
6486
6506
  }
6487
- let visibleCount = 0;
6488
- for (const reg of optionRegistrations.values()) {
6489
- if (!reg.lastFiltered) visibleCount += 1;
6490
- }
6507
+ const visibleCount = getVisibleOptionCount();
6491
6508
  const inputValue = trigger?.value ?? '';
6492
6509
  const isEmpty = visibleCount === 0;
6493
6510
  if (visibleCount !== lastOptionsLength || isEmpty && inputValue !== lastInputValue) {
@@ -6498,6 +6515,12 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6498
6515
  inputValue
6499
6516
  });
6500
6517
  }
6518
+
6519
+ // Re-evaluate aria-expanded when the combobox is open — visible content may have
6520
+ // changed due to filtering, option register/unregister, or skeleton transitions.
6521
+ if (isOpenState) {
6522
+ trigger?.setAttribute('aria-expanded', String(hasVisibleContent()));
6523
+ }
6501
6524
  }
6502
6525
 
6503
6526
  // ── Skeleton loading tracking ──────────────────────────────
@@ -6505,9 +6528,6 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6505
6528
  /** Delay before announcing loading in the live region (ms). */
6506
6529
  const LOADING_ANNOUNCEMENT_DELAY = 500;
6507
6530
 
6508
- /** Number of currently mounted skeleton placeholders. */
6509
- let skeletonCount = 0;
6510
-
6511
6531
  /** Timer for debounced loading announcement. */
6512
6532
  let loadingTimer;
6513
6533
 
@@ -6533,6 +6553,7 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6533
6553
  */
6534
6554
  function onSkeletonCountChange() {
6535
6555
  const isLoading = skeletonCount > 0;
6556
+ lastLoadingState = isLoading;
6536
6557
  notify('loadingChange', isLoading);
6537
6558
  if (isLoading) {
6538
6559
  startLoadingAnnouncementTimer();
@@ -6735,8 +6756,8 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6735
6756
  startLoadingAnnouncementTimer();
6736
6757
  }
6737
6758
 
6738
- // Update aria-expanded on trigger
6739
- trigger?.setAttribute('aria-expanded', String(isOpen));
6759
+ // Update aria-expanded on trigger (false when no visible options or skeletons)
6760
+ trigger?.setAttribute('aria-expanded', String(isOpen && hasVisibleContent()));
6740
6761
  notify('open', isOpen);
6741
6762
  },
6742
6763
  select(option) {
@@ -6871,6 +6892,17 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6871
6892
  },
6872
6893
  subscribe(event, callback) {
6873
6894
  subscribers[event].add(callback);
6895
+ // Replay current loading state to late subscribers so that framework wrappers
6896
+ // that subscribe after initial mount (e.g. async Vue watchers) don't miss the
6897
+ // initial events fired during mount.
6898
+ if (event === 'open' && isOpenState) {
6899
+ callback(true);
6900
+ }
6901
+ if (event === 'loadingChange' && lastLoadingState) {
6902
+ callback(true);
6903
+ }
6904
+
6905
+ // Cleanup function
6874
6906
  return () => {
6875
6907
  subscribers[event].delete(callback);
6876
6908
  };
@@ -6882,6 +6914,7 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6882
6914
  filterValue = '';
6883
6915
  lastOptionsLength = 0;
6884
6916
  lastInputValue = '';
6917
+ lastLoadingState = false;
6885
6918
  optionRegistrations.clear();
6886
6919
  sectionRegistrations.clear();
6887
6920
  skeletonCount = 0;
@@ -7278,6 +7311,10 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
7278
7311
  setHandle
7279
7312
  } = useComboboxContext();
7280
7313
  const [isOpen] = useComboboxOpen();
7314
+ const state = useComboboxEvent('optionsChange', {
7315
+ optionsLength: 0
7316
+ });
7317
+ const isLoading = useComboboxEvent('loadingChange', false);
7281
7318
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7282
7319
  const {
7283
7320
  as,
@@ -7319,7 +7356,7 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
7319
7356
  value,
7320
7357
  labelDisplayMode,
7321
7358
  listboxId,
7322
- isOpen,
7359
+ isOpen: isOpen && (!!state?.optionsLength || isLoading),
7323
7360
  ref: mergedRef
7324
7361
  }, {
7325
7362
  Button: ButtonComp,
@@ -8112,6 +8149,10 @@ const ComboboxInput = forwardRef((props, ref) => {
8112
8149
  setHandle
8113
8150
  } = useComboboxContext();
8114
8151
  const [isOpen, setIsOpen] = useComboboxOpen();
8152
+ const state = useComboboxEvent('optionsChange', {
8153
+ optionsLength: 0
8154
+ });
8155
+ const isLoading = useComboboxEvent('loadingChange', false);
8115
8156
  const {
8116
8157
  inputRef: externalInputRef,
8117
8158
  toggleButtonProps,
@@ -8160,7 +8201,7 @@ const ComboboxInput = forwardRef((props, ref) => {
8160
8201
  ...otherProps,
8161
8202
  ref,
8162
8203
  listboxId,
8163
- isOpen,
8204
+ isOpen: isOpen && (!!state?.optionsLength || isLoading),
8164
8205
  filter,
8165
8206
  inputRef: mergedInputRef,
8166
8207
  textFieldRef: anchorRef,