# Multiselect

Level: molecule.

## Purpose

`wbl-multiselect` lets a user choose several values from a supplied list and
shows that controlled selection in one field. Use it for a reusable field
outside Filter Row. It filters the supplied local list as the user types. Do
not use it for free-form option creation, asynchronous search, or a
Filter-Row-specific draft-and-apply selection; those are separate boundaries.

## Anatomy

- Optional label, required marker, placeholder, description, clear action, and
  opening chevron.
- A combobox field trigger that becomes an in-field filter on opening, plus an
  attached checkbox listbox.
- `Text` presentation renders the selected labels as a summary.
- `Tag` presentation renders each selected label as a closable compact Tag.

## Variants and visual states

- `variant`: `text` or `tag`.
- `size`: `L` (56px), `M` (44px), or `S` (32px) when empty. Text keeps those
  field heights when it is a one-line summary.
- `state`: default, active while its listbox is open, and disabled.
- Switching Text `valueLayout` changes only the value overflow/wrapping rule.
  It must preserve the truncate layout's top and bottom content padding, label
  position, and one-line field height; wrap may add height only after a value
  actually occupies an additional line.
- A filled Tag field wraps selected Tags with a 4px gap. The verified two-line
  heights are 80px (`L`), 68px (`M`), and 56px (`S`); additional wrapping must
  grow the field rather than clipping selected values.
- The listbox stays 4px from the field in every size and placement. Clear uses
  `cross-S`; its rendered icon size matches the chevron for its field size.

### Text summary

- With `valueLayout='truncate'`, render one line and show no more than
  `maxVisibleItems` labels followed by `, ещё N` for the remaining values. When
  `maxVisibleItems=null`, all labels participate in the one-line summary and
  the suffix is hidden.
- With `valueLayout='wrap'`, render all selected labels in a wrapping summary,
  hide `, ещё N`, and grow the field with the summary while retaining the same
  visible top and bottom insets as the fixed summary. With one rendered value
  line, `wrap` has exactly the same label position and field geometry as
  `truncate`; additional height comes only from a new value line.
- The component counts hidden values from `maxVisibleItems`; layout width is
  never used to infer the suffix count.

### Tag summary

- Each selected value is a `wbl-tag` with `size="S"`, `appearance="selected"`,
  and `closable=true` unless the whole Multiselect is disabled.
- When the Tag field is open, its in-field filter follows the selected Tags in
  the same wrapping flow. It takes the remaining row space or moves to the next
  row; it never replaces or overlays a selected Tag.
- The Tag in-field filter and its placeholder use
  `--typography-description-puma` in every size.
- Closing one Tag removes only that value. Clear removes every selected value.

## Behavior

- `items` and `selectedIds` are controlled inputs. `selectedIds` is the source
  of truth; the component never retains an uncommitted selection.
- Selecting or deselecting an option immediately emits the next immutable
  `selectedIds` array. There is no Apply action or draft state.
- The trigger toggles the listbox. Clear and individual Tag removal emit their
  changes immediately without introducing a draft state or closing the listbox.
- Opening the field focuses an in-field filter. It matches `items` by a
  case-insensitive substring of `label`; filtering is local, resets when the
  listbox closes, and never alters `selectedIds` on its own.
- Disabled Multiselects neither open nor emit selection, clear, or removal
  changes. Disabled options remain visible but cannot be toggled.
- Async loading, remote search, option creation, and Filter Row behavior are
  intentionally out of scope.

## Accessibility

- The closed trigger has `role="combobox"`, `aria-haspopup="listbox"`,
  `aria-expanded`, and `aria-controls` bound to the overlay listbox. On open,
  the native filter input owns the combobox semantics and receives focus.
- The overlay uses listbox semantics, `aria-multiselectable="true"`, and
  option semantics with `aria-selected`; it does not use menu semantics.
- Enter, Space, and ArrowDown open the listbox. In the listbox, ArrowUp and
  ArrowDown move the active option; Space toggles it; Escape closes and returns
  focus to the trigger. In the filter input, ArrowDown moves to the first
  visible option and Escape closes the listbox. Disabled options are skipped by
  keyboard navigation.
- Clear and every visible Tag close control have a meaningful accessible name.

## Figma source

- [Text](https://www.figma.com/design/EO9tuCsqwuHL8cZ4Eb2psR/%F0%9F%9A%9B-WB-Logistics-UI-kit?node-id=17867-14645): `17867:14645`.
- [\_FieldValue](https://www.figma.com/design/EO9tuCsqwuHL8cZ4Eb2psR/%F0%9F%9A%9B-WB-Logistics-UI-kit?node-id=17867-15072): `17867:15072`.
- [Tag](https://www.figma.com/design/EO9tuCsqwuHL8cZ4Eb2psR/%F0%9F%9A%9B-WB-Logistics-UI-kit?node-id=21874-11775): `21874:11775`.

The Text Figma master exposes `Value layout`: `Fixed / truncate` maps to the
Angular `valueLayout='truncate'`, while `Growing / wrap` maps to
`valueLayout='wrap'`. `_FieldValue` remains private Figma assembly.

The local in-field filter is runtime behavior; it has no separate published
Figma variant.

Reusable Figma assembly is recorded in [the Multiselect sidecar](wbl-multiselect.figma.md).

## Local component contract

### Classes and selectors

- `WblMultiselectComponent` — `wbl-multiselect`
  (`src/app/design-system/patterns/multiselect/multiselect.component.ts`).

### Public types

- `WblMultiselectVariant = 'text' | 'tag'`.
- `WblMultiselectSize = 'L' | 'M' | 'S'`.
- `WblMultiselectValueLayout = 'truncate' | 'wrap'`.
- `WblMultiselectItem = { id: string; label: string; disabled?: boolean }`.

### Inputs

- `size = input<WblMultiselectSize>('L')`
- `variant = input<WblMultiselectVariant>('text')`
- `items = input<readonly WblMultiselectItem[]>([])`
- `selectedIds = input<readonly string[]>([])`
- `opened = input(false)`
- `disabled = input(false)` — keeps the field closed and non-interactive: it
  cannot open the dropdown, render Tag close controls, or emit selection,
  clear, or Tag-removal changes.
- `label = input('Label')`
- `required = input(false)`
- `placeholder = input('Placeholder')`
- `description = input('')`
- `clearable = input(true)`
- `valueLayout = input<WblMultiselectValueLayout>('truncate')` — Text only.
- `maxVisibleItems = input<number | null>(2)` — Text only; `null` keeps all
  labels in the one-line summary.
- `ariaLabel = input<string | null>(null)`

The local filter is enabled for both variants. It is intentionally internal:
the component filters the supplied `items` by `label` and has no query input or
remote-search API.

### Outputs

- `selectedIdsChange = output<string[]>()`
- `openedChange = output<boolean>()`

### Slots and composition

- The component has no content-projection slot.
- It composes `wbl-dropdown` in `semanticRole="listbox"` and checkbox mode for
  its overlay. It uses `wbl-tag` only for the Tag presentation.

### Local evidence

- Code: `src/app/design-system/patterns/multiselect/multiselect.component.ts`.
- Focused test: `src/app/design-system/patterns/multiselect/multiselect.component.spec.ts`.
- Public export: `export * from './patterns/multiselect/multiselect.component';`
  in `src/app/design-system/index.ts`.
- Storybook: `storybook/stories/multiselect.stories.ts`
  (`Design System/Patterns/wbl-multiselect`).
