Skip to content
Open
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
75 changes: 73 additions & 2 deletions apps/www/src/content/docs/components/combobox/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,54 @@ export const groupDemo = {
<Combobox.Input placeholder="Search items" width={240} />
<Combobox.Content>
<Combobox.Group>
<Combobox.Label>Fruits</Combobox.Label>
<Combobox.GroupLabel>Fruits</Combobox.GroupLabel>
<Combobox.Item value="apple">Apple</Combobox.Item>
<Combobox.Item value="banana">Banana</Combobox.Item>
</Combobox.Group>
<Combobox.Separator />
<Combobox.Group>
<Combobox.Label>Vegetables</Combobox.Label>
<Combobox.GroupLabel>Vegetables</Combobox.GroupLabel>
<Combobox.Item value="carrot">Carrot</Combobox.Item>
<Combobox.Item value="broccoli">Broccoli</Combobox.Item>
</Combobox.Group>
</Combobox.Content>
</Combobox>`
};

export const labelDemo = {
type: 'code',
code: `
<Combobox multiple>
<Flex direction="column" gap={2}>
<Combobox.Label>Fruits</Combobox.Label>
<Flex align="center" gap={2}>
<Combobox.Input placeholder="Select fruits" width={300} />
<Combobox.Clear />
</Flex>
</Flex>
<Combobox.Content>
<Combobox.Item>Apple</Combobox.Item>
<Combobox.Item>Banana</Combobox.Item>
<Combobox.Item>Grape</Combobox.Item>
<Combobox.Item>Orange</Combobox.Item>
</Combobox.Content>
</Combobox>`
};

export const emptyDemo = {
type: 'code',
code: `
<Combobox>
<Combobox.Input placeholder="Type 'xyz'" width={240} />
<Combobox.Content>
<Combobox.Empty>No fruit matches your search</Combobox.Empty>
<Combobox.Item>Apple</Combobox.Item>
<Combobox.Item>Banana</Combobox.Item>
<Combobox.Item>Grape</Combobox.Item>
</Combobox.Content>
</Combobox>`
};

export const iconDemo = {
type: 'code',
code: `
Expand Down Expand Up @@ -149,3 +183,40 @@ export const controlledDemo = {
);
}`
};

export const createItemsDemo = {
type: 'code',
code: `
function CreateItemsDemo() {
const users = [
{ id: "u1", name: "Jane Doe" },
{ id: "u2", name: "John Smith" },
{ id: "u3", name: "Ada Lovelace" }
];
const items = React.useMemo(
() =>
Combobox.createItems(users, {
getValue: user => user.id,
getLabel: user => user.name
}),
[]
);
const [value, setValue] = React.useState(null);

return (
<Flex direction="column" gap={5}>
<Text>Selected ID: {value || "None"}</Text>
<Combobox items={items} value={value} onValueChange={setValue}>
<Combobox.Input placeholder="Pick a user" />
<Combobox.Content>
{user => (
<Combobox.Item key={user.id} value={user.id}>
{user.name}
</Combobox.Item>
)}
</Combobox.Content>
</Combobox>
</Flex>
);
}`
};
85 changes: 78 additions & 7 deletions apps/www/src/content/docs/components/combobox/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
iconDemo,
withFieldDemo,
controlledDemo,
createItemsDemo,
labelDemo,
emptyDemo,
} from "./demo.ts";

<Demo data={playground} />
Expand All @@ -24,8 +27,18 @@ Import and assemble the component:
import { Combobox } from "@raystack/apsara";

<Combobox>
<Combobox.Label />
<Combobox.Input />
<Combobox.Content />
<Combobox.Clear />
<Combobox.Content>
<Combobox.Empty />
<Combobox.Status />
<Combobox.Group>
<Combobox.GroupLabel />
<Combobox.Item />
</Combobox.Group>
<Combobox.Separator />
</Combobox.Content>
</Combobox>
```

Expand Down Expand Up @@ -53,22 +66,44 @@ Wrap the Combobox with [Field](/docs/components/field) to add a label, descripti

### Groups and separators

Organize items into groups with labels and visual separators. Groups and labels are automatically hidden when the user is searching.
Organize items into groups with `Combobox.GroupLabel` and separators. Groups, group labels, and separators are hidden while the user is searching.

<Demo data={groupDemo} />

### Multiple selection

Pass the `multiple` prop to enable multi-select. Selected values appear as chips in the input. Items display checkboxes in multiple mode.

The input shows the first two chips and a "+N" count for the rest. Press Backspace in an empty input to remove the last value. Press Left Arrow at the start of the input to move focus to the chips. On a focused chip, press Backspace or Delete to remove it.

<Demo data={multipleDemo} />

### Label and clear button

`Combobox.Label` renders a visible label linked to the input. `Combobox.Clear` renders a button that clears the value. It shows only when there is a value or input text. Place both anywhere inside `Combobox`.

<Demo data={labelDemo} />

### Empty state

`Combobox.Content` shows "No results" when no item matches the input. To change the text, add `Combobox.Empty` as a direct child of `Combobox.Content`.

<Demo data={emptyDemo} />

### Controlled

Use `value` and `onValueChange` for controlled behavior.

<Demo data={controlledDemo} />

### Using IDs as values

When your app stores IDs instead of item objects, wrap the data with `Combobox.createItems`. `getValue` returns the value that `value` and `onValueChange` use. `getLabel` returns the text shown in the input and used for filtering. Pass a function as the child of `Combobox.Content` to render each item that matches the input.

Create the collection at module scope for static data. For data that changes, wrap it in `React.useMemo` keyed by the data.

<Demo data={createItemsDemo} />

## API Reference

Root and input first, then the parts that make up the popup list.
Expand Down Expand Up @@ -101,17 +136,41 @@ When no `value` prop is provided, the text content of `children` is used as the

<auto-type-table path="./props.ts" name="ComboboxItemProps" />

### Label Props

A visible label for the input. It renders a `<label>` whose `htmlFor` is the input `id`. Inside a [Field](/docs/components/field), use the Field label instead.

<auto-type-table path="./props.ts" name="ComboboxLabelProps" />

### Clear Props

A button that clears the value and the input text. It renders [IconButton](/docs/components/icon-button) and shows only when there is something to clear.

<auto-type-table path="./props.ts" name="ComboboxClearProps" />

### Empty Props

The text shown when no item matches the input. `Combobox.Content` renders one with "No results" by default. Add `Combobox.Empty` as a direct child of `Combobox.Content` to change the text. Pass `null` as children to show nothing.

<auto-type-table path="./props.ts" name="ComboboxEmptyProps" />

### Status Props

A visually hidden live region. Screen readers announce its text when it changes. `Combobox.Content` renders one by default that announces the number of matching items, for example "3 results". Add `Combobox.Status` as a direct child of `Combobox.Content` to change the text, for example to announce loading.

<auto-type-table path="./props.ts" name="ComboboxStatusProps" />

### Group Props

A way to group related combobox items together.

<auto-type-table path="./props.ts" name="ComboboxGroupProps" />

### Label Props
### GroupLabel Props

Renders a label in a combobox group. This component should be wrapped with `Combobox.Group`.
Renders a label in a combobox group. Wrap it in `Combobox.Group`.

<auto-type-table path="./props.ts" name="ComboboxLabelProps" />
<auto-type-table path="./props.ts" name="ComboboxGroupLabelProps" />

### Separator Props

Expand All @@ -125,12 +184,20 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test

| Slot | Element |
|------|---------|
| `combobox-label` | The label for the input |
| `combobox-input` | The text input that opens the popup |
| `combobox-chips` | The container for chips and the input in multiple mode |
| `combobox-chip` | A selected value in multiple mode |
| `combobox-chip-remove` | The button that removes a chip |
| `combobox-chip-overflow` | The "+N" count of chips that are not shown |
| `combobox-clear` | The button that clears the value |
| `combobox-positioner` | Wrapper that positions the popup |
| `combobox-content` | The popup container |
| `combobox-status` | The hidden live region that announces the number of results |
| `combobox-empty` | The text shown when no item matches |
| `combobox-list` | The scrollable list of items |
| `combobox-group` | A group of related items |
| `combobox-label` | The label for a group |
| `combobox-group-label` | The label for a group |
| `combobox-separator` | The divider between items or groups |
| `combobox-item` | A selectable option row |
| `combobox-item-text` | The text label inside an item (when its children is a string) |
Expand All @@ -147,7 +214,11 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test
the whole time, so typing to filter never loses the user's place.
- In multiple mode, each item renders a checkbox that mirrors its selected
state, and selected values appear in the input as removable chips.
- The chips use Base UI's `Combobox.Chips`, which has `role="toolbar"`. Arrow
keys move between chips, and Backspace or Delete removes the focused chip.
- `Combobox.Empty` and `Combobox.Status` are polite live regions. Screen
readers announce "No results" and the number of matching items.
- Group labels use Base UI's `Combobox.GroupLabel`, which links the label to
its group via `aria-labelledby`, so wrap `Combobox.Label` in `Combobox.Group`.
its group via `aria-labelledby`, so wrap `Combobox.GroupLabel` in `Combobox.Group`.
- Wrap the Combobox with [Field](/docs/components/field) to associate a
visible label, description, and error message with the input.
77 changes: 77 additions & 0 deletions apps/www/src/content/docs/components/combobox/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export interface ComboboxRootProps {
* @default false
*/
modal?: boolean;

/**
* The items to filter. Accepts an array of items or a collection from `Combobox.createItems`.
* When set, the root filters the items and `Combobox.Content` can render them with a function child.
*
* @remarks `unknown[] | ReturnType<typeof Combobox.createItems>`
*/
items?: unknown[];

/** The `id` of the input. `Combobox.Label` uses it. */
id?: string;
}

export interface ComboboxInputProps {
Expand Down Expand Up @@ -96,6 +107,13 @@ export interface ComboboxContentProps {
* @defaultValue The theme's `radius`
*/
radius?: 'none' | 'small' | 'medium' | 'large' | 'full';

/**
* The list content. Pass a function to render each item that matches the input when `items` is set on the root.
*/
children?:
| React.ReactNode
| ((item: unknown, index: number) => React.ReactNode);
}

export interface ComboboxItemProps {
Expand All @@ -120,6 +138,65 @@ export interface ComboboxGroupProps {
}

export interface ComboboxLabelProps {
/** Shows `optionalText` after the label when `false`. */
required?: boolean;

/**
* Text shown after the label when `required` is `false`.
* @default "(optional)"
*/
optionalText?: string;

/** Additional CSS class names. */
className?: string;
}

export interface ComboboxClearProps {
/**
* Size of the button.
* @default 2
*/
size?: 1 | 2 | 3 | 4;

/**
* Accessible name of the button.
* @default "Clear"
*/
'aria-label'?: string;

/**
* The icon inside the button.
* @default <ClearIcon />
*/
children?: React.ReactNode;

/** Additional CSS class names. */
className?: string;
}

export interface ComboboxEmptyProps {
/**
* The text shown when no item matches.
* @default "No results"
*/
children?: React.ReactNode;

/** Additional CSS class names. */
className?: string;
}

export interface ComboboxStatusProps {
/**
* The text to announce.
* @defaultValue The number of matching items, for example "3 results"
*/
children?: React.ReactNode;

/** Additional CSS class names. */
className?: string;
}

export interface ComboboxGroupLabelProps {
/** Additional CSS class names. */
className?: string;
}
Expand Down
Loading
Loading