@lumx/react 4.16.0-alpha.3 → 4.16.0-alpha.5

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 (4) hide show
  1. package/index.d.ts +23 -22
  2. package/index.js +442 -314
  3. package/index.js.map +1 -1
  4. package/package.json +3 -3
package/index.js CHANGED
@@ -1687,7 +1687,7 @@ const IconButton$1 = props => {
1687
1687
  size = DEFAULT_PROPS$1a.size,
1688
1688
  ...forwardedProps
1689
1689
  } = props;
1690
- const iconNode = image ? /*#__PURE__*/jsx("img", {
1690
+ const defaultChildren = image ? /*#__PURE__*/jsx("img", {
1691
1691
  // no need to set alt as an aria-label is already set on the button
1692
1692
  alt: "",
1693
1693
  src: image
@@ -1700,12 +1700,7 @@ const IconButton$1 = props => {
1700
1700
  ...forwardedProps,
1701
1701
  'aria-label': label,
1702
1702
  variant: 'icon',
1703
- children: /*#__PURE__*/jsxs(Fragment, {
1704
- children: [iconNode, /*#__PURE__*/jsx("span", {
1705
- className: visuallyHidden(),
1706
- children: label
1707
- })]
1708
- })
1703
+ children: defaultChildren
1709
1704
  });
1710
1705
  };
1711
1706
  IconButton$1.displayName = COMPONENT_NAME$1r;
@@ -5330,6 +5325,111 @@ Tooltip.displayName = COMPONENT_NAME$1i;
5330
5325
  Tooltip.className = CLASSNAME$1h;
5331
5326
  Tooltip.defaultProps = DEFAULT_PROPS$12;
5332
5327
 
5328
+ /** Create a pending navigation store; discards intent on abort. */
5329
+ function createPendingNavigation(signal) {
5330
+ let pending = null;
5331
+ const clear = () => {
5332
+ pending = null;
5333
+ };
5334
+ signal.addEventListener('abort', clear);
5335
+ return {
5336
+ get hasPending() {
5337
+ return pending !== null;
5338
+ },
5339
+ defer(navigate) {
5340
+ pending = navigate;
5341
+ },
5342
+ flush() {
5343
+ if (pending?.()) pending = null;
5344
+ },
5345
+ clear
5346
+ };
5347
+ }
5348
+
5349
+ /**
5350
+ * Create the pure selection layer for a 1D list.
5351
+ *
5352
+ * Everything here is side-effect-free: it only *queries* the DOM to resolve and report
5353
+ * items. No focus is moved and no {@link FocusNavigationCallbacks} are invoked — that is
5354
+ * the job of the mover layer built on top of this (`createListFocusNavigation`).
5355
+ *
5356
+ * @param options List navigation options (container, itemSelector, itemDisabledSelector, getActiveItem).
5357
+ * @returns The public {@link ListFocusNavigationSelectors} plus the internal
5358
+ * {@link ListSelectorHelpers} the mover layer consumes.
5359
+ */
5360
+ function createListSelectors(options) {
5361
+ const {
5362
+ container,
5363
+ itemSelector,
5364
+ itemDisabledSelector,
5365
+ getActiveItem = () => null
5366
+ } = options;
5367
+
5368
+ /** Combined CSS selector matching enabled (non-disabled) items. */
5369
+ const enabledItemSelector = itemDisabledSelector ? `${itemSelector}:not(${itemDisabledSelector})` : itemSelector;
5370
+ function createItemWalker(enabledOnly = true) {
5371
+ const selector = enabledOnly ? enabledItemSelector : itemSelector;
5372
+ return createSelectorTreeWalker(container, selector);
5373
+ }
5374
+ function findFirstEnabled() {
5375
+ return container.querySelector(enabledItemSelector);
5376
+ }
5377
+ function findLastEnabled() {
5378
+ const items = container.querySelectorAll(enabledItemSelector);
5379
+ return items.length > 0 ? items[items.length - 1] : null;
5380
+ }
5381
+ function findMatching(predicate) {
5382
+ const walker = createItemWalker(false);
5383
+ let node = walker.nextNode();
5384
+ while (node) {
5385
+ if (predicate(node)) return node;
5386
+ node = walker.nextNode();
5387
+ }
5388
+ return null;
5389
+ }
5390
+ function findNearestEnabled(anchor) {
5391
+ if (!container.contains(anchor)) return findFirstEnabled();
5392
+
5393
+ // If the anchor itself is an enabled item, return it directly.
5394
+ if (anchor instanceof HTMLElement && anchor.matches(enabledItemSelector)) {
5395
+ return anchor;
5396
+ }
5397
+
5398
+ // Walk forward from the anchor for the nearest enabled item.
5399
+ const walker = createItemWalker();
5400
+ walker.currentNode = anchor;
5401
+ const next = walker.nextNode();
5402
+ if (next instanceof HTMLElement) return next;
5403
+
5404
+ // No enabled item after anchor — walk backward (reuse same walker).
5405
+ walker.currentNode = anchor;
5406
+ return walker.previousNode();
5407
+ }
5408
+ const selectors = {
5409
+ enabledItemSelector,
5410
+ get activeItem() {
5411
+ return getActiveItem();
5412
+ },
5413
+ get hasNavigableItems() {
5414
+ return container.querySelector(enabledItemSelector) !== null;
5415
+ },
5416
+ getFirst: findFirstEnabled,
5417
+ getLast: findLastEnabled,
5418
+ getMatching: findMatching,
5419
+ findNearestEnabled
5420
+ };
5421
+ const helpers = {
5422
+ createItemWalker,
5423
+ findFirstEnabled,
5424
+ findLastEnabled,
5425
+ getActiveItem
5426
+ };
5427
+ return {
5428
+ selectors,
5429
+ helpers
5430
+ };
5431
+ }
5432
+
5333
5433
  /**
5334
5434
  * Transition the active item: deactivate the current one (if any) and activate the new one.
5335
5435
  * Reads the current active item via `getActiveItem` so there is no internal state to desync.
@@ -5344,6 +5444,12 @@ function transition(getActiveItem, callbacks, newItem) {
5344
5444
  /**
5345
5445
  * Create a focus navigation controller for a 1D list.
5346
5446
  *
5447
+ * The controller is composed of two layers:
5448
+ * - a pure, side-effect-free **selection** layer ({@link createListSelectors}) that only
5449
+ * resolves/reports items by querying the DOM, and
5450
+ * - a **mover** layer (this function) that commits focus by calling
5451
+ * {@link FocusNavigationCallbacks} on top of the selectors.
5452
+ *
5347
5453
  * This controller is **stateless** — it does not maintain an internal reference to
5348
5454
  * the active item. Instead it reads the active item from the DOM each time via the
5349
5455
  * `getActiveItem` callback provided in the options. This avoids any desync between
@@ -5356,36 +5462,24 @@ function transition(getActiveItem, callbacks, newItem) {
5356
5462
  */
5357
5463
  function createListFocusNavigation(options, callbacks, signal) {
5358
5464
  const {
5359
- container,
5360
5465
  itemSelector,
5466
+ container,
5361
5467
  direction = 'vertical',
5362
- wrap = false,
5363
- itemDisabledSelector,
5364
- getActiveItem = () => null
5468
+ wrap = false
5365
5469
  } = options;
5470
+ const {
5471
+ selectors,
5472
+ helpers
5473
+ } = createListSelectors(options);
5474
+ const {
5475
+ getActiveItem,
5476
+ createItemWalker,
5477
+ findFirstEnabled,
5478
+ findLastEnabled
5479
+ } = helpers;
5366
5480
 
5367
- /** Combined CSS selector matching enabled (non-disabled) items. */
5368
- const enabledItemSelector = itemDisabledSelector ? `${itemSelector}:not(${itemDisabledSelector})` : itemSelector;
5369
-
5370
- /**
5371
- * Create a TreeWalker over items in the container.
5372
- * @param enabledOnly When true (default), disabled items are skipped.
5373
- */
5374
- function createItemWalker(enabledOnly = true) {
5375
- const selector = enabledOnly ? enabledItemSelector : itemSelector;
5376
- return createSelectorTreeWalker(container, selector);
5377
- }
5378
-
5379
- /** Find the first enabled item in the container. */
5380
- function findFirstEnabled() {
5381
- return container.querySelector(enabledItemSelector);
5382
- }
5383
-
5384
- /** Find the last enabled item in the container. */
5385
- function findLastEnabled() {
5386
- const items = container.querySelectorAll(enabledItemSelector);
5387
- return items.length > 0 ? items[items.length - 1] : null;
5388
- }
5481
+ // Deferred navigation intent (replayed once items are committed to the DOM)
5482
+ const pending = createPendingNavigation(signal);
5389
5483
 
5390
5484
  /** Navigate to the first enabled item and activate it. */
5391
5485
  function goToFirst() {
@@ -5402,7 +5496,9 @@ function createListFocusNavigation(options, callbacks, signal) {
5402
5496
  transition(getActiveItem, callbacks, last);
5403
5497
  return true;
5404
5498
  }
5405
- function navigateByOffset(offset) {
5499
+
5500
+ /** Go to item at an offset */
5501
+ function goToOffset(offset) {
5406
5502
  const active = getActiveItem();
5407
5503
  if (offset === 0) return active !== null;
5408
5504
  const forward = offset > 0;
@@ -5413,7 +5509,7 @@ function createListFocusNavigation(options, callbacks, signal) {
5413
5509
  const started = forward ? goToFirst() : goToLast();
5414
5510
  if (!started) return false;
5415
5511
  if (stepsNeeded === 1) return true;
5416
- return navigateByOffset(forward ? offset - 1 : offset + 1);
5512
+ return goToOffset(forward ? offset - 1 : offset + 1);
5417
5513
  }
5418
5514
 
5419
5515
  // Walk from the active item using a TreeWalker.
@@ -5442,15 +5538,14 @@ function createListFocusNavigation(options, callbacks, signal) {
5442
5538
  transition(getActiveItem, callbacks, lastFound);
5443
5539
  return true;
5444
5540
  }
5445
- const navigateForward = () => navigateByOffset(1);
5446
- const navigateBackward = () => navigateByOffset(-1);
5447
5541
 
5448
- /** Clear the active item. */
5542
+ /** Clear the active item and discard any pending navigation intent. */
5449
5543
  function clear() {
5450
5544
  const current = getActiveItem();
5451
5545
  if (current) {
5452
5546
  callbacks.onDeactivate(current);
5453
5547
  }
5548
+ pending.clear();
5454
5549
  callbacks.onClear?.();
5455
5550
  }
5456
5551
 
@@ -5458,69 +5553,40 @@ function createListFocusNavigation(options, callbacks, signal) {
5458
5553
  signal.addEventListener('abort', clear);
5459
5554
  return {
5460
5555
  type: 'list',
5461
- enabledItemSelector,
5462
- get activeItem() {
5463
- return getActiveItem();
5464
- },
5465
- get hasActiveItem() {
5466
- return getActiveItem() !== null;
5467
- },
5468
- get hasNavigableItems() {
5469
- return container.querySelector(enabledItemSelector) !== null;
5470
- },
5471
- goToFirst,
5472
- goToLast,
5556
+ selectors,
5473
5557
  goToItem(item) {
5474
5558
  if (!item.matches(itemSelector)) return false;
5475
5559
  if (!container.contains(item)) return false;
5476
5560
  transition(getActiveItem, callbacks, item);
5477
5561
  return true;
5478
5562
  },
5479
- goToOffset(offset) {
5480
- return navigateByOffset(offset);
5481
- },
5482
- goToItemMatching(predicate) {
5483
- const walker = createItemWalker(false);
5484
- let node = walker.nextNode();
5485
- while (node) {
5486
- if (predicate(node)) {
5487
- transition(getActiveItem, callbacks, node);
5488
- return true;
5489
- }
5490
- node = walker.nextNode();
5563
+ goToOffset,
5564
+ clear,
5565
+ goTo(resolve) {
5566
+ const target = resolve(selectors);
5567
+ if (target && target.matches(itemSelector) && container.contains(target)) {
5568
+ transition(getActiveItem, callbacks, target);
5569
+ pending.clear();
5570
+ return true;
5491
5571
  }
5572
+ // Target not resolvable yet (e.g. items not committed to the DOM) — defer
5573
+ pending.defer(() => this.goTo(resolve));
5492
5574
  return false;
5493
5575
  },
5494
- findNearestEnabled(anchor) {
5495
- if (!container.contains(anchor)) return findFirstEnabled();
5496
-
5497
- // If the anchor itself is an enabled item, return it directly.
5498
- if (anchor instanceof HTMLElement && anchor.matches(enabledItemSelector)) {
5499
- return anchor;
5500
- }
5501
-
5502
- // Walk forward from the anchor for the nearest enabled item.
5503
- const walker = createItemWalker();
5504
- walker.currentNode = anchor;
5505
- const next = walker.nextNode();
5506
- if (next instanceof HTMLElement) return next;
5507
-
5508
- // No enabled item after anchor — walk backward (reuse same walker).
5509
- walker.currentNode = anchor;
5510
- return walker.previousNode();
5576
+ flushPendingNavigation() {
5577
+ pending.flush();
5511
5578
  },
5512
- clear,
5513
5579
  goUp() {
5514
- return direction === 'vertical' ? navigateBackward() : false;
5580
+ return direction === 'vertical' ? goToOffset(-1) : false;
5515
5581
  },
5516
5582
  goDown() {
5517
- return direction === 'vertical' ? navigateForward() : false;
5583
+ return direction === 'vertical' ? goToOffset(1) : false;
5518
5584
  },
5519
5585
  goLeft() {
5520
- return direction === 'horizontal' ? navigateBackward() : false;
5586
+ return direction === 'horizontal' ? goToOffset(-1) : false;
5521
5587
  },
5522
5588
  goRight() {
5523
- return direction === 'horizontal' ? navigateForward() : false;
5589
+ return direction === 'horizontal' ? goToOffset(1) : false;
5524
5590
  }
5525
5591
  };
5526
5592
  }
@@ -5543,7 +5609,7 @@ function createListFocusNavigation(options, callbacks, signal) {
5543
5609
  * @param initialItem Optional item to silently pre-select on creation (no callbacks fired).
5544
5610
  */
5545
5611
  function createActiveItemState(callbacks, signal, initialItem) {
5546
- let activeItem = initialItem ?? null;
5612
+ let activeItem = null;
5547
5613
  function clear() {
5548
5614
  if (activeItem) {
5549
5615
  callbacks.onDeactivate(activeItem);
@@ -5570,105 +5636,165 @@ function createActiveItemState(callbacks, signal, initialItem) {
5570
5636
  };
5571
5637
  }
5572
5638
 
5639
+ /** Deepest last-child descendant of `el` (or `el` itself if it has no children). */
5640
+ function lastDescendant(element) {
5641
+ let node = element;
5642
+ while (node.lastElementChild) node = node.lastElementChild;
5643
+ return node;
5644
+ }
5645
+
5646
+ /** Return the first item of an iterable, or `null` if it is empty. */
5647
+ function first(iterable) {
5648
+ const {
5649
+ value,
5650
+ done
5651
+ } = iterable[Symbol.iterator]().next();
5652
+ return done ? null : value;
5653
+ }
5654
+
5573
5655
  /**
5574
- * Create a focus navigation controller for a 2D grid.
5656
+ * Create the pure selection layer for a 2D grid.
5575
5657
  *
5576
- * Supports Up/Down between rows (with column memory) and Left/Right between cells
5577
- * (with wrapping across rows).
5658
+ * Everything here is side-effect-free: it only *queries* the DOM to resolve and report
5659
+ * rows/cells. No focus is moved and no {@link FocusNavigationCallbacks} are invoked — that
5660
+ * is the job of the mover layer built on top of this.
5578
5661
  *
5579
- * @param options Grid navigation options (container, rowSelector, cellSelector, isRowVisible, wrap).
5580
- * @param callbacks Callbacks for focus state changes.
5581
- * @param signal AbortSignal for cleanup.
5582
- * @returns FocusNavigationController instance.
5662
+ * @param options Grid navigation options (container, rowSelector, cellSelector).
5663
+ * @param getActive Reader for the currently active cell (owned by the mover's active-item state).
5664
+ * @param isVisible Predicate deciding whether a row is visible (derived from `isRowVisible`).
5665
+ * @returns The public {@link FocusNavigationSelectors} plus the internal
5666
+ * {@link GridSelectorHelpers} the mover layer consumes.
5583
5667
  */
5584
- function createGridFocusNavigation(options, callbacks, signal) {
5668
+ function createGridSelectors(options, getActive, isVisible) {
5585
5669
  const {
5586
5670
  container,
5587
5671
  rowSelector,
5588
- cellSelector,
5589
- isRowVisible,
5590
- wrap = false
5672
+ cellSelector
5591
5673
  } = options;
5592
- const state = createActiveItemState(callbacks, signal);
5593
- /** Remembered column index for Up/Down navigation (column memory). */
5594
- let rememberedCol = 0;
5595
-
5596
- /** Check if a row element is visible (passes the isRowVisible filter). */
5597
- function isVisible(row) {
5598
- return !isRowVisible || isRowVisible(row);
5599
- }
5600
-
5601
- /** Check if a row is navigable (visible with at least one cell). */
5602
5674
  function isNavigableRow(row) {
5603
5675
  return isVisible(row) && row.querySelector(cellSelector) !== null;
5604
5676
  }
5605
5677
 
5606
- /** Create a TreeWalker scoped to row elements within the container. */
5607
- function createRowWalker() {
5608
- return createSelectorTreeWalker(container, rowSelector);
5609
- }
5610
-
5611
5678
  /**
5612
- * Iterate navigable rows from the container using a TreeWalker.
5613
- * - `'first'`: returns the first navigable row (or null).
5614
- * - `'last'`: returns the last navigable row (or null).
5615
- * - `'all'`: returns all navigable rows as an array.
5679
+ * Lazily walk navigable rows (visible, with cells) starting from `startNode` in
5680
+ * `direction`, projecting each row through `dive`.
5616
5681
  */
5617
-
5618
- function findVisibleRows(mode) {
5619
- const walker = createRowWalker();
5620
- if (mode === 'all') {
5621
- const result = [];
5622
- let node = walker.nextNode();
5623
- while (node) {
5624
- if (isNavigableRow(node)) result.push(node);
5625
- node = walker.nextNode();
5626
- }
5627
- return result;
5628
- }
5629
- let found = null;
5630
- let node = walker.nextNode();
5682
+ function* findRow(direction, startNode = null, dive) {
5683
+ const walker = createSelectorTreeWalker(container, rowSelector);
5684
+ if (startNode) walker.currentNode = startNode;
5685
+ const advance = direction === 'next' ? () => walker.nextNode() : () => walker.previousNode();
5686
+ let node = advance();
5631
5687
  while (node) {
5632
5688
  if (isNavigableRow(node)) {
5633
- if (mode === 'first') return node;
5634
- found = node;
5689
+ const result = dive ? dive(node) : node;
5690
+ if (result) yield result;
5635
5691
  }
5636
- node = walker.nextNode();
5692
+ node = advance();
5637
5693
  }
5638
- return found;
5639
5694
  }
5640
5695
 
5641
- /** Get the cells within a single row element. */
5696
+ /** Find the first or last navigable row (visible, with cells), or null. */
5697
+ function findVisibleRow(mode) {
5698
+ return mode === 'first' ? first(findRow('next')) : first(findRow('prev', lastDescendant(container)));
5699
+ }
5700
+
5701
+ /** Collect all navigable rows (visible, with cells) in DOM order. */
5702
+ function findAllVisibleRows() {
5703
+ return Array.from(findRow('next'));
5704
+ }
5642
5705
  function getRowCells(row) {
5643
5706
  return Array.from(row.querySelectorAll(cellSelector));
5644
5707
  }
5645
-
5646
- /** Find the row element containing a cell, using closest(). */
5647
5708
  function findParentRow(cell) {
5648
5709
  const row = cell.closest(rowSelector);
5649
5710
  return row && container.contains(row) ? row : null;
5650
5711
  }
5651
-
5652
- /**
5653
- * Walk to the next or previous visible row (with cells) from a given row
5654
- * using a TreeWalker. Avoids building the full visible rows array.
5655
- */
5656
5712
  function findAdjacentVisibleRow(fromRow, direction) {
5657
- const walker = createRowWalker();
5658
- walker.currentNode = fromRow;
5659
- const advance = direction === 'next' ? () => walker.nextNode() : () => walker.previousNode();
5660
- let node = advance();
5661
- while (node) {
5662
- if (isNavigableRow(node)) return node;
5663
- node = advance();
5664
- }
5665
- return null;
5713
+ return first(findRow(direction, fromRow));
5666
5714
  }
5667
5715
 
5668
- /**
5669
- * Activate the cell at the given column in a row element.
5670
- * Clamps col to the row's available cells.
5671
- */
5716
+ /** Resolve the first cell of the boundary row (without moving focus). */
5717
+ function getBoundaryCell(mode) {
5718
+ const row = findVisibleRow(mode);
5719
+ if (!row) return null;
5720
+ const cells = getRowCells(row);
5721
+ return cells.length > 0 ? cells[0] : null;
5722
+ }
5723
+
5724
+ /** Resolve the first cell matching a predicate (without moving focus). */
5725
+ function getMatching(predicate) {
5726
+ return first(findRow('next', null, row => getRowCells(row).find(predicate) ?? null));
5727
+ }
5728
+ const selectors = {
5729
+ get activeItem() {
5730
+ return getActive();
5731
+ },
5732
+ get hasNavigableItems() {
5733
+ return first(findRow('next')) !== null;
5734
+ },
5735
+ getFirst: () => getBoundaryCell('first'),
5736
+ getLast: () => getBoundaryCell('last'),
5737
+ getMatching
5738
+ };
5739
+ const helpers = {
5740
+ findFirstVisibleRow: () => findVisibleRow('first'),
5741
+ findLastVisibleRow: () => findVisibleRow('last'),
5742
+ findAllVisibleRows,
5743
+ getRowCells,
5744
+ findParentRow,
5745
+ findAdjacentVisibleRow
5746
+ };
5747
+ return {
5748
+ selectors,
5749
+ helpers
5750
+ };
5751
+ }
5752
+
5753
+ /**
5754
+ * Create a focus navigation controller for a 2D grid.
5755
+ *
5756
+ * The controller is composed of two layers:
5757
+ * - a pure, side-effect-free **selection** layer ({@link createGridSelectors}) that only
5758
+ * resolves/reports rows and cells by querying the DOM, and
5759
+ * - a **mover** layer (this function) that commits focus by updating the active-item state
5760
+ * (which fires {@link FocusNavigationCallbacks}) on top of the selectors.
5761
+ *
5762
+ * Supports Up/Down between rows (with column memory) and Left/Right between cells
5763
+ * (with wrapping across rows).
5764
+ *
5765
+ * @param options Grid navigation options (container, rowSelector, cellSelector, isRowVisible, wrap).
5766
+ * @param callbacks Callbacks for focus state changes.
5767
+ * @param signal AbortSignal for cleanup.
5768
+ * @returns FocusNavigationController instance.
5769
+ */
5770
+ function createGridFocusNavigation(options, callbacks, signal) {
5771
+ const {
5772
+ isRowVisible,
5773
+ wrap = false
5774
+ } = options;
5775
+ const state = createActiveItemState(callbacks, signal);
5776
+
5777
+ /** Whether a row is visible (passes the optional `isRowVisible` filter). */
5778
+ const isVisible = row => !isRowVisible || isRowVisible(row);
5779
+ const {
5780
+ selectors,
5781
+ helpers
5782
+ } = createGridSelectors(options, () => state.active, isVisible);
5783
+ const {
5784
+ findFirstVisibleRow,
5785
+ findLastVisibleRow,
5786
+ findAllVisibleRows,
5787
+ getRowCells,
5788
+ findParentRow,
5789
+ findAdjacentVisibleRow
5790
+ } = helpers;
5791
+
5792
+ /** Deferred navigation intent (replayed once cells are committed to the DOM). */
5793
+ const pending = createPendingNavigation(signal);
5794
+ /** Remembered column index for Up/Down navigation (column memory). */
5795
+ let rememberedCol = 0;
5796
+
5797
+ /** Activate the cell at the given column in a row element. */
5672
5798
  function focusCellInRow(row, col) {
5673
5799
  const cells = getRowCells(row);
5674
5800
  if (cells.length === 0) return false;
@@ -5676,14 +5802,18 @@ function createGridFocusNavigation(options, callbacks, signal) {
5676
5802
  state.setActive(cells[clampedCol]);
5677
5803
  return true;
5678
5804
  }
5805
+
5806
+ /** Got to first cell in first row */
5679
5807
  function goToFirst() {
5680
- const row = findVisibleRows('first');
5808
+ const row = findFirstVisibleRow();
5681
5809
  if (!row) return false;
5682
5810
  rememberedCol = 0;
5683
5811
  return focusCellInRow(row, 0);
5684
5812
  }
5813
+
5814
+ /** Got to first cell in last row */
5685
5815
  function goToLast() {
5686
- const row = findVisibleRows('last');
5816
+ const row = findLastVisibleRow();
5687
5817
  if (!row) return false;
5688
5818
  rememberedCol = 0;
5689
5819
  return focusCellInRow(row, 0);
@@ -5708,7 +5838,7 @@ function createGridFocusNavigation(options, callbacks, signal) {
5708
5838
  // Wrap to the adjacent row (or opposite boundary row), activating the first or last cell.
5709
5839
  const rowDirection = step > 0 ? 'next' : 'prev';
5710
5840
  const adjacentRow = findAdjacentVisibleRow(currentRow, rowDirection);
5711
- const targetRow = adjacentRow ?? (step > 0 ? findVisibleRows('first') : findVisibleRows('last'));
5841
+ const targetRow = adjacentRow ?? (step > 0 ? findFirstVisibleRow() : findLastVisibleRow());
5712
5842
  if (!targetRow) return false;
5713
5843
  const targetCells = getRowCells(targetRow);
5714
5844
  if (targetCells.length === 0) return false;
@@ -5730,27 +5860,17 @@ function createGridFocusNavigation(options, callbacks, signal) {
5730
5860
  if (adjacentRow) return focusCellInRow(adjacentRow, rememberedCol);
5731
5861
  if (wrap) {
5732
5862
  // Wrap to the opposite boundary row.
5733
- const wrapRow = direction === 'next' ? findVisibleRows('first') : findVisibleRows('last');
5863
+ const wrapRow = direction === 'next' ? findFirstVisibleRow() : findLastVisibleRow();
5734
5864
  if (wrapRow) return focusCellInRow(wrapRow, rememberedCol);
5735
5865
  }
5736
5866
  return false;
5737
5867
  }
5738
5868
  return {
5739
5869
  type: 'grid',
5740
- get activeItem() {
5741
- return state.active;
5742
- },
5743
- get hasActiveItem() {
5744
- return state.active !== null;
5745
- },
5746
- get hasNavigableItems() {
5747
- return findVisibleRows('first') !== null;
5748
- },
5749
- goToFirst,
5750
- goToLast,
5870
+ selectors,
5751
5871
  goToOffset(offset) {
5752
5872
  if (offset === 0) return state.active !== null;
5753
- const visibleRows = findVisibleRows('all');
5873
+ const visibleRows = findAllVisibleRows();
5754
5874
  if (visibleRows.length === 0) return false;
5755
5875
  if (!state.active) {
5756
5876
  // No active item: jump to first or last row, then apply remaining offset.
@@ -5769,25 +5889,6 @@ function createGridFocusNavigation(options, callbacks, signal) {
5769
5889
  if (targetIdx === rowIdx) return false;
5770
5890
  return focusCellInRow(visibleRows[targetIdx], rememberedCol);
5771
5891
  },
5772
- goToItemMatching(predicate) {
5773
- // Iterate visible rows lazily — short-circuit on first match.
5774
- const walker = createRowWalker();
5775
- let row = walker.nextNode();
5776
- while (row) {
5777
- if (isNavigableRow(row)) {
5778
- const cells = getRowCells(row);
5779
- for (let c = 0; c < cells.length; c++) {
5780
- if (predicate(cells[c])) {
5781
- rememberedCol = c;
5782
- state.setActive(cells[c]);
5783
- return true;
5784
- }
5785
- }
5786
- }
5787
- row = walker.nextNode();
5788
- }
5789
- return false;
5790
- },
5791
5892
  goToItem(item) {
5792
5893
  // Use closest() to find the parent row, then find col within that row only.
5793
5894
  const row = findParentRow(item);
@@ -5801,6 +5902,20 @@ function createGridFocusNavigation(options, callbacks, signal) {
5801
5902
  },
5802
5903
  clear() {
5803
5904
  state.clear();
5905
+ pending.clear();
5906
+ },
5907
+ goTo(resolve) {
5908
+ const target = resolve(selectors);
5909
+ if (target && this.goToItem(target)) {
5910
+ pending.clear();
5911
+ return true;
5912
+ }
5913
+ // Target not resolvable yet (e.g. cells not committed to the DOM) — defer
5914
+ pending.defer(() => this.goTo(resolve));
5915
+ return false;
5916
+ },
5917
+ flushPendingNavigation() {
5918
+ pending.flush();
5804
5919
  },
5805
5920
  goUp() {
5806
5921
  return goVertical('prev');
@@ -5936,7 +6051,7 @@ function setupRovingTabIndex(options, signal) {
5936
6051
  }
5937
6052
  }
5938
6053
  if (!hasTabStop) {
5939
- const fallback = container.querySelector(nav.enabledItemSelector);
6054
+ const fallback = container.querySelector(nav.selectors.enabledItemSelector);
5940
6055
  if (fallback) setTabIndex(fallback, '0');
5941
6056
  }
5942
6057
  }
@@ -5946,7 +6061,7 @@ function setupRovingTabIndex(options, signal) {
5946
6061
  const items = Array.from(container.querySelectorAll(itemSelector));
5947
6062
  const {
5948
6063
  activeItem
5949
- } = nav;
6064
+ } = nav.selectors;
5950
6065
 
5951
6066
  // Prefer either the current active item (from DOM) or the current selected item
5952
6067
  let preferredTabStopIndex = activeItem && items.indexOf(activeItem);
@@ -5963,7 +6078,7 @@ function setupRovingTabIndex(options, signal) {
5963
6078
  /** Ensure a tab stop exists; find a fallback near `anchor` and optionally move focus to it. */
5964
6079
  function ensureTabStop(shouldFocus, anchor) {
5965
6080
  if (container.querySelector(itemActiveSelector)) return;
5966
- const fallback = (anchor && nav.findNearestEnabled(anchor)) ?? container.querySelector(nav.enabledItemSelector);
6081
+ const fallback = (anchor && nav.selectors.findNearestEnabled(anchor)) ?? container.querySelector(nav.selectors.enabledItemSelector);
5967
6082
  if (!fallback) return;
5968
6083
  if (shouldFocus) {
5969
6084
  nav.goToItem(fallback);
@@ -6036,7 +6151,7 @@ function setupRovingTabIndex(options, signal) {
6036
6151
 
6037
6152
  // Handle disabled
6038
6153
  if (disabledTargets.length > 0) {
6039
- const currentActive = nav.activeItem;
6154
+ const currentActive = nav.selectors.activeItem;
6040
6155
  for (const target of disabledTargets) {
6041
6156
  setTabIndex(target, '-1');
6042
6157
  }
@@ -6068,7 +6183,7 @@ function setupRovingTabIndex(options, signal) {
6068
6183
 
6069
6184
  container.addEventListener('keydown', evt => {
6070
6185
  // Adopt focus target if nothing is active yet.
6071
- if (!nav.activeItem) {
6186
+ if (!nav.selectors.activeItem) {
6072
6187
  const target = evt.target;
6073
6188
  if (target.matches(itemSelector) && container.contains(target)) {
6074
6189
  nav.goToItem(target);
@@ -6089,10 +6204,10 @@ function setupRovingTabIndex(options, signal) {
6089
6204
  handled = nav.goUp();
6090
6205
  break;
6091
6206
  case 'Home':
6092
- handled = nav.goToFirst();
6207
+ handled = nav.goTo(s => s.getFirst());
6093
6208
  break;
6094
6209
  case 'End':
6095
- handled = nav.goToLast();
6210
+ handled = nav.goTo(s => s.getLast());
6096
6211
  break;
6097
6212
  }
6098
6213
  if (handled) {
@@ -6253,12 +6368,24 @@ SelectionChipGroup.className = CLASSNAME$1j;
6253
6368
  /**
6254
6369
  * Get the value for a combobox option element.
6255
6370
  * Uses `data-value` when set; falls back to the element's trimmed `textContent`.
6371
+ *
6372
+ * This is the *selection* value , which may differ from the visible label
6256
6373
  */
6257
6374
  function getOptionValue(option) {
6258
6375
  if (option.dataset.value !== undefined) return option.dataset.value;
6259
6376
  return option.textContent?.trim() ?? '';
6260
6377
  }
6261
6378
 
6379
+ /**
6380
+ * Get the visible label for a combobox option element (its trimmed `textContent`).
6381
+ *
6382
+ * Used for typeahead matching: the user types the characters they see, which is the
6383
+ * option's label — not its `data-value` (which can be an unrelated id).
6384
+ */
6385
+ function getOptionLabel(option) {
6386
+ return option.textContent?.trim() ?? '';
6387
+ }
6388
+
6262
6389
  /** Returns true when an option carries aria-disabled="true". */
6263
6390
  function isOptionDisabled(option) {
6264
6391
  return option.getAttribute('aria-disabled') === 'true';
@@ -6270,20 +6397,8 @@ function isActionCell(cell) {
6270
6397
  if (!row) return false;
6271
6398
  return row.querySelector('[role="gridcell"]') !== cell;
6272
6399
  }
6273
-
6274
- /** Predicate matching an option element that carries `aria-selected="true"`. */
6275
6400
  const isSelected = el => el.getAttribute('aria-selected') === 'true';
6276
6401
 
6277
- /** Navigate to the selected option, or to the first option if none is selected. */
6278
- function goToSelectedOrFirst(nav) {
6279
- if (!nav.goToItemMatching(isSelected)) nav.goToFirst();
6280
- }
6281
-
6282
- /** Navigate to the selected option, or to the last option if none is selected. */
6283
- function goToSelectedOrLast(nav) {
6284
- if (!nav.goToItemMatching(isSelected)) nav.goToLast();
6285
- }
6286
-
6287
6402
  /**
6288
6403
  * Compute the current state of a section and notify when it changed.
6289
6404
  *
@@ -6596,12 +6711,12 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6596
6711
  const nav = handle.focusNav;
6597
6712
  switch (event.key) {
6598
6713
  case 'Enter':
6599
- if (handle.isOpen && nav?.hasActiveItem && nav.activeItem) {
6714
+ if (handle.isOpen && nav?.selectors.activeItem) {
6600
6715
  // Capture activeItem before click — the click handler may close
6601
6716
  // the popover and clear the focus navigation state.
6602
6717
  const {
6603
6718
  activeItem
6604
- } = nav;
6719
+ } = nav.selectors;
6605
6720
  // "Click" on active option
6606
6721
  if (!isOptionDisabled(activeItem)) {
6607
6722
  activeItem.click();
@@ -6620,39 +6735,35 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6620
6735
  // let Enter pass through so it can submit a surrounding form
6621
6736
  break;
6622
6737
  case 'ArrowDown':
6623
- if (nav?.hasNavigableItems) {
6624
- if (handle.isOpen && !altKey) {
6625
- if (nav.hasActiveItem) {
6626
- if (nav.type === 'grid') {
6627
- nav.goDown();
6628
- } else if (!nav.goToOffset(1) && wrapNavigation) {
6629
- nav.goToFirst();
6630
- }
6631
- } else {
6632
- goToSelectedOrFirst(nav);
6738
+ if (!handle.isOpen) {
6739
+ handle.setIsOpen(true);
6740
+ if (!altKey) nav?.goTo(s => s.getMatching(isSelected) ?? s.getFirst());
6741
+ } else if (nav?.selectors.hasNavigableItems && !altKey) {
6742
+ if (nav.selectors.activeItem) {
6743
+ if (nav.type === 'grid') {
6744
+ nav.goDown();
6745
+ } else if (!nav.goToOffset(1) && wrapNavigation) {
6746
+ nav.goTo(s => s.getFirst());
6633
6747
  }
6634
6748
  } else {
6635
- // Open the listbox and focus selected option, fall back to first.
6636
- handle.setIsOpen(true);
6637
- if (!altKey) goToSelectedOrFirst(nav);
6749
+ nav.goTo(s => s.getMatching(isSelected) ?? s.getFirst());
6638
6750
  }
6639
6751
  }
6640
6752
  flag = true;
6641
6753
  break;
6642
6754
  case 'ArrowUp':
6643
- if (nav?.hasNavigableItems) {
6644
- if (!handle.isOpen && !altKey) {
6645
- // Open the listbox and focus selected option, fall back to last.
6646
- handle.setIsOpen(true);
6647
- goToSelectedOrLast(nav);
6648
- } else if (handle.isOpen && nav.hasActiveItem) {
6755
+ if (!handle.isOpen && !altKey) {
6756
+ handle.setIsOpen(true);
6757
+ nav?.goTo(s => s.getMatching(isSelected) ?? s.getLast());
6758
+ } else if (handle.isOpen && nav?.selectors.hasNavigableItems) {
6759
+ if (nav.selectors.activeItem) {
6649
6760
  if (nav.type === 'grid') {
6650
6761
  nav.goUp();
6651
6762
  } else if (!nav.goToOffset(-1) && wrapNavigation) {
6652
- nav.goToLast();
6763
+ nav.goTo(s => s.getLast());
6653
6764
  }
6654
- } else if (handle.isOpen && !nav.hasActiveItem && !altKey) {
6655
- goToSelectedOrLast(nav);
6765
+ } else if (!altKey) {
6766
+ nav.goTo(s => s.getMatching(isSelected) ?? s.getLast());
6656
6767
  }
6657
6768
  }
6658
6769
  flag = true;
@@ -6667,13 +6778,13 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6667
6778
  flag = true;
6668
6779
  break;
6669
6780
  case 'PageUp':
6670
- if (handle.isOpen && nav?.hasActiveItem) {
6781
+ if (handle.isOpen && nav?.selectors.activeItem) {
6671
6782
  nav.goToOffset(-10);
6672
6783
  }
6673
6784
  flag = true;
6674
6785
  break;
6675
6786
  case 'PageDown':
6676
- if (handle.isOpen && nav?.hasActiveItem) {
6787
+ if (handle.isOpen && nav?.selectors.activeItem) {
6677
6788
  nav.goToOffset(10);
6678
6789
  }
6679
6790
  flag = true;
@@ -6759,25 +6870,16 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6759
6870
  // Update aria-expanded on trigger
6760
6871
  trigger?.setAttribute('aria-expanded', String(isOpen));
6761
6872
  notify('open', isOpen);
6762
-
6763
- // When opening, always notify the current options state so that
6764
- // subscribers (ComboboxState) get the correct initial value.
6765
- // Without this, a list that starts empty never fires `optionsChange`
6766
- // because `lastOptionsLength` is initialized to `0` and `notifyVisibilityChange`
6767
- // only fires on *changes*.
6768
- if (isOpen) {
6769
- const inputValue = trigger?.value ?? '';
6770
- notify('optionsChange', {
6771
- optionsLength: lastOptionsLength,
6772
- inputValue
6773
- });
6774
- }
6775
6873
  },
6776
6874
  select(option) {
6777
6875
  callbacks.onSelect?.({
6778
6876
  value: option ? getOptionValue(option) : ''
6779
6877
  });
6780
6878
  },
6879
+ flushPendingNavigation() {
6880
+ // Do navigations actions we could not do because the combobox items were not mounted yet
6881
+ focusNav?.flushPendingNavigation();
6882
+ },
6781
6883
  registerOption(element, callback) {
6782
6884
  const filterLower = filterValue.toLowerCase();
6783
6885
  const text = getOptionValue(element).toLowerCase();
@@ -6950,18 +7052,9 @@ function createTypeahead(getWalker, getItemValue, signal) {
6950
7052
  }
6951
7053
  signal.addEventListener('abort', reset);
6952
7054
 
6953
- // Handle typeahead keys
6954
- function handle(key, currentItem) {
6955
- // Clear any pending reset timeout.
6956
- if (searchTimeout !== undefined) {
6957
- clearTimeout(searchTimeout);
6958
- }
6959
-
6960
- // Accumulate the character.
6961
- searchString += key.toLowerCase();
6962
-
6963
- // Schedule clearing the search string after inactivity.
6964
- searchTimeout = setTimeout(reset, SEARCH_TIMEOUT);
7055
+ // Match the current accumulated search string against the live DOM.
7056
+ function match(currentItem) {
7057
+ if (!searchString) return null;
6965
7058
  const walker = getWalker();
6966
7059
  if (!walker) return null;
6967
7060
 
@@ -7022,8 +7115,29 @@ function createTypeahead(getWalker, getItemValue, signal) {
7022
7115
  }
7023
7116
  }
7024
7117
  }
7118
+
7119
+ // Handle typeahead keys
7120
+ function handle(key, currentItem) {
7121
+ // Clear any pending reset timeout.
7122
+ if (searchTimeout !== undefined) {
7123
+ clearTimeout(searchTimeout);
7124
+ }
7125
+
7126
+ // Accumulate the character.
7127
+ searchString += key.toLowerCase();
7128
+
7129
+ // Schedule clearing the search string after inactivity.
7130
+ searchTimeout = setTimeout(reset, SEARCH_TIMEOUT);
7131
+ return match(currentItem);
7132
+ }
7133
+
7134
+ // Re-run the match for the current buffer without mutating it.
7135
+ function rematch(currentItem) {
7136
+ return match(currentItem);
7137
+ }
7025
7138
  return {
7026
7139
  handle,
7140
+ rematch,
7027
7141
  reset
7028
7142
  };
7029
7143
  }
@@ -7060,7 +7174,7 @@ function setupComboboxButton(button, callbacks) {
7060
7174
  // In list mode, walk option elements.
7061
7175
  const selector = combobox.focusNav?.type === 'grid' ? '[role="gridcell"]' : '[role="option"]';
7062
7176
  return createSelectorTreeWalker(combobox.listbox, selector);
7063
- }, getOptionValue, signal);
7177
+ }, getOptionLabel, signal);
7064
7178
 
7065
7179
  // Click toggles the listbox.
7066
7180
  button.addEventListener('click', () => combobox.setIsOpen(!combobox.isOpen), {
@@ -7071,59 +7185,65 @@ function setupComboboxButton(button, callbacks) {
7071
7185
  switch (event.key) {
7072
7186
  case 'Tab':
7073
7187
  // Selects the focused option
7074
- if (combobox.isOpen && nav?.hasActiveItem && nav.activeItem) {
7075
- combobox.select(nav.activeItem);
7188
+ if (combobox.isOpen && nav?.selectors.activeItem) {
7189
+ combobox.select(nav.selectors.activeItem);
7076
7190
  }
7077
7191
  // Return false to continue normal 'Tab' behavior (focus next).
7078
7192
  return false;
7079
7193
  case ' ':
7080
7194
  // Space acts like Enter in button mode.
7081
- if (combobox.isOpen && nav?.hasActiveItem && nav.activeItem) {
7195
+ if (combobox.isOpen && nav?.selectors.activeItem) {
7082
7196
  // Click the active item — delegated handler handles select + close.
7083
- nav.activeItem.click();
7197
+ nav.selectors.activeItem.click();
7084
7198
  } else {
7085
7199
  combobox.setIsOpen(true);
7086
7200
  }
7087
7201
  return true;
7088
7202
  case 'ArrowUp':
7089
7203
  // Alt+ArrowUp: select the focused option and close.
7090
- if (event.altKey && combobox.isOpen && nav?.hasActiveItem && nav.activeItem) {
7091
- combobox.select(nav.activeItem);
7204
+ if (event.altKey && combobox.isOpen && nav?.selectors.activeItem) {
7205
+ combobox.select(nav.selectors.activeItem);
7092
7206
  combobox.setIsOpen(false);
7093
7207
  return true;
7094
7208
  }
7095
7209
  // All other ArrowUp cases handled by base handler.
7096
7210
  return false;
7097
7211
  case 'Home':
7212
+ // `goTo` focuses the first option immediately when open, or defers
7213
+ // until the options commit when opening from closed.
7098
7214
  combobox.setIsOpen(true);
7099
- nav?.goToFirst();
7215
+ nav?.goTo(n => n.getFirst());
7100
7216
  return true;
7101
7217
  case 'End':
7102
7218
  combobox.setIsOpen(true);
7103
- nav?.goToLast();
7219
+ nav?.goTo(n => n.getLast());
7104
7220
  return true;
7105
7221
  case 'ArrowLeft':
7106
7222
  // Grid mode: navigate to previous cell.
7107
- if (nav?.type === 'grid' && combobox.isOpen && nav.hasActiveItem) {
7223
+ if (nav?.type === 'grid' && combobox.isOpen && nav.selectors.activeItem) {
7108
7224
  nav.goLeft();
7109
7225
  return true;
7110
7226
  }
7111
7227
  return false;
7112
7228
  case 'ArrowRight':
7113
7229
  // Grid mode: navigate to next cell.
7114
- if (nav?.type === 'grid' && combobox.isOpen && nav.hasActiveItem) {
7230
+ if (nav?.type === 'grid' && combobox.isOpen && nav.selectors.activeItem) {
7115
7231
  nav.goRight();
7116
7232
  return true;
7117
7233
  }
7118
7234
  return false;
7235
+ case 'Escape':
7236
+ // Close if open; never clear selection (button-mode has no text input).
7237
+ if (combobox.isOpen) {
7238
+ combobox.setIsOpen(false);
7239
+ }
7240
+ return true;
7119
7241
  default:
7120
7242
  // Printable characters → typeahead.
7121
7243
  if (isPrintableKey(event)) {
7122
7244
  combobox.setIsOpen(true);
7123
- const match = typeahead.handle(event.key, nav?.activeItem ?? null);
7124
- if (match && nav) {
7125
- nav.goToItem(match);
7126
- }
7245
+ typeahead.handle(event.key, nav?.selectors.activeItem ?? null);
7246
+ nav?.goTo(n => typeahead.rematch(n.activeItem));
7127
7247
  return true;
7128
7248
  }
7129
7249
  return false;
@@ -7286,9 +7406,10 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
7286
7406
 
7287
7407
  // If `as` is provided, use it directly; otherwise use the LumX Button (full theme/disabled behavior).
7288
7408
  const ButtonComp = as ?? Button;
7289
- const internalRef = useRef(null);
7290
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7291
- const mergedRef = useMergeRefs(ref, internalRef, anchorRef);
7409
+
7410
+ // Track the button DOM element via state (instead of useRef) so it re-triggers the useEffect
7411
+ const [buttonElement, setButtonElement] = useState(null);
7412
+ const mergedRef = useMergeRefs(ref, setButtonElement, anchorRef);
7292
7413
 
7293
7414
  // Keep onSelect in a ref to avoid re-creating the handle on every render
7294
7415
  const onSelectRef = useRef(onSelect);
@@ -7296,9 +7417,8 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
7296
7417
 
7297
7418
  // Create the combobox handle with button-mode controller on mount
7298
7419
  useEffect(() => {
7299
- const button = internalRef.current;
7300
- if (!button) return undefined;
7301
- const handle = setupComboboxButton(button, {
7420
+ if (!buttonElement) return undefined;
7421
+ const handle = setupComboboxButton(buttonElement, {
7302
7422
  onSelect(option) {
7303
7423
  onSelectRef.current?.(option);
7304
7424
  }
@@ -7308,7 +7428,7 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
7308
7428
  handle.destroy();
7309
7429
  setHandle(null);
7310
7430
  };
7311
- }, [setHandle]);
7431
+ }, [buttonElement, setHandle]);
7312
7432
  return ComboboxButton$1({
7313
7433
  ...buttonProps,
7314
7434
  label,
@@ -7447,7 +7567,7 @@ function setupComboboxInput(input, options) {
7447
7567
  case 'ArrowLeft':
7448
7568
  case 'ArrowRight':
7449
7569
  // Grid mode: navigate cells when active item exists.
7450
- if (nav?.type === 'grid' && nav.hasActiveItem) {
7570
+ if (nav?.type === 'grid' && nav.selectors.activeItem) {
7451
7571
  if (event.key === 'ArrowLeft') nav.goLeft();else nav.goRight();
7452
7572
  return true;
7453
7573
  }
@@ -8306,25 +8426,30 @@ const ComboboxList = forwardRef((props, ref) => {
8306
8426
  const listContextValue = useMemo(() => ({
8307
8427
  type
8308
8428
  }), [type]);
8429
+ const [isOpen] = useComboboxOpen();
8430
+ const options = useComboboxEvent('optionsChange', undefined);
8431
+ const visibleCount = options?.optionsLength ?? 0;
8309
8432
 
8310
- // Register the list as the listbox when the handle becomes available
8433
+ // Register list as listbox when handle is available.
8311
8434
  useEffect(() => {
8312
8435
  const list = internalRef.current;
8313
8436
  if (!list) return undefined;
8314
8437
  return handle?.registerListbox(list);
8315
8438
  }, [handle]);
8316
8439
 
8317
- // Track loading state for aria-busy (auto-derived from skeleton registrations).
8318
- // Uses both the handle's synchronous getter (for initial state) and the loadingChange event
8319
- // (for subsequent updates), because child useEffects (skeleton registration) run before
8320
- // parent subscriptions — relying on events alone would miss the initial notification.
8440
+ // Track loading state for aria-busy
8321
8441
  const [isLoading, setIsLoading] = useState(false);
8322
8442
  useEffect(() => {
8323
8443
  if (!handle) return undefined;
8324
- // Read current state synchronously (catches registrations that happened before subscription)
8444
+ // Read current state synchronously (catches registrations before subscription).
8325
8445
  setIsLoading(handle.isLoading);
8326
8446
  return handle.subscribe('loadingChange', setIsLoading);
8327
8447
  }, [handle]);
8448
+
8449
+ // Flush pending keyboard navigation after options commit on open.
8450
+ useLayoutEffect(() => {
8451
+ if (isOpen) handle?.flushPendingNavigation();
8452
+ }, [isOpen, visibleCount, handle]);
8328
8453
  return /*#__PURE__*/jsx(ComboboxListContext.Provider, {
8329
8454
  value: listContextValue,
8330
8455
  children: ComboboxList$1({
@@ -8335,7 +8460,7 @@ const ComboboxList = forwardRef((props, ref) => {
8335
8460
  ref: mergedRef,
8336
8461
  id: listboxId,
8337
8462
  type,
8338
- children
8463
+ children: isOpen ? children : null
8339
8464
  })
8340
8465
  });
8341
8466
  });
@@ -16825,6 +16950,24 @@ function toggleSelection(options, getOptionId, currentValue, selectedOptionId, i
16825
16950
  return updated;
16826
16951
  }
16827
16952
 
16953
+ /**
16954
+ * Get the display name for a single option value.
16955
+ *
16956
+ * Resolves the option's display name by trying `getOptionName` first,
16957
+ * then falling back to `getOptionId`, returning `''` for nullish values.
16958
+ */
16959
+ function getOptionDisplayName(value, getOptionName, getOptionId) {
16960
+ if (value === undefined || value === null) return '';
16961
+ if (getOptionName) {
16962
+ const name = getWithSelector(getOptionName, value);
16963
+ if (name != null) return String(name);
16964
+ }
16965
+ if (getOptionId) {
16966
+ return String(getWithSelector(getOptionId, value));
16967
+ }
16968
+ return '';
16969
+ }
16970
+
16828
16971
  /**
16829
16972
  * Render options as ComboboxOption elements.
16830
16973
  * Framework-specific components are passed as a second argument.
@@ -16949,7 +17092,7 @@ const SelectButton$2 = (props, {
16949
17092
  * each option to its name and join with `, `. Falsy entries (undefined, empty names)
16950
17093
  * are filtered out so partial state still renders cleanly.
16951
17094
  */
16952
- const displayValue = value != null ? castArray(value).map(v => v != null && getWithSelector(getOptionName, v)).filter(Boolean).join(', ') : '';
17095
+ const displayValue = value != null ? castArray(value).map(v => getOptionDisplayName(v, getOptionName, getOptionId)).filter(Boolean).join(', ') : '';
16953
17096
  return /*#__PURE__*/jsxs(Combobox.Provider, {
16954
17097
  onOpen: onOpen,
16955
17098
  children: [/*#__PURE__*/jsx(Combobox.Button, {
@@ -17078,8 +17221,8 @@ const DefaultButton = forwardRef((props, ref) => /*#__PURE__*/jsx(Button, {
17078
17221
  * fully replaced and only props valid for that component apply.
17079
17222
  *
17080
17223
  * Discriminated on `selectionType`:
17081
- * - default / `'single'` → `value?: O`, `onChange?: (newValue?: O) => void`.
17082
- * - `'multiple'` → `value?: O[]`, `onChange?: (newValue?: O[]) => void`.
17224
+ * - default / `'single'` → `value?: O`, `onChange?: (newValue: O) => void`.
17225
+ * - `'multiple'` → `value?: O[]`, `onChange?: (newValue: O[]) => void`.
17083
17226
  *
17084
17227
  * `as` and `selectionType` are top-level on this type (rather than buried in
17085
17228
  * an intersection or union member) so that TS can infer `E` from `as` and
@@ -17092,10 +17235,11 @@ const DefaultButton = forwardRef((props, ref) => /*#__PURE__*/jsx(Button, {
17092
17235
 
17093
17236
  /**
17094
17237
  * Single-selection props (`selectionType` defaults to `'single'`).
17095
- * Backwards-compatible alias — existing consumers do not need to set `selectionType`.
17096
17238
  */
17097
17239
 
17098
- /** Multi-selection props (`selectionType: 'multiple'` is required to opt in). */
17240
+ /**
17241
+ * Multi-selection props (`selectionType: 'multiple'` is required to opt in).
17242
+ */
17099
17243
 
17100
17244
  /**
17101
17245
  * `React.forwardRef` re-typed to preserve our polymorphic generics
@@ -17139,6 +17283,9 @@ const SelectButton$1 = React__default.forwardRef((props, ref) => {
17139
17283
  const next = toggleSelection(options, getOptionId, value, selectedOption?.value, isMultiple);
17140
17284
  onChange?.(next);
17141
17285
  }, [getOptionId, isMultiple, onChange, options, value]);
17286
+
17287
+ // If as is defined and not the Button, render as, else render DefaultButton (with mdiMenuDown right icon)
17288
+ const buttonAs = as && as !== Button ? as : DefaultButton;
17142
17289
  return SelectButton$2({
17143
17290
  options,
17144
17291
  getOptionId,
@@ -17151,10 +17298,9 @@ const SelectButton$1 = React__default.forwardRef((props, ref) => {
17151
17298
  isMultiselectable: isMultiple,
17152
17299
  label,
17153
17300
  labelDisplayMode,
17154
- // With no `as`, the default trigger adds the chevron.
17155
17301
  buttonProps: {
17156
17302
  ...buttonProps,
17157
- as: as ?? DefaultButton,
17303
+ as: buttonAs,
17158
17304
  ref
17159
17305
  },
17160
17306
  popoverProps,
@@ -17185,24 +17331,6 @@ const SelectButton = Object.assign(SelectButton$1, {
17185
17331
  Option: ComboboxOption
17186
17332
  });
17187
17333
 
17188
- /**
17189
- * Get the display name for a single option value.
17190
- *
17191
- * Resolves the option's display name by trying `getOptionName` first,
17192
- * then falling back to `getOptionId`, returning `''` for nullish values.
17193
- */
17194
- function getOptionDisplayName(value, getOptionName, getOptionId) {
17195
- if (value === undefined || value === null) return '';
17196
- if (getOptionName) {
17197
- const name = getWithSelector(getOptionName, value);
17198
- if (name != null) return String(name);
17199
- }
17200
- if (getOptionId) {
17201
- return String(getWithSelector(getOptionId, value));
17202
- }
17203
- return '';
17204
- }
17205
-
17206
17334
  /**
17207
17335
  * Component display name.
17208
17336
  */