Skip to content

Commit a7eb061

Browse files
authored
feat(select): auto-render options from items prop when children omitted (#97)
1 parent 02d0d65 commit a7eb061

File tree

6 files changed

+72
-62
lines changed

6 files changed

+72
-62
lines changed

.changeset/sad-hawks-write.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/kumo": minor
3+
---
4+
5+
feat(select): auto-render options from items prop when children omitted
6+
7+
The Select component now automatically renders Select.Option elements from the items prop when children are not explicitly provided.

packages/kumo-docs-astro/src/components/demos/SelectDemo.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ export function SelectBasicDemo() {
1010
value={value}
1111
onValueChange={(v) => setValue(v ?? "apple")}
1212
items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
13-
>
14-
<Select.Option value="apple">Apple</Select.Option>
15-
<Select.Option value="banana">Banana</Select.Option>
16-
<Select.Option value="cherry">Cherry</Select.Option>
17-
</Select>
13+
/>
1814
);
1915
}
2016

@@ -31,11 +27,7 @@ export function SelectLabelValueDemo() {
3127
documentation: "Documentation",
3228
feature: "Feature",
3329
}}
34-
>
35-
<Select.Option value="bug">Bug</Select.Option>
36-
<Select.Option value="documentation">Documentation</Select.Option>
37-
<Select.Option value="feature">Feature</Select.Option>
38-
</Select>
30+
/>
3931
);
4032
}
4133

@@ -53,11 +45,7 @@ export function SelectPlaceholderDemo() {
5345
{ value: "documentation", label: "Documentation" },
5446
{ value: "feature", label: "Feature" },
5547
]}
56-
>
57-
<Select.Option value="bug">Bug</Select.Option>
58-
<Select.Option value="documentation">Documentation</Select.Option>
59-
<Select.Option value="feature">Feature</Select.Option>
60-
</Select>
48+
/>
6149
);
6250
}
6351

@@ -111,20 +99,23 @@ export function SelectLoadingDataDemo() {
11199
return () => clearTimeout(timer);
112100
}, []);
113101

102+
const items = data?.reduce(
103+
(acc, item) => {
104+
acc[item] = item;
105+
return acc;
106+
},
107+
{} as Record<string, string>,
108+
);
109+
114110
return (
115111
<Select
116112
className="w-[200px]"
117113
loading={loading}
118114
value={value}
119115
onValueChange={(v) => setValue(v as string | null)}
120116
placeholder="Please select"
121-
>
122-
{data?.map((item) => (
123-
<Select.Option key={item} value={item}>
124-
{item}
125-
</Select.Option>
126-
))}
127-
</Select>
117+
items={items}
118+
/>
128119
);
129120
}
130121

packages/kumo-docs-astro/src/pages/components/select.astro

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import {
2929
<p class="mb-4 flex flex-col gap-4">
3030
<span class="text-kumo-strong">
3131
A simple select component. Use the <code class="rounded bg-kumo-control px-1 py-0.5 text-sm">items</code> prop
32-
to map values to display labels — this controls what text appears in the
33-
trigger when an option is selected. Without <code class="rounded bg-kumo-control px-1 py-0.5 text-sm">items</code>,
34-
the raw value string is shown in the trigger.
32+
to provide options — when <code class="rounded bg-kumo-control px-1 py-0.5 text-sm">items</code> is provided without
33+
children, the select automatically renders options. This also controls what text appears in the
34+
trigger when an option is selected. For custom option rendering, you can still provide manual
35+
<code class="rounded bg-kumo-control px-1 py-0.5 text-sm">Select.Option</code> children.
3536
</span>
3637
</p>
3738

@@ -47,11 +48,7 @@ function App() {
4748
value={value}
4849
onValueChange={(v) => setValue(v ?? "apple")}
4950
items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
50-
>
51-
<Select.Option value="apple">Apple</Select.Option>
52-
<Select.Option value="banana">Banana</Select.Option>
53-
<Select.Option value="cherry">Cherry</Select.Option>
54-
</Select>
51+
/>
5552
)
5653
}`}
5754
>
@@ -87,11 +84,7 @@ function App() {
8784
documentation: "Documentation",
8885
feature: "Feature",
8986
}}
90-
>
91-
<Select.Option value="bug">Bug</Select.Option>
92-
<Select.Option value="documentation">Documentation</Select.Option>
93-
<Select.Option value="feature">Feature</Select.Option>
94-
</Select>
87+
/>
9588
)
9689
}
9790
@@ -109,11 +102,7 @@ function App() {
109102
{value: "documentation", label: "Documentation"},
110103
{value: "feature", label: "Feature"},
111104
]}
112-
>
113-
<Select.Option value="bug">Bug</Select.Option>
114-
<Select.Option value="documentation">Documentation</Select.Option>
115-
<Select.Option value="feature">Feature</Select.Option>
116-
</Select>
105+
/>
117106
)
118107
}`}
119108
>
@@ -151,11 +140,7 @@ function App() {
151140
{ value: "documentation", label: "Documentation" },
152141
{ value: "feature", label: "Feature" },
153142
]}
154-
>
155-
<Select.Option value="bug">Bug</Select.Option>
156-
<Select.Option value="documentation">Documentation</Select.Option>
157-
<Select.Option value="feature">Feature</Select.Option>
158-
</Select>
143+
/>
159144
)
160145
}`}
161146
>

packages/kumo/ai/component-registry.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,12 +3840,12 @@
38403840
}
38413841
},
38423842
"examples": [
3843-
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v ?? \"apple\")}\n items={{ apple: \"Apple\", banana: \"Banana\", cherry: \"Cherry\" }}\n >\n <Select.Option value=\"apple\">Apple</Select.Option>\n <Select.Option value=\"banana\">Banana</Select.Option>\n <Select.Option value=\"cherry\">Cherry</Select.Option>\n </Select>",
3844-
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v as string)}\n items={{\n bug: \"Bug\",\n documentation: \"Documentation\",\n feature: \"Feature\",\n }}\n >\n <Select.Option value=\"bug\">Bug</Select.Option>\n <Select.Option value=\"documentation\">Documentation</Select.Option>\n <Select.Option value=\"feature\">Feature</Select.Option>\n </Select>",
3845-
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v as string | null)}\n items={[\n { value: null, label: \"Please select\" },\n { value: \"bug\", label: \"Bug\" },\n { value: \"documentation\", label: \"Documentation\" },\n { value: \"feature\", label: \"Feature\" },\n ]}\n >\n <Select.Option value=\"bug\">Bug</Select.Option>\n <Select.Option value=\"documentation\">Documentation</Select.Option>\n <Select.Option value=\"feature\">Feature</Select.Option>\n </Select>",
3843+
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v ?? \"apple\")}\n items={{ apple: \"Apple\", banana: \"Banana\", cherry: \"Cherry\" }}\n />",
3844+
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v as string)}\n items={{\n bug: \"Bug\",\n documentation: \"Documentation\",\n feature: \"Feature\",\n }}\n />",
3845+
"<Select\n className=\"w-[200px]\"\n value={value}\n onValueChange={(v) => setValue(v as string | null)}\n items={[\n { value: null, label: \"Please select\" },\n { value: \"bug\", label: \"Bug\" },\n { value: \"documentation\", label: \"Documentation\" },\n { value: \"feature\", label: \"Feature\" },\n ]}\n />",
38463846
"<Select\n className=\"w-[200px]\"\n renderValue={(v) => (\n <span>\n {v.emoji} {v.label}\n </span>\n )}\n value={value}\n onValueChange={(v) => setValue(v as (typeof languages)[0])}\n >\n {languages.map((language) => (\n <Select.Option key={language.value} value={language}>\n {language.emoji} {language.label}\n </Select.Option>\n ))}\n </Select>",
38473847
"<Select className=\"w-[200px]\" loading />",
3848-
"<Select\n className=\"w-[200px]\"\n loading={loading}\n value={value}\n onValueChange={(v) => setValue(v as string | null)}\n placeholder=\"Please select\"\n >\n {data?.map((item) => (\n <Select.Option key={item} value={item}>\n {item}\n </Select.Option>\n ))}\n </Select>",
3848+
"<Select\n className=\"w-[200px]\"\n loading={loading}\n value={value}\n onValueChange={(v) => setValue(v as string | null)}\n placeholder=\"Please select\"\n items={items}\n />",
38493849
"<Select\n className=\"w-[250px]\"\n multiple\n renderValue={(value) => {\n if (value.length > 3) {\n return (\n <span className=\"line-clamp-1\">\n {value.slice(2).join(\", \") + ` and ${value.length - 2} more`}\n </span>\n );\n }\n return <span>{value.join(\", \")}</span>;\n }}\n value={value}\n onValueChange={(v) => setValue(v as string[])}\n >\n <Select.Option value=\"Name\">Name</Select.Option>\n <Select.Option value=\"Location\">Location</Select.Option>\n <Select.Option value=\"Size\">Size</Select.Option>\n <Select.Option value=\"Read\">Read</Select.Option>\n <Select.Option value=\"Write\">Write</Select.Option>\n <Select.Option value=\"CreatedAt\">Created At</Select.Option>\n </Select>",
38503850
"<Select\n className=\"w-[200px]\"\n onValueChange={(v) => setValue(v as (typeof authors)[0] | null)}\n value={value}\n isItemEqualToValue={(item, value) => item?.id === value?.id}\n renderValue={(author) => {\n return author?.name ?? \"Please select author\";\n }}\n >\n {authors.map((author) => (\n <Select.Option key={author.id} value={author}>\n <div className=\"flex w-[300px] items-center justify-between gap-2\">\n <Text>{author.name}</Text>\n <Text variant=\"secondary\">{author.title}</Text>\n </div>\n </Select.Option>\n ))}\n </Select>"
38513851
],

packages/kumo/ai/component-registry.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3839,11 +3839,7 @@ Option sub-component
38393839
value={value}
38403840
onValueChange={(v) => setValue(v ?? "apple")}
38413841
items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
3842-
>
3843-
<Select.Option value="apple">Apple</Select.Option>
3844-
<Select.Option value="banana">Banana</Select.Option>
3845-
<Select.Option value="cherry">Cherry</Select.Option>
3846-
</Select>
3842+
/>
38473843
```
38483844

38493845
```tsx
@@ -3876,13 +3872,8 @@ Option sub-component
38763872
value={value}
38773873
onValueChange={(v) => setValue(v as string | null)}
38783874
placeholder="Please select"
3879-
>
3880-
{data?.map((item) => (
3881-
<Select.Option key={item} value={item}>
3882-
{item}
3883-
</Select.Option>
3884-
))}
3885-
</Select>
3875+
items={items}
3876+
/>
38863877
```
38873878

38883879
```tsx

packages/kumo/src/components/select/select.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,38 @@ export function selectVariants(_props: KumoSelectVariantsProps = {}) {
6363
);
6464
}
6565

66+
/**
67+
* Auto-generates Select.Option elements from items prop.
68+
* Only used when children are not explicitly provided.
69+
* Filters out null values (typically used for placeholders).
70+
*/
71+
function renderOptionsFromItems<T>(
72+
items:
73+
| Record<string, ReactNode>
74+
| ReadonlyArray<{ label: ReactNode; value: T }>,
75+
): ReactNode {
76+
// Handle object map format: { key: "Label" }
77+
if (!Array.isArray(items)) {
78+
return Object.entries(items).map(([key, label]) => (
79+
<Option key={key} value={key as T}>
80+
{label}
81+
</Option>
82+
));
83+
}
84+
85+
// Handle array format: [{ value, label }] - filter out null values
86+
return items
87+
.filter((item) => item.value !== null)
88+
.map((item, index) => (
89+
<Option
90+
key={typeof item.value === "string" ? item.value : `option-${index}`}
91+
value={item.value}
92+
>
93+
{item.label}
94+
</Option>
95+
));
96+
}
97+
6698
type SelectPropsGeneric<
6799
T,
68100
Multiple extends boolean | undefined = false,
@@ -207,6 +239,10 @@ export function Select<T, Multiple extends boolean | undefined = false>({
207239
}
208240
}
209241

242+
// Auto-render children from items if children not provided
243+
const renderedChildren =
244+
children ?? (items ? renderOptionsFromItems(items) : null);
245+
210246
const selectControl = (
211247
<SelectBase.Root
212248
{...props}
@@ -243,7 +279,7 @@ export function Select<T, Multiple extends boolean | undefined = false>({
243279
"min-w-[calc(var(--anchor-width)+3px)] p-1.5", // spacing
244280
)}
245281
>
246-
{children}
282+
{renderedChildren}
247283
</SelectBase.Popup>
248284
</SelectBase.Positioner>
249285
</SelectBase.Portal>

0 commit comments

Comments
 (0)