cursedbelt 5.92.3 → 5.93.0
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/dist/react/components/ComboboxField.d.ts +10 -0
- package/dist/react/components/ComboboxField.d.ts.map +1 -1
- package/dist/react/components/ComboboxField.js +4 -4
- package/dist/react/components/ComboboxField.js.map +1 -1
- package/package.json +1 -1
- package/src/react/components/ComboboxField.spec.tsx +39 -0
- package/src/react/components/ComboboxField.tsx +20 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
import { type ControlSize } from '../lib/field.js';
|
|
2
3
|
/** A single option in the combobox list. */
|
|
3
4
|
export interface ComboboxOption {
|
|
@@ -15,6 +16,15 @@ interface ComboboxBase {
|
|
|
15
16
|
loadOptions?: (query: string) => Promise<ComboboxOption[]>;
|
|
16
17
|
/** Allow typing a value that isn't in the list and creating it on the fly. */
|
|
17
18
|
creatable?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* What the list says when nothing matches — *"Nobody in the tree by that name"*.
|
|
21
|
+
*
|
|
22
|
+
* 🔴 Required in practice for a combobox whose options come from DATA (`loadOptions`, or a
|
|
23
|
+
* list read from a store): the generic line cannot say what belongs here, and the
|
|
24
|
+
* `choice-needs-empty-state` guardrail reports one without it. Rendered as plain text, not
|
|
25
|
+
* as an item, so it can never be picked or keyboard-focused. Default "No options found".
|
|
26
|
+
*/
|
|
27
|
+
emptyLabel?: ReactNode;
|
|
18
28
|
placeholder?: string;
|
|
19
29
|
disabled?: boolean;
|
|
20
30
|
id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAEjF,OAAO,EAGL,KAAK,WAAW,EAGjB,MAAM,iBAAiB,CAAC;AAKzB,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B;oEACgE;IAChE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC3D,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;uCAEmC;IACnC,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,YAAY;IAC5D,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAwB,SAAQ,YAAY;IAC3D,QAAQ,EAAE,IAAI,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG,uBAAuB,CAAC;AA6EpF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BA8MtD"}
|
|
@@ -10,12 +10,12 @@ import { TAP_TARGET, TAP_TARGET_HEIGHT } from '../lib/tapTarget.js';
|
|
|
10
10
|
const DEBOUNCE_MS = 250;
|
|
11
11
|
/** Sentinel value used to represent the "Create new" option inside Base UI. */
|
|
12
12
|
const CREATE_SENTINEL = '__cb_create__';
|
|
13
|
-
function ComboboxList({ creatable, query, onCreateClick }) {
|
|
13
|
+
function ComboboxList({ creatable, query, onCreateClick, emptyLabel }) {
|
|
14
14
|
const filtered = Combobox.useFilteredItems();
|
|
15
15
|
const trimmed = query.trim();
|
|
16
16
|
const hasExact = filtered.some((o) => o.label.toLowerCase() === trimmed.toLowerCase() || o.value === trimmed);
|
|
17
17
|
const showCreate = creatable && trimmed !== '' && !hasExact;
|
|
18
|
-
return (_jsxs(Combobox.List, { className: 'py-1', children: [filtered.map((item, index) => (_jsxs(Combobox.Item, { value: item.value, index: index, disabled: item.disabled, className: cn(FLOATING_ITEM_BASE, 'pr-8', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), children: [item.color ? (_jsx("span", { "aria-hidden": true, className: 'size-2 shrink-0 rounded-full', style: { backgroundColor: item.color } })) : null, _jsx("span", { className: 'flex-1 truncate', children: item.label }), _jsx(Combobox.ItemIndicator, { className: 'absolute right-2 flex size-4 items-center justify-center', children: _jsx(Check, { className: 'size-3' }) })] }, item.value))), showCreate && (_jsxs(Combobox.Item, { value: CREATE_SENTINEL, className: cn(FLOATING_ITEM_BASE, 'gap-1.5', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), onClick: () => onCreateClick(trimmed), children: [_jsx(Plus, { className: 'size-3', "aria-hidden": true }), "Create \u201C", trimmed, "\u201D"] })), _jsx(Combobox.Empty, { className: 'py-2 text-center text-sm text-muted-foreground', children:
|
|
18
|
+
return (_jsxs(Combobox.List, { className: 'py-1', children: [filtered.map((item, index) => (_jsxs(Combobox.Item, { value: item.value, index: index, disabled: item.disabled, className: cn(FLOATING_ITEM_BASE, 'pr-8', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), children: [item.color ? (_jsx("span", { "aria-hidden": true, className: 'size-2 shrink-0 rounded-full', style: { backgroundColor: item.color } })) : null, _jsx("span", { className: 'flex-1 truncate', children: item.label }), _jsx(Combobox.ItemIndicator, { className: 'absolute right-2 flex size-4 items-center justify-center', children: _jsx(Check, { className: 'size-3' }) })] }, item.value))), showCreate && (_jsxs(Combobox.Item, { value: CREATE_SENTINEL, className: cn(FLOATING_ITEM_BASE, 'gap-1.5', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), onClick: () => onCreateClick(trimmed), children: [_jsx(Plus, { className: 'size-3', "aria-hidden": true }), "Create \u201C", trimmed, "\u201D"] })), _jsx(Combobox.Empty, { className: 'py-2 text-center text-sm text-muted-foreground', children: emptyLabel })] }));
|
|
19
19
|
}
|
|
20
20
|
/* -------------------------------------------------------------------------- */
|
|
21
21
|
/* Public component */
|
|
@@ -31,7 +31,7 @@ function ComboboxList({ creatable, query, onCreateClick }) {
|
|
|
31
31
|
* type so it renders inside a {@link Field} with label + validation.
|
|
32
32
|
*/
|
|
33
33
|
export function ComboboxField(props) {
|
|
34
|
-
const { options = [], loadOptions, creatable = false, placeholder = 'Select…', disabled, id, name, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, className, size = 'md',
|
|
34
|
+
const { options = [], loadOptions, creatable = false, emptyLabel = 'No options found', placeholder = 'Select…', disabled, id, name, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, className, size = 'md',
|
|
35
35
|
// Named so they stay off the input: the discriminated union's own shape, read via
|
|
36
36
|
// `(props as …).multiple`/`.value`/`.onChange` below, never off this destructuring.
|
|
37
37
|
multiple: _multiple, value: _value, onChange: _onChange, ...rest } = props;
|
|
@@ -120,7 +120,7 @@ export function ComboboxField(props) {
|
|
|
120
120
|
// preflight (desk) would otherwise add the UA's inset border and control face (task 2757).
|
|
121
121
|
'm-0 min-w-24 flex-1 border-0 bg-transparent px-3 font-[inherit] text-base text-field-fg outline-none placeholder:text-field-placeholder sm:pointer-fine:text-sm', TAP_TARGET_HEIGHT, multiple && 'py-1.5') }), loading ? (_jsx(Loader2, { className: 'mr-2 size-4 shrink-0 animate-spin opacity-50', "aria-label": 'Loading options' })) : (_jsx(Combobox.Trigger, { "aria-label": 'Toggle options', className: `mr-2 flex size-5 shrink-0 items-center justify-center text-muted-foreground transition hover:text-foreground ${TAP_TARGET}`, children: _jsx(ChevronDown, { className: 'size-4' }) }))] }), _jsx(Combobox.Portal, { children: _jsx(Combobox.Positioner, { sideOffset: 4, collisionPadding: FLOATING_COLLISION_PADDING, className: 'z-[var(--z-popover)]', children: _jsx(Combobox.Popup, { className: cn(FLOATING_CONTENT_BASE,
|
|
122
122
|
// guardrails-ignore no-raw-overflow: floating popover panel outside the page's ScrollArea, height-capped by max-h-64 — no keyboard-inset concern.
|
|
123
|
-
'max-h-64 min-w-[var(--anchor-width)] overflow-y-auto p-0', 'transition-[transform,opacity] duration-100', 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0', 'data-[ending-style]:scale-95 data-[ending-style]:opacity-0'), children: _jsx(ComboboxList, { creatable: creatable, query: query, onCreateClick: handleCreate }) }) }) })] }));
|
|
123
|
+
'max-h-64 min-w-[var(--anchor-width)] overflow-y-auto p-0', 'transition-[transform,opacity] duration-100', 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0', 'data-[ending-style]:scale-95 data-[ending-style]:opacity-0'), children: _jsx(ComboboxList, { creatable: creatable, query: query, onCreateClick: handleCreate, emptyLabel: emptyLabel }) }) }) })] }));
|
|
124
124
|
}
|
|
125
125
|
// FRM-3: a closed list is sized by its longest option, as SelectField is. A list that can hold
|
|
126
126
|
// a value nobody has seen yet — fetched (`loadOptions`) or typed (`creatable`) — keeps a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboboxField.js","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,
|
|
1
|
+
{"version":3,"file":"ComboboxField.js","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.93.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -156,6 +156,45 @@ describe('ComboboxField (async)', () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
describe('ComboboxField (empty)', () => {
|
|
160
|
+
// Forge task 3691: photos' tree picker searches the family tree by name and had to opt out
|
|
161
|
+
// of `choice-needs-empty-state`, because the belt's fixed line could not say what belongs.
|
|
162
|
+
const typeNobody = (container: HTMLElement) => {
|
|
163
|
+
act(() => {
|
|
164
|
+
fireEvent.click(container.querySelector('[aria-label="Toggle options"]') as HTMLElement);
|
|
165
|
+
});
|
|
166
|
+
act(() => {
|
|
167
|
+
fireEvent.input(container.querySelector('input') as HTMLInputElement, {
|
|
168
|
+
target: { value: 'Zebedee' },
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
test('🔴 a search that finds nothing says the caller\'s emptyLabel, not the generic line', async () => {
|
|
174
|
+
const { container, getByText, queryByText } = render(
|
|
175
|
+
<ComboboxField
|
|
176
|
+
options={OPTIONS}
|
|
177
|
+
emptyLabel='Nobody in the tree by that name'
|
|
178
|
+
value={null}
|
|
179
|
+
onChange={() => {}}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
typeNobody(container);
|
|
183
|
+
await waitFor(() => expect(getByText('Nobody in the tree by that name')).not.toBeNull(), {
|
|
184
|
+
timeout: 1000,
|
|
185
|
+
});
|
|
186
|
+
expect(queryByText('No options found')).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('without one, the generic line stands in', async () => {
|
|
190
|
+
const { container, getByText } = render(
|
|
191
|
+
<ComboboxField options={OPTIONS} value={null} onChange={() => {}} />,
|
|
192
|
+
);
|
|
193
|
+
typeNobody(container);
|
|
194
|
+
await waitFor(() => expect(getByText('No options found')).not.toBeNull(), { timeout: 1000 });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
159
198
|
describe('ComboboxField on a touch screen', () => {
|
|
160
199
|
// binary-server's design audit at 390 (forge task 2621): the multi-select's text input
|
|
161
200
|
// measured 190×36 — the group never took the coarse-pointer floor every other field control
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Combobox } from '@base-ui/react/combobox';
|
|
4
4
|
import { Check, ChevronDown, Loader2, Plus, X } from 'lucide-react';
|
|
5
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../lib/cn.js';
|
|
7
7
|
import {
|
|
8
8
|
CONTROL_HEIGHT_CLASS,
|
|
@@ -32,6 +32,15 @@ interface ComboboxBase {
|
|
|
32
32
|
loadOptions?: (query: string) => Promise<ComboboxOption[]>;
|
|
33
33
|
/** Allow typing a value that isn't in the list and creating it on the fly. */
|
|
34
34
|
creatable?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* What the list says when nothing matches — *"Nobody in the tree by that name"*.
|
|
37
|
+
*
|
|
38
|
+
* 🔴 Required in practice for a combobox whose options come from DATA (`loadOptions`, or a
|
|
39
|
+
* list read from a store): the generic line cannot say what belongs here, and the
|
|
40
|
+
* `choice-needs-empty-state` guardrail reports one without it. Rendered as plain text, not
|
|
41
|
+
* as an item, so it can never be picked or keyboard-focused. Default "No options found".
|
|
42
|
+
*/
|
|
43
|
+
emptyLabel?: ReactNode;
|
|
35
44
|
placeholder?: string;
|
|
36
45
|
disabled?: boolean;
|
|
37
46
|
id?: string;
|
|
@@ -71,9 +80,10 @@ interface ListProps {
|
|
|
71
80
|
creatable: boolean;
|
|
72
81
|
query: string;
|
|
73
82
|
onCreateClick: (q: string) => void;
|
|
83
|
+
emptyLabel: ReactNode;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
function ComboboxList({ creatable, query, onCreateClick }: ListProps) {
|
|
86
|
+
function ComboboxList({ creatable, query, onCreateClick, emptyLabel }: ListProps) {
|
|
77
87
|
const filtered = Combobox.useFilteredItems<ComboboxOption>();
|
|
78
88
|
const trimmed = query.trim();
|
|
79
89
|
const hasExact = filtered.some(
|
|
@@ -123,7 +133,7 @@ function ComboboxList({ creatable, query, onCreateClick }: ListProps) {
|
|
|
123
133
|
</Combobox.Item>
|
|
124
134
|
)}
|
|
125
135
|
<Combobox.Empty className='py-2 text-center text-sm text-muted-foreground'>
|
|
126
|
-
|
|
136
|
+
{emptyLabel}
|
|
127
137
|
</Combobox.Empty>
|
|
128
138
|
</Combobox.List>
|
|
129
139
|
);
|
|
@@ -148,6 +158,7 @@ export function ComboboxField(props: ComboboxFieldProps) {
|
|
|
148
158
|
options = [],
|
|
149
159
|
loadOptions,
|
|
150
160
|
creatable = false,
|
|
161
|
+
emptyLabel = 'No options found',
|
|
151
162
|
placeholder = 'Select…',
|
|
152
163
|
disabled,
|
|
153
164
|
id,
|
|
@@ -337,7 +348,12 @@ export function ComboboxField(props: ComboboxFieldProps) {
|
|
|
337
348
|
'data-[ending-style]:scale-95 data-[ending-style]:opacity-0',
|
|
338
349
|
)}
|
|
339
350
|
>
|
|
340
|
-
<ComboboxList
|
|
351
|
+
<ComboboxList
|
|
352
|
+
creatable={creatable}
|
|
353
|
+
query={query}
|
|
354
|
+
onCreateClick={handleCreate}
|
|
355
|
+
emptyLabel={emptyLabel}
|
|
356
|
+
/>
|
|
341
357
|
</Combobox.Popup>
|
|
342
358
|
</Combobox.Positioner>
|
|
343
359
|
</Combobox.Portal>
|