@octanejs/cmdk 0.1.46 → 0.1.48
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/package.json +6 -6
- package/src/cmdk.tsrx +10 -1
- package/src/components/command.tsrx +133 -132
- package/src/components/empty.tsrx +3 -1
- package/src/components/group.tsrx +5 -3
- package/src/components/item.tsrx +14 -12
- package/src/components/list.tsrx +3 -1
- package/src/hooks.tsrx +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/cmdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
".": "./src/index.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@octanejs/radix": "0.1.
|
|
34
|
+
"@octanejs/radix": "0.1.64"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"octane": "^0.
|
|
37
|
+
"octane": "^0.11.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@octanejs/testing-library": "0.1.
|
|
40
|
+
"@octanejs/testing-library": "0.1.61",
|
|
41
41
|
"@testing-library/jest-dom": "^6.9.1",
|
|
42
|
-
"@tsrx/react": "^0.
|
|
42
|
+
"@tsrx/react": "^0.4.5",
|
|
43
43
|
"cmdk": "1.1.1",
|
|
44
44
|
"esbuild": "^0.28.1",
|
|
45
|
-
"octane": "0.
|
|
45
|
+
"octane": "0.11.0",
|
|
46
46
|
"@types/react": "19.2.17",
|
|
47
47
|
"@types/react-dom": "19.2.3",
|
|
48
48
|
"react": "19.2.7",
|
package/src/cmdk.tsrx
CHANGED
|
@@ -50,4 +50,13 @@ const Command = Object.assign(CommandRoot, {
|
|
|
50
50
|
|
|
51
51
|
export { Command, CommandRoot, Item, Input, List, Group, Separator, Dialog, Empty, Loading };
|
|
52
52
|
export { useCmdk as useCommandState };
|
|
53
|
-
export {
|
|
53
|
+
export {
|
|
54
|
+
Item as CommandItem,
|
|
55
|
+
Input as CommandInput,
|
|
56
|
+
List as CommandList,
|
|
57
|
+
Group as CommandGroup,
|
|
58
|
+
Separator as CommandSeparator,
|
|
59
|
+
Dialog as CommandDialog,
|
|
60
|
+
Empty as CommandEmpty,
|
|
61
|
+
Loading as CommandLoading,
|
|
62
|
+
};
|
|
@@ -18,15 +18,13 @@ import {
|
|
|
18
18
|
import type { CommandProps, Context, State, Store } from '../types';
|
|
19
19
|
|
|
20
20
|
export function CommandRoot(props: CommandProps) @{
|
|
21
|
-
const state = useLazyRef<State>(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
21
|
+
const state = useLazyRef<State>(() => ({
|
|
22
|
+
search: '',
|
|
23
|
+
value: props.value ?? props.defaultValue ?? '',
|
|
24
|
+
selectedItemId: undefined,
|
|
25
|
+
filtered: { count: 0, items: new Map(), groups: new Set() },
|
|
26
|
+
forceMountedCount: 0,
|
|
27
|
+
}));
|
|
30
28
|
const allItems = useLazyRef<Set<string>>(() => new Set());
|
|
31
29
|
const allForceMounted = useLazyRef<Set<string>>(() => new Set());
|
|
32
30
|
const allGroups = useLazyRef<Map<string, Set<string>>>(() => new Map());
|
|
@@ -228,7 +226,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
228
226
|
};
|
|
229
227
|
},
|
|
230
228
|
filter: () => propsRef.current.shouldFilter !== false,
|
|
231
|
-
label: label || props['aria-label'] as string,
|
|
229
|
+
label: label || (props['aria-label'] as string),
|
|
232
230
|
getDisablePointerSelection: () => propsRef.current.disablePointerSelection ?? false,
|
|
233
231
|
listId,
|
|
234
232
|
inputId,
|
|
@@ -239,13 +237,13 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
239
237
|
);
|
|
240
238
|
|
|
241
239
|
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
* DEV ONLY. Selection is keyed by VALUE, not by item id, so two items sharing
|
|
241
|
+
* a value both report `aria-selected="true"` while only the first one — the
|
|
242
|
+
* one `getSelectedItem()`'s `querySelector` finds — actually responds to
|
|
243
|
+
* Enter. That is an authoring error (cmdk requires unique values) rather than
|
|
244
|
+
* something the runtime can resolve, so report it instead of silently
|
|
245
|
+
* rendering a listbox with two selected options.
|
|
246
|
+
*/
|
|
249
247
|
function trackValueOwner(previous: string | undefined, next: string) {
|
|
250
248
|
if (previous) {
|
|
251
249
|
const remaining = (valueOwners.current.get(previous) ?? 1) - 1;
|
|
@@ -267,34 +265,34 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
/**
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
268
|
+
* Queue a teardown's follow-up work.
|
|
269
|
+
*
|
|
270
|
+
* Octane runs a removed child's effect cleanups DURING the parent's render
|
|
271
|
+
* (`renderBranchSlot` → `unmountBlock` → `unmountScope` →
|
|
272
|
+
* `runEffectCleanupCallback`), so calling `schedule` straight from a teardown
|
|
273
|
+
* lands its `ss({})` mid-render and the runtime reports "Cannot update a
|
|
274
|
+
* component (`CommandRoot`) while rendering a different component" — advice a
|
|
275
|
+
* teardown cannot act on, since it does not choose when it runs. Hopping a
|
|
276
|
+
* microtask moves the update out of the render pass; the queued work itself is
|
|
277
|
+
* unchanged and still runs before paint.
|
|
278
|
+
*
|
|
279
|
+
* Only the teardowns defer. Every other caller schedules from a layout effect,
|
|
280
|
+
* where the synchronous update is both correct and load-bearing (the search →
|
|
281
|
+
* filter → select-first chain has to settle inside the same commit).
|
|
282
|
+
*/
|
|
285
283
|
function scheduleFromTeardown(id: string | number, cb: () => void) {
|
|
286
284
|
queueMicrotask(() => schedule(id, cb));
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
* Drop everything `context.value` registered for `id`. Items, force-mounted
|
|
289
|
+
* items AND groups all register their value through the same `useValue` hook,
|
|
290
|
+
* so all three teardown paths have to release through here. Releasing on only
|
|
291
|
+
* one of them leaves the entry alive past the component: the dev-only owner
|
|
292
|
+
* count then reports a duplicate for a value just one live component holds
|
|
293
|
+
* (re-showing a force-mounted item counts 2, then 3, ...), and `ids` /
|
|
294
|
+
* `filtered.items` keep growing for the Command's lifetime.
|
|
295
|
+
*/
|
|
298
296
|
function releaseValue(id: string) {
|
|
299
297
|
if (process.env.NODE_ENV !== 'production') {
|
|
300
298
|
trackValueOwner(ids.current.get(id)?.value, '');
|
|
@@ -357,10 +355,10 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
357
355
|
}
|
|
358
356
|
|
|
359
357
|
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
358
|
+
* Inline styles this component applied, mapped to the values that were there
|
|
359
|
+
* before, so clearing a search restores the consumer's own inline styling
|
|
360
|
+
* rather than deleting it.
|
|
361
|
+
*/
|
|
364
362
|
const styled = useLazyRef<Map<HTMLElement, Record<string, string>>>(() => new Map());
|
|
365
363
|
|
|
366
364
|
function setStyle(element: HTMLElement, property: string, value: string) {
|
|
@@ -384,7 +382,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
384
382
|
}
|
|
385
383
|
|
|
386
384
|
/** The direct child of `container` that positions `item` — the item itself, or
|
|
387
|
-
|
|
385
|
+
* the wrapper element it sits inside. */
|
|
388
386
|
function positionedNode(item: Element, container: Element): HTMLElement | null {
|
|
389
387
|
// Walk up to the child the container actually lays out. `order` applies to
|
|
390
388
|
// a flex CHILD, so an item nested in a wrapper has to rank through that
|
|
@@ -405,30 +403,30 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
405
403
|
}
|
|
406
404
|
|
|
407
405
|
/**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
406
|
+
* Order items by score, and groups by their best item score, using CSS `order`.
|
|
407
|
+
*
|
|
408
|
+
* OCTANE DIVERGENCE[runtime-adaptation-divergences][differential:cmdk-filter-selection-empty]: cmdk's sort is DOM-authoritative — it puts matches in
|
|
409
|
+
* score order by physically relocating item nodes into the list sizer. Octane
|
|
410
|
+
* fences every component's DOM with a pair of comment markers and tracks the
|
|
411
|
+
* range between them, and a template construct like `@for` or `@if` wraps each
|
|
412
|
+
* item in a SECOND, outer range. Relocating a node carries it out of every one
|
|
413
|
+
* of those ranges at once: the loop later clears a range that is now empty and
|
|
414
|
+
* the real node is orphaned in the list forever (observed as every item
|
|
415
|
+
* surviving a removal, stacked above a "No results found." empty state).
|
|
416
|
+
*
|
|
417
|
+
* Carrying the flanking markers along only ever repairs the innermost range,
|
|
418
|
+
* so it breaks again at each new nesting construct. Expressing the order
|
|
419
|
+
* declaratively removes the class of bug instead of an instance of it: no node
|
|
420
|
+
* is ever moved, so no range is ever violated, and clearing the search
|
|
421
|
+
* restores true source order for free — physical reordering left a scrambled
|
|
422
|
+
* residue that accumulated across searches and never recovered.
|
|
423
|
+
*
|
|
424
|
+
* The cost is that the sizer and each group's item container become flex
|
|
425
|
+
* columns while a filter is active, so a consumer who relies on physical DOM
|
|
426
|
+
* order (`:nth-child` styling, drag handles) or on a custom container
|
|
427
|
+
* `display` sees a divergence from upstream. This marker authenticates ranking
|
|
428
|
+
* and aria wiring only; event/ref adaptations are out of scope for this case.
|
|
429
|
+
*/
|
|
432
430
|
function sort() {
|
|
433
431
|
if (!state.current.search || propsRef.current.shouldFilter === false) {
|
|
434
432
|
// No active filter: source order is the answer, so drop everything we set.
|
|
@@ -474,29 +472,28 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
474
472
|
//
|
|
475
473
|
// The sort is stable on the DOM index, so items that score equally keep
|
|
476
474
|
// their source order.
|
|
477
|
-
getValidItems()
|
|
478
|
-
(item, index) => ({
|
|
475
|
+
getValidItems()
|
|
476
|
+
.map((item, index) => ({
|
|
479
477
|
item,
|
|
480
478
|
index,
|
|
481
479
|
score: scores.get(item.getAttribute('id') ?? '') ?? 0,
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
item
|
|
485
|
-
rank
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
480
|
+
}))
|
|
481
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
482
|
+
.map(({ item }) => item)
|
|
483
|
+
.forEach((item, rank) => {
|
|
484
|
+
const group = item.closest(GROUP_ITEMS_SELECTOR);
|
|
485
|
+
const container = group ?? listInsertionElement;
|
|
486
|
+
if (group) asOrderedContainer(group);
|
|
487
|
+
const node = positionedNode(item, container);
|
|
488
|
+
if (!node) return;
|
|
489
|
+
// A grouped item is ranked against its own container's children, so the
|
|
490
|
+
// global rank works there — it is sparse but monotonic. A sizer child
|
|
491
|
+
// shares one rank space with the group hosts numbered below, so it needs
|
|
492
|
+
// a DENSE counter: the global rank skips ahead whenever a grouped item
|
|
493
|
+
// outranks it, which let an ungrouped item tie a group host and leave the
|
|
494
|
+
// two painting in DOM order.
|
|
495
|
+
setStyle(node, 'order', String(group ? rank + 1 : (topLevel += 1)));
|
|
496
|
+
});
|
|
500
497
|
|
|
501
498
|
// OCTANE DIVERGENCE[corrected-group-ordering][differential:cmdk-groups]: upstream looks each group element up by
|
|
502
499
|
// `[cmdk-group=""][data-value="<groupId>"]`, but `data-value` holds the
|
|
@@ -507,18 +504,19 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
507
504
|
// attribute directly also avoids escaping problems for values containing
|
|
508
505
|
// quotes (upstream encodes the selector but writes `data-value` raw).
|
|
509
506
|
const groupElements = Array.from(listInsertionElement.querySelectorAll(GROUP_SELECTOR));
|
|
510
|
-
groups
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
507
|
+
groups
|
|
508
|
+
.sort((a, b) => b[1] - a[1])
|
|
509
|
+
.forEach(([groupId], rank) => {
|
|
510
|
+
const groupValue = ids.current.get(groupId)?.value;
|
|
511
|
+
if (!groupValue) return;
|
|
512
|
+
const element = groupElements.find((el) => el.getAttribute(VALUE_ATTR) === groupValue);
|
|
513
|
+
// Through the positioned node for the same reason items do: a group
|
|
514
|
+
// wrapped in another element is not itself the sizer's flex child, so
|
|
515
|
+
// ordering the group host would do nothing and leave the wrapper
|
|
516
|
+
// unranked at `order: 0`, ahead of everything.
|
|
517
|
+
const node = element ? positionedNode(element, listInsertionElement) : null;
|
|
518
|
+
if (node) setStyle(node, 'order', String(topLevel + rank + 1));
|
|
519
|
+
});
|
|
522
520
|
}
|
|
523
521
|
|
|
524
522
|
function getSelectedItem() {
|
|
@@ -539,26 +537,26 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
539
537
|
|
|
540
538
|
/** Element children of `container`, in the order they are painted. */
|
|
541
539
|
function orderedChildren(container: Element): HTMLElement[] {
|
|
542
|
-
return Array.from(container.children)
|
|
543
|
-
(child): child is HTMLElement => child instanceof HTMLElement
|
|
544
|
-
|
|
545
|
-
(a, b) => a.order - b.order || a.index - b.index
|
|
546
|
-
|
|
540
|
+
return Array.from(container.children)
|
|
541
|
+
.filter((child): child is HTMLElement => child instanceof HTMLElement)
|
|
542
|
+
.map((child, index) => ({ child, index, order: Number(child.style.order) || 0 }))
|
|
543
|
+
.sort((a, b) => a.order - b.order || a.index - b.index)
|
|
544
|
+
.map(({ child }) => child);
|
|
547
545
|
}
|
|
548
546
|
|
|
549
547
|
/**
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
548
|
+
* Selectable items in the order the user sees them.
|
|
549
|
+
*
|
|
550
|
+
* Selection and arrow-key navigation both read this, so it has to agree with
|
|
551
|
+
* what is on screen. While a filter is active the ranking lives in CSS `order`
|
|
552
|
+
* rather than in DOM order (see `sort()`), and the two disagree — walking the
|
|
553
|
+
* DOM would select and navigate in source order behind a differently-ordered
|
|
554
|
+
* list. Descending through the ranked containers keeps group ranking and
|
|
555
|
+
* within-group ranking composed in the right order.
|
|
556
|
+
*
|
|
557
|
+
* With no filter active nothing carries an `order`, so DOM order already IS
|
|
558
|
+
* visual order and the flat query is both correct and cheaper.
|
|
559
|
+
*/
|
|
562
560
|
function getValidItems(): Element[] {
|
|
563
561
|
const root = listInnerRef.current;
|
|
564
562
|
if (!root) return [];
|
|
@@ -592,19 +590,20 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
592
590
|
let newSelected = items[index + change];
|
|
593
591
|
|
|
594
592
|
if (propsRef.current?.loop) {
|
|
595
|
-
newSelected =
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
593
|
+
newSelected =
|
|
594
|
+
index + change < 0
|
|
595
|
+
? items[items.length - 1]
|
|
596
|
+
: index + change === items.length
|
|
597
|
+
? items[0]
|
|
598
|
+
: items[index + change];
|
|
600
599
|
}
|
|
601
600
|
|
|
602
601
|
if (newSelected) store.setState('value', newSelected.getAttribute(VALUE_ATTR) || '');
|
|
603
602
|
}
|
|
604
603
|
|
|
605
604
|
/** Groups in the order they are painted, for the same reason `getValidItems`
|
|
606
|
-
|
|
607
|
-
|
|
605
|
+
* exists: while a filter is active the ranking lives in CSS `order`, so DOM
|
|
606
|
+
* order and on-screen order disagree. */
|
|
608
607
|
function getOrderedGroups(): Element[] {
|
|
609
608
|
const root = listInnerRef.current;
|
|
610
609
|
if (!root) return [];
|
|
@@ -613,13 +612,14 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
613
612
|
// Read the rank off the positioned node, not the group host: a wrapped group
|
|
614
613
|
// carries its `order` on the wrapper, so reading the host would see 0 for
|
|
615
614
|
// every such group and collapse navigation back to DOM order.
|
|
616
|
-
return groups
|
|
617
|
-
(group, index) => ({
|
|
615
|
+
return groups
|
|
616
|
+
.map((group, index) => ({
|
|
618
617
|
group,
|
|
619
618
|
index,
|
|
620
619
|
order: Number(positionedNode(group, root)?.style.order) || 0,
|
|
621
|
-
})
|
|
622
|
-
|
|
620
|
+
}))
|
|
621
|
+
.sort((a, b) => a.order - b.order || a.index - b.index)
|
|
622
|
+
.map(({ group }) => group);
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
function updateSelectedByGroup(change: 1 | -1) {
|
|
@@ -632,8 +632,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
632
632
|
// filter is active.
|
|
633
633
|
const groups = getOrderedGroups();
|
|
634
634
|
const items = getValidItems();
|
|
635
|
-
let index =
|
|
636
|
-
group ? groups.indexOf(group) : -1;
|
|
635
|
+
let index = group ? groups.indexOf(group) : -1;
|
|
637
636
|
let item: Element | undefined;
|
|
638
637
|
|
|
639
638
|
while (index >= 0 && !item) {
|
|
@@ -731,7 +730,9 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
731
730
|
}
|
|
732
731
|
}}
|
|
733
732
|
>
|
|
734
|
-
<label cmdk-label="" for={inputId} id={labelId} style={srOnlyStyles}>
|
|
733
|
+
<label cmdk-label="" for={inputId} id={labelId} style={srOnlyStyles}>
|
|
734
|
+
{label}
|
|
735
|
+
</label>
|
|
735
736
|
<StoreContext value={store}>
|
|
736
737
|
<CommandContext value={context}>{children}</CommandContext>
|
|
737
738
|
</StoreContext>
|
|
@@ -26,5 +26,7 @@ export function Empty(props: EmptyProps) @{
|
|
|
26
26
|
if (!render) return null;
|
|
27
27
|
|
|
28
28
|
const { ref: _ref, children, ...etc } = props;
|
|
29
|
-
<div ref={props.ref ?? null} {...etc} cmdk-empty="" role="presentation">
|
|
29
|
+
<div ref={props.ref ?? null} {...etc} cmdk-empty="" role="presentation">
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
30
32
|
}
|
|
@@ -12,8 +12,8 @@ export function Group(props: GroupProps) @{
|
|
|
12
12
|
const headingRef = useRef<HTMLDivElement | null>(null);
|
|
13
13
|
const headingId = useId();
|
|
14
14
|
const context = useCommand()!;
|
|
15
|
-
const render = useCmdk(
|
|
16
|
-
|
|
15
|
+
const render = useCmdk((s) =>
|
|
16
|
+
forceMount
|
|
17
17
|
? true
|
|
18
18
|
: context.filter() === false
|
|
19
19
|
? true
|
|
@@ -43,7 +43,9 @@ export function Group(props: GroupProps) @{
|
|
|
43
43
|
hidden={render ? undefined : true}
|
|
44
44
|
>
|
|
45
45
|
@if (heading) {
|
|
46
|
-
<div ref={headingRef} cmdk-group-heading="" aria-hidden={true} id={headingId}>
|
|
46
|
+
<div ref={headingRef} cmdk-group-heading="" aria-hidden={true} id={headingId}>
|
|
47
|
+
{heading}
|
|
48
|
+
</div>
|
|
47
49
|
}
|
|
48
50
|
<div cmdk-group-items="" role="group" aria-labelledby={heading ? headingId : undefined}>
|
|
49
51
|
<GroupContext value={contextValue}>{children}</GroupContext>
|
package/src/components/item.tsrx
CHANGED
|
@@ -28,23 +28,23 @@ export function Item(props: ItemProps) @{
|
|
|
28
28
|
|
|
29
29
|
const store = useStore()!;
|
|
30
30
|
const selected = useCmdk((s) => Boolean(s.value && s.value === value.current));
|
|
31
|
-
const render = useCmdk(
|
|
32
|
-
|
|
31
|
+
const render = useCmdk((s) =>
|
|
32
|
+
forceMount
|
|
33
33
|
? true
|
|
34
34
|
: context.filter() === false
|
|
35
35
|
? true
|
|
36
36
|
: !s.search
|
|
37
37
|
? true
|
|
38
38
|
: // An item with no entry in `filtered.items` has never been
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// scored — that is not the same as having scored zero, and
|
|
40
|
+
// treating it as zero deadlocks: the item renders null, so
|
|
41
|
+
// `ref.current` stays null, so `useValue` has no textContent to
|
|
42
|
+
// infer from, so it registers an empty value, which scores zero
|
|
43
|
+
// forever. Upstream reads string children synchronously; octane's
|
|
44
|
+
// compiled children are opaque, so the first render IS the
|
|
45
|
+
// registration. Rendering unscored items lets that first pass
|
|
46
|
+
// happen; the next filter pass hides it if it does not match.
|
|
47
|
+
s.filtered.items.has(id)
|
|
48
48
|
? (s.filtered.items.get(id) ?? 0) > 0
|
|
49
49
|
: true,
|
|
50
50
|
);
|
|
@@ -90,5 +90,7 @@ export function Item(props: ItemProps) @{
|
|
|
90
90
|
data-selected={Boolean(selected)}
|
|
91
91
|
onPointerMove={disabled || context.getDisablePointerSelection() ? undefined : select}
|
|
92
92
|
onClick={disabled ? undefined : onSelect}
|
|
93
|
-
>
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</div>
|
|
94
96
|
}
|
package/src/components/list.tsrx
CHANGED
|
@@ -45,6 +45,8 @@ export function List(props: ListProps) @{
|
|
|
45
45
|
aria-label={label}
|
|
46
46
|
id={context.listId}
|
|
47
47
|
>
|
|
48
|
-
<div ref={[height, context.listInnerRef]} cmdk-list-sizer="">
|
|
48
|
+
<div ref={[height, context.listInnerRef]} cmdk-list-sizer="">
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
49
51
|
</div>
|
|
50
52
|
}
|
package/src/hooks.tsrx
CHANGED
|
@@ -82,8 +82,7 @@ export function useValue(
|
|
|
82
82
|
return undefined;
|
|
83
83
|
})();
|
|
84
84
|
|
|
85
|
-
const keywords =
|
|
86
|
-
Array.isArray(aliases) ? aliases.map((alias) => alias.trim()) : [];
|
|
85
|
+
const keywords = Array.isArray(aliases) ? aliases.map((alias) => alias.trim()) : [];
|
|
87
86
|
context.value(id, value, keywords);
|
|
88
87
|
ref.current?.setAttribute(VALUE_ATTR, value ?? '');
|
|
89
88
|
valueRef.current = value;
|