Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/few-bears-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": minor
---

Add `Select.Group` and `Select.GroupLabel` compound APIs so consumers can render grouped select options without importing Base UI primitives directly.
48 changes: 48 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/SelectDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* [INPUT]: 依赖 react 状态与 @cloudflare/kumo 的 Select/Text/Badge 组件,提供 docs 页面示例所需的交互数据。
* [OUTPUT]: 对外提供 Select 各种 demo 组件,包括 grouped items、多选溢出与自定义展示示例。
* [POS]: kumo-docs-astro 中 Select 文档的示例源,供页面展示与 demo metadata codegen 消费。
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
*/
import { useState, useEffect } from "react";
import { Select, Text, Badge } from "@cloudflare/kumo";

Expand Down Expand Up @@ -269,6 +275,48 @@ export function SelectMultipleWithIndicatorDemo() {
);
}

const groupedDevices = [
{
category: "iPhone",
items: ["iPhone 16 Pro", "iPhone 16", "iPhone 15"],
},
{
category: "Android",
items: ["Pixel 9 Pro", "Galaxy S25", "Xiaomi 15"],
},
{
category: "iPad",
items: ["iPad Pro 13", "iPad Air 11"],
},
] as const;

const shouldShowGroupLabel = groupedDevices.length > 1;

export function SelectGroupedDemo() {
const [value, setValue] = useState<string>(groupedDevices[0].items[0]);

return (
<Select
className="w-[240px]"
value={value}
onValueChange={(v) => setValue(v as string)}
>
{groupedDevices.map((group) => (
<Select.Group key={group.category}>
{shouldShowGroupLabel && (
<Select.GroupLabel>{group.category}</Select.GroupLabel>
)}
{group.items.map((device) => (
<Select.Option key={device} value={device}>
{device}
</Select.Option>
))}
</Select.Group>
))}
</Select>
);
}

const authors = [
{ id: 1, name: "John Doe", title: "Programmer" },
{ id: 2, name: "Alice Smith", title: "Software Engineer" },
Expand Down
51 changes: 51 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
SelectLoadingDemo,
SelectLoadingDataDemo,
SelectMultipleDemo,
SelectMultipleOverflowDemo,
SelectMultipleWithIndicatorDemo,
SelectGroupedDemo,
SelectComplexDemo,
} from "~/components/demos/SelectDemo";

Expand Down Expand Up @@ -218,6 +221,54 @@ export default function Example() {
<ComponentExample demo="SelectMultipleDemo">
<SelectMultipleDemo client:load />
</ComponentExample>

<p>
Long option names are automatically truncated so the trigger layout stays
intact. For a clearer summary, prefer a custom{" "}
<code class="rounded bg-kumo-control px-1 py-0.5 text-sm">
renderValue
</code>{" "}
instead of relying on truncation alone.
</p>

<ComponentExample demo="SelectMultipleOverflowDemo">
<SelectMultipleOverflowDemo client:load />
</ComponentExample>
</ComponentSection>

<ComponentSection>
<Heading level={3}>Multiple with Custom Indicator</Heading>
<p>
Use{" "}
<code class="rounded bg-kumo-control px-1 py-0.5 text-sm">
renderValue
</code>{" "}
to show a stable summary for multi-select values. This pattern keeps the
trigger readable even when the selected labels are long.
</p>

<ComponentExample demo="SelectMultipleWithIndicatorDemo">
<SelectMultipleWithIndicatorDemo client:load />
</ComponentExample>
</ComponentSection>

<ComponentSection>
<Heading level={3}>Grouped Items</Heading>
<p>
Use{" "}
<code class="rounded bg-kumo-control px-1 py-0.5 text-sm">
Select.Group
</code>{" "}
and{" "}
<code class="rounded bg-kumo-control px-1 py-0.5 text-sm">
Select.GroupLabel
</code>{" "}
to organize large option sets without reaching into Base UI primitives.
</p>

<ComponentExample demo="SelectGroupedDemo">
<SelectGroupedDemo client:load />
</ComponentExample>
</ComponentSection>

{/* More Example */}
Expand Down
30 changes: 30 additions & 0 deletions packages/kumo/src/components/select/select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* [INPUT]: 依赖 vitest 与 testing-library/react 渲染 Select 组件,覆盖 label、placeholder 与 grouped API 行为。
* [OUTPUT]: 对外提供 Select 单元测试,验证可见标签、占位符与 compound group API。
* [POS]: components/select 的行为回归保护网,防止上游同步或重构破坏公开交互。
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
*/
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { useState } from "react";
Expand Down Expand Up @@ -290,4 +296,28 @@ describe("Select", () => {
expect(screen.getByText("Choose a database")).toBeTruthy();
});
});

describe("compound group API", () => {
it("exposes Group and GroupLabel compound components", () => {
expect(Select.Group).toBeDefined();
expect(Select.GroupLabel).toBeDefined();
});

it("renders grouped options via Select compound API", () => {
expect(() =>
render(
<Select defaultValue="iphone-16">
<Select.Group>
<Select.GroupLabel>iPhone</Select.GroupLabel>
<Select.Option value="iphone-16">iPhone 16</Select.Option>
</Select.Group>
<Select.Group>
<Select.GroupLabel>Android</Select.GroupLabel>
<Select.Option value="pixel-9">Pixel 9</Select.Option>
</Select.Group>
</Select>,
),
).not.toThrow();
});
});
});
31 changes: 31 additions & 0 deletions packages/kumo/src/components/select/select.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* [INPUT]: 依赖 @base-ui/react/select 的选择器原语,依赖 field/button/loader/cn 提供表单包装、样式与加载态。
* [OUTPUT]: 对外提供 Select 组件、Select.Option、Select.Group、Select.GroupLabel 以及 Select 样式元数据。
* [POS]: components/select 的核心实现,承接 docs、registry 与消费方的选择器 API。
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
*/
import { Select as SelectBase } from "@base-ui/react/select";
import { CaretUpDownIcon, CheckIcon } from "@phosphor-icons/react";
import { useId } from "react";
Expand Down Expand Up @@ -379,6 +385,29 @@ type OptionProps<T> = {
value: T;
};

type GroupProps = SelectBase.Group.Props;

function Group({ className, ...props }: GroupProps & { className?: string }) {
return <SelectBase.Group {...props} className={className} />;
}

type GroupLabelProps = SelectBase.GroupLabel.Props;

function GroupLabel({
className,
...props
}: GroupLabelProps & { className?: string }) {
return (
<SelectBase.GroupLabel
{...props}
className={cn(
"select-none px-2 py-1.5 text-base font-medium text-kumo-subtle",
className,
)}
/>
);
}

function Option<T>({ children, value }: OptionProps<T>) {
return (
<SelectBase.Item
Expand All @@ -394,3 +423,5 @@ function Option<T>({ children, value }: OptionProps<T>) {
}

Select.Option = Option;
Select.Group = Group;
Select.GroupLabel = GroupLabel;