import { ArgTypes, Meta, Story } from "@storybook/addon-docs/blocks";
import { Combobox } from "./Combobox";
import * as ComboboxStories from "./Combobox.stories";

<Meta of={ComboboxStories} />

# Combobox

A `Combobox` is an enhanced `Select` input, which allows users to filter results as they type. It supports all of the common input props, with the main difference being that the `options` prop is supplied as data (or a function), rather than nested `children`.

`Combobox` is fully accessible, and supports keyboard navigation and screen readers.

### Static / synchronous use

When you know all of the options data upfront, you can provide it as either:

- A flat array of strings
- An array of objects with type `Option` (see below)
- An array of groups of Options, with headings

The default `onChange` behaviour, while typing, is to filter the options list based on input value.

The `onSelected` prop is called when an option is selected, passing `Option` as its parameter.

```ts
type ComboboxOption = string | { id?: string; label: string; value: string };

type ComboboxOptionGroup = {
  heading: string;
  options: readonly T[];
};

type ComboboxOptions = T[] | ComboboxOptionGroup<T>[];
```

### Dynamic / asynchronous use

`Combobox` provides a useful primitive with which to build more dynamic selector components, such as `Trade Selector` and `Address Lookup`.

For these kinds of component, options (list) data is not known and must be fetched, typically from an API, as the user types. Use the `asyncOptions` prop with a function that receives the current input and returns a promise of options:

```ts
type AsyncOptionsFetcher = (
  inputValue: string,
  options?: { signal?: AbortSignal },
) => Promise<Options>;
```

The Combobox calls `asyncOptions` **imperatively** when the debounced input changes. Pass the optional `signal` to your fetch so in-flight requests can be cancelled when the user types again.

- Provide a `delay` value (e.g. 300ms) to debounce API requests.
- Use `minSearchLength` to avoid fetching until the user has typed enough characters.

**When options come from parent state** (e.g. TanStack Query's `data` from `useQuery`, or a separate fetch triggered by `onChange`), use the sync `options` prop instead of `asyncOptions`. The Combobox only re-calls `asyncOptions` when the debounced input changes, so it won't see updates when your parent state changes. Pass `options={...}` derived from your state so the options update when the fetch completes.

**When using TanStack Query** (or similar) with `asyncOptions`, avoid calling Hooks (like `useQuery`) inside `asyncOptions`, as this violates the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks). Instead, use `queryClient.fetchQuery` inside `asyncOptions`, or call `useQuery` at the component level and pass `options={data ?? []}` to the Combobox:

- ❌ **Bad**: `useQuery` inside `asyncOptions` — `useQuery` is a React Hook and must be called at the top level of a component, not from an imperative callback. While `useQuery` *can* re-run when the query key includes the input, it should not be invoked from `asyncOptions`.
- ✅ **Good**: `queryClient.fetchQuery` inside `asyncOptions` — imperative, query key includes input, respects AbortSignal.

Example with TanStack Query:

{/* prettier-ignore */}
```jsx
import { useQueryClient } from "@tanstack/react-query";
import { Combobox } from "@simplybusiness/mobius";

function FruitSelector() {
  const queryClient = useQueryClient();
  const fetchOptions = async (
    inputValue: string,
    { signal }: { signal?: AbortSignal } = {}
  ) => {
    return queryClient.fetchQuery({
      queryKey: ["fruits", inputValue],
      queryFn: () => fetchFruits(inputValue, { signal }),
    });
  };
  return (
    <Combobox
      label="Select a fruit"
      asyncOptions={fetchOptions}
      delay={300}
      minSearchLength={3}
    />
  );
}
// fetchFruits(inputValue, { signal }) is your API function — pass signal for cancellation
```

### Overriding selection behaviour

While is it often sufficient to fetch data, present it in the list and use an `onSelected` handler to do something with the chosen option, there are times when you may want to override the default behaviour. E.g. Address Lookup can return results that are not full addresses, but are groups, which need further drill down.

In these cases, you can enhance the `onSelected` handler with a `callback` function. If a selected option has a callback, then it will be invoked, passing the option as params.

When a selected option does not have a callback, the standard handler is invoked.

## Install

```bash
yarn add @simplybusiness/mobius
```

## Usage

```js
import { ComboboxStories } from "@simplybusiness/mobius";
```

### Using with [`react-hook-form`](https://www.react-hook-form.com/)

To use Combobox with react-hook-form, wrap it in a `Controller`
component. The Controller will manage the form state and validation for
the Combobox input.

{/* prettier-ignore */}
```jsx
import { Combobox } from "@simplybusiness/mobius";
import { Controller } from "react-hook-form";

<Controller
  name="fruit"
  control={control}
  render={({ field }) => (
    <Combobox
      {...field}
      options={["Apple", "Apricot", "etc."]}
      initialValue={field.value}
      onSelected={(option) => field.onChange(option.label)}
    />
  )}
/>
```

## Examples

### Default

<Story of={ComboboxStories.Default} />

{/* prettier-ignore */}
```jsx
import { Combobox } from "@simplybusiness/mobius";

<Combobox options={["Apple", "Apricot", "etc."]} />
```

### With an options object

<Story of={ComboboxStories.OptionsObject} />

{/* prettier-ignore */}
```jsx
import { Combobox } from "@simplybusiness/mobius";

<Combobox options={[
  { id: "apple", label: "Apple" },
  { id: "apricot", label: "Apricot" },
  { id: "etc", label: "etc." }
]} />
```

### With Icon

<Story of={ComboboxStories.WithIcon} />

{/* prettier-ignore */}
```jsx
import { Combobox, Icon } from "@simplybusiness/mobius";
import { search } from "@simplybusiness/icons";

<Combobox
  options={[...OPTIONS]}
  icon={<Icon icon={search}/>}
/>
```

### Custom Option Component

You can customize how options are rendered using the `optionComponent` prop. This allows you to create custom layouts, add icons, or apply custom styling to individual options.

The `optionComponent` prop accepts a component that receives two props:

- `option`: The option data object
- `isHighlighted`: A boolean indicating if the option is currently highlighted

#### Example

```jsx
const options = [
  { id: "apple", label: "Apple", title: "Heading 1" },
  { id: "apricot", label: "Apricot", title: "Heading 2" },
  { id: "etc", label: "etc.", title: "Heading 3" },
];

function MyCustomOptionComponent({ option, isHighlighted }) {
  return (
    <div>
      <Text elementType="h2">{option.title}</Text>
      <Text>{option.label}</Text>
    </div>
  );
}

<Combobox options={OPTIONS} optionComponent={MyCustomOptionComponent} />;
```

<Story of={ComboboxStories.CustomOptionComponent} />

### State Selector

This is a simple example of using `Combobox` to create a US State Selector, which uses a flat list of state strings as the options array.

<Story of={ComboboxStories.StateSelector} />

{/* prettier-ignore */}
```jsx
import { Combobox, Icon } from "@simplybusiness/mobius";
import { search } from "@simplybusiness/icons";

<Combobox
  options={[...US_STATES]}
  placeholder="Choose your state"
  icon={<Icon icon={search}/>}
/>
```

## Accessibility

TODO: Add accessibility information

## Props

<ArgTypes of={Combobox} />

## Component HTML Structure and Class names

The following HTML is rendered for a Combobox:

```html
<label class="mobius-label" htmlFor="{input-id}">{Label text}</label>
```
