@octanejs/cmdk 0.1.18 → 0.1.20
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/README.md +7 -4
- package/package.json +9 -7
- package/src/cmdk.tsrx +2 -1
- package/src/components/command.tsrx +14 -4
- package/src/components/dialog.tsrx +4 -0
- package/src/components/empty.tsrx +2 -0
- package/src/components/item.tsrx +4 -5
- package/src/hooks.tsrx +2 -1
package/README.md
CHANGED
|
@@ -15,7 +15,9 @@ wiring to Octane (native events, ref-as-prop, `useId`), and building on
|
|
|
15
15
|
> and `defaultFilter` — with filtering, score sorting, keyboard navigation,
|
|
16
16
|
> controlled modes, and SSR + hydration. `asChild` is the one unsupported prop.
|
|
17
17
|
> A differential suite runs the same fixture through this port and the published
|
|
18
|
-
> `cmdk@1.1.1` on React and asserts byte-equal HTML.
|
|
18
|
+
> `cmdk@1.1.1` on React and asserts byte-equal HTML for its declared cases. The
|
|
19
|
+
> canonical upstream Playwright suite is vendored but not yet adapted
|
|
20
|
+
> exhaustively, so parity remains recorded-unverified. See [`UPSTREAM.md`](./UPSTREAM.md),
|
|
19
21
|
> [`docs/cmdk-port-plan.md`](../../docs/cmdk-port-plan.md) and the authoritative
|
|
20
22
|
> [`docs/bindings-status.md`](../../docs/bindings-status.md).
|
|
21
23
|
|
|
@@ -47,9 +49,10 @@ Standard Octane binding adaptations (see
|
|
|
47
49
|
on. Upstream never registers them, so it does neither.
|
|
48
50
|
- `Command.Dialog` also accepts `defaultOpen` and `modal`; upstream forwards
|
|
49
51
|
only `open`/`onOpenChange`.
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
- Score ranking uses CSS `order` while filtering instead of physically moving
|
|
53
|
+
renderer-owned nodes. Clearing the search removes those declarations and
|
|
54
|
+
restores source order; DOM-order selectors can differ from visual order while
|
|
55
|
+
a filter is active.
|
|
53
56
|
|
|
54
57
|
## License
|
|
55
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/cmdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -30,21 +30,23 @@
|
|
|
30
30
|
".": "./src/index.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@octanejs/radix": "0.1.
|
|
33
|
+
"@octanejs/radix": "0.1.36"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"octane": "0.1.
|
|
36
|
+
"octane": "0.1.37"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/jest-dom": "^6.9.1",
|
|
40
40
|
"@tsrx/react": "^0.2.56",
|
|
41
41
|
"cmdk": "1.1.1",
|
|
42
42
|
"esbuild": "^0.28.1",
|
|
43
|
-
"react": "
|
|
44
|
-
"react-dom": "
|
|
43
|
+
"@types/react": "19.2.17",
|
|
44
|
+
"@types/react-dom": "19.2.3",
|
|
45
|
+
"react": "19.2.7",
|
|
46
|
+
"react-dom": "19.2.7",
|
|
45
47
|
"vitest": "^4.1.10",
|
|
46
|
-
"@octanejs/testing-library": "0.1.
|
|
47
|
-
"octane": "0.1.
|
|
48
|
+
"@octanejs/testing-library": "0.1.34",
|
|
49
|
+
"octane": "0.1.37"
|
|
48
50
|
},
|
|
49
51
|
"scripts": {
|
|
50
52
|
"test": "vitest run"
|
package/src/cmdk.tsrx
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
// - `sort()` reorders only the surviving (score > 0) items — moving a
|
|
20
20
|
// to-be-unmounted node orphans it in an octane hydrated tree (see
|
|
21
21
|
// ./components/command.tsrx).
|
|
22
|
-
// -
|
|
22
|
+
// - OCTANE DIVERGENCE[as-child-unsupported][differential:cmdk-as-child]:
|
|
23
|
+
// `asChild` is NOT supported: cmdk's SlottableWithNestedChildren clones a child
|
|
23
24
|
// element via React.cloneElement/isValidElement and re-parents the component's
|
|
24
25
|
// own content into it. Octane's compiled children are opaque render bodies, so
|
|
25
26
|
// that introspect-and-clone has no faithful equivalent — components always
|
|
@@ -130,6 +130,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
130
130
|
}
|
|
131
131
|
ids.current.set(id, { value: itemValue ?? '', keywords });
|
|
132
132
|
state.current.filtered.items.set(id, score(itemValue ?? '', keywords));
|
|
133
|
+
// OCTANE DIVERGENCE[value-change-filter-refresh][differential:cmdk-value-change-filter]:
|
|
133
134
|
// Re-derive the aggregate before sorting. Scoring this one item
|
|
134
135
|
// updates `filtered.items` but leaves `filtered.count` and
|
|
135
136
|
// `filtered.groups` describing the previous values, and those two
|
|
@@ -167,7 +168,8 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
167
168
|
// An item leaving its group has to be dropped from that group's member
|
|
168
169
|
// set too, or the set accumulates dead ids for the Command's lifetime
|
|
169
170
|
// (unbounded growth in a menu whose results churn) and filterItems()
|
|
170
|
-
// walks them on every keystroke.
|
|
171
|
+
// walks them on every keystroke. Documented in status.json; no paired
|
|
172
|
+
// DOM oracle distinguishes the bookkeeping from upstream.
|
|
171
173
|
if (groupId) allGroups.current.get(groupId)?.delete(id);
|
|
172
174
|
const selectedItem = getSelectedItem();
|
|
173
175
|
|
|
@@ -194,9 +196,13 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
194
196
|
schedule(8, () => store.emit());
|
|
195
197
|
|
|
196
198
|
return () => {
|
|
199
|
+
// OCTANE DIVERGENCE[force-mount-empty-count][differential:cmdk-force-mount-empty]:
|
|
200
|
+
// Releasing the force-mount count on unmount is required so Empty can
|
|
201
|
+
// reappear once no force-mounted item remains visible.
|
|
197
202
|
releaseValue(id);
|
|
198
203
|
allForceMounted.current.delete(id);
|
|
199
204
|
state.current.forceMountedCount = allForceMounted.current.size;
|
|
205
|
+
// OCTANE DIVERGENCE[selected-force-mount-teardown][differential:cmdk-selected-force-mount]:
|
|
200
206
|
// A force-mounted item is selectable like any other, so losing the
|
|
201
207
|
// selected one has to move the selection on — exactly as the plain
|
|
202
208
|
// item teardown does. Without this the value stays pointing at a node
|
|
@@ -250,6 +256,8 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
250
256
|
const owners = (valueOwners.current.get(next) ?? 0) + 1;
|
|
251
257
|
valueOwners.current.set(next, owners);
|
|
252
258
|
if (owners > 1) {
|
|
259
|
+
// OCTANE DIVERGENCE[duplicate-value-warning][differential:cmdk-duplicate-value]:
|
|
260
|
+
// Development builds report duplicate values; upstream accepts them silently.
|
|
253
261
|
console.warn(
|
|
254
262
|
`[@octanejs/cmdk] ${owners} items share the value ${JSON.stringify(next)}. ` +
|
|
255
263
|
`Selection is keyed by value, so they will all appear selected at once and ` +
|
|
@@ -399,7 +407,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
399
407
|
/**
|
|
400
408
|
* Order items by score, and groups by their best item score, using CSS `order`.
|
|
401
409
|
*
|
|
402
|
-
* OCTANE DIVERGENCE: cmdk's sort is DOM-authoritative — it puts matches in
|
|
410
|
+
* OCTANE DIVERGENCE[runtime-adaptation-divergences][differential:cmdk-filter-selection-empty]: cmdk's sort is DOM-authoritative — it puts matches in
|
|
403
411
|
* score order by physically relocating item nodes into the list sizer. Octane
|
|
404
412
|
* fences every component's DOM with a pair of comment markers and tracks the
|
|
405
413
|
* range between them, and a template construct like `@for` or `@if` wraps each
|
|
@@ -418,7 +426,8 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
418
426
|
* The cost is that the sizer and each group's item container become flex
|
|
419
427
|
* columns while a filter is active, so a consumer who relies on physical DOM
|
|
420
428
|
* order (`:nth-child` styling, drag handles) or on a custom container
|
|
421
|
-
* `display` sees a divergence from upstream.
|
|
429
|
+
* `display` sees a divergence from upstream. This marker authenticates ranking
|
|
430
|
+
* and aria wiring only; event/ref adaptations are out of scope for this case.
|
|
422
431
|
*/
|
|
423
432
|
function sort() {
|
|
424
433
|
if (!state.current.search || propsRef.current.shouldFilter === false) {
|
|
@@ -453,6 +462,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
453
462
|
// ranks below continue past them instead of tying.
|
|
454
463
|
let topLevel = 0;
|
|
455
464
|
|
|
465
|
+
// OCTANE DIVERGENCE[force-mount-rank-order][differential:cmdk-force-mount-rank]:
|
|
456
466
|
// EVERY valid item gets a rank, not just the matches. A non-match usually
|
|
457
467
|
// unmounts and never needs one, but a force-mounted item never scores and
|
|
458
468
|
// never unmounts — and an unranked child keeps the initial `order` of 0,
|
|
@@ -488,7 +498,7 @@ export function CommandRoot(props: CommandProps) @{
|
|
|
488
498
|
setStyle(node, 'order', String(group ? rank + 1 : topLevel += 1));
|
|
489
499
|
});
|
|
490
500
|
|
|
491
|
-
// OCTANE DIVERGENCE: upstream looks each group element up by
|
|
501
|
+
// OCTANE DIVERGENCE[corrected-group-ordering][differential:cmdk-groups]: upstream looks each group element up by
|
|
492
502
|
// `[cmdk-group=""][data-value="<groupId>"]`, but `data-value` holds the
|
|
493
503
|
// group's VALUE (its heading text), never its id — so upstream's selector
|
|
494
504
|
// can never match and its group reordering is effectively dead code.
|
|
@@ -23,6 +23,10 @@ export function Dialog(props: DialogProps) {
|
|
|
23
23
|
...etc
|
|
24
24
|
} = props;
|
|
25
25
|
|
|
26
|
+
// OCTANE DIVERGENCE[dialog-default-open][differential:cmdk-dialog-default-open]:
|
|
27
|
+
// Upstream forwards only open/onOpenChange; the port also wires defaultOpen
|
|
28
|
+
// through to the Radix root. `modal` is forwarded too, but that prop is outside
|
|
29
|
+
// this record's observing case.
|
|
26
30
|
return createElement(
|
|
27
31
|
RadixDialog.Root,
|
|
28
32
|
{ open, defaultOpen, onOpenChange, modal },
|
|
@@ -6,6 +6,7 @@ import type { EmptyProps } from '../types';
|
|
|
6
6
|
export function Empty(props: EmptyProps) @{
|
|
7
7
|
const store = useStore()!;
|
|
8
8
|
|
|
9
|
+
// OCTANE DIVERGENCE[force-mount-empty-count][differential:cmdk-force-mount-empty]:
|
|
9
10
|
// Force-mounted items never enter `filtered.count`, so consult both: "no
|
|
10
11
|
// results" must not render while any item is visible.
|
|
11
12
|
const snapshot = () => {
|
|
@@ -13,6 +14,7 @@ export function Empty(props: EmptyProps) @{
|
|
|
13
14
|
return state.filtered.count === 0 && state.forceMountedCount === 0;
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
// OCTANE DIVERGENCE[ssr-empty-suppressed][differential:cmdk-ssr-empty]:
|
|
16
18
|
// The server snapshot is `false`. Items register in layout effects, which do
|
|
17
19
|
// not run on the server, so the count is unavoidably 0 there — rendering on
|
|
18
20
|
// that would ship "No results found." above a fully-populated list on every
|
package/src/components/item.tsrx
CHANGED
|
@@ -40,11 +40,10 @@ export function Item(props: ItemProps) @{
|
|
|
40
40
|
// treating it as zero deadlocks: the item renders null, so
|
|
41
41
|
// `ref.current` stays null, so `useValue` has no textContent to
|
|
42
42
|
// infer from, so it registers an empty value, which scores zero
|
|
43
|
-
// forever. Upstream
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// next filter pass hides it if it genuinely does not match.
|
|
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.
|
|
48
47
|
s.filtered.items.has(id)
|
|
49
48
|
? (s.filtered.items.get(id) ?? 0) > 0
|
|
50
49
|
: true,
|
package/src/hooks.tsrx
CHANGED
|
@@ -109,10 +109,11 @@ export function useScheduleLayoutEffect() {
|
|
|
109
109
|
try {
|
|
110
110
|
f();
|
|
111
111
|
} catch (error) {
|
|
112
|
+
// OCTANE DIVERGENCE[batcher-isolation][differential:cmdk-batcher-isolation]:
|
|
112
113
|
// Isolate, but never swallow. A scheduled callback can run user code
|
|
113
114
|
// (selectFirstItem -> setState('value') -> onValueChange), so a failure
|
|
114
115
|
// here must stay visible; report it the same way octane reports an
|
|
115
|
-
// exception raised inside an effect
|
|
116
|
+
// exception raised inside an effect. Upstream rethrows out of the flush.
|
|
116
117
|
console.error(error);
|
|
117
118
|
}
|
|
118
119
|
}
|