[![npm](https://img.shields.io/npm/v/@highlight-ui/select)](https://www.npmjs.com/package/@highlight-ui/select)
[![personio.design](https://img.shields.io/static/v1?label=Personio&message=zeroheight&color=yellow)](https://www.personio.design/40d648a47/p/31ff8eselect/b/26fe12)
[![storybook.personio.design](https://img.shields.io/static/v1?label=Personio&message=storybook&color=red)](https://storybook.personio.design/?path=/story/forms-and-input-select--basic)

# @highlight-ui/select

Feature-rich select control.

Collects user information by displaying a list, triggered by click action, allowing single or multiple selection.

## Features

- Supports selection of one or multiple options
- Asynchronous flow
  - built-in search bar
  - search term highlighted results
  - optional search in sub labels
  - dynamically generated options
  - loader
- Option groups
  - split multiple options my line separator and title
- Option types
  - single-line
  - colored
  - multi-line
  - avatar
- Option metadata
  - built with TS generics
- Footer
  - apply action
  - cancel action
- Creatable option
- Support for i18n
- Controlled component
- Variants
  - inline
  - full-width
- 🆕 🧪 Early access features
  - New search input
  - New trigger

## Installation

Using npm:

```bash
npm install @highlight-ui/select
```

Using yarn:

```bash
yarn add @highlight-ui/select
```

Using pnpm:

```bash
pnpm install @highlight-ui/select
```

Once the package is installed, you can import the library:

```
import { Select } from '@highlight-ui/select';
```

## Usage

### Single

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function SingleSelectExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      options={[
        { label: 'Genesis', value: '23095' },
        { label: 'Destinee', value: '26211' },
      ]}
    />
  );
}
```

### Multiple

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function MultiSelectExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  return (
    <Select
      multiple
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      options={[
        { label: 'Genesis', value: '23095' },
        { label: 'Destinee', value: '26211' },
      ]}
    />
  );
}
```

### 🆕 🧪 Early access features

The following sections highlight new features that can be enabled via props.

#### **Search input**

When the content is open, a search input will be visible if the `searchable` prop is set to `true` and the number of options is equal or above the number as provided through the `minOptionsToDisplaySearch` prop.

By default `enableNewSearchInput` will be `false`. Below is a comparison of what it renders, based on its value.

| `enableNewSearchInput` | Component            | Comments                                                                                   |
| :--------------------- | :------------------- | :----------------------------------------------------------------------------------------- |
| `false`                | `DefaultSearchInput` | Wrapper with `role="search"` containing a `<label>` which groups an `<Icon>` and `<input>` |
| `true`                 | `SearchInput`        | Uses the `<TextInput>` component with an `<Icon>` as prefix                                |

#### **Trigger**

By default `enableFlowTriggers` will be `false`. Below is a comparison of what it renders, based on its value.

| `enableFlowTriggers` | `multiple` | Component                                        | Comments                                                                                                                      |
| :------------------- | :--------- | :----------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
| `false`              | `false`    | `DefaultTrigger` or Custom `renderTrigger()`     | `<button>` showing the selected option (by default) or a custom element                                                       |
| `false`              | `true`     | `DefaultSearchInput` or Custom `renderTrigger()` | `<button>` showing the selected options count and clear icon (by default) or a custom element                                 |
| `true`               | `false`    | `InputTrigger`                                   | `<InputContainer>` component with an `<IconButton>` as suffix, showing the selected option                                    |
| `true`               | `true`     | `MultiInputTrigger`                              | `<InputContainer>` component with an `<IconButton>` as suffix, showing the selected options as `<SelectChip>`s and clear icon |

## Advanced usage

### Asynchronous flow

This example focus on loading options asynchronously.
In real-world example, in the `onListVisibilityChange` handler hardcoded options would be replaced by API call.

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

const initialOptions: SelectOption[] = [
  { label: 'Genesis', value: '23095' },
  { label: 'Destinee', value: '26211' },
  { label: 'Chyna', value: '86910' },
  { label: 'Alexie', value: '49249' },
  { label: 'Natalie', value: '18694' },
];

export default function AsyncExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  const [options, setOptions] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      searchable
      isLoading={isLoading}
      minOptionsToDisplaySearch={1}
      onListVisibilityChange={(visible) => {
        if (visible) {
          setIsLoading(true);
          // mocks API call. Should be replaced with network request
          setTimeout(() => {
            setOptions(initialOptions);
            setIsLoading(false);
          }, 3000);
        }
      }}
      options={options}
    />
  );
}
```

### Option groups

Option groups provide a way to visually group multiple options. Groups are determined by specifying a group title in each option.

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function OptionGroupsExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      options={[
        { label: 'Genesis', value: '23095', group: 'One' },
        { label: 'Destinee', value: '26211', group: 'Two' },
      ]}
    />
  );
}
```

### Option types

Four different option types are supported:

- single-line
- colored
- multi-line
- avatar

Option types are determined by specifying `optionType` prop.
Additionally, each option type has its own props which are specified in each option.

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function OptionTypesExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      optionType="colored"
      options={[
        { label: 'Genesis', value: '23095', color: 'red' },
        { label: 'Destinee', value: '26211', color: 'green' },
      ]}
    />
  );
}
```

In addition to the `label`, `value` and `group`, these props are used to define option type:

| Prop           | Type                | Option type           | Description                                             |
| :------------- | :------------------ | :-------------------- | :------------------------------------------------------ |
| `disabled`     | `boolean`           | all                   | Should the option be disabled or enabled                |
| `metadata`     | `ComponentMetadata` | all                   | Object used for testing. Contains testId and actionName |
| `subLabel`     | `string`            | multi-line and avatar | Smaller, descriptive text used below main label         |
| `avatar`       | `string`            | avatar                | URL used to fetch avatar image                          |
| `imageLoading` | `eager` or `lazy`   | avatar                | Specifies how browser will load avatar image            |

### Creatable option

Select has a support for options dynamically created by user.

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function CreatableOptionExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  const [options, setOptions] = useState([
    {
      label: 'Genesis',
      value: '23095',
    },
  ]);
  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      creatableOptions={{
        createButtonProps: {
          label: 'Add',
          loading: false,
        },
        inputProps: {
          placeholder: 'Create a new one',
        },
        onCreate: (item) => {
          setOptions([...options, { label: item.value, value: item.value }]);
        },
      }}
      options={options}
    />
  );
}
```

⚠️ Submit footer cannot be used alongside creatable options.

#### CreatableOptionsProps

| Prop                         | Type                                 | Required | Default | Description                                             |
| :--------------------------- | :----------------------------------- | :------- | :------ | :------------------------------------------------------ |
| `createButtonProps.label`    | `string`                             | Yes      |         | Text used in the submit button                          |
| `createButtonProps.loading`  | `boolean`                            | No       | `false` | Defines if the submit button will be in loading state   |
| `createButtonProps.metadata` | `ComponentMetadata`                  | No       | `null`  | Object used for testing. Contains testId and actionName |
| `inputProps.placeholder`     | `string`                             | No       | `null`  | Placeholder used in search bar                          |
| `inputProps.metadata`        | `ComponentMetadata`                  | No       | `null`  | Object used for testing. Contains testId and actionName |
| `onCreate`                   | `function(item: SelectOption): void` | Yes      | `null`  | Function called whenever new option is created          |

### Submit footer

Select has a support for submit footer.It provides distinction between selected and submitted options. This means that changes in selected options can be discarded.

```tsx
import React from 'react';
import { Select, SelectOption } from '@highlight-ui/select';

export default function SubmitFooterExample() {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  const [submittedOptions, setSubmittedOptions] = useState<SelectOption[]>([]);

  return (
    <Select
      triggerLabel="Pick a name"
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      multiple
      options={[
        { label: 'Genesis', value: '23095' },
        { label: 'Destinee', value: '26211' },
      ]}
      submitOptions={{
        onCancel: () => setSelectedOptions(submittedOptions),
        onSubmit: (options) => setSubmittedOptions(options),
      }}
    />
  );
}
```

⚠ Creatable options cannot be used alongside submit footer.

#### SubmitOptions

| Prop                        | Type                                      | Required | Default  | Description                                           |
| :-------------------------- | :---------------------------------------- | :------- | :------- | ----------------------------------------------------- |
| `submitOptions.onSubmit`    | `function(options: SelectOption[]): void` | Yes      |          | Function called whenever the submit button is pressed |
| `submitOptions.onCancel`    | `function(): void`                        | Yes      |          | Function called whenever the cancel button is pressed |
| `submitOptions.submitLabel` | `string`                                  | No       | 'Apply'  | Label used in the submit button                       |
| `submitOptions.cancelLabel` | `string`                                  | No       | 'Cancel' | Label used in the cancel button                       |

## Props 📜

ℹ️ Whenever SelectOption is used, there is an option to extend this interface and provide your own metadata.

| Prop                        | Type                                                                | Required | Default              | Description                                                                                                |
| :-------------------------- | :------------------------------------------------------------------ | :------- | :------------------- | :--------------------------------------------------------------------------------------------------------- |
| `options`                   | `SelectOption[]`                                                    | Yes      |                      | List of objects                                                                                            |
| `onSelect`                  | `function(values: SelectOption[]): void`                            | Yes      |                      | Gets called each time an option is selected                                                                |
| `selectedOptions`           | `SelectOption[]`                                                    | Yes      |                      | List of selected options                                                                                   |
| `className`                 | `string`                                                            | No       | `null`               | Class name to apply to the element                                                                         |
| `listClassName`             | `string`                                                            | No       | `null`               | Class name to apply to the List element                                                                    |
| `closeOnSelect`             | `boolean`                                                           | No       | `false`              | Should the options list disappear by each selection?                                                       |
| `disabled`                  | `boolean`                                                           | No       | `false`              | Should trigger be enabled or disabled                                                                      |
| `selectAllLabel`            | `string`                                                            | No       | 'Select all'         | Label for `Select all` functionality when `multiple` prop is `true`                                        |
| `metadata`                  | `ComponentMetadata`                                                 | No       | `null`               | Object used for testing. Contains testId and actionName                                                    |
| `onSearchChange`            | `function(search: string): void`                                    | No       | `null`               | Gets called whenever text is typed in search bar                                                           |
| `onListVisibilityChange`    | `function(visible: boolean): void`                                  | No       | `null`               | Gets called whenever the visibility of the select's list has changed                                       |
| `emptyStateLabel`           | `string`                                                            | No       | 'Nothing to display' | Label for displaying the empty state of the Select list                                                    |
| `renderEmptyState`          | `function(props: SelectEmptyStateProps): ReactNode`                 | No       | `null`               | Function to return custom empty options list message                                                       |
| `renderTrigger`             | `function(props: SelectTriggerProps & ref: RefCallback): ReactNode` | No       | `null`               | Function to return a custom trigger                                                                        |
| `optionType`                | `single-line or colored or multi-line or avatar`                    | No       | 'single-line'        | String specifying the option type that should be rendered.                                                 |
| `variant`                   | `inline or full-width`                                              | No       | 'inline'             | `inline` when used outside forms and `full-width` when used inside forms                                   |
| `searchable`                | `boolean`                                                           | No       | `true`               | Determines whether or not a search input should be rendered in the select list                             |
| `searchAutofocus`           | `boolean`                                                           | No       | `true`               | Whether or not the search field should get the focus automatically each time the list opens up             |
| `searchInitialValue`        | `string`                                                            | No       | `null`               | Initial search field value                                                                                 |
| `searchNoResultsLabel`      | `string`                                                            | No       | 'No results for'     | No search results message                                                                                  |
| `searchPlaceholder`         | `string`                                                            | No       | 'Search'             | No search results message                                                                                  |
| `minOptionsToDisplaySearch` | `number`                                                            | No       | 8                    | If Select is searchable search input will render if the number of options is more or equal to this number. |
| `triggerLabel`              | `string`                                                            | No       | `null`               | Label being shown by default on the trigger button                                                         |
| `outline`                   | `default or warning or error`                                       | No       | 'default'            | String specifying the outline that should be rendered                                                      |
| `autoFocus`                 | `boolean`                                                           | No       | `false`              | Automatically focus the Select's trigger on mount                                                          |
| `triggerId`                 | `string`                                                            | No       | `null`               | HTML id attribute asssigned to trigger component                                                           |
| `creatableOptions`          | `CreatableOptionsProps`                                             | No       | `null`               | Options used for creatable option feature                                                                  |
| `isClearable`               | `boolean`                                                           | No       | `false`              | Defines wether to render the clearing x button                                                             |
| `highlightedText`           | `string`                                                            | No       | `null`               | Highlights this text in the options list only when the Select is not searchable.                           |
| `isLoading`                 | `boolean`                                                           | No       | `false`              | Display the loading state of the list                                                                      |
| `submitOptions`             | `SubmitOptions`                                                     | No       | `null`               | Options used for the submit button                                                                         |
| `enableFlowTriggers`        | `boolean`                                                           | No       | `false`              | Allow toggling between original and new trigger                                                            |
| `enableNewSearchInput`      | `boolean`                                                           | No       | `false`              | Allow toggling between original and new search input                                                       |
| `limitTwoRows`              | `boolean`                                                           | No       | `true`               | Limit selected chips to only display two rows. Only applicable to multi-select.                            |
| `enableSubLabelSearch` | `boolean` | No | `false` | Search in subLabel text when `optionType` is `multi-line` |

### SelectEmptyStateProps

| Prop    | Type     | Required | Default | Description              |
| :------ | :------- | :------- | :------ | ------------------------ |
| `label` | `string` | No       | `null`  | Label shown in the empty |

## Accessibility ♿️

Select tries to mimic [native HTML select](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) a11y features as best as possible.

This is well-known to be a notoriously [hard job](https://www.24a11y.com/2019/select-your-poison/)
There are some smaller deviation to native select's behaviour:

- Keyboard navigation
  - using keyboard letters won't focus on list options

## Testing

This example serves as starting point on how you can use `Select` component as part of your tests.

Some of the props are already defined in the `SelectWrapper` since they are mandatory but they can be overriden by sending props to the `renderSelectWrapper` function.

```tsx
import React, { useState } from 'react';
import { render } from '@testing-library/react';
import { Select, SelectOption } from '@highlight-ui/select';

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
type SelectWrapperProps = Optional<
  SelectProps<SelectOption>,
  'selectedOptions' | 'onSelect' | 'options'
>;

function SelectWrapper(props: SelectWrapperProps) {
  const [selectedOptions, setSelectedOptions] = useState<SelectOption[]>([]);
  return (
    <Select
      selectedOptions={selectedOptions}
      onSelect={setSelectedOptions}
      options={[
        { label: 'Genesis', value: '23095', color: 'red' },
        { label: 'Destinee', value: '26211', color: 'green' },
      ]}
      {...props}
    />
  );
}

describe('SelectTest', () => {
  const renderSelectWrapper = (props: SelectWrapperProps) => {
    return render(<SelectWrapper {...props} />);
  };

  it('test description', () => {
    renderSelectWrapper({});
    // write your expect here
  });
});
```

## Place in design system 💻

`Select` component's flexibility is used to implement a number of different components:

- [date-time-picker](https://www.npmjs.com/package/@highlight-ui/date-time-picker)
- [tag-picker](https://www.npmjs.com/package/@highlight-ui/tag-picker)
- [form-field](https://www.npmjs.com/package/@highlight-ui/form-field)

For picking a color from a color palette, [color-picker](https://www.npmjs.com/package/@highlight-ui/color-picker) should be used.

For picking unselectable menu item which will open URL or call custom callback, look at the [dropdown-menu](https://www.npmjs.com/package/@highlight-ui/dropdown-menu)

## Contributing 🖌️

Please visit [personio.design](https://www.personio.design/40d648a47/v/0/p/31ff8e-select/b/26fe12).

If you're interested in contributing, please visit our [contribution page](https://www.personio.design/40d648a47/v/0/p/138faf-about-the-process).
