npm package: @polarityio/pi-select
Description: Feature-rich select/dropdown with single and multi-select, async search, and custom rendering
Latest version: 1.0.3

# Polarity Integration Component Library

## Select Component

A feature-rich select/dropdown component supporting single and multi-select modes, synchronous and asynchronous search, custom option rendering, and intelligent dropdown positioning via Floating UI.

## Installation

```bash
npm install @polarityio/pi-select
```

### Peer Dependencies

- [lit](https://lit.dev/) ^3.0.0

## Usage

```javascript
import '@polarityio/pi-select';
```

### Basic Single Select

```javascript
import { html } from 'lit';

html`
  <pi-select
    placeholder="Choose a fruit"
    .options=${['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']}
  ></pi-select>
`;
```

### Multi-Select

```javascript
import { html } from 'lit';

html`
  <pi-select
    multiple
    placeholder="Select tags"
    .options=${['Bug', 'Feature', 'Enhancement', 'Documentation']}
    .selected=${['Bug', 'Feature']}
  ></pi-select>
`;
```

### Async Search

```javascript
import { html } from 'lit';

html`
  <pi-select
    placeholder="Search users..."
    search-placeholder="Type a name..."
    search-message="Type to search for users"
    loading-message="Searching..."
    .search=${async (term) => {
      const res = await fetch(`/api/users?q=${term}`);
      return res.json();
    }}
    label-field="name"
    search-field="name"
  ></pi-select>
`;
```

### Custom Option Rendering

```javascript
import { html } from 'lit';

const select = document.querySelector('pi-select');

select.options = [
  { name: 'Alice', role: 'Admin', avatar: '/alice.png' },
  { name: 'Bob', role: 'Editor', avatar: '/bob.png' },
];

select.labelField = 'name';
select.searchField = 'name';

select.renderOption = (option) => html`
  <div style="display: flex; align-items: center; gap: 8px;">
    <img src="${option.avatar}" width="24" height="24" style="border-radius: 50%;" />
    <div>
      <div>${option.name}</div>
      <small style="opacity: 0.7;">${option.role}</small>
    </div>
  </div>
`;
```

### Handling Events

```javascript
const select = document.querySelector('pi-select');

select.addEventListener('pi-change', (e) => {
  console.log('Selected:', e.detail.selected);
  console.log('Previous:', e.detail.previous);
});

select.addEventListener('pi-search', (e) => {
  console.log('Search term:', e.detail.term);
  console.log('Results:', e.detail.results);
});

select.addEventListener('pi-open', () => console.log('Dropdown opened'));
select.addEventListener('pi-close', () => console.log('Dropdown closed'));
```

## API Reference

### Properties

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `T[]` | `[]` | Array of option items to display in the dropdown |
| `selected` | `T \| T[] \| undefined` | `undefined` | Currently selected option(s). Array for multiple mode. |
| `multiple` | `boolean` | `false` | Enable multi-select mode |
| `placeholder` | `string` | `''` | Placeholder text when no option is selected |
| `disabled` | `boolean` | `false` | Disable the entire select component |
| `searchEnabled` | `boolean` | `true` | Enable search/filter functionality |
| `searchField` | `string \| undefined` | `undefined` | Object property name to search within (for object options) |
| `labelField` | `string \| undefined` | `undefined` | Object property name to display as the option label |
| `allowClear` | `boolean` | `false` | Show a clear button to reset the selection |
| `closeOnSelect` | `boolean \| undefined` | `undefined` | Close dropdown after selection. Defaults to `true` in single mode, `false` in multiple mode. |
| `searchPlaceholder` | `string` | `''` | Placeholder text for the search input |
| `noMatchesMessage` | `string` | `'No results found'` | Message displayed when no options match the search |
| `searchMessage` | `string` | `'Type to search'` | Message shown in async search mode before the first search |
| `loadingMessage` | `string` | `'Loading...'` | Message displayed while loading async search results |
| `highlightOnHover` | `boolean` | `true` | Highlight options on mouse hover |
| `matchTriggerWidth` | `boolean` | `true` | Make dropdown width match the trigger width |
| `triggerClass` | `string` | `''` | Additional CSS class(es) for the trigger element |
| `dropdownClass` | `string` | `''` | Additional CSS class(es) for the dropdown element |
| `triggerTabindex` | `number` | `0` | Tab index for keyboard navigation |
| `searchDebounce` | `number` | `150` | Debounce delay in milliseconds for search filtering |
| `loading` | `boolean` | `false` | Indicates async search is in progress |
| `search` | `(term: string) => Promise<T[]>` | `undefined` | Async search function. When provided, enables async search mode. |
| `matcher` | `(option: T, term: string) => number` | `undefined` | Custom matcher function for filtering. Return `-1` for no match, `0+` for match position. |
| `renderOption` | `(option: T) => TemplateResult \| string` | `undefined` | Custom render callback for option display in the dropdown |
| `renderSelectedItem` | `(option: T) => TemplateResult \| string` | `undefined` | Custom render callback for selected item display in the trigger |

### Events

| Event Name | Detail | Description |
| --- | --- | --- |
| `pi-change` | `{ selected: T \| T[] \| undefined, previous: T \| T[] \| undefined }` | Fired when the selection changes |
| `pi-open` | `{}` | Fired when the dropdown opens |
| `pi-close` | `{}` | Fired when the dropdown closes |
| `pi-search` | `{ term: string, results: T[] }` | Fired on search input with the term and filtered results |
| `pi-focus` | `{}` | Fired when the select gains focus |
| `pi-blur` | `{}` | Fired when the select loses focus |

### Slots

| Slot Name | Description |
| --- | --- |
| `before-options` | Content displayed before the options list in the dropdown |
| `after-options` | Content displayed after the options list in the dropdown |
| `loading` | Custom loading message (replaces `loadingMessage` when provided) |
| `no-matches` | Custom no-matches message (replaces `noMatchesMessage` when provided) |

### CSS Custom Properties

| Property | Default | Description |
| --- | --- | --- |
| `--pi-select-dropdown-z-index` | `1000` | Dropdown z-index |
| `--pi-select-line-height` | `1.75em` | Select line height |
| `--pi-select-trigger-padding` | `4px 16px 4px 8px` | Trigger padding |
| `--pi-select-border-color` | `var(--pi-color-border-element)` | Border color |
| `--pi-select-border-radius` | `var(--pi-size-radius-base)` | Border radius |
| `--pi-select-background` | `var(--pi-color-background-container-base)` | Background color |
| `--pi-select-text-color` | `var(--pi-color-font-primary)` | Text color |
| `--pi-select-disabled-bg` | `var(--pi-color-background-container-disabled)` | Disabled background |
| `--pi-select-placeholder-color` | `var(--pi-color-font-disabled)` | Placeholder color |
| `--pi-select-loading-icon-color` | `var(--pi-color-font-disabled)` | Loading icon color |
| `--pi-select-dropdown-border` | `1px solid var(--pi-select-border-color)` | Dropdown border |
| `--pi-select-dropdown-box-shadow` | `var(--pi-shadow-sm)` | Dropdown shadow |
| `--pi-select-dropdown-margin` | `2px 0 0` | Dropdown margin from trigger |
| `--pi-select-option-padding` | `4px 8px` | Option padding |
| `--pi-select-search-input-border-radius` | `3px` | Search input border radius |
| `--pi-select-search-field-border` | `1px solid var(--pi-select-border-color)` | Search field border |
| `--pi-select-visible-options` | `7` | Maximum number of visible options before scrolling |
| `--pi-select-highlight-bg` | `var(--pi-color-background-primary)` | Highlighted option background |
| `--pi-select-highlight-color` | `var(--pi-color-font-inverse)` | Highlighted option text color |
| `--pi-select-selected-bg` | `var(--pi-color-background-hover)` | Selected option background |
| `--pi-select-disabled-color` | `var(--pi-color-font-disabled)` | Disabled option text color |
| `--pi-select-tag-bg` | `var(--pi-color-background-secondary)` | Multi-select tag background |
| `--pi-select-tag-color` | `var(--pi-color-font-primary)` | Multi-select tag text color |
| `--pi-select-tag-border` | `1px solid var(--pi-color-border-container)` | Multi-select tag border |
| `--pi-select-tag-border-radius` | `3px` | Multi-select tag border radius |
| `--pi-select-tag-padding` | `0 4px` | Multi-select tag padding |

### CSS Parts

| Part Name | Description |
| --- | --- |
| `trigger` | The select trigger/button area |
| `selected-item` | Selected item text (single mode) |
| `placeholder` | Placeholder text |
| `clear-btn` | Clear button |
| `loading-icon` | Loading spinner icon |
| `status-icon` | Dropdown arrow icon |
| `tags-area` | Area containing tags (multiple mode) |
| `tag` | Individual tag (multiple mode) |
| `tag-label` | Tag label text (multiple mode) |
| `tag-remove` | Tag remove button (multiple mode) |
| `search-input` | Search input field |
| `icons` | Icons container (multiple mode) |
| `dropdown` | Dropdown container |
| `search` | Search input container (single mode) |
| `options-list` | Options list container (`role="listbox"`) |
| `option` | Individual option |
| `option-highlighted` | Highlighted option (keyboard/hover) |
| `option-selected` | Selected option |
| `option-disabled` | Disabled option |
| `no-matches` | No-matches message element |
| `loading` | Loading message element |

### Public Methods

| Method | Description |
| --- | --- |
| `open()` | Opens the dropdown. No-op if already open or disabled. |
| `close()` | Closes the dropdown and returns focus to the trigger. |
| `toggle()` | Toggles the dropdown between open and closed states. |

### Keyboard Navigation

| Key | Behavior |
| --- | --- |
| `ArrowDown` | Open dropdown or move highlight to the next option |
| `ArrowUp` | Open dropdown or move highlight to the previous option |
| `Home` | Move highlight to the first option |
| `End` | Move highlight to the last option |
| `Enter` / `Space` | Select highlighted option or open dropdown |
| `Escape` | Close dropdown |
| `Tab` | Close dropdown and move focus |
| `Backspace` | Remove last selected tag (multiple mode, empty search) |
| *(type-ahead)* | Jump to matching option when dropdown is closed |

## Theming

Customize the appearance using CSS custom properties. This component uses design tokens from `@polarityio/pi-shared` for consistent theming across the component library.

```css
pi-select {
  --pi-select-border-radius: 8px;
  --pi-select-visible-options: 10;
  --pi-select-highlight-bg: #3b82f6;
}

pi-select::part(trigger) {
  min-height: 40px;
}

pi-select::part(tag) {
  border-radius: 12px;
}
```

## License

MIT