Comprehensive tests for Select component edge cases, TypeScript inference, and API interactions. Used to verify fixes for the strictNullChecks bug and renderValue/placeholder behavior.
value={objectOrNull} should not cause T to infer as never.
The onValueChange callback should receive User | null.
const [selected, setSelected] = useState<User | null>(null);
<Select<User>
label="Select User"
placeholder="Choose a user..."
value={selected}
onValueChange={(value) => setSelected(value)} // value: User | null
renderValue={(user) => <span>{user.name}</span>}
>
{users.map((user) => (
<Select.Option key={user.id} value={user}>
{user.name}
</Select.Option>
))}
</Select> renderValue is only called with non-null values.
The placeholder shows when no value is selected.
<Select<Country>
label="Select Country"
placeholder="Pick a country..." // Shows when value is null
value={country}
onValueChange={setCountry}
renderValue={(c) => ( // Only called when c is non-null
<span>{c.flag} {c.name}</span>
)}
>
{countries.map((c) => (
<Select.Option key={c.code} value={c}>
{c.flag} {c.name}
</Select.Option>
))}
</Select>
Using items prop with array of { label, value } objects.
Auto-generates Select.Option elements.
const priorities: Priority[] = [
{ level: 1, name: "Critical", color: "text-kumo-danger" },
{ level: 2, name: "High", color: "text-kumo-warning" },
// ...
];
<Select<Priority>
label="Priority"
placeholder="Select priority..."
value={priority}
onValueChange={setPriority}
items={priorities.map((p) => ({
label: <span className={p.color}>[{p.level}] {p.name}</span>,
value: p,
}))}
renderValue={(p) => (
<span className={p.color}>[{p.level}] {p.name}</span>
)}
/>
Using items prop with object map.
Supports SelectItemDescriptor for disabled options.
<Select
label="Status"
placeholder="Select status..."
value={status}
onValueChange={setStatus}
items={{
active: "Active", // Simple string label
pending: "Pending",
inactive: "Inactive",
archived: { label: "Archived", disabled: true }, // SelectItemDescriptor
}}
/> multiple mode with object values.
renderValue receives T[] instead of T.
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
<Select<User, true> // Note: second generic is true
label="Select Users"
placeholder="Choose users..."
multiple // Enable multi-select
value={selectedUsers} // T[] not T
onValueChange={setSelectedUsers} // (users: User[]) => void
renderValue={(users) => ( // users is User[]
<span>{users.map((u) => u.name).join(", ")}</span>
)}
>
{users.map((user) => (
<Select.Option key={user.id} value={user}>
{user.name}
</Select.Option>
))}
</Select>
Toggle between controlled (value) and uncontrolled (defaultValue) modes.
// Controlled: you manage the state
const [value, setValue] = useState<string | null>(null);
<Select
value={value}
onValueChange={setValue}
items={{ apple: "Apple", banana: "Banana" }}
/>
// Uncontrolled: component manages state internally
<Select
defaultValue="banana"
items={{ apple: "Apple", banana: "Banana" }}
/> Using compound components for structured option lists.
<Select label="Category" placeholder="Select...">
<Select.Group>
<Select.GroupLabel>Fruits</Select.GroupLabel>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="banana">Banana</Select.Option>
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.GroupLabel>Vegetables</Select.GroupLabel>
<Select.Option value="carrot">Carrot</Select.Option>
<Select.Option value="broccoli">Broccoli</Select.Option>
</Select.Group>
</Select>
Auto-wraps with Field component when label is provided.
Please select an option from the list.
<Select
label="Required Field"
placeholder="This field is required..."
required // Shows required indicator
value={value}
onValueChange={setValue}
description="Helper text below" // Shows description
error={!value ? "Required" : undefined} // Shows error message
labelTooltip="Tooltip on hover" // Info icon with tooltip
items={{ a: "Option A", b: "Option B" }}
/> All four sizes match Input component sizing.
<Select size="xs" ... /> // Extra small: h-5 (20px) <Select size="sm" ... /> // Small: h-6.5 (26px) <Select size="base" ... /> // Default: h-9 (36px) <Select size="lg" ... /> // Large: h-10 (40px)
Shows skeleton and disables interaction while loading.
const [loading, setLoading] = useState(true);
<Select
label="Data Loading"
placeholder="Select when loaded..."
loading={loading} // Shows SkeletonLine, disables trigger
items={{ ... }}
/>
Individual options can be disabled via SelectItemDescriptor or disabled prop on Option.
// Via items prop with SelectItemDescriptor
<Select
items={{
available: "Available",
disabled: { label: "Disabled", disabled: true },
}}
/>
// Via Select.Option prop
<Select>
<Select.Option value="available">Available</Select.Option>
<Select.Option value="disabled" disabled>Disabled</Select.Option>
</Select> TypeScript infers types from usage when possible.
// No explicit <T> - inferred from items/value
const [value, setValue] = useState<string | null>(null);
<Select
value={value}
onValueChange={(v) => setValue(v)} // v inferred as string | null
items={{ foo: "Foo", bar: "Bar" }}
/>
// With explicit generic for complex types
<Select<User>
value={user}
onValueChange={setUser}
renderValue={(u) => u.name} // u is User, not unknown
>
...
</Select>