@lumx/react 4.16.0-next.0 → 4.16.0-next.1
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.
- package/index.js +312 -309
- package/index.js.map +1 -1
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -5325,15 +5325,25 @@ Tooltip.displayName = COMPONENT_NAME$1i;
|
|
|
5325
5325
|
Tooltip.className = CLASSNAME$1h;
|
|
5326
5326
|
Tooltip.defaultProps = DEFAULT_PROPS$12;
|
|
5327
5327
|
|
|
5328
|
-
/**
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
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
|
+
};
|
|
5337
5347
|
}
|
|
5338
5348
|
|
|
5339
5349
|
/**
|
|
@@ -5362,10 +5372,7 @@ function createListFocusNavigation(options, callbacks, signal) {
|
|
|
5362
5372
|
/** Combined CSS selector matching enabled (non-disabled) items. */
|
|
5363
5373
|
const enabledItemSelector = itemDisabledSelector ? `${itemSelector}:not(${itemDisabledSelector})` : itemSelector;
|
|
5364
5374
|
|
|
5365
|
-
/**
|
|
5366
|
-
* Create a TreeWalker over items in the container.
|
|
5367
|
-
* @param enabledOnly When true (default), disabled items are skipped.
|
|
5368
|
-
*/
|
|
5375
|
+
/** Create a TreeWalker over items in the container. */
|
|
5369
5376
|
function createItemWalker(enabledOnly = true) {
|
|
5370
5377
|
const selector = enabledOnly ? enabledItemSelector : itemSelector;
|
|
5371
5378
|
return createSelectorTreeWalker(container, selector);
|
|
@@ -5382,110 +5389,76 @@ function createListFocusNavigation(options, callbacks, signal) {
|
|
|
5382
5389
|
return items.length > 0 ? items[items.length - 1] : null;
|
|
5383
5390
|
}
|
|
5384
5391
|
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
const first = findFirstEnabled();
|
|
5388
|
-
if (!first) return false;
|
|
5389
|
-
transition(getActiveItem, callbacks, first);
|
|
5390
|
-
return true;
|
|
5391
|
-
}
|
|
5392
|
+
// Deferred navigation intent (replayed once items are committed to the DOM)
|
|
5393
|
+
const pending = createPendingNavigation(signal);
|
|
5392
5394
|
|
|
5393
|
-
/**
|
|
5394
|
-
function
|
|
5395
|
-
const last = findLastEnabled();
|
|
5396
|
-
if (!last) return false;
|
|
5397
|
-
transition(getActiveItem, callbacks, last);
|
|
5398
|
-
return true;
|
|
5399
|
-
}
|
|
5400
|
-
function navigateByOffset(offset) {
|
|
5401
|
-
const active = getActiveItem();
|
|
5402
|
-
if (offset === 0) return active !== null;
|
|
5395
|
+
/** Find item at offset (lazily walk nodes) */
|
|
5396
|
+
function findAtOffset(offset) {
|
|
5403
5397
|
const forward = offset > 0;
|
|
5404
5398
|
const stepsNeeded = Math.abs(offset);
|
|
5405
|
-
|
|
5406
|
-
// No active item — fall back to first/last.
|
|
5407
|
-
if (!active) {
|
|
5408
|
-
const started = forward ? goToFirst() : goToLast();
|
|
5409
|
-
if (!started) return false;
|
|
5410
|
-
if (stepsNeeded === 1) return true;
|
|
5411
|
-
return navigateByOffset(forward ? offset - 1 : offset + 1);
|
|
5412
|
-
}
|
|
5413
|
-
|
|
5414
|
-
// Walk from the active item using a TreeWalker.
|
|
5399
|
+
const active = getActiveItem();
|
|
5415
5400
|
const walker = createItemWalker();
|
|
5416
|
-
walker.currentNode = active;
|
|
5417
5401
|
const step = forward ? () => walker.nextNode() : () => walker.previousNode();
|
|
5418
|
-
let
|
|
5419
|
-
let
|
|
5420
|
-
|
|
5402
|
+
let target = null;
|
|
5403
|
+
let remaining = stepsNeeded;
|
|
5404
|
+
if (active) {
|
|
5405
|
+
// Walk from the active item.
|
|
5406
|
+
walker.currentNode = active;
|
|
5407
|
+
} else if (!forward) {
|
|
5408
|
+
// Walking backward with no active item: position at the last enabled item
|
|
5409
|
+
target = walker.lastChild();
|
|
5410
|
+
if (!target) return null;
|
|
5411
|
+
remaining -= 1;
|
|
5412
|
+
}
|
|
5413
|
+
for (let i = 0; i < remaining; i++) {
|
|
5421
5414
|
const next = step();
|
|
5422
5415
|
if (next) {
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
// Hit boundary — wrap around to the opposite end.
|
|
5416
|
+
target = next;
|
|
5417
|
+
} else if (active && wrap) {
|
|
5418
|
+
// Hit boundary with an active item — wrap around to the opposite end.
|
|
5427
5419
|
const wrapped = forward ? findFirstEnabled() : findLastEnabled();
|
|
5428
5420
|
if (!wrapped || wrapped === active) break;
|
|
5429
|
-
|
|
5430
|
-
stepsCompleted += 1;
|
|
5421
|
+
target = wrapped;
|
|
5431
5422
|
walker.currentNode = wrapped;
|
|
5432
5423
|
} else {
|
|
5433
5424
|
break;
|
|
5434
5425
|
}
|
|
5435
5426
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5427
|
+
return target;
|
|
5428
|
+
}
|
|
5429
|
+
function findMatching(predicate) {
|
|
5430
|
+
const walker = createItemWalker(false);
|
|
5431
|
+
let node = walker.nextNode();
|
|
5432
|
+
while (node) {
|
|
5433
|
+
if (predicate(node)) return node;
|
|
5434
|
+
node = walker.nextNode();
|
|
5435
|
+
}
|
|
5436
|
+
return null;
|
|
5439
5437
|
}
|
|
5440
|
-
const navigateForward = () => navigateByOffset(1);
|
|
5441
|
-
const navigateBackward = () => navigateByOffset(-1);
|
|
5442
5438
|
|
|
5443
|
-
/** Clear the active item. */
|
|
5439
|
+
/** Clear the active item and discard any pending navigation intent. */
|
|
5444
5440
|
function clear() {
|
|
5445
5441
|
const current = getActiveItem();
|
|
5446
5442
|
if (current) {
|
|
5447
5443
|
callbacks.onDeactivate(current);
|
|
5448
5444
|
}
|
|
5445
|
+
pending.clear();
|
|
5449
5446
|
callbacks.onClear?.();
|
|
5450
5447
|
}
|
|
5451
5448
|
|
|
5452
5449
|
// Cleanup on abort.
|
|
5453
5450
|
signal.addEventListener('abort', clear);
|
|
5454
|
-
|
|
5455
|
-
type: 'list',
|
|
5451
|
+
const selectors = {
|
|
5456
5452
|
enabledItemSelector,
|
|
5457
5453
|
get activeItem() {
|
|
5458
5454
|
return getActiveItem();
|
|
5459
5455
|
},
|
|
5460
|
-
get hasActiveItem() {
|
|
5461
|
-
return getActiveItem() !== null;
|
|
5462
|
-
},
|
|
5463
5456
|
get hasNavigableItems() {
|
|
5464
5457
|
return container.querySelector(enabledItemSelector) !== null;
|
|
5465
5458
|
},
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
if (!item.matches(itemSelector)) return false;
|
|
5470
|
-
if (!container.contains(item)) return false;
|
|
5471
|
-
transition(getActiveItem, callbacks, item);
|
|
5472
|
-
return true;
|
|
5473
|
-
},
|
|
5474
|
-
goToOffset(offset) {
|
|
5475
|
-
return navigateByOffset(offset);
|
|
5476
|
-
},
|
|
5477
|
-
goToItemMatching(predicate) {
|
|
5478
|
-
const walker = createItemWalker(false);
|
|
5479
|
-
let node = walker.nextNode();
|
|
5480
|
-
while (node) {
|
|
5481
|
-
if (predicate(node)) {
|
|
5482
|
-
transition(getActiveItem, callbacks, node);
|
|
5483
|
-
return true;
|
|
5484
|
-
}
|
|
5485
|
-
node = walker.nextNode();
|
|
5486
|
-
}
|
|
5487
|
-
return false;
|
|
5488
|
-
},
|
|
5459
|
+
getFirst: findFirstEnabled,
|
|
5460
|
+
getLast: findLastEnabled,
|
|
5461
|
+
getMatching: findMatching,
|
|
5489
5462
|
findNearestEnabled(anchor) {
|
|
5490
5463
|
if (!container.contains(anchor)) return findFirstEnabled();
|
|
5491
5464
|
|
|
@@ -5503,19 +5476,57 @@ function createListFocusNavigation(options, callbacks, signal) {
|
|
|
5503
5476
|
// No enabled item after anchor — walk backward (reuse same walker).
|
|
5504
5477
|
walker.currentNode = anchor;
|
|
5505
5478
|
return walker.previousNode();
|
|
5506
|
-
}
|
|
5479
|
+
}
|
|
5480
|
+
};
|
|
5481
|
+
|
|
5482
|
+
/**
|
|
5483
|
+
* Navigate via a resolver. The resolver receives the selectors to find the target.
|
|
5484
|
+
* If the target is valid, focus is committed (deactivate current, activate target).
|
|
5485
|
+
* If the target is not resolvable yet, the intent is deferred for later replay.
|
|
5486
|
+
*/
|
|
5487
|
+
function goTo(resolve) {
|
|
5488
|
+
const target = resolve(selectors);
|
|
5489
|
+
if (target && target.matches(itemSelector) && container.contains(target)) {
|
|
5490
|
+
const current = getActiveItem();
|
|
5491
|
+
if (current !== target) {
|
|
5492
|
+
if (current) callbacks.onDeactivate(current);
|
|
5493
|
+
callbacks.onActivate(target);
|
|
5494
|
+
}
|
|
5495
|
+
pending.clear();
|
|
5496
|
+
return true;
|
|
5497
|
+
}
|
|
5498
|
+
// Target not resolvable yet (e.g. items not committed to the DOM) — defer
|
|
5499
|
+
pending.defer(() => goTo(resolve));
|
|
5500
|
+
return false;
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
/** Go to the item at the given offset from the active item. */
|
|
5504
|
+
function goToOffset(offset) {
|
|
5505
|
+
if (offset === 0) return getActiveItem() !== null;
|
|
5506
|
+
const target = findAtOffset(offset);
|
|
5507
|
+
if (!target) return false;
|
|
5508
|
+
return goTo(() => target);
|
|
5509
|
+
}
|
|
5510
|
+
return {
|
|
5511
|
+
type: 'list',
|
|
5512
|
+
selectors,
|
|
5513
|
+
goToOffset,
|
|
5507
5514
|
clear,
|
|
5515
|
+
goTo,
|
|
5516
|
+
flushPendingNavigation() {
|
|
5517
|
+
pending.flush();
|
|
5518
|
+
},
|
|
5508
5519
|
goUp() {
|
|
5509
|
-
return direction === 'vertical' ?
|
|
5520
|
+
return direction === 'vertical' ? goToOffset(-1) : false;
|
|
5510
5521
|
},
|
|
5511
5522
|
goDown() {
|
|
5512
|
-
return direction === 'vertical' ?
|
|
5523
|
+
return direction === 'vertical' ? goToOffset(1) : false;
|
|
5513
5524
|
},
|
|
5514
5525
|
goLeft() {
|
|
5515
|
-
return direction === 'horizontal' ?
|
|
5526
|
+
return direction === 'horizontal' ? goToOffset(-1) : false;
|
|
5516
5527
|
},
|
|
5517
5528
|
goRight() {
|
|
5518
|
-
return direction === 'horizontal' ?
|
|
5529
|
+
return direction === 'horizontal' ? goToOffset(1) : false;
|
|
5519
5530
|
}
|
|
5520
5531
|
};
|
|
5521
5532
|
}
|
|
@@ -5565,13 +5576,32 @@ function createActiveItemState(callbacks, signal, initialItem) {
|
|
|
5565
5576
|
};
|
|
5566
5577
|
}
|
|
5567
5578
|
|
|
5579
|
+
/** Deepest last-child descendant of `el` (or `el` itself if it has no children). */
|
|
5580
|
+
function lastDescendant(element) {
|
|
5581
|
+
let node = element;
|
|
5582
|
+
while (node.lastElementChild) node = node.lastElementChild;
|
|
5583
|
+
return node;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
/** Return the first item of an iterable, or `null` if it is empty. */
|
|
5587
|
+
function first(iterable) {
|
|
5588
|
+
const {
|
|
5589
|
+
value,
|
|
5590
|
+
done
|
|
5591
|
+
} = iterable[Symbol.iterator]().next();
|
|
5592
|
+
return done ? null : value;
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5568
5595
|
/**
|
|
5569
5596
|
* Create a focus navigation controller for a 2D grid.
|
|
5570
5597
|
*
|
|
5598
|
+
* Resolves rows and cells by querying the DOM and commits focus by updating the
|
|
5599
|
+
* active-item state (which fires {@link FocusNavigationCallbacks}).
|
|
5600
|
+
*
|
|
5571
5601
|
* Supports Up/Down between rows (with column memory) and Left/Right between cells
|
|
5572
5602
|
* (with wrapping across rows).
|
|
5573
5603
|
*
|
|
5574
|
-
* @param options Grid navigation options (container, rowSelector, cellSelector,
|
|
5604
|
+
* @param options Grid navigation options (container, rowSelector, cellSelector, wrap).
|
|
5575
5605
|
* @param callbacks Callbacks for focus state changes.
|
|
5576
5606
|
* @param signal AbortSignal for cleanup.
|
|
5577
5607
|
* @returns FocusNavigationController instance.
|
|
@@ -5581,89 +5611,46 @@ function createGridFocusNavigation(options, callbacks, signal) {
|
|
|
5581
5611
|
container,
|
|
5582
5612
|
rowSelector,
|
|
5583
5613
|
cellSelector,
|
|
5584
|
-
isRowVisible,
|
|
5585
5614
|
wrap = false
|
|
5586
5615
|
} = options;
|
|
5587
5616
|
const state = createActiveItemState(callbacks, signal);
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
* - `'last'`: returns the last navigable row (or null).
|
|
5610
|
-
* - `'all'`: returns all navigable rows as an array.
|
|
5611
|
-
*/
|
|
5612
|
-
|
|
5613
|
-
function findVisibleRows(mode) {
|
|
5614
|
-
const walker = createRowWalker();
|
|
5615
|
-
if (mode === 'all') {
|
|
5616
|
-
const result = [];
|
|
5617
|
-
let node = walker.nextNode();
|
|
5618
|
-
while (node) {
|
|
5619
|
-
if (isNavigableRow(node)) result.push(node);
|
|
5620
|
-
node = walker.nextNode();
|
|
5621
|
-
}
|
|
5622
|
-
return result;
|
|
5623
|
-
}
|
|
5624
|
-
let found = null;
|
|
5625
|
-
let node = walker.nextNode();
|
|
5626
|
-
while (node) {
|
|
5627
|
-
if (isNavigableRow(node)) {
|
|
5628
|
-
if (mode === 'first') return node;
|
|
5629
|
-
found = node;
|
|
5617
|
+
const isNavigableRow = row => row.querySelector(cellSelector) !== null;
|
|
5618
|
+
const getFirstCell = row => row.querySelector(cellSelector);
|
|
5619
|
+
const getRowCells = row => Array.from(row.querySelectorAll(cellSelector));
|
|
5620
|
+
|
|
5621
|
+
/** Lazily walk navigable rows from `start` in a `direction`, projecting each row through `dive`. */
|
|
5622
|
+
function* findRows(start = 'first', direction = 'next', dive) {
|
|
5623
|
+
// Tree walker
|
|
5624
|
+
const walker = createSelectorTreeWalker(container, rowSelector);
|
|
5625
|
+
|
|
5626
|
+
// Start at a specific row
|
|
5627
|
+
if (start instanceof HTMLElement) walker.currentNode = start;
|
|
5628
|
+
// Start from the last
|
|
5629
|
+
else if (start === 'last') walker.currentNode = lastDescendant(container);
|
|
5630
|
+
|
|
5631
|
+
// Walk nodes
|
|
5632
|
+
let node;
|
|
5633
|
+
do {
|
|
5634
|
+
node = direction === 'next' ? walker.nextNode() : walker.previousNode();
|
|
5635
|
+
if (node && isNavigableRow(node)) {
|
|
5636
|
+
const result = dive ? dive(node) : node;
|
|
5637
|
+
if (result) yield result;
|
|
5630
5638
|
}
|
|
5631
|
-
|
|
5632
|
-
}
|
|
5633
|
-
return found;
|
|
5634
|
-
}
|
|
5635
|
-
|
|
5636
|
-
/** Get the cells within a single row element. */
|
|
5637
|
-
function getRowCells(row) {
|
|
5638
|
-
return Array.from(row.querySelectorAll(cellSelector));
|
|
5639
|
+
} while (node);
|
|
5639
5640
|
}
|
|
5640
|
-
|
|
5641
|
-
|
|
5641
|
+
const findFirstVisibleRow = () => first(findRows());
|
|
5642
|
+
const findLastVisibleRow = () => first(findRows('last', 'prev'));
|
|
5642
5643
|
function findParentRow(cell) {
|
|
5643
5644
|
const row = cell.closest(rowSelector);
|
|
5644
5645
|
return row && container.contains(row) ? row : null;
|
|
5645
5646
|
}
|
|
5646
5647
|
|
|
5647
|
-
/**
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
function findAdjacentVisibleRow(fromRow, direction) {
|
|
5652
|
-
const walker = createRowWalker();
|
|
5653
|
-
walker.currentNode = fromRow;
|
|
5654
|
-
const advance = direction === 'next' ? () => walker.nextNode() : () => walker.previousNode();
|
|
5655
|
-
let node = advance();
|
|
5656
|
-
while (node) {
|
|
5657
|
-
if (isNavigableRow(node)) return node;
|
|
5658
|
-
node = advance();
|
|
5659
|
-
}
|
|
5660
|
-
return null;
|
|
5661
|
-
}
|
|
5648
|
+
/** Deferred navigation intent (replayed once cells are committed to the DOM). */
|
|
5649
|
+
const pending = createPendingNavigation(signal);
|
|
5650
|
+
/** Remembered column index for Up/Down navigation (column memory). */
|
|
5651
|
+
let rememberedCol = 0;
|
|
5662
5652
|
|
|
5663
|
-
/**
|
|
5664
|
-
* Activate the cell at the given column in a row element.
|
|
5665
|
-
* Clamps col to the row's available cells.
|
|
5666
|
-
*/
|
|
5653
|
+
/** Activate the cell at the given column in a row element. */
|
|
5667
5654
|
function focusCellInRow(row, col) {
|
|
5668
5655
|
const cells = getRowCells(row);
|
|
5669
5656
|
if (cells.length === 0) return false;
|
|
@@ -5671,14 +5658,30 @@ function createGridFocusNavigation(options, callbacks, signal) {
|
|
|
5671
5658
|
state.setActive(cells[clampedCol]);
|
|
5672
5659
|
return true;
|
|
5673
5660
|
}
|
|
5661
|
+
|
|
5662
|
+
/** Activate the given cell (validates it is in the grid, updates column memory). */
|
|
5663
|
+
function activateCell(item) {
|
|
5664
|
+
const row = findParentRow(item);
|
|
5665
|
+
if (!row) return false;
|
|
5666
|
+
const cells = getRowCells(row);
|
|
5667
|
+
const col = cells.indexOf(item);
|
|
5668
|
+
if (col === -1) return false;
|
|
5669
|
+
rememberedCol = col;
|
|
5670
|
+
state.setActive(item);
|
|
5671
|
+
return true;
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
/** Got to first cell in first row */
|
|
5674
5675
|
function goToFirst() {
|
|
5675
|
-
const row =
|
|
5676
|
+
const row = findFirstVisibleRow();
|
|
5676
5677
|
if (!row) return false;
|
|
5677
5678
|
rememberedCol = 0;
|
|
5678
5679
|
return focusCellInRow(row, 0);
|
|
5679
5680
|
}
|
|
5681
|
+
|
|
5682
|
+
/** Got to first cell in last row */
|
|
5680
5683
|
function goToLast() {
|
|
5681
|
-
const row =
|
|
5684
|
+
const row = findLastVisibleRow();
|
|
5682
5685
|
if (!row) return false;
|
|
5683
5686
|
rememberedCol = 0;
|
|
5684
5687
|
return focusCellInRow(row, 0);
|
|
@@ -5702,8 +5705,8 @@ function createGridFocusNavigation(options, callbacks, signal) {
|
|
|
5702
5705
|
|
|
5703
5706
|
// Wrap to the adjacent row (or opposite boundary row), activating the first or last cell.
|
|
5704
5707
|
const rowDirection = step > 0 ? 'next' : 'prev';
|
|
5705
|
-
const adjacentRow =
|
|
5706
|
-
const targetRow = adjacentRow ?? (step > 0 ?
|
|
5708
|
+
const adjacentRow = first(findRows(currentRow, rowDirection));
|
|
5709
|
+
const targetRow = adjacentRow ?? (step > 0 ? findFirstVisibleRow() : findLastVisibleRow());
|
|
5707
5710
|
if (!targetRow) return false;
|
|
5708
5711
|
const targetCells = getRowCells(targetRow);
|
|
5709
5712
|
if (targetCells.length === 0) return false;
|
|
@@ -5721,31 +5724,35 @@ function createGridFocusNavigation(options, callbacks, signal) {
|
|
|
5721
5724
|
}
|
|
5722
5725
|
const currentRow = findParentRow(state.active);
|
|
5723
5726
|
if (!currentRow) return false;
|
|
5724
|
-
const adjacentRow =
|
|
5727
|
+
const adjacentRow = first(findRows(currentRow, direction));
|
|
5725
5728
|
if (adjacentRow) return focusCellInRow(adjacentRow, rememberedCol);
|
|
5726
5729
|
if (wrap) {
|
|
5727
5730
|
// Wrap to the opposite boundary row.
|
|
5728
|
-
const wrapRow = direction === 'next' ?
|
|
5731
|
+
const wrapRow = direction === 'next' ? findFirstVisibleRow() : findLastVisibleRow();
|
|
5729
5732
|
if (wrapRow) return focusCellInRow(wrapRow, rememberedCol);
|
|
5730
5733
|
}
|
|
5731
5734
|
return false;
|
|
5732
5735
|
}
|
|
5733
|
-
|
|
5734
|
-
type: 'grid',
|
|
5736
|
+
const selectors = {
|
|
5735
5737
|
get activeItem() {
|
|
5736
5738
|
return state.active;
|
|
5737
5739
|
},
|
|
5738
|
-
get hasActiveItem() {
|
|
5739
|
-
return state.active !== null;
|
|
5740
|
-
},
|
|
5741
5740
|
get hasNavigableItems() {
|
|
5742
|
-
return
|
|
5741
|
+
return first(findRows()) !== null;
|
|
5743
5742
|
},
|
|
5744
|
-
|
|
5745
|
-
|
|
5743
|
+
// First cell in first row
|
|
5744
|
+
getFirst: () => first(findRows('first', 'next', getFirstCell)),
|
|
5745
|
+
// First cell in last row
|
|
5746
|
+
getLast: () => first(findRows('last', 'prev', getFirstCell)),
|
|
5747
|
+
// First cell matching predicate
|
|
5748
|
+
getMatching: predicate => first(findRows('first', 'next', row => getRowCells(row).find(predicate)))
|
|
5749
|
+
};
|
|
5750
|
+
return {
|
|
5751
|
+
type: 'grid',
|
|
5752
|
+
selectors,
|
|
5746
5753
|
goToOffset(offset) {
|
|
5747
5754
|
if (offset === 0) return state.active !== null;
|
|
5748
|
-
const visibleRows =
|
|
5755
|
+
const visibleRows = Array.from(findRows());
|
|
5749
5756
|
if (visibleRows.length === 0) return false;
|
|
5750
5757
|
if (!state.active) {
|
|
5751
5758
|
// No active item: jump to first or last row, then apply remaining offset.
|
|
@@ -5764,38 +5771,22 @@ function createGridFocusNavigation(options, callbacks, signal) {
|
|
|
5764
5771
|
if (targetIdx === rowIdx) return false;
|
|
5765
5772
|
return focusCellInRow(visibleRows[targetIdx], rememberedCol);
|
|
5766
5773
|
},
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
rememberedCol = c;
|
|
5777
|
-
state.setActive(cells[c]);
|
|
5778
|
-
return true;
|
|
5779
|
-
}
|
|
5780
|
-
}
|
|
5781
|
-
}
|
|
5782
|
-
row = walker.nextNode();
|
|
5774
|
+
clear() {
|
|
5775
|
+
state.clear();
|
|
5776
|
+
pending.clear();
|
|
5777
|
+
},
|
|
5778
|
+
goTo(resolve) {
|
|
5779
|
+
const target = resolve(selectors);
|
|
5780
|
+
if (target && activateCell(target)) {
|
|
5781
|
+
pending.clear();
|
|
5782
|
+
return true;
|
|
5783
5783
|
}
|
|
5784
|
+
// Target not resolvable yet (e.g. cells not committed to the DOM) — defer
|
|
5785
|
+
pending.defer(() => this.goTo(resolve));
|
|
5784
5786
|
return false;
|
|
5785
5787
|
},
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
const row = findParentRow(item);
|
|
5789
|
-
if (!row || !isVisible(row)) return false;
|
|
5790
|
-
const cells = getRowCells(row);
|
|
5791
|
-
const col = cells.indexOf(item);
|
|
5792
|
-
if (col === -1) return false;
|
|
5793
|
-
rememberedCol = col;
|
|
5794
|
-
state.setActive(item);
|
|
5795
|
-
return true;
|
|
5796
|
-
},
|
|
5797
|
-
clear() {
|
|
5798
|
-
state.clear();
|
|
5788
|
+
flushPendingNavigation() {
|
|
5789
|
+
pending.flush();
|
|
5799
5790
|
},
|
|
5800
5791
|
goUp() {
|
|
5801
5792
|
return goVertical('prev');
|
|
@@ -5931,7 +5922,7 @@ function setupRovingTabIndex(options, signal) {
|
|
|
5931
5922
|
}
|
|
5932
5923
|
}
|
|
5933
5924
|
if (!hasTabStop) {
|
|
5934
|
-
const fallback = container.querySelector(nav.enabledItemSelector);
|
|
5925
|
+
const fallback = container.querySelector(nav.selectors.enabledItemSelector);
|
|
5935
5926
|
if (fallback) setTabIndex(fallback, '0');
|
|
5936
5927
|
}
|
|
5937
5928
|
}
|
|
@@ -5941,7 +5932,7 @@ function setupRovingTabIndex(options, signal) {
|
|
|
5941
5932
|
const items = Array.from(container.querySelectorAll(itemSelector));
|
|
5942
5933
|
const {
|
|
5943
5934
|
activeItem
|
|
5944
|
-
} = nav;
|
|
5935
|
+
} = nav.selectors;
|
|
5945
5936
|
|
|
5946
5937
|
// Prefer either the current active item (from DOM) or the current selected item
|
|
5947
5938
|
let preferredTabStopIndex = activeItem && items.indexOf(activeItem);
|
|
@@ -5958,10 +5949,10 @@ function setupRovingTabIndex(options, signal) {
|
|
|
5958
5949
|
/** Ensure a tab stop exists; find a fallback near `anchor` and optionally move focus to it. */
|
|
5959
5950
|
function ensureTabStop(shouldFocus, anchor) {
|
|
5960
5951
|
if (container.querySelector(itemActiveSelector)) return;
|
|
5961
|
-
const fallback = (anchor && nav.findNearestEnabled(anchor)) ?? container.querySelector(nav.enabledItemSelector);
|
|
5952
|
+
const fallback = (anchor && nav.selectors.findNearestEnabled(anchor)) ?? container.querySelector(nav.selectors.enabledItemSelector);
|
|
5962
5953
|
if (!fallback) return;
|
|
5963
5954
|
if (shouldFocus) {
|
|
5964
|
-
nav.
|
|
5955
|
+
nav.goTo(() => fallback);
|
|
5965
5956
|
} else {
|
|
5966
5957
|
setTabIndex(fallback, '0');
|
|
5967
5958
|
}
|
|
@@ -6031,7 +6022,7 @@ function setupRovingTabIndex(options, signal) {
|
|
|
6031
6022
|
|
|
6032
6023
|
// Handle disabled
|
|
6033
6024
|
if (disabledTargets.length > 0) {
|
|
6034
|
-
const currentActive = nav.activeItem;
|
|
6025
|
+
const currentActive = nav.selectors.activeItem;
|
|
6035
6026
|
for (const target of disabledTargets) {
|
|
6036
6027
|
setTabIndex(target, '-1');
|
|
6037
6028
|
}
|
|
@@ -6063,10 +6054,10 @@ function setupRovingTabIndex(options, signal) {
|
|
|
6063
6054
|
|
|
6064
6055
|
container.addEventListener('keydown', evt => {
|
|
6065
6056
|
// Adopt focus target if nothing is active yet.
|
|
6066
|
-
if (!nav.activeItem) {
|
|
6057
|
+
if (!nav.selectors.activeItem) {
|
|
6067
6058
|
const target = evt.target;
|
|
6068
6059
|
if (target.matches(itemSelector) && container.contains(target)) {
|
|
6069
|
-
nav.
|
|
6060
|
+
nav.goTo(() => target);
|
|
6070
6061
|
}
|
|
6071
6062
|
}
|
|
6072
6063
|
let handled = false;
|
|
@@ -6084,10 +6075,10 @@ function setupRovingTabIndex(options, signal) {
|
|
|
6084
6075
|
handled = nav.goUp();
|
|
6085
6076
|
break;
|
|
6086
6077
|
case 'Home':
|
|
6087
|
-
handled = nav.
|
|
6078
|
+
handled = nav.goTo(s => s.getFirst());
|
|
6088
6079
|
break;
|
|
6089
6080
|
case 'End':
|
|
6090
|
-
handled = nav.
|
|
6081
|
+
handled = nav.goTo(s => s.getLast());
|
|
6091
6082
|
break;
|
|
6092
6083
|
}
|
|
6093
6084
|
if (handled) {
|
|
@@ -6248,12 +6239,24 @@ SelectionChipGroup.className = CLASSNAME$1j;
|
|
|
6248
6239
|
/**
|
|
6249
6240
|
* Get the value for a combobox option element.
|
|
6250
6241
|
* Uses `data-value` when set; falls back to the element's trimmed `textContent`.
|
|
6242
|
+
*
|
|
6243
|
+
* This is the *selection* value , which may differ from the visible label
|
|
6251
6244
|
*/
|
|
6252
6245
|
function getOptionValue(option) {
|
|
6253
6246
|
if (option.dataset.value !== undefined) return option.dataset.value;
|
|
6254
6247
|
return option.textContent?.trim() ?? '';
|
|
6255
6248
|
}
|
|
6256
6249
|
|
|
6250
|
+
/**
|
|
6251
|
+
* Get the visible label for a combobox option element (its trimmed `textContent`).
|
|
6252
|
+
*
|
|
6253
|
+
* Used for typeahead matching: the user types the characters they see, which is the
|
|
6254
|
+
* option's label — not its `data-value` (which can be an unrelated id).
|
|
6255
|
+
*/
|
|
6256
|
+
function getOptionLabel(option) {
|
|
6257
|
+
return option.textContent?.trim() ?? '';
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6257
6260
|
/** Returns true when an option carries aria-disabled="true". */
|
|
6258
6261
|
function isOptionDisabled(option) {
|
|
6259
6262
|
return option.getAttribute('aria-disabled') === 'true';
|
|
@@ -6265,20 +6268,8 @@ function isActionCell(cell) {
|
|
|
6265
6268
|
if (!row) return false;
|
|
6266
6269
|
return row.querySelector('[role="gridcell"]') !== cell;
|
|
6267
6270
|
}
|
|
6268
|
-
|
|
6269
|
-
/** Predicate matching an option element that carries `aria-selected="true"`. */
|
|
6270
6271
|
const isSelected = el => el.getAttribute('aria-selected') === 'true';
|
|
6271
6272
|
|
|
6272
|
-
/** Navigate to the selected option, or to the first option if none is selected. */
|
|
6273
|
-
function goToSelectedOrFirst(nav) {
|
|
6274
|
-
if (!nav.goToItemMatching(isSelected)) nav.goToFirst();
|
|
6275
|
-
}
|
|
6276
|
-
|
|
6277
|
-
/** Navigate to the selected option, or to the last option if none is selected. */
|
|
6278
|
-
function goToSelectedOrLast(nav) {
|
|
6279
|
-
if (!nav.goToItemMatching(isSelected)) nav.goToLast();
|
|
6280
|
-
}
|
|
6281
|
-
|
|
6282
6273
|
/**
|
|
6283
6274
|
* Compute the current state of a section and notify when it changed.
|
|
6284
6275
|
*
|
|
@@ -6330,7 +6321,7 @@ function notifySection(sectionElement, sectionRegistrations, optionRegistrations
|
|
|
6330
6321
|
* @param notify Notify subscribers of combobox events.
|
|
6331
6322
|
* @returns The created focus navigation controller.
|
|
6332
6323
|
*/
|
|
6333
|
-
function setupListbox(handle, signal, notify) {
|
|
6324
|
+
function setupListbox(handle, signal, notify, options) {
|
|
6334
6325
|
const trigger = handle.trigger;
|
|
6335
6326
|
const listbox = handle.listbox;
|
|
6336
6327
|
const isGrid = listbox.getAttribute('role') === 'grid';
|
|
@@ -6383,6 +6374,7 @@ function setupListbox(handle, signal, notify) {
|
|
|
6383
6374
|
focusNav = createListFocusNavigation({
|
|
6384
6375
|
container: listbox,
|
|
6385
6376
|
itemSelector,
|
|
6377
|
+
wrap: options?.wrapNavigation,
|
|
6386
6378
|
getActiveItem: () => {
|
|
6387
6379
|
const id = trigger.getAttribute('aria-activedescendant');
|
|
6388
6380
|
return id ? document.getElementById(id) : null;
|
|
@@ -6591,12 +6583,12 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6591
6583
|
const nav = handle.focusNav;
|
|
6592
6584
|
switch (event.key) {
|
|
6593
6585
|
case 'Enter':
|
|
6594
|
-
if (handle.isOpen && nav?.
|
|
6586
|
+
if (handle.isOpen && nav?.selectors.activeItem) {
|
|
6595
6587
|
// Capture activeItem before click — the click handler may close
|
|
6596
6588
|
// the popover and clear the focus navigation state.
|
|
6597
6589
|
const {
|
|
6598
6590
|
activeItem
|
|
6599
|
-
} = nav;
|
|
6591
|
+
} = nav.selectors;
|
|
6600
6592
|
// "Click" on active option
|
|
6601
6593
|
if (!isOptionDisabled(activeItem)) {
|
|
6602
6594
|
activeItem.click();
|
|
@@ -6614,40 +6606,38 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6614
6606
|
// Otherwise (closed popup, or multi-select with no active item),
|
|
6615
6607
|
// let Enter pass through so it can submit a surrounding form
|
|
6616
6608
|
break;
|
|
6609
|
+
|
|
6610
|
+
// Open if closed, else move focus within listbox (wrap if enabled).
|
|
6617
6611
|
case 'ArrowDown':
|
|
6618
|
-
if (
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
} else {
|
|
6627
|
-
goToSelectedOrFirst(nav);
|
|
6628
|
-
}
|
|
6612
|
+
if (!handle.isOpen) {
|
|
6613
|
+
handle.setIsOpen(true);
|
|
6614
|
+
// Focus first or selected item on open.
|
|
6615
|
+
if (!altKey) nav?.goTo(s => s.getMatching(isSelected) ?? s.getFirst());
|
|
6616
|
+
} else if (nav?.selectors.hasNavigableItems && !altKey) {
|
|
6617
|
+
if (nav.selectors.activeItem) {
|
|
6618
|
+
// Go down
|
|
6619
|
+
nav.goDown();
|
|
6629
6620
|
} else {
|
|
6630
|
-
//
|
|
6631
|
-
|
|
6632
|
-
if (!altKey) goToSelectedOrFirst(nav);
|
|
6621
|
+
// Focus first or selected item when no active item.
|
|
6622
|
+
nav.goTo(s => s.getMatching(isSelected) ?? s.getFirst());
|
|
6633
6623
|
}
|
|
6634
6624
|
}
|
|
6635
6625
|
flag = true;
|
|
6636
6626
|
break;
|
|
6627
|
+
|
|
6628
|
+
// Open if closed, else move focus within listbox (wrap if enabled).
|
|
6637
6629
|
case 'ArrowUp':
|
|
6638
|
-
if (
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
} else if (handle.isOpen && !nav.hasActiveItem && !altKey) {
|
|
6650
|
-
goToSelectedOrLast(nav);
|
|
6630
|
+
if (!handle.isOpen && !altKey) {
|
|
6631
|
+
handle.setIsOpen(true);
|
|
6632
|
+
// Focus last or selected item on open.
|
|
6633
|
+
nav?.goTo(s => s.getMatching(isSelected) ?? s.getLast());
|
|
6634
|
+
} else if (handle.isOpen && nav?.selectors.hasNavigableItems) {
|
|
6635
|
+
if (nav.selectors.activeItem) {
|
|
6636
|
+
// Go up
|
|
6637
|
+
nav.goUp();
|
|
6638
|
+
} else if (!altKey) {
|
|
6639
|
+
// Focus last or selected item when no active item.
|
|
6640
|
+
nav.goTo(s => s.getMatching(isSelected) ?? s.getLast());
|
|
6651
6641
|
}
|
|
6652
6642
|
}
|
|
6653
6643
|
flag = true;
|
|
@@ -6662,13 +6652,13 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6662
6652
|
flag = true;
|
|
6663
6653
|
break;
|
|
6664
6654
|
case 'PageUp':
|
|
6665
|
-
if (handle.isOpen && nav?.
|
|
6655
|
+
if (handle.isOpen && nav?.selectors.activeItem) {
|
|
6666
6656
|
nav.goToOffset(-10);
|
|
6667
6657
|
}
|
|
6668
6658
|
flag = true;
|
|
6669
6659
|
break;
|
|
6670
6660
|
case 'PageDown':
|
|
6671
|
-
if (handle.isOpen && nav?.
|
|
6661
|
+
if (handle.isOpen && nav?.selectors.activeItem) {
|
|
6672
6662
|
nav.goToOffset(10);
|
|
6673
6663
|
}
|
|
6674
6664
|
flag = true;
|
|
@@ -6713,7 +6703,9 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6713
6703
|
});
|
|
6714
6704
|
}
|
|
6715
6705
|
if (listbox && !focusNav) {
|
|
6716
|
-
focusNav = setupListbox(handle, abortController.signal, notify
|
|
6706
|
+
focusNav = setupListbox(handle, abortController.signal, notify, {
|
|
6707
|
+
wrapNavigation
|
|
6708
|
+
});
|
|
6717
6709
|
}
|
|
6718
6710
|
}
|
|
6719
6711
|
handle = {
|
|
@@ -6754,25 +6746,16 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6754
6746
|
// Update aria-expanded on trigger
|
|
6755
6747
|
trigger?.setAttribute('aria-expanded', String(isOpen));
|
|
6756
6748
|
notify('open', isOpen);
|
|
6757
|
-
|
|
6758
|
-
// When opening, always notify the current options state so that
|
|
6759
|
-
// subscribers (ComboboxState) get the correct initial value.
|
|
6760
|
-
// Without this, a list that starts empty never fires `optionsChange`
|
|
6761
|
-
// because `lastOptionsLength` is initialized to `0` and `notifyVisibilityChange`
|
|
6762
|
-
// only fires on *changes*.
|
|
6763
|
-
if (isOpen) {
|
|
6764
|
-
const inputValue = trigger?.value ?? '';
|
|
6765
|
-
notify('optionsChange', {
|
|
6766
|
-
optionsLength: lastOptionsLength,
|
|
6767
|
-
inputValue
|
|
6768
|
-
});
|
|
6769
|
-
}
|
|
6770
6749
|
},
|
|
6771
6750
|
select(option) {
|
|
6772
6751
|
callbacks.onSelect?.({
|
|
6773
6752
|
value: option ? getOptionValue(option) : ''
|
|
6774
6753
|
});
|
|
6775
6754
|
},
|
|
6755
|
+
flushPendingNavigation() {
|
|
6756
|
+
// Do navigations actions we could not do because the combobox items were not mounted yet
|
|
6757
|
+
focusNav?.flushPendingNavigation();
|
|
6758
|
+
},
|
|
6776
6759
|
registerOption(element, callback) {
|
|
6777
6760
|
const filterLower = filterValue.toLowerCase();
|
|
6778
6761
|
const text = getOptionValue(element).toLowerCase();
|
|
@@ -6867,7 +6850,9 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
|
|
|
6867
6850
|
if (trigger && abortController) {
|
|
6868
6851
|
if (!hadListbox) {
|
|
6869
6852
|
// First listbox — set up focus nav and listbox listeners
|
|
6870
|
-
focusNav = setupListbox(handle, abortController.signal, notify
|
|
6853
|
+
focusNav = setupListbox(handle, abortController.signal, notify, {
|
|
6854
|
+
wrapNavigation
|
|
6855
|
+
});
|
|
6871
6856
|
} else {
|
|
6872
6857
|
// Replacing listbox — full re-attach
|
|
6873
6858
|
detach();
|
|
@@ -6945,18 +6930,9 @@ function createTypeahead(getWalker, getItemValue, signal) {
|
|
|
6945
6930
|
}
|
|
6946
6931
|
signal.addEventListener('abort', reset);
|
|
6947
6932
|
|
|
6948
|
-
//
|
|
6949
|
-
function
|
|
6950
|
-
|
|
6951
|
-
if (searchTimeout !== undefined) {
|
|
6952
|
-
clearTimeout(searchTimeout);
|
|
6953
|
-
}
|
|
6954
|
-
|
|
6955
|
-
// Accumulate the character.
|
|
6956
|
-
searchString += key.toLowerCase();
|
|
6957
|
-
|
|
6958
|
-
// Schedule clearing the search string after inactivity.
|
|
6959
|
-
searchTimeout = setTimeout(reset, SEARCH_TIMEOUT);
|
|
6933
|
+
// Match the current accumulated search string against the live DOM.
|
|
6934
|
+
function match(currentItem) {
|
|
6935
|
+
if (!searchString) return null;
|
|
6960
6936
|
const walker = getWalker();
|
|
6961
6937
|
if (!walker) return null;
|
|
6962
6938
|
|
|
@@ -7017,8 +6993,29 @@ function createTypeahead(getWalker, getItemValue, signal) {
|
|
|
7017
6993
|
}
|
|
7018
6994
|
}
|
|
7019
6995
|
}
|
|
6996
|
+
|
|
6997
|
+
// Handle typeahead keys
|
|
6998
|
+
function handle(key, currentItem) {
|
|
6999
|
+
// Clear any pending reset timeout.
|
|
7000
|
+
if (searchTimeout !== undefined) {
|
|
7001
|
+
clearTimeout(searchTimeout);
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
// Accumulate the character.
|
|
7005
|
+
searchString += key.toLowerCase();
|
|
7006
|
+
|
|
7007
|
+
// Schedule clearing the search string after inactivity.
|
|
7008
|
+
searchTimeout = setTimeout(reset, SEARCH_TIMEOUT);
|
|
7009
|
+
return match(currentItem);
|
|
7010
|
+
}
|
|
7011
|
+
|
|
7012
|
+
// Re-run the match for the current buffer without mutating it.
|
|
7013
|
+
function rematch(currentItem) {
|
|
7014
|
+
return match(currentItem);
|
|
7015
|
+
}
|
|
7020
7016
|
return {
|
|
7021
7017
|
handle,
|
|
7018
|
+
rematch,
|
|
7022
7019
|
reset
|
|
7023
7020
|
};
|
|
7024
7021
|
}
|
|
@@ -7055,7 +7052,7 @@ function setupComboboxButton(button, callbacks) {
|
|
|
7055
7052
|
// In list mode, walk option elements.
|
|
7056
7053
|
const selector = combobox.focusNav?.type === 'grid' ? '[role="gridcell"]' : '[role="option"]';
|
|
7057
7054
|
return createSelectorTreeWalker(combobox.listbox, selector);
|
|
7058
|
-
},
|
|
7055
|
+
}, getOptionLabel, signal);
|
|
7059
7056
|
|
|
7060
7057
|
// Click toggles the listbox.
|
|
7061
7058
|
button.addEventListener('click', () => combobox.setIsOpen(!combobox.isOpen), {
|
|
@@ -7066,47 +7063,49 @@ function setupComboboxButton(button, callbacks) {
|
|
|
7066
7063
|
switch (event.key) {
|
|
7067
7064
|
case 'Tab':
|
|
7068
7065
|
// Selects the focused option
|
|
7069
|
-
if (combobox.isOpen && nav?.
|
|
7070
|
-
combobox.select(nav.activeItem);
|
|
7066
|
+
if (combobox.isOpen && nav?.selectors.activeItem) {
|
|
7067
|
+
combobox.select(nav.selectors.activeItem);
|
|
7071
7068
|
}
|
|
7072
7069
|
// Return false to continue normal 'Tab' behavior (focus next).
|
|
7073
7070
|
return false;
|
|
7074
7071
|
case ' ':
|
|
7075
7072
|
// Space acts like Enter in button mode.
|
|
7076
|
-
if (combobox.isOpen && nav?.
|
|
7073
|
+
if (combobox.isOpen && nav?.selectors.activeItem) {
|
|
7077
7074
|
// Click the active item — delegated handler handles select + close.
|
|
7078
|
-
nav.activeItem.click();
|
|
7075
|
+
nav.selectors.activeItem.click();
|
|
7079
7076
|
} else {
|
|
7080
7077
|
combobox.setIsOpen(true);
|
|
7081
7078
|
}
|
|
7082
7079
|
return true;
|
|
7083
7080
|
case 'ArrowUp':
|
|
7084
7081
|
// Alt+ArrowUp: select the focused option and close.
|
|
7085
|
-
if (event.altKey && combobox.isOpen && nav?.
|
|
7086
|
-
combobox.select(nav.activeItem);
|
|
7082
|
+
if (event.altKey && combobox.isOpen && nav?.selectors.activeItem) {
|
|
7083
|
+
combobox.select(nav.selectors.activeItem);
|
|
7087
7084
|
combobox.setIsOpen(false);
|
|
7088
7085
|
return true;
|
|
7089
7086
|
}
|
|
7090
7087
|
// All other ArrowUp cases handled by base handler.
|
|
7091
7088
|
return false;
|
|
7092
7089
|
case 'Home':
|
|
7090
|
+
// `goTo` focuses the first option immediately when open, or defers
|
|
7091
|
+
// until the options commit when opening from closed.
|
|
7093
7092
|
combobox.setIsOpen(true);
|
|
7094
|
-
nav?.
|
|
7093
|
+
nav?.goTo(n => n.getFirst());
|
|
7095
7094
|
return true;
|
|
7096
7095
|
case 'End':
|
|
7097
7096
|
combobox.setIsOpen(true);
|
|
7098
|
-
nav?.
|
|
7097
|
+
nav?.goTo(n => n.getLast());
|
|
7099
7098
|
return true;
|
|
7100
7099
|
case 'ArrowLeft':
|
|
7101
7100
|
// Grid mode: navigate to previous cell.
|
|
7102
|
-
if (nav?.type === 'grid' && combobox.isOpen && nav.
|
|
7101
|
+
if (nav?.type === 'grid' && combobox.isOpen && nav.selectors.activeItem) {
|
|
7103
7102
|
nav.goLeft();
|
|
7104
7103
|
return true;
|
|
7105
7104
|
}
|
|
7106
7105
|
return false;
|
|
7107
7106
|
case 'ArrowRight':
|
|
7108
7107
|
// Grid mode: navigate to next cell.
|
|
7109
|
-
if (nav?.type === 'grid' && combobox.isOpen && nav.
|
|
7108
|
+
if (nav?.type === 'grid' && combobox.isOpen && nav.selectors.activeItem) {
|
|
7110
7109
|
nav.goRight();
|
|
7111
7110
|
return true;
|
|
7112
7111
|
}
|
|
@@ -7121,10 +7120,8 @@ function setupComboboxButton(button, callbacks) {
|
|
|
7121
7120
|
// Printable characters → typeahead.
|
|
7122
7121
|
if (isPrintableKey(event)) {
|
|
7123
7122
|
combobox.setIsOpen(true);
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
nav.goToItem(match);
|
|
7127
|
-
}
|
|
7123
|
+
typeahead.handle(event.key, nav?.selectors.activeItem ?? null);
|
|
7124
|
+
nav?.goTo(n => typeahead.rematch(n.activeItem));
|
|
7128
7125
|
return true;
|
|
7129
7126
|
}
|
|
7130
7127
|
return false;
|
|
@@ -7448,7 +7445,7 @@ function setupComboboxInput(input, options) {
|
|
|
7448
7445
|
case 'ArrowLeft':
|
|
7449
7446
|
case 'ArrowRight':
|
|
7450
7447
|
// Grid mode: navigate cells when active item exists.
|
|
7451
|
-
if (nav?.type === 'grid' && nav.
|
|
7448
|
+
if (nav?.type === 'grid' && nav.selectors.activeItem) {
|
|
7452
7449
|
if (event.key === 'ArrowLeft') nav.goLeft();else nav.goRight();
|
|
7453
7450
|
return true;
|
|
7454
7451
|
}
|
|
@@ -8307,25 +8304,30 @@ const ComboboxList = forwardRef((props, ref) => {
|
|
|
8307
8304
|
const listContextValue = useMemo(() => ({
|
|
8308
8305
|
type
|
|
8309
8306
|
}), [type]);
|
|
8307
|
+
const [isOpen] = useComboboxOpen();
|
|
8308
|
+
const options = useComboboxEvent('optionsChange', undefined);
|
|
8309
|
+
const visibleCount = options?.optionsLength ?? 0;
|
|
8310
8310
|
|
|
8311
|
-
// Register
|
|
8311
|
+
// Register list as listbox when handle is available.
|
|
8312
8312
|
useEffect(() => {
|
|
8313
8313
|
const list = internalRef.current;
|
|
8314
8314
|
if (!list) return undefined;
|
|
8315
8315
|
return handle?.registerListbox(list);
|
|
8316
8316
|
}, [handle]);
|
|
8317
8317
|
|
|
8318
|
-
// Track loading state for aria-busy
|
|
8319
|
-
// Uses both the handle's synchronous getter (for initial state) and the loadingChange event
|
|
8320
|
-
// (for subsequent updates), because child useEffects (skeleton registration) run before
|
|
8321
|
-
// parent subscriptions — relying on events alone would miss the initial notification.
|
|
8318
|
+
// Track loading state for aria-busy
|
|
8322
8319
|
const [isLoading, setIsLoading] = useState(false);
|
|
8323
8320
|
useEffect(() => {
|
|
8324
8321
|
if (!handle) return undefined;
|
|
8325
|
-
// Read current state synchronously (catches registrations
|
|
8322
|
+
// Read current state synchronously (catches registrations before subscription).
|
|
8326
8323
|
setIsLoading(handle.isLoading);
|
|
8327
8324
|
return handle.subscribe('loadingChange', setIsLoading);
|
|
8328
8325
|
}, [handle]);
|
|
8326
|
+
|
|
8327
|
+
// Flush pending keyboard navigation after options commit on open.
|
|
8328
|
+
useEffect(() => {
|
|
8329
|
+
if (isOpen) handle?.flushPendingNavigation();
|
|
8330
|
+
}, [isOpen, visibleCount, handle]);
|
|
8329
8331
|
return /*#__PURE__*/jsx(ComboboxListContext.Provider, {
|
|
8330
8332
|
value: listContextValue,
|
|
8331
8333
|
children: ComboboxList$1({
|
|
@@ -8336,7 +8338,7 @@ const ComboboxList = forwardRef((props, ref) => {
|
|
|
8336
8338
|
ref: mergedRef,
|
|
8337
8339
|
id: listboxId,
|
|
8338
8340
|
type,
|
|
8339
|
-
children
|
|
8341
|
+
children: isOpen ? children : null
|
|
8340
8342
|
})
|
|
8341
8343
|
});
|
|
8342
8344
|
});
|
|
@@ -9163,6 +9165,7 @@ const Popover$1 = (props, {
|
|
|
9163
9165
|
[`position-${position}`]: Boolean(position),
|
|
9164
9166
|
'is-hidden': Boolean(isHidden)
|
|
9165
9167
|
})),
|
|
9168
|
+
hidden: isHidden || undefined,
|
|
9166
9169
|
style: isHidden ? undefined : popoverStyle,
|
|
9167
9170
|
"data-popper-placement": position,
|
|
9168
9171
|
children: [unmountSentinel, /*#__PURE__*/jsxs(ClickAwayProvider, {
|