Select Component Stress Test

Comprehensive tests for Select component edge cases, TypeScript inference, and API interactions. Used to verify fixes for the strictNullChecks bug and renderValue/placeholder behavior.

1. strictNullChecks Bug

value={objectOrNull} should not cause T to infer as never. The onValueChange callback should receive User | null.

Select User (Object | Null)
Selected: None
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>

2. renderValue + placeholder

renderValue is only called with non-null values. The placeholder shows when no value is selected.

Select Country
Status: No selection (placeholder should be visible above)
<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>

3. items Array Prop

Using items prop with array of { label, value } objects. Auto-generates Select.Option elements.

Priority (items array)
Selected: None
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>
  )}
/>

4. items Object Map

Using items prop with object map. Supports SelectItemDescriptor for disabled options.

Status (object map)
Selected: None
<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
  }}
/>

5. Multiple Selection

multiple mode with object values. renderValue receives T[] instead of T.

Select Users (Multiple)
Selected (0): None
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>

6. Controlled vs Uncontrolled

Toggle between controlled (value) and uncontrolled (defaultValue) modes.

Controlled Select
Controlled value: None
// 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" }}
/>

7. Groups and Separators

Using compound components for structured option lists.

Grouped Options
Selected: None
<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>

8. Field Integration

Auto-wraps with Field component when label is provided.

Required Field

Please select an option from the list.

Value: None | Valid: No
<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" }}
/>

9. Size Variants

All four sizes match Input component sizing.

Extra Small (xs)
Small (sm)
Base (default)
Large (lg)
<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)

10. Loading State

Shows skeleton and disables interaction while loading.

Data Loading
const [loading, setLoading] = useState(true);

<Select
  label="Data Loading"
  placeholder="Select when loaded..."
  loading={loading}  // Shows SkeletonLine, disables trigger
  items={{ ... }}
/>

11. Disabled Options

Individual options can be disabled via SelectItemDescriptor or disabled prop on Option.

With Disabled Options
Selected: None
// 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>

12. Type Inference (No Explicit Generic)

TypeScript infers types from usage when possible.

Inferred Types
Inferred value: None
// 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>

Test Coverage Summary