diff --git a/.changeset/yellow-rockets-cry.md b/.changeset/yellow-rockets-cry.md
new file mode 100644
index 0000000000..fb5722bfeb
--- /dev/null
+++ b/.changeset/yellow-rockets-cry.md
@@ -0,0 +1,5 @@
+---
+"@kvib/react": minor
+---
+
+Legger til SearchAsync-komponent - dette gjør at det er mulig med en dropdown i et søkefelt.
diff --git a/apps/storybook/stories/components/search/search-async/SearchAsync.mdx b/apps/storybook/stories/components/search/search-async/SearchAsync.mdx
new file mode 100644
index 0000000000..455a2c43c1
--- /dev/null
+++ b/apps/storybook/stories/components/search/search-async/SearchAsync.mdx
@@ -0,0 +1,39 @@
+import { Meta, Canvas, Story, Controls } from "@storybook/blocks";
+import * as SearchAsyncStories from "./SearchAsync.stories";
+
+
+
+# SearchAsync
+
+SearchAsync er et søkefelt som laster inn søkeresultater fra en liste med objekter. Resultatene blir vist i en dropdown der brukeren kan velge hvilket resultat som skal bli valgt.
+
+## Ta i bruk
+
+```jsx
+import { SearchAsync } from "@kvib/react";
+```
+
+## Props
+
+
+
+
+## Async søkeresultater
+
+
+
+## Søkeresultater med delay
+
+
+
+## Standardalternativer
+
+
+
+## Størrelser
+
+
+
+## Varianter
+
+
diff --git a/apps/storybook/stories/components/search/search-async/SearchAsync.stories.tsx b/apps/storybook/stories/components/search/search-async/SearchAsync.stories.tsx
new file mode 100644
index 0000000000..b23aa6c7e7
--- /dev/null
+++ b/apps/storybook/stories/components/search/search-async/SearchAsync.stories.tsx
@@ -0,0 +1,193 @@
+import { SearchAsync as KvibSearchAsync, Stack as KvibStack, Box, Icon } from "@kvib/react/src";
+import { Meta, StoryObj } from "@storybook/react";
+
+const meta: Meta = {
+ title: "Komponenter/Search/SearchAsync",
+ component: KvibSearchAsync,
+ parameters: {
+ docs: {
+ story: { inline: true },
+ canvas: { sourceState: "shown" },
+ },
+ a11y: {
+ // Label warnings + contrast ratio because of chakra wrapper.
+ disable: true,
+ },
+ },
+ argTypes: {
+ loadOptions: {
+ control: "text",
+ },
+ handleFromChange: {
+ control: "text",
+ },
+ placeholder: {
+ table: {
+ type: { summary: "string" },
+ },
+ control: "text",
+ },
+ autoFocus: {
+ table: {
+ type: { summary: "boolean" },
+ },
+ control: "boolean",
+ },
+ debounceTime: {
+ table: {
+ type: { summary: "number" },
+ },
+ control: "number",
+ },
+ className: {
+ table: {
+ type: { summary: "string" },
+ },
+ control: "text",
+ },
+ isClearable: {
+ table: {
+ type: { summary: "boolean" },
+ },
+ control: "boolean",
+ },
+ dropdownIndicator: {
+ table: {
+ type: { summary: "Element" },
+ },
+ control: "text",
+ },
+ size: {
+ table: {
+ type: { summary: "sm | md | lg" },
+ defaultValue: { summary: "md" },
+ },
+ options: ["sm", "md", "lg"],
+ control: { type: "radio" },
+ },
+ defaultOptions: {
+ table: {
+ type: { summary: "OptionsOrGroups>" },
+ },
+ control: "array",
+ },
+ variant: {
+ table: {
+ type: { summary: "outline | filled | flushed | unstyled" },
+ defaultValue: { summary: "outline" },
+ },
+ options: ["outline", "filled", "flushed", "unstyled"],
+ control: { type: "radio" },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const SearchAsync: Story = {
+ args: {},
+ render: (args) => (
+
+
+
+ ),
+};
+
+const fruits = [
+ { label: "Eple", value: "eple" },
+ { label: "Banan", value: "banan" },
+ { label: "Kirsebær", value: "kirsebær" },
+ { label: "Pære", value: "pære" },
+ { label: "Svarthyll", value: "svarthyll" },
+];
+
+const mockLoadOptions = (inputValue: string, callback: (options: typeof fruits) => void) => {
+ setTimeout(() => {
+ const filteredFruits = fruits.filter((fruit) => fruit.label.toLowerCase().includes(inputValue.toLowerCase()));
+ callback(filteredFruits);
+ }, 500);
+};
+
+export const SearchAsyncResults: Story = {
+ args: {
+ loadOptions: mockLoadOptions,
+ handleFromChange: (selectedOption) => {
+ console.log("Selected Option:", selectedOption);
+ },
+ placeholder: "Søk etter frukt...",
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const SearchAsyncResultsDebounce: Story = {
+ args: {
+ loadOptions: mockLoadOptions,
+ handleFromChange: (selectedOption) => {
+ console.log("Selected Option:", selectedOption);
+ },
+ debounceTime: 3000,
+ placeholder: "Søk etter frukt...",
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const SearchAsyncDropdownIndicator: Story = {
+ args: {
+ loadOptions: mockLoadOptions,
+ handleFromChange: (selectedOption) => {
+ console.log("Selected Option:", selectedOption);
+ },
+ dropdownIndicator: ,
+ defaultOptions: fruits,
+ placeholder: "Søk etter frukt...",
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const SearchAsyncSizes: Story = {
+ args: {
+ loadOptions: mockLoadOptions,
+ handleFromChange: (selectedOption) => {
+ console.log("Selected Option:", selectedOption);
+ },
+ placeholder: "Søk etter frukt...",
+ },
+ render: (args) => (
+
+
+
+
+
+ ),
+};
+
+export const SearchAsyncVariants: Story = {
+ args: {
+ loadOptions: mockLoadOptions,
+ handleFromChange: (selectedOption) => {
+ console.log("Selected Option:", selectedOption);
+ },
+ placeholder: "Søk etter frukt...",
+ },
+ render: (args) => (
+
+
+
+
+
+
+ ),
+};
diff --git a/apps/storybook/stories/components/search/Search.mdx b/apps/storybook/stories/components/search/search/Search.mdx
similarity index 100%
rename from apps/storybook/stories/components/search/Search.mdx
rename to apps/storybook/stories/components/search/search/Search.mdx
diff --git a/apps/storybook/stories/components/search/Search.stories.tsx b/apps/storybook/stories/components/search/search/Search.stories.tsx
similarity index 98%
rename from apps/storybook/stories/components/search/Search.stories.tsx
rename to apps/storybook/stories/components/search/search/Search.stories.tsx
index 2e7094a3f1..bf8ecd6380 100644
--- a/apps/storybook/stories/components/search/Search.stories.tsx
+++ b/apps/storybook/stories/components/search/search/Search.stories.tsx
@@ -2,7 +2,7 @@ import { Search as KvibSearch } from "@kvib/react/src/search";
import { Meta, StoryObj } from "@storybook/react";
const meta: Meta = {
- title: "Komponenter/Search",
+ title: "Komponenter/Search/Search",
component: KvibSearch,
parameters: {
docs: {
diff --git a/package-lock.json b/package-lock.json
index 53940d664e..6c05a953b9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
},
"apps/storybook": {
"name": "@kvib/storybook",
- "version": "1.0.0",
+ "version": "1.0.1",
"license": "ISC",
"dependencies": {
"@storybook/addon-a11y": "^7.0.23"
@@ -4310,6 +4310,28 @@
"integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==",
"dev": true
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
+ "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.1.1"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz",
+ "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==",
+ "dependencies": {
+ "@floating-ui/core": "^1.4.1",
+ "@floating-ui/utils": "^0.1.1"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz",
+ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw=="
+ },
"node_modules/@fontsource/mulish": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/@fontsource/mulish/-/mulish-4.5.14.tgz",
@@ -10739,8 +10761,7 @@
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"node_modules/@types/qs": {
"version": "6.9.7",
@@ -10756,7 +10777,6 @@
"version": "18.2.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.4.tgz",
"integrity": "sha512-IvAIhJTmKAAJmCIcaa6+5uagjyh+9GvcJ/thPZcw+i+vx+22eHlTy2Q1bJg/prES57jehjebq9DnIhOTtIhmLw==",
- "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -10772,11 +10792,18 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
+ "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
- "devOptional": true
+ "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
},
"node_modules/@types/semver": {
"version": "6.2.3",
@@ -12859,6 +12886,26 @@
"node": ">=4"
}
},
+ "node_modules/chakra-react-select": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/chakra-react-select/-/chakra-react-select-4.7.0.tgz",
+ "integrity": "sha512-BHo4OnLhsfmxMr7ntIL7Sp55zhZKgeU2XOQK/9a0bnnRH6qyd0GMxwII+XnF3TsRHdtrI/2HxdQuX3KKQPFuDg==",
+ "dependencies": {
+ "react-select": "5.7.4"
+ },
+ "peerDependencies": {
+ "@chakra-ui/form-control": "^2.0.0",
+ "@chakra-ui/icon": "^3.0.0",
+ "@chakra-ui/layout": "^2.0.0",
+ "@chakra-ui/media-query": "^3.0.0",
+ "@chakra-ui/menu": "^2.0.0",
+ "@chakra-ui/spinner": "^2.0.0",
+ "@chakra-ui/system": "^2.0.0",
+ "@emotion/react": "^11.8.1",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -14031,6 +14078,15 @@
"utila": "~0.4"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -22635,6 +22691,11 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+ },
"node_modules/memoizerific": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
@@ -24715,6 +24776,26 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-select": {
+ "version": "5.7.4",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.4.tgz",
+ "integrity": "sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^6.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0",
+ "use-isomorphic-layout-effect": "^1.1.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -24742,6 +24823,21 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -27270,6 +27366,19 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-resize-observer": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
@@ -27997,7 +28106,7 @@
},
"packages/react": {
"name": "@kvib/react",
- "version": "1.21.1",
+ "version": "1.21.3",
"license": "MIT",
"dependencies": {
"@chakra-ui/react": "^2.5.1",
@@ -28005,6 +28114,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/mulish": "^4.5.14",
+ "chakra-react-select": "^4.7.0",
"framer-motion": "^10.5.0",
"material-symbols": "^0.5.4"
},
diff --git a/packages/react/package.json b/packages/react/package.json
index 0ac449b84a..fc44b09c0f 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -39,6 +39,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/mulish": "^4.5.14",
+ "chakra-react-select": "^4.7.0",
"framer-motion": "^10.5.0",
"material-symbols": "^0.5.4"
}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 8ca4c8b3fc..3f7aa8dd0e 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -56,3 +56,4 @@ export * from "./typography";
export * from "./visually-hidden";
export * from "./datepicker";
export * from "./file-upload";
+export * from "./search-async";
diff --git a/packages/react/src/search-async/SearchAsync.tsx b/packages/react/src/search-async/SearchAsync.tsx
new file mode 100644
index 0000000000..afcd87d121
--- /dev/null
+++ b/packages/react/src/search-async/SearchAsync.tsx
@@ -0,0 +1,115 @@
+import { ReactNode, useEffect, useRef } from "react";
+import { Text } from "@kvib/react/src";
+import { AsyncSelect as ReactSearch } from "chakra-react-select";
+import { GroupBase, OptionsOrGroups, SingleValue } from "chakra-react-select";
+import { SizeProp, Variant } from "chakra-react-select/dist/types/types";
+
+// Props interface is defined with a generic T to allow flexibility in the data type of options.
+export interface Props {
+ /** Function to fetch/select options based on input. */
+ loadOptions: (inputValue: string, callback: (options: OptionsOrGroups>) => void) => void;
+
+ /** Callback for when the selection changes. */
+ handleFromChange: (newValue: SingleValue) => void;
+
+ /** Placeholder text for the input field. */
+ placeholder?: string;
+
+ /** Determines if the input is focused on mount. */
+ autoFocus?: boolean;
+
+ /** Time delay (ms) before invoking `loadOptions`. */
+ debounceTime?: number;
+
+ /** Additional CSS class for the component. */
+ className?: string;
+
+ /** Allows a clear button in the input. */
+ isClearable?: boolean;
+
+ /** Custom JSX for the dropdown indicator. */
+ dropdownIndicator?: JSX.Element;
+
+ /** Size of the input (e.g., "small", "medium"). */
+ size?: SizeProp;
+
+ /** Default options shown when no input is given. */
+ defaultOptions?: OptionsOrGroups>;
+
+ /** Visual style variant of the input. */
+ variant?: Variant;
+}
+
+// SearchAsync uses the async version of react-select to fetch and display options.
+export const SearchAsync = ({
+ loadOptions,
+ handleFromChange,
+ placeholder,
+ debounceTime,
+ autoFocus,
+ className,
+ isClearable = true,
+ dropdownIndicator,
+ size,
+ defaultOptions,
+ variant,
+}: Props) => {
+ const noOptionsMessage = ({ inputValue }: { inputValue: string }): ReactNode => {
+ if (inputValue.replaceAll(/\s/g, "").length < 1) {
+ return null;
+ }
+ return Fant ingen resultater;
+ };
+
+ // We use our custom useDebounce hook to debounce the loadOptions function,
+ // ensuring it doesn't get called too frequently.
+ const loadOptionsDebounce = useDebounce(loadOptions, debounceTime);
+
+ return (
+ dropdownIndicator ?? null,
+ // Only use separator when there is a dropdownindicator
+ ...(!dropdownIndicator ? { IndicatorSeparator: () => null } : {}),
+ }}
+ isClearable={isClearable}
+ autoFocus={autoFocus}
+ className={className ? className : ""}
+ onChange={handleFromChange}
+ noOptionsMessage={noOptionsMessage}
+ loadingMessage={() => Laster...}
+ loadOptions={debounceTime ? loadOptionsDebounce : loadOptions}
+ blurInputOnSelect={false}
+ placeholder={placeholder ? placeholder : "Søk her..."}
+ size={size}
+ defaultOptions={defaultOptions}
+ variant={variant}
+ />
+ );
+};
+
+type Timer = ReturnType;
+type SomeFunction = (inputValue: string, callback: (options: OptionsOrGroups>) => void) => void;
+
+const useDebounce = (func: SomeFunction, delay = 300) => {
+ // useRef is used to hold a mutable reference to the timer which doesn't cause re-renders.
+ const timer = useRef();
+
+ // useEffect is used to handle cleanup: clearing the timer when the component unmounts.
+ useEffect(() => {
+ return () => {
+ if (!timer.current) return;
+ clearTimeout(timer.current);
+ };
+ }, []);
+
+ // The debounced function. It resets the timer every time it's called, delaying the execution of the
+ // original function until the user stops calling the debounced function for a specified duration.
+ return (inputValue: string, callback: (options: OptionsOrGroups>) => void) => {
+ const newTimer = setTimeout(() => {
+ return func(inputValue, callback);
+ }, delay);
+ clearTimeout(timer.current);
+ timer.current = newTimer;
+ };
+};
diff --git a/packages/react/src/search-async/index.tsx b/packages/react/src/search-async/index.tsx
new file mode 100644
index 0000000000..dd82b3b100
--- /dev/null
+++ b/packages/react/src/search-async/index.tsx
@@ -0,0 +1 @@
+export * from "./SearchAsync";