();
+ return (
+ <>
+ {
+ setOptions(queryEmojiData(query));
+ }}
+ onChange={(result: EmojiDatum | null) => setEmoji(result)}
+ optionValue="name"
+ optionLabel="name"
+ placeholder="Search an emojiโฆ"
+ itemComponent={(props: any) => (
+
+ {props.item.rawValue.emoji}
+
+ )}
+ class={style.search__root_cmdk}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ ๐ฌ No emoji found
+
+
+
+
+
+ Emoji selected: {emoji()?.emoji} {emoji()?.name}
+
+ >
+ );
+}
diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx
index dc9f20f8..a6c5807b 100644
--- a/apps/docs/src/routes/docs/core.tsx
+++ b/apps/docs/src/routes/docs/core.tsx
@@ -160,6 +160,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
title: "Radio Group",
href: "/docs/core/components/radio-group",
},
+ {
+ title: "Search",
+ href: "/docs/core/components/search",
+ status: "new",
+ },
{
title: "Select",
href: "/docs/core/components/select",
diff --git a/apps/docs/src/routes/docs/core/components/combobox.mdx b/apps/docs/src/routes/docs/core/components/combobox.mdx
index 2f347007..7f877fe8 100644
--- a/apps/docs/src/routes/docs/core/components/combobox.mdx
+++ b/apps/docs/src/routes/docs/core/components/combobox.mdx
@@ -935,6 +935,7 @@ We expose a CSS custom property `--kb-combobox-content-transform-origin` which c
| readOnly | `boolean`
Whether the combobox items can be selected but not changed by the user. |
| autoComplete | `string`
Describes the type of autocomplete functionality the input should provide if any. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete) |
| translations | [`ComboboxIntlTranslations`](https://github.com/kobaltedev/kobalte/blob/main/packages/core/src/combobox/combobox.intl.ts)
Localization strings. |
+| noResetInputOnBlur | `boolean`
Prevents input reset on combobox blur when content is displayed. |
`Combobox` also accepts the following props to customize the placement of the `Combobox.Content`.
@@ -1069,4 +1070,4 @@ We expose a CSS custom property `--kb-combobox-content-transform-origin` which c
| Alt + ArrowUp | When focus is on the input, closes the combobox. |
| Home | When focus is on the input, moves virtual focus to first item. |
| End | When focus is on the input, moves virtual focus to last item. |
-| Esc | When the combobox is open, closes the combobox.
When the combobox is closed, clear the input and selection. |
+| Esc | When the combobox is open, closes the combobox.
When the combobox is closed, clear the input and selection unless `noResetInputOnBlur` is set to `true`. |
diff --git a/apps/docs/src/routes/docs/core/components/search.mdx b/apps/docs/src/routes/docs/core/components/search.mdx
new file mode 100644
index 00000000..26373664
--- /dev/null
+++ b/apps/docs/src/routes/docs/core/components/search.mdx
@@ -0,0 +1,582 @@
+import { Preview, TabsSnippets, Kbd, Callout } from "../../../../components";
+import { BasicExample, DebounceExample, CmdkStyleExample } from "../../../../examples/search";
+
+# Search
+
+Search a searchbox text input with a menu.
+Handle the case where dataset filtering needs to occur outside the combobox component.
+
+## Import
+
+```ts
+import { Search } from "@kobalte/core/search";
+// or
+import { Root, Label, ... } from "@kobalte/core/search";
+```
+
+## Features
+
+- Inherits all the features of [combobox](/docs/core/components/combobox), except result filtering which should be managed externally.
+- Debouncing text input to rate limit search suggestions calls.
+- Optional indicator to show when suggestions are loading.
+
+## Anatomy
+
+The search consists of:
+
+- **Search:** The root container for a search component.
+- **Search.Label:** The label that gives the user information on the search component.
+- **Search.Description:** The description that gives the user more information on the component.
+- **Search.Control:** Contains the search input and indicator.
+- **Search.Indicator:** Wrapper for icon to indicate loading status.
+- **Search.Icon:** A small icon often displayed next to the input as a visual affordance for the fact it can be open.
+- **Search.Input:** The input used to search and reflects the selected suggestion values.
+- **Search.Portal:** Portals its children into the `body` when the search is open.
+- **Search.Content:** Contains the content to be rendered when the search is open.
+- **Search.Arrow:** An optional arrow element to render alongside the search content.
+- **Search.Listbox:** Contains a list of items and allows a user to search one or more of them.
+- **Search.Section:** Used to render the label of an option group. It won't be focusable using arrow keys.
+- **Search.Item:** An item of the search suggestion.
+- **Search.ItemLabel:** An accessible label to be announced for the item.
+- **Search.ItemDescription:** An optional accessible description to be announced for the item.
+- **Search.NoResult:** Displayed when no suggestion options are given.
+
+```tsx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Example
+
+
+
+
+
+
+
+ index.tsx
+ style.css
+
+ {/* */}
+
+ ```tsx
+ import { Search } from "@kobalte/core/search";
+ import { MagnifyingGlassIcon, ReloadIcon } from "some-icon-library";
+ import { createSignal } from "solid-js";
+ import "./style.css";
+
+ import { queryEmojiData } from "your-search-function";
+
+ function App() {
+ const [options, setOptions] = createSignal([]);
+ const [emoji, setEmoji] = createSignal();
+ return (
+ <>
+ setOptions(queryEmojiData(query))}
+ onChange={result => setEmoji(result)}
+ optionValue="name"
+ optionLabel="name"
+ placeholder="Search an emojiโฆ"
+ itemComponent={props => (
+
+ {props.item.rawValue.emoji}
+
+ )}
+ >
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ e.preventDefault()}>
+
+
+ ๐ฌ No emoji found
+
+
+
+
+
+ Emoji selected: {emoji()?.emoji} {emoji()?.name}
+
+ >
+ )
+ }
+ ```
+
+
+
+ ```css
+ .search__control {
+ display: inline-flex;
+ justify-content: space-between;
+ width: 240px;
+ border-radius: 6px;
+ font-size: 16px;
+ line-height: 1;
+ outline: none;
+ background-color: white;
+ border: 1px solid hsl(240 6% 90%);
+ color: hsl(240 4% 16%);
+ transition:
+ border-color 250ms,
+ color 250ms;
+ }
+ .search__control[data-invalid] {
+ border-color: hsl(0 72% 51%);
+ color: hsl(0 72% 51%);
+ }
+ .search__control_multi {
+ width: 100%;
+ min-width: 240px;
+ max-width: 300px;
+ }
+ .search__input {
+ appearance: none;
+ display: inline-flex;
+ width: 100%;
+ min-height: 40px;
+ padding-left: 16px;
+ font-size: 16px;
+ background: transparent;
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ outline: none;
+ }
+ .search__input::placeholder {
+ color: hsl(240 4% 46%);
+ }
+ .search__indicator {
+ appearance: none;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ width: auto;
+ outline: none;
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ padding: 0 10px;
+ background-color: hsl(240 5% 96%);
+ border-right: 1px solid hsl(240 6% 90%);
+ color: hsl(240 4% 16%);
+ font-size: 16px;
+ line-height: 0;
+ transition: 250ms background-color;
+ }
+ .search__icon {
+ height: 20px;
+ width: 20px;
+ flex: 0 0 16px;
+ display: grid;
+ justify-items: center;
+ }
+ .load__icon {
+ height: 20px;
+ width: 20px;
+ display: grid;
+ justify-items: center;
+ flex: 0 0 14px;
+ }
+ .center__icon {
+ margin: auto;
+ }
+ .spin__icon {
+ animation: spin 600ms linear;
+ animation-iteration-count: infinite;
+ margin: auto;
+ }
+ .search__description {
+ margin-top: 8px;
+ color: hsl(240 5% 26%);
+ font-size: 12px;
+ user-select: none;
+ }
+ .search__content {
+ background-color: white;
+ border-radius: 6px;
+ border: 1px solid hsl(240 6% 90%);
+ box-shadow:
+ 0 4px 6px -1px rgb(0 0 0 / 0.1),
+ 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ transform-origin: var(--kb-search-content-transform-origin);
+ animation: contentHide 250ms ease-in forwards;
+ }
+ .search__content[data-expanded] {
+ animation: contentShow 250ms ease-out;
+ }
+ .search__listbox {
+ overflow-y: auto;
+ max-height: 360px;
+ padding: 8px;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
+ row-gap: 6px;
+ column-gap: 6px;
+ line-height: 1;
+ }
+ .search__listbox:focus {
+ outline: none;
+ }
+ .search__item {
+ font-size: 24px;
+ line-height: 1;
+ color: hsl(240 4% 16%);
+ border-radius: 16px;
+ padding: 8px;
+ position: relative;
+ user-select: none;
+ outline: none;
+ display: grid;
+ justify-items: center;
+ }
+ .search__item[data-disabled] {
+ color: hsl(240 5% 65%);
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ .search__item[data-highlighted] {
+ outline: none;
+ box-sizing: border-box;
+ background-color: hsl(240 5% 96%);
+ box-shadow: inset 0 0 0 2px hsl(200 98% 39%);
+ }
+ .search__section {
+ padding: 8px 0 0 8px;
+ font-size: 14px;
+ line-height: 32px;
+ color: hsl(240 4% 46%);
+ }
+ .search__no_result {
+ text-align: center;
+ padding: 8px;
+ padding-bottom: 24px;
+ margin: auto;
+ color: hsl(240 4% 46%);
+ }
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+ @keyframes contentShow {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+ @keyframes contentHide {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+ }
+ .result__content {
+ margin-top: 16px;
+ font-size: 16px;
+ line-height: 1;
+ }
+ ```
+
+
+ {/* */}
+
+
+## Usage
+
+### Debounce
+
+Set `debounceOptionsMillisecond`, to prevent new search queries immediately on input change. Instead, search queries are requested once input is idle for a set time.
+
+Show a debouncing icon by adding a `loadingComponent` to `Search.Indicator`.
+
+
+
+
+
+ ```tsx
+ setOptions(queryEmojiData(query))}
+ onChange={result => setEmoji(result)}
+ debounceOptionsMillisecond={300}
+ optionValue="name"
+ optionLabel="name"
+ placeholder="Search an emojiโฆ"
+ itemComponent={(props: any) => (
+
+ {props.item.rawValue.emoji}
+
+ )}
+ >
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ e.preventDefault()}>
+
+
+ ๐ฌ No emoji found
+
+
+
+
+ ```
+
+### Cmdk style
+
+To achieve the command menu look, add the `open` prop to permanently open dropdown. Replace `Search.Portal` and `Search.Content` with a `div` to directly mount your content below the search input.
+
+
+
+
+
+ ```tsx
+ setOptions(queryEmojiData(query))}
+ onChange={result => setEmoji(result)}
+ debounceOptionsMillisecond={300}
+ optionValue="name"
+ optionLabel="name"
+ placeholder="Search an emojiโฆ"
+ itemComponent={(props: any) => (
+
+ {props.item.rawValue.emoji}
+
+ )}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ ๐ฌ No emoji found
+
+
+
+ ```
+
+## API Reference
+
+### Search
+
+`Search` is equivalent to the `Root` import from `@kobalte/core/search`.
+
+| Prop | Description |
+| :---------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| options | `Array`
An array of options to display as the available options. |
+| optionValue | `keyof T \| ((option: T) => string \| number)`
Property name or getter function to use as the value of an option. This is the value that will be submitted when the search component is part of a `