# List

- Entry point: `@koobiq/components/list`
- NgModule: `KbqListModule`
- Page: https://koobiq.io/en/components/list/overview

## Overview

A list shows a set of related items and lets the user select one or several of them. To pick a single
value from a closed set inside a form use [Select](https://koobiq.io/en/components/select.md), and for hierarchical data use
[Tree](https://koobiq.io/en/components/tree.md).

**Example: List** (`list-overview`)

```ts list-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqListModule } from '@koobiq/components/list';

@Component({
    selector: 'list-overview-example',
    imports: [KbqListModule, FormsModule],
    template: `
        <kbq-list-selection aria-label="Items" [(ngModel)]="selected">
            <kbq-list-option disabled [value]="'Item 1'">Item 1</kbq-list-option>
            <kbq-list-option [value]="'Item 2'">Item 2</kbq-list-option>
            <kbq-list-option [value]="'Item 3'">Item 3</kbq-list-option>
            <kbq-list-option [value]="'Item 4'">Item 4</kbq-list-option>
            <kbq-list-option [value]="'Item 5'">Item 5</kbq-list-option>
            <kbq-list-option [value]="'Item 6'">Item 6</kbq-list-option>
            <kbq-list-option [value]="'Item 7'">Item 7</kbq-list-option>
            <kbq-list-option [value]="'Item 8'">Item 8</kbq-list-option>
            <kbq-list-option [value]="'Item 9'">Item 9</kbq-list-option>
            <kbq-list-option [value]="'Item 10'">Item 10</kbq-list-option>
        </kbq-list-selection>
        <br />
        <div>Selected: {{ selected }}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListOverviewExample {
    selected = [];
}
```

### Selection

#### Clicking

By default a click selects exactly one option: `autoSelect` clears the rest of the selection first. Set
`autoSelect="false"` to make a click toggle only the option it landed on and leave the others alone.

`noUnselectLast` keeps the list from ending up empty: `Ctrl` + click on the only selected option is
ignored. Set it to `false` to allow deselecting everything.

Both are on by default and both are turned off automatically by `multiple="checkbox"`, where a click is
expected to toggle a single row. Setting either one yourself pins it, so a later change of `multiple`
leaves your value alone.

#### Multiple selection

`multiple` takes the mode as its value. `multiple="checkbox"` renders a checkbox in every option, so the
selection is readable without focus:

**Example: List multiple checkbox** (`list-multiple-checkbox`)

```ts list-multiple-checkbox-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqListModule } from '@koobiq/components/list';

@Component({
    selector: 'list-multiple-checkbox-example',
    imports: [KbqListModule, FormsModule],
    template: `
        <kbq-list-selection multiple="checkbox" aria-label="Items" [(ngModel)]="selected">
            <kbq-list-option [disabled]="true" [value]="'Item 1'">Item 1</kbq-list-option>
            <kbq-list-option [value]="'Item 2'">Item 2</kbq-list-option>
            <kbq-list-option [value]="'Item 3'">Item 3</kbq-list-option>
            <kbq-list-option [value]="'Item 4'">Item 4</kbq-list-option>
            <kbq-list-option [value]="'Item 5'">Item 5</kbq-list-option>
            <kbq-list-option [value]="'Item 6'">Item 6</kbq-list-option>
            <kbq-list-option [value]="'Item 7'">Item 7</kbq-list-option>
            <kbq-list-option [value]="'Item 8'">Item 8</kbq-list-option>
            <kbq-list-option [value]="'Item 9'">Item 9</kbq-list-option>
            <kbq-list-option [value]="'Item 10'">Item 10</kbq-list-option>
        </kbq-list-selection>
        <br />
        <div>Selected: {{ selected }}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListMultipleCheckboxExample {
    selected = [];
}
```

`multiple="keyboard"` selects without any checkbox: a plain click still moves the selection to one
option, and the user extends it with `Ctrl` + click or `Shift` + click.

**Example: List multiple keyboard** (`list-multiple-keyboard`)

```ts list-multiple-keyboard-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqListModule } from '@koobiq/components/list';

@Component({
    selector: 'list-multiple-keyboard-example',
    imports: [KbqListModule, FormsModule],
    template: `
        <kbq-list-selection multiple="keyboard" aria-label="Items" [(ngModel)]="selected">
            <kbq-list-option disabled [value]="'Item 1'">Item 1</kbq-list-option>
            <kbq-list-option [value]="'Item 2'">Item 2</kbq-list-option>
            <kbq-list-option [value]="'Item 3'">Item 3</kbq-list-option>
            <kbq-list-option [value]="'Item 4'">Item 4</kbq-list-option>
            <kbq-list-option [value]="'Item 5'">Item 5</kbq-list-option>
            <kbq-list-option [value]="'Item 6'">Item 6</kbq-list-option>
            <kbq-list-option [value]="'Item 7'">Item 7</kbq-list-option>
            <kbq-list-option [value]="'Item 8'">Item 8</kbq-list-option>
            <kbq-list-option [value]="'Item 9'">Item 9</kbq-list-option>
            <kbq-list-option [value]="'Item 10'">Item 10</kbq-list-option>
        </kbq-list-selection>
        <br />
        <div>Selected: {{ selected }}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListMultipleKeyboardExample {
    selected = [];
}
```

The mode is a closed set of values:

| value                                                  | mode             |
| ------------------------------------------------------ | ---------------- |
| `multiple="checkbox"`                                  | checkbox         |
| `multiple="keyboard"`                                  | keyboard         |
| `multiple`, `multiple="true"`, `[multiple]="true"`     | checkbox         |
| no attribute, `multiple="false"`, `[multiple]="false"` | single selection |

Single selection is the default, so the way to ask for it is to leave `multiple` off entirely.
Anything else — `multiple="single"` included — falls back to single selection and is reported in the
console in dev mode.

`multiple` is a normal input, so the mode can be bound and changed at any time:

```html
<kbq-list-selection [multiple]="mode()" [(ngModel)]="selected">
    <kbq-list-option [value]="item">{{ item }}</kbq-list-option>
</kbq-list-selection>
```

Narrowing a list that has several options selected keeps the first selected one, drops the rest, reports the
shortened value through the form control and emits `selectionChange` for each option it deselected.

Note that this applies to `kbq-list-selection` and `kbq-tree-selection` only. On
[Select](https://koobiq.io/en/components/select.md) and `kbq-tree-select`, `multiple` still cannot be changed after
initialization and throws if you try.

Subscribe to `selectionChange` rather than to `selectionModel.changed`: changing the mode replaces the
selection model, and a subscription taken on the model directly is left behind on the discarded instance.

#### Select all

When users often have to select every value, or to leave out just a few, put a "Select all" master checkbox
at the top of the list with the `selectAll` attribute — `multiple="checkbox"` mode only:

"Select all" is not rendered with single selection, `multiple="keyboard"`, `horizontal`, an empty list or a
virtual scroller.

**Example: List select all** (`list-select-all`)

```ts list-select-all-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqListModule } from '@koobiq/components/list';
import { KbqScrollbar } from '@koobiq/components/scrollbar';

@Component({
    selector: 'list-select-all-example',
    imports: [KbqListModule, KbqScrollbar, FormsModule],
    template: `
        <div class="example-list-select-all">
            <kbq-scrollbar class="example-list-select-all__scrollbar">
                <kbq-list-selection
                    class="layout-padding-horizontal-xxs"
                    aria-label="Incident types"
                    multiple="checkbox"
                    selectAll
                    [(ngModel)]="selected"
                >
                    @for (option of options; track option) {
                        <kbq-list-option [value]="option">{{ option }}</kbq-list-option>
                    }
                </kbq-list-selection>
            </kbq-scrollbar>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .example-list-select-all {
            width: 400px;
        }

        /* 8.5 rows of 32px: the half row is what says there is more below. The parent stays a block, because
           the scrollbar host is a flex item whose flex-basis would otherwise outrank this height. */
        .example-list-select-all__scrollbar {
            height: 272px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListSelectAllExample {
    protected readonly options = [
        'Account Compromise',
        'Data Breach',
        'Denial of Service (DoS)',
        'Insider Threat',
        'Malware Infection',
        'Man-in-the-Middle Attack',
        'Phishing',
        'Privilege Escalation',
        'Ransomware',
        'Social Engineering',
        'SQL Injection',
        'Supply Chain Attack',
        'Unauthorized Access',
        'Zero-Day Exploit'
    ];

    protected selected: string[] = [];
}
```

#### Matching values

The list compares the values coming from the form control with the values of its options by identity. When
the options carry objects, pass a `compareWith` function so that a value from the model is matched to its
option:

```html
<kbq-list-selection multiple="checkbox" [compareWith]="compareById" [(ngModel)]="selected">
    @for (item of items(); track item.id) {
        <kbq-list-option [value]="item">{{ item.name }}</kbq-list-option>
    }
</kbq-list-selection>
```

`compareWith` also decides when an option keeps its selection: replacing the objects behind `[value]` with
equal ones — an immutable update, a refetch — leaves the selection alone. Bind a stable reference: an
expression that builds a new function on every change detection pass makes the list re-match on each one.

### Grouping

**Example: List groups** (`list-groups`)

```ts list-groups-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqListModule } from '@koobiq/components/list';

@Component({
    selector: 'list-groups-example',
    imports: [KbqListModule],
    template: `
        <kbq-list-selection aria-label="Security controls">
            @for (category of securityCategories; track category) {
                <kbq-optgroup [disabled]="category.disabled" [label]="category.categoryName">
                    @for (securityItem of category.securityItems; track securityItem.id) {
                        <kbq-list-option [value]="securityItem.id">
                            {{ securityItem.displayName }}
                        </kbq-list-option>
                    }
                </kbq-optgroup>
            }
            <kbq-list-option [value]="'rbac-11'">Role-Based Access Control (RBAC)</kbq-list-option>
        </kbq-list-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListGroupsExample {
    securityCategories = [
        {
            categoryName: 'Network Security',
            securityItems: [
                { id: 'firewall-0', displayName: 'Firewall' },
                { id: 'vpn-1', displayName: 'Virtual Private Network (VPN)' },
                { id: 'ids-2', displayName: 'Intrusion Detection System (IDS)' }
            ]
        },
        {
            categoryName: 'Application Security',
            disabled: true,
            securityItems: [
                { id: 'sast-3', displayName: 'Static Application Security Testing (SAST)' },
                { id: 'owasp-4', displayName: 'OWASP Top 10' },
                { id: 'code-review-5', displayName: 'Code Review' }
            ]
        },
        {
            categoryName: 'Cloud Security',
            securityItems: [
                { id: 'iam-6', displayName: 'Identity and Access Management (IAM)' },
                { id: 'cspm-7', displayName: 'Cloud Security Posture Management (CSPM)' },
                { id: 'casb-8', displayName: 'Cloud Access Security Broker (CASB)' }
            ]
        },
        {
            categoryName: 'Endpoint Security',
            securityItems: [
                { id: 'antivirus-9', displayName: 'Antivirus' },
                { id: 'edr-10', displayName: 'Endpoint Detection and Response (EDR)' }
            ]
        }
    ];
}
```

### Horizontal list

`horizontal` lays the options out in a row and rebinds the navigation to the Left/Right arrows.

```html
<kbq-list-selection horizontal aria-label="Alignment">
    <kbq-list-option [value]="'left'">Left</kbq-list-option>
    <kbq-list-option [value]="'right'">Right</kbq-list-option>
</kbq-list-selection>
```

### Action button

**Example: List action button** (`list-action-button`)

```ts list-action-button-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeModule } from '@koobiq/components/badge';
import { PopUpPlacements } from '@koobiq/components/core';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqListModule } from '@koobiq/components/list';
import { KbqToolTipModule } from '@koobiq/components/tooltip';

@Component({
    selector: 'list-action-button-example',
    imports: [KbqListModule, FormsModule, KbqDropdownModule, KbqToolTipModule, KbqBadgeModule],
    template: `
        <kbq-list-selection style="width: 400px" aria-label="Elements" [autoSelect]="false" [(ngModel)]="selected">
            <kbq-list-option [value]="'An element with a very very very long name'">
                <div>Element with a very very very very very very long name</div>

                <kbq-option-action
                    [kbqDropdownTriggerFor]="dropdown"
                    [kbqPlacement]="PopUpPlacements.Right"
                    [kbqTooltip]="'Tooltip text'"
                />
            </kbq-list-option>
            @for (item of [1, 2, 3, 4, 5, 6, 7, 8, 9]; track item) {
                <kbq-list-option [value]="getText(item)">
                    <div class="layout-row layout-align-space-between">
                        Item {{ item }}
                        <kbq-badge style="align-self: center" badgeColor="theme" [compact]="true">badge</kbq-badge>
                    </div>
                    <kbq-option-action
                        [kbqDropdownTriggerFor]="dropdown"
                        [kbqPlacement]="PopUpPlacements.Right"
                        [kbqTooltip]="'Tooltip text'"
                    />
                </kbq-list-option>
            }
        </kbq-list-selection>
        <br />
        <div>Selected: {{ selected }}</div>

        <kbq-dropdown #dropdown>
            <button kbq-dropdown-item>item 1</button>
            <button kbq-dropdown-item>item 2</button>
            <button kbq-dropdown-item>item 3</button>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListActionButtonExample {
    selected = [];
    protected readonly PopUpPlacements = PopUpPlacements;

    getText(text: string | number): string {
        return `Item ${text}`;
    }
}
```

### Virtual scroll

**Example: List with virtual-scroll** (`list-virtual-scroll`)

```ts list-virtual-scroll-example.ts
import { ScrollingModule } from '@angular/cdk/scrolling';
import { JsonPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqListModule } from '@koobiq/components/list';
import { KbqScrollbarViewport } from '@koobiq/components/scrollbar';

@Component({
    selector: 'list-virtual-scroll-example',
    imports: [KbqListModule, FormsModule, ScrollingModule, JsonPipe, KbqScrollbarViewport],
    template: `
        <div>Selected: {{ selected | json }}</div>
        <br />
        <kbq-list-selection style="height: 200px" multiple aria-label="Items" [(ngModel)]="selected">
            <cdk-virtual-scroll-viewport
                kbqScrollbarViewport
                style="height: 100%"
                itemSize="32"
                minBufferPx="300"
                maxBufferPx="300"
            >
                <kbq-list-option *cdkVirtualFor="let option of options" [value]="option">
                    {{ option.label }}
                </kbq-list-option>
            </cdk-virtual-scroll-viewport>
        </kbq-list-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListVirtualScrollExample {
    readonly options = Array.from({ length: 10000 }).map((_, i) => ({
        id: i,
        label: `Option #${i}`
    }));

    selected = [];
}
```

### Keyboard

The list is a single tab stop. Tabbing into it focuses the first selected option, or the first item of the
list when nothing is selected. Inside it the arrows move the active option, `Home` and `End` jump to the
ends, `PageUp` and `PageDown` move by a page, `Space` and `Enter` toggle the active option, and typing
letters jumps to the option that starts with them.

#### Selecting everything

`Ctrl`/`Cmd` + `A` selects every option that is not disabled, in multiple selection mode only. By default a
repeated press keeps them selected; `selectAllToggle` makes it deselect them instead. While a
[`selectAll`](#select-all) row is on screen the shortcut always toggles both ways, so it and the master
checkbox cannot disagree; in the modes that render no row it keeps its select-only default. The batch is
reported through `onSelectAll`, which carries the options the shortcut could act on; `selectionChange` stays
silent for it.

The behaviour can be replaced wholesale with the `selectAllHandler` input. It receives the keyboard event
and the list, and it has to be a function — anything else throws. Note that the handler replaces the
shortcut only: the master checkbox keeps running the built-in toggle, so with `selectAll` on you own both
paths.

#### Copying

`Ctrl`/`Cmd` + `C` copies the active option. Subscribe to `onCopy` to decide what lands in the clipboard:
the event carries the list, the option and the original keyboard event, and the list does nothing else.
Without a subscriber the list falls back to copying `String(value)` of the active option itself.

### Drag and drop

Set the `draggable` property on `kbq-list-selection` to let the user reorder options.

A single option can opt out with `draggable="false"` on `kbq-list-option`. It cannot be picked up by
either the pointer or the keyboard, but — unlike a disabled option — it still takes focus and can be
selected. The rest of the list keeps moving around it, so its own index can still shift.

The list does not open a gap while an option is being dragged: the surrounding options stay put, the
dragged one keeps its place as a faded row, and a line marks the position the option would land in.
No line is shown while the pointer is over the place the option already occupies — dropping it there
would change nothing.

Nothing else reacts while the drag lasts. The options stop responding to hover, because what takes the
drop is the list rather than the row under the pointer, and the cursor turns to `no-drop` whenever the
pointer leaves every list that would accept the option.

What follows the pointer is a plate carrying the option's own text — its label, and its caption on a
line of its own — cut off with an ellipsis once it reaches its maximum width. Set `dragPreview="full"`
on `kbq-list-selection` to drag a copy of the whole row instead, checkbox, icons and action button
included.

```html
<kbq-list-selection draggable dragPreview="full" (dropped)="dropped($event)">
    <kbq-list-option [value]="item">{{ item.name }}</kbq-list-option>
</kbq-list-selection>
```

A draggable row keeps the cursor it would have anyway — it is still a row that can be clicked and
selected, and being draggable is no reason to stop looking like one. Set `dragCursor="grab"` where the
whole row is meant to read as a handle:

```html
<kbq-list-selection draggable dragCursor="grab" (dropped)="dropped($event)">
    <kbq-list-option [value]="item">{{ item.name }}</kbq-list-option>
</kbq-list-selection>
```

The list never changes the data itself — it reports the move through the `dropped` event and you
apply it, usually with `moveItemInArray` from `@angular/cdk/drag-drop`. Track the options by their
identity (`track item.id`): with a positional key such as `track $index` the option at a given
position is kept and rebound to a different value, and an option drops its selection when its value
changes.

**Example: Draggable list** (`list-draggable`)

```ts list-draggable-example.ts
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { KbqDividerModule } from '@koobiq/components/divider';
import { KbqListModule, KbqListSelectionDroppedEvent } from '@koobiq/components/list';

@Component({
    selector: 'list-draggable-example',
    imports: [KbqListModule, KbqDividerModule],
    template: `
        <kbq-list-selection multiple="checkbox" [draggable]="true" (dropped)="dropped($event)">
            @for (item of beforeGroup(); track item.id) {
                <kbq-list-option [value]="item">{{ item.name }}</kbq-list-option>
            }
            <kbq-optgroup label="Group header">
                @for (item of grouped(); track item.id) {
                    <kbq-list-option [value]="item">{{ item.name }}</kbq-list-option>
                }
            </kbq-optgroup>
            <!-- Decorative: a listbox owns options and groups, and a separator has nothing to announce. -->
            <kbq-divider aria-hidden="true" />
            @for (item of afterDivider(); track item.id) {
                <kbq-list-option [draggable]="!item.pinned" [value]="item">{{ item.name }}</kbq-list-option>
            }
        </kbq-list-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListDraggableExample {
    protected readonly items = signal([
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' },
        { id: 4, name: 'Item 4' },
        { id: 5, name: 'Item 5' },
        { id: 6, name: 'Item 6' },
        { id: 7, name: 'Item 7. Sorting is prohibited', pinned: true }
    ]);

    // The group and the divider sit at fixed positions, so the sections are plain slices and the indices
    // `dropped` reports address the array directly. An option dragged past a boundary changes section.
    protected readonly beforeGroup = computed(() => this.items().slice(0, 2));
    protected readonly grouped = computed(() => this.items().slice(2, 5));
    protected readonly afterDivider = computed(() => this.items().slice(5));

    protected dropped({ previousIndex, currentIndex }: KbqListSelectionDroppedEvent): void {
        const items = [...this.items()];

        moveItemInArray(items, previousIndex, currentIndex);

        this.items.set(items);
    }
}
```

An option can also be picked up by one area of the row instead of the whole of it: project an element
carrying `cdkDragHandle` into it, and only that element starts a drag. A handle always shows the grab
cursor — that is what it is for, so it needs no `dragCursor`, and the row around it keeps its own cursor
whatever the list asks for. The directive is re-exported from `KbqListModule`, so nothing else has to be
imported.

**Example: Draggable list with a handle** (`list-draggable-handle`)

```ts list-draggable-handle-example.ts
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqListModule, KbqListSelectionDroppedEvent } from '@koobiq/components/list';

@Component({
    selector: 'list-draggable-handle-example',
    imports: [KbqListModule, KbqIconModule],
    template: `
        <kbq-list-selection [draggable]="true" (dropped)="dropped($event)">
            @for (item of items(); track item.id) {
                <kbq-list-option [value]="item">
                    <i kbq-icon="kbq-grip-vertical-s_16" cdkDragHandle></i>
                    {{ item.name }}
                </kbq-list-option>
            }
        </kbq-list-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListDraggableHandleExample {
    protected readonly items = signal([
        { id: 1, name: 'Banana' },
        { id: 2, name: 'Cherry' },
        { id: 3, name: 'Grapefruit' },
        { id: 4, name: 'Mango' }
    ]);

    protected dropped({ previousIndex, currentIndex }: KbqListSelectionDroppedEvent): void {
        const items = [...this.items()];

        moveItemInArray(items, previousIndex, currentIndex);

        this.items.set(items);
    }
}
```

Options can also be moved into another list. Pass the other `kbq-list-selection` through `connectedTo`
on both lists, and apply the move with `transferArrayItem`. An option arrives in the target list
unselected unless the target's own value already contains it.

**Example: Draggable list with transfer between lists** (`list-draggable-connected`)

```ts list-draggable-connected-example.ts
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core';
import { KbqListModule, KbqListSelectionDroppedEvent } from '@koobiq/components/list';

type Metric = { id: number; name: string };

@Component({
    selector: 'list-draggable-connected-example',
    imports: [KbqListModule],
    template: `
        <div class="layout-row layout-gap-l">
            <kbq-list-selection
                #available="kbqListSelection"
                class="flex"
                dragCursor="grab"
                [connectedTo]="[selected]"
                [draggable]="true"
                (dropped)="dropped($event)"
            >
                @for (metric of availableMetrics(); track metric.id) {
                    <kbq-list-option [value]="metric">{{ metric.name }}</kbq-list-option>
                }
            </kbq-list-selection>

            <kbq-list-selection
                #selected="kbqListSelection"
                class="flex"
                dragCursor="grab"
                [connectedTo]="[available]"
                [draggable]="true"
                (dropped)="dropped($event)"
            >
                @for (metric of selectedMetrics(); track metric.id) {
                    <kbq-list-option [value]="metric">{{ metric.name }}</kbq-list-option>
                }
            </kbq-list-selection>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListDraggableConnectedExample {
    protected readonly availableMetrics = signal<Metric[]>([
        { id: 1, name: 'CPU load' },
        { id: 2, name: 'Memory usage' },
        { id: 3, name: 'Disk I/O' }
    ]);

    protected readonly selectedMetrics = signal<Metric[]>([{ id: 4, name: 'Network traffic' }]);

    protected dropped({
        previousIndex,
        currentIndex,
        previousContainer,
        container,
        option
    }: KbqListSelectionDroppedEvent): void {
        // `previousContainer` and `container` identify the lists, but only the consumer knows which
        // array backs which list — here the dragged value itself is the lookup key.
        const fromAvailable = this.availableMetrics().includes(option.value);
        const source = fromAvailable ? this.availableMetrics : this.selectedMetrics;

        if (previousContainer === container) {
            this.reorder(source, previousIndex, currentIndex);

            return;
        }

        const target = fromAvailable ? this.selectedMetrics : this.availableMetrics;

        this.transfer(source, target, previousIndex, currentIndex);
    }

    private reorder(list: WritableSignal<Metric[]>, previousIndex: number, currentIndex: number): void {
        const items = [...list()];

        moveItemInArray(items, previousIndex, currentIndex);

        list.set(items);
    }

    private transfer(
        source: WritableSignal<Metric[]>,
        target: WritableSignal<Metric[]>,
        previousIndex: number,
        currentIndex: number
    ): void {
        const from = [...source()];
        const to = [...target()];

        transferArrayItem(from, to, previousIndex, currentIndex);

        source.set(from);
        target.set(to);
    }
}
```

Options are numbered across the whole list, so a `kbq-optgroup` can be reordered through and an option
dragged past its boundary changes group. Dragging is not supported inside `cdk-virtual-scroll-viewport`,
though: the indices reported by `dropped` count only the rendered options, so applying the move to the
backing array silently affects the wrong item. That combination logs a warning in development mode.

Reordering is pointer-only: there is no keyboard equivalent, so it is out of reach for anyone who
cannot drag.

Lists connected by `id` rather than by a component reference show no drop indicator — an `id` cannot
be resolved back to the list instance that would have to draw it.

### Accessibility

`kbq-list-selection` is announced as a `listbox` and every `kbq-list-option` as an `option` carrying its own `aria-selected`. The "select all" row stays an `option` too — a `checkbox` is not a valid child of a listbox — and reports the batch state on `aria-checked`, including `mixed` for a partial selection; it carries no `aria-selected` of its own. With `multiple` the list is additionally marked `aria-multiselectable`, and with `horizontal` it reports `aria-orientation="horizontal"` and moves the active option with the Left/Right arrows. The list is a single tab stop; roving focus moves between the options. A disabled list or option is reported through `aria-disabled`. The built-in pseudo-checkbox is decorative and stays out of the accessibility tree; if you project your own `kbq-pseudo-checkbox` instead (`externalPseudoCheckbox`), mark it `aria-hidden="true"` too — the option's own `aria-selected` already carries the selected state.

A listbox needs an accessible name, so give the list one with `aria-label` or `aria-labelledby`:

```html
<kbq-list-selection aria-label="Mailboxes">
    <kbq-list-option [value]="'inbox'">Inbox</kbq-list-option>
    <kbq-list-option [value]="'starred'">Starred</kbq-list-option>
</kbq-list-selection>
```

`kbq-list` and `kbq-list-item` deliberately carry no role: they are a plain container used both for semantic lists and for purely visual grouping. Add `role="list"` / `role="listitem"` yourself when the content is a real list.

### Recommendations

- Give every `kbq-list-selection` an accessible name. It is announced as a listbox, and a listbox without
  a name tells a screen reader nothing about what is being chosen.
- Prefer `multiple="checkbox"` when the selection has to be readable at a glance. `multiple="keyboard"`
  keeps the rows compact but hides the state from anyone who does not know to look for the highlight.
- Reach for virtual scroll once the list is long enough to scroll for a while, but remember that dragging
  cannot be combined with it.
- Keep the option label short enough to fit on one line. A list is a picker, not a place for paragraphs —
  move the detail into a caption or a tooltip.

## API

### Components

#### KbqList (component)

A plain, non-selectable list container.

Intentionally carries no ARIA role: it is used both as a semantic list (consumers add
`role="list"` themselves, as `kbq-file-upload` does) and as a purely visual grouping.
For a selectable listbox use `kbq-list-selection` instead.

```ts
@Component({ selector: 'kbq-list' })
class KbqList {}
```

#### KbqListItem (component)

A single row of a `kbq-list`. Non-selectable — see `kbq-list-option` for the selectable variant.

```ts
@Component({ selector: 'kbq-list-item, a[kbq-list-item]' })
class KbqListItem {
    getHostElement(): HTMLElement;
}
```

#### KbqListOption (component)

Component for list-options of selection-list. Each list-option can automatically
generate a checkbox and can put current item into the selectionModel of selection-list
if the current item is selected.

```ts
@Component({ selector: 'kbq-list-option', exportAs: 'kbqListOption' })
class KbqListOption<T = any> implements IFocusableOption, KbqTitleTextRef {
    readonly checkboxPosition = input<'before' | 'after'>();
    value = input<T>();
    disabled = input<boolean>(false);
    draggable = input<boolean>(true);
    showCheckbox = input<boolean | undefined>();
    selected = input<boolean | undefined>();
    listSelection: KbqListSelection<T>;
    readonly group: KbqOptgroup | null;
    hasFocus: boolean;
    preventBlur: boolean;
    readonly onFocus: Subject<KbqOptionEvent<T>>;
    readonly onBlur: Subject<KbqOptionEvent<T>>;
    readonly actionButton: Signal<KbqOptionActionComponent | undefined>;
    get tooltipTrigger(): KbqTooltipTrigger | undefined;
    get dropdownTrigger(): KbqDropdownTrigger | undefined;
    readonly pseudoCheckbox: Signal<KbqPseudoCheckbox | undefined>;
    readonly text: Signal<ElementRef<any>>;
    get textElement(): ElementRef<HTMLElement> | undefined;
    toggle(): void;
    getLabel(): string;
    setSelected(selected: boolean): void;
    focus(): void;
    blur(): void;
    getHostElement(): HTMLElement;
}
```

- `[value]`: `T` — Value of the option, reported through the list's `ControlValueAccessor`.
- `[disabled]`: `boolean` — Whether the option is disabled, through its own input, its group or the list.
- `[draggable]`: `boolean` — Whether the option can be reordered by dragging. Set it to `false` to pin a single option while
  the rest of the list stays draggable. Unrelated to `disabled`: the option keeps taking focus and
  selection. Reads the resolved state, which the option can only narrow — the list gates its whole drop
  list, so an option cannot opt back in on its own.
- `[showCheckbox]`: `boolean | undefined` — Whether the option renders a pseudo-checkbox. Falls back to the list's own mode until bound.
- `[selected]`: `boolean | undefined` — Whether the option is selected. The state lives in the list's `SelectionModel`, not on the option,
  so it can also be set from code.
- `tooltipTrigger`: `KbqTooltipTrigger | undefined` — Tooltip trigger projected into the option.
- `dropdownTrigger`: `KbqDropdownTrigger | undefined` — Dropdown trigger projected into the option.
- `textElement`: `ElementRef<HTMLElement> | undefined` — Not a duplicate of `text`: this is the `KbqTitleTextRef` property that `title.directive.ts`
  reads through `KBQ_TITLE_TEXT_REF`, a plain `ElementRef` rather than a signal.
- `toggle()`: `void` — Toggles the selected state of this option.
- `setSelected()`: `void` — Sets the selected state on the list's `SelectionModel`; the `selected` property and input go through it.
- `focus()`: `void` — Moves DOM focus to this option, unless it is disabled or already focused.
- `blur()`: `void` — Marks this option as blurred after the next render, unless `preventBlur` is set.
- `getHostElement()`: `HTMLElement` — The option's host DOM element.

#### KbqListSelection (component)

```ts
@Component({ selector: 'kbq-list-selection', exportAs: 'kbqListSelection' })
class KbqListSelection<T = any> implements ControlValueAccessor {
    autoSelect = input<boolean | undefined>();
    noUnselectLast = input<boolean | undefined>();
    draggable = input<boolean>(false);
    readonly connectedTo = input<KbqListSelection<T> | string | readonly (KbqListSelection<T> | string)[]>(
        []
    );
    readonly dragPreview = input<KbqListDragPreview>('text');
    readonly dragCursor = input<KbqListDragCursor>('auto');
    readonly selectAllToggle = input<boolean>(false);
    readonly selectAllEnabled = input<boolean>(false, {
        alias: 'selectAll'
    });
    multiple = input<KbqMultipleInput | undefined>();
    readonly horizontal = input<boolean>(false);
    disabled = input<boolean | undefined>();
    readonly compareWith = input<(o1: T, o2: T) => boolean>(
        (a1, a2) => a1 === a2
    );
    selectAllHandler = input<((event: KeyboardEvent, list: KbqListSelection<T>) => void) | undefined>();
    readonly onSelectAll = output<KbqListSelectAllEvent<KbqListOption<T>>>();
    readonly dropped = output<KbqListSelectionDroppedEvent>();
    readonly selectionChange = output<KbqListSelectionChange<T>>();
    keyManager: FocusKeyManager<KbqListOption<T>>;
    get options(): QueryList<KbqListOption<T>>;
    readonly onCopy: EventEmitter<KbqListCopyEvent<KbqListOption<T>>>;
    readonly selectAllOption: Signal<KbqListOption<T> | undefined>;
    get allOptionsSelected(): boolean;
    get selectAllState(): KbqPseudoCheckboxState;
    multipleMode: MultipleMode | null;
    userTabIndex: number | null;
    get showCheckbox(): boolean;
    selectionModel: SelectionModel<KbqListOption<T>>;
    get optionFocusChanges(): Observable<KbqOptionEvent<T>>;
    get optionBlurChanges(): Observable<KbqOptionEvent<T>>;
    toggleSelectAll(): void;
    focus(): void;
    blur(): void;
    selectAll(): void;
    deselectAll(): void;
    updateScrollSize(): void;
    setSelectedOptionsByClick(
        option: KbqListOption<T>,
        shiftKey: boolean,
        ctrlKey: boolean
    ): void;
    setSelectedOptionsByKey(
        option: KbqListOption<T>,
        shiftKey: boolean,
        ctrlKey: boolean
    ): void;
    selectActiveOptions(): void;
    getSelectedOptionValues(): T[];
    toggleFocusedOption(): void;
    canDeselectLast(listOption: KbqListOption<T>): boolean;
    removeOptionFromList(option: KbqListOption<T>): void;
    onKeyDown(event: KeyboardEvent): void;
    reportValueChange(): void;
    emitChangeEvent(option: KbqListOption<T>): void;
}
```

- `[autoSelect]`: `boolean | undefined` — Whether clicking an option clears the rest of the selection. Defaults to `true`, and to `false` for
  `multiple="checkbox"`, where a click is expected to toggle a single row. A bound value replaces the
  default derived from `multiple`.
- `[noUnselectLast]`: `boolean | undefined` — Whether the last selected option can be deselected. Derived from the mode like `autoSelect`.
- `[draggable]`: `boolean` — Whether options can be reordered by dragging them.
  Reordering never mutates the data — handle the `dropped` event and move the item yourself.
- `[connectedTo]`: `KbqListSelection<T> | string | readonly (KbqListSelection<T> | string)[]` — Lists that options of this list can be moved into. Accepts `KbqListSelection` instances or the
  `id` of another list. `cdkDropListGroup` on a common ancestor connects lists automatically.
- `[dragPreview]`: `KbqListDragPreview` — What the option's floating copy looks like while it is dragged. Defaults to `text`: a compact
  plate carrying the option's label and caption, capped at a readable width. Use `full` to drag a
  clone of the whole row, checkbox, icons and action button included.
- `[dragCursor]`: `KbqListDragCursor` — Whether a draggable option shows the grab cursor over the whole row. Defaults to `auto`, which
  leaves the row the cursor it has anyway. Use `grab` where the whole row is meant to read as a
  handle. An option carrying a projected `cdkDragHandle` ignores this — the handle keeps the grab.
- `[selectAllToggle]`: `boolean` — When `true`, a repeated Ctrl/Cmd+A deselects all options. Off by default (Ctrl+A only selects).
- `[selectAll]`: `boolean` — Whether to render the "select all" master checkbox above the options. `multiple="checkbox"` only —
  a vertical list, since the row is a full-width heading of the column it governs.

  The row acts on every option the user can toggle, disabled ones excluded. Enabling it also makes
  Ctrl/Cmd + A a two-way toggle, so the shortcut and the checkbox never disagree (`selectAllToggle`
  is implied).

  Aliased rather than named `selectAll`, which is already taken by the imperative method of that name.
- `[multiple]`: `KbqMultipleInput | undefined` — Selection mode of the list.

  `checkbox` renders a checkbox in every option, `keyboard` selects without one. A bare `multiple` and
  `true` both mean `checkbox`; `single`, `false` and an absent attribute mean single selection. The mode
  can be changed at any time — narrowing it to single selection keeps the first selected option and
  reports the shortened value through the `ControlValueAccessor`.

  The getter reports whether more than one option can be selected; read `multipleMode` for the
  mode itself.
- `[disabled]`: `boolean | undefined` — Whether the list is disabled, bound with `[disabled]` or set by a form control.
- `[compareWith]`: `(o1: T, o2: T) => boolean` — Function used for comparing an option against the selected value when determining which
  options should appear as selected. The first argument is the value of an options. The second
  one is a value from the selected value. A boolean must be returned.

  Bind a stable reference — a field or a bound method. An expression that builds a new function on
  every change detection pass makes the list re-resolve every value against every option each pass.
- `[selectAllHandler]`: `((event: KeyboardEvent, list: KbqListSelection<T>) => void) | undefined` — Function for handling the combination Ctrl + A (select all). By default, the internal handler is used,
  which toggles the selection of all non-disabled options.
- `(dropped)`: `KbqListSelectionDroppedEvent` — Emits when an option changes its position by dragging.
- `onCopy`: `EventEmitter<KbqListCopyEvent<KbqListOption<T>>>` — Emits when the active option is copied with Ctrl/Cmd + C. Subscribing replaces the built-in clipboard
  copy, so the event stays an `EventEmitter`: the decision is made by reading `observed`, which
  `OutputEmitterRef` does not have. A template listener subscribes to it through the output below.
- `selectAllOption`: `Signal<KbqListOption<T> | undefined>` — Reference to the built-in "select all" row, rendered only while `selectAll` is on.

  Located by template reference rather than by the `KbqListOption` type: that class is declared
  below this one in the same file, and a query locator is evaluated while this class is being
  defined, which would read it in its temporal dead zone.
- `allOptionsSelected`: `boolean` — Whether every option "select all" can act on is selected.
- `selectAllState`: `KbqPseudoCheckboxState` — State of the "select all" master checkbox.
- `multipleMode`: `MultipleMode | null` — Resolved selection mode, or `null` when only one option can be selected.
- `selectionModel`: `SelectionModel<KbqListOption<T>>` — Model backing the selection.

  The instance is replaced whenever `multiple` changes the multiplicity, because CDK freezes that
  flag at construction. Subscribe to `selectionChange` rather than to `selectionModel.changed`:
  a subscription taken on the model directly is left behind on the discarded instance.
- `toggleSelectAll()`: `void` — Selects every option "select all" can act on, or deselects them all when they are already selected.

  Backs both the master checkbox and Ctrl/Cmd + A, so the two can never disagree. Reports the value
  once for the whole batch instead of once per option.
- `blur()`: `void` — Clears the active key-manager item (unless an option is still focused) and marks the list as touched.
- `selectAll()`: `void` — Selects every option and reports the new value to the `ControlValueAccessor`.

  Unlike the `selectAll` attribute and Ctrl/Cmd + A, this acts on disabled options as well — it is
  an imperative command, not something the user performed. See `toggleSelectAll` for the
  behaviour the master checkbox has.
- `deselectAll()`: `void` — Deselects every option, disabled ones included, and reports the new value. See `selectAll`.
- `getSelectedOptionValues()`: `T[]` — Values of the currently selected options.
- `canDeselectLast()`: `boolean` — Whether `listOption` is allowed to be deselected, given `noUnselectLast`.
- `onKeyDown()`: `void` — Handles keydown events on the list.

### Directives

#### KbqListOptionCaption (directive)

Marker directive for the secondary line of a `kbq-list-option`.

Purely a styling hook: it adds the `kbq-list-option-caption` class the list theme keys its
muted caption typography and per-state colors off.

```ts
@Directive({ selector: '[kbq-list-option-caption]' })
class KbqListOptionCaption {}
```

### Classes

#### KbqListCopyEvent (class)

Event class that occurs when copying an item from the KbqListSelection.
Used to pass data about the copied item and copy context.

```ts
class KbqListCopyEvent<T> {
    source: KbqListSelection<any>;
    option: T;
    event?: KeyboardEvent;
    constructor(
        source: KbqListSelection<any>,
        option: T,
        event?: KeyboardEvent
    );
}
```

#### KbqListSelectAllEvent (class)

```ts
class KbqListSelectAllEvent<T> {
    source: KbqListSelection<any>;
    options: T[];
    constructor(source: KbqListSelection<any>, options: T[]);
}
```

#### KbqListSelectionChange (class)

```ts
class KbqListSelectionChange<T = any> {
    source: KbqListSelection<T>;
    option: KbqListOption<T>;
    constructor(source: KbqListSelection<T>, option: KbqListOption<T>);
}
```

### Types

#### KbqListDragCursor (type)

Whether a draggable option advertises with the cursor that the whole row can be picked up.

`auto` leaves the row the cursor it would have anyway, `grab` turns the row into a visible handle.
A projected `cdkDragHandle` takes the grab over either way.

```ts
type KbqListDragCursor = 'auto' | 'grab';
```

#### KbqListDragPreview (type)

How the floating copy of an option is rendered while it is dragged.

`text` shows a compact plate with the option's own text and nothing else, `full` a clone of the
whole row.

```ts
type KbqListDragPreview = 'text' | 'full';
```

#### KbqListSelectionDroppedEvent (type)

Event emitted when an option changes its position by dragging.

```ts
type KbqListSelectionDroppedEvent = Pick<CdkDragDrop<KbqListSelection>, 'previousIndex' | 'currentIndex'> & {
    option: KbqListOption;
    container: KbqListSelection;
    previousContainer: KbqListSelection;
    event: MouseEvent | TouchEvent;
};
```

- `option`: `KbqListOption` — Option that has been moved.
- `container`: `KbqListSelection` — List the option has been moved into.
- `previousContainer`: `KbqListSelection` — List the option has been taken from. Equal to `container` when reordering within a single list.
- `event`: `MouseEvent | TouchEvent` — Pointer event that ended the drag.

#### KbqOptionEvent (interface)

```ts
interface KbqOptionEvent<T = any> {
    option: KbqListOption<T>;
}
```

### Constants

#### KBQ_SELECTION_LIST_VALUE_ACCESSOR (const)

```ts
const KBQ_SELECTION_LIST_VALUE_ACCESSOR: Provider;
```

## Examples

### Multiple selection with mixed state

In the privilege assignment interface, clicking the "Assign" button opens a window where you can specify which users should receive a specific privilege. A list item with a checkbox in the indeterminate state indicates that the privilege is assigned to only some users. Activating this list item will assign the privilege to all selected users.

**Example: List multiple checkbox** (`list-intermediate-state`)

```ts list-intermediate-state-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqPseudoCheckbox, KbqPseudoCheckboxState, PopUpPlacements, PopUpSizes } from '@koobiq/components/core';
import { KbqEmptyState } from '@koobiq/components/empty-state';
import { KbqTrim } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInput, KbqInputModule } from '@koobiq/components/input';
import { KbqListModule, KbqListSelectionChange } from '@koobiq/components/list';
import { KbqPopoverModule, KbqPopoverTrigger } from '@koobiq/components/popover';
import { KbqScrollbar } from '@koobiq/components/scrollbar';
import { KbqUsernameModule } from '@koobiq/components/username';
import { merge, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

interface ExampleUser {
    lastName: string;
    firstName: string;
    middleName: string;
    login: string;
    displayName: string;
}

@Component({
    selector: 'list-intermediate-state-example',
    imports: [
        KbqListModule,
        FormsModule,
        KbqButtonModule,
        KbqIconModule,
        KbqInputModule,
        KbqInput,
        KbqPopoverModule,
        KbqTrim,
        KbqUsernameModule,
        KbqPseudoCheckbox,
        ReactiveFormsModule,
        AsyncPipe,
        KbqEmptyState,
        KbqScrollbar
    ],
    templateUrl: './list-intermediate-state-example.html',
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;

            padding-top: var(--kbq-size-7xl);
            padding-bottom: var(--kbq-size-7xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListIntermediateStateExample {
    PopUpSizes = PopUpSizes;
    PopUpPlacements = PopUpPlacements;

    users: ExampleUser[] = [
        {
            lastName: '',
            firstName: '',
            middleName: '',
            login: 'Administrator',
            displayName: 'Administrator'
        },
        {
            lastName: '',
            firstName: 'Алексей',
            middleName: '',
            login: 'AlexFree',
            displayName: 'AlexFree'
        },
        {
            lastName: '',
            firstName: '',
            middleName: '',
            login: 'NightWolf',
            displayName: 'NightWolf'
        },
        {
            lastName: '',
            firstName: '',
            middleName: '',
            login: 'SkyRider',
            displayName: 'SkyRider'
        },
        {
            lastName: 'Smith',
            firstName: 'John',
            middleName: '',
            login: 'agent.smith',
            displayName: 'Smith J.'
        },
        {
            lastName: 'Волкова',
            firstName: '',
            middleName: '',
            login: 'VolkovaV',
            displayName: 'VolkovaV'
        },
        {
            lastName: '',
            firstName: 'Ольга',
            middleName: '',
            login: 'olga_f',
            displayName: 'olga_f'
        },
        {
            lastName: 'Белова',
            firstName: 'Татьяна',
            middleName: 'Михайловна',
            login: 'tat_bel',
            displayName: 'Белова Т. М.'
        },
        {
            lastName: 'Громов',
            firstName: 'Дмитрий',
            middleName: 'Игоревич',
            login: 'grom_d',
            displayName: 'Громов Д. И.'
        },
        {
            lastName: 'Жуков',
            firstName: 'Роман',
            middleName: '',
            login: 'RomanZhuk',
            displayName: 'Жуков Р.'
        },
        {
            lastName: 'Иванов',
            firstName: 'Павел',
            middleName: 'Сергеевич',
            login: 'pavel_i',
            displayName: 'Иванов П. С.'
        },
        {
            lastName: 'Костин',
            firstName: 'Олег',
            middleName: 'Семёнович',
            login: 'kostin_o',
            displayName: 'Костин О. С.'
        },
        {
            lastName: 'Кузнецов',
            firstName: 'Максим',
            middleName: '',
            login: 'mkuznetsov',
            displayName: 'Кузнецов М.'
        },
        {
            lastName: 'Макаров',
            firstName: 'Кирилл',
            middleName: 'Петрович',
            login: 'kirillmak',
            displayName: 'Макаров К. П.'
        },
        {
            lastName: 'Никитин',
            firstName: 'Степан',
            middleName: 'Андреевич',
            login: 'step_nik',
            displayName: 'Никитин С. А.'
        },
        {
            lastName: 'Орлова',
            firstName: 'Мария',
            middleName: '',
            login: 'MashaBright',
            displayName: 'Орлова М.'
        },
        {
            lastName: 'Петров',
            firstName: 'Илья',
            middleName: 'Александрович',
            login: 'petrov_ilya',
            displayName: 'Петров И. А.'
        },
        {
            lastName: 'Сидорова',
            firstName: 'Елена',
            middleName: '',
            login: 'ElenaStar',
            displayName: 'Сидорова Е.'
        },
        {
            lastName: 'Смирнова',
            firstName: 'Вероника',
            middleName: '',
            login: 'VeraSmile',
            displayName: 'Смирнова В.'
        },
        {
            lastName: 'Соколова',
            firstName: 'Анна',
            middleName: 'Дмитриевна',
            login: 'anna_s',
            displayName: 'Соколова А. Д.'
        },
        {
            lastName: 'Тихонова',
            firstName: 'Валерия',
            middleName: 'Алексеевна',
            login: 'val_ti',
            displayName: 'Тихонова В. А.'
        },
        {
            lastName: 'Яковлев',
            firstName: 'Егор',
            middleName: '',
            login: 'egor_y',
            displayName: 'Яковлев Е.'
        }
    ];

    usersSelectedInOtherGroups: ExampleUser[] = [this.users[0]];
    savedUsersSelectedInOtherGroups: ExampleUser[] = [];

    selected: ExampleUser[] = [this.users[5], this.users[8], this.users[11]];
    savedSelection: ExampleUser[] = [];
    needRestore: boolean = true;

    searchControl: UntypedFormControl = new UntypedFormControl();
    filteredUsers: Observable<ExampleUser[]>;

    readonly popover = viewChild.required(KbqPopoverTrigger);

    constructor() {
        this.filteredUsers = merge(
            of(this.users),
            this.searchControl.valueChanges.pipe(map((value) => this.getFilteredUsers(value)))
        );
    }

    getCheckboxState(user: ExampleUser): KbqPseudoCheckboxState {
        if (this.selected.includes(user)) return true;

        if (this.usersSelectedInOtherGroups.includes(user)) return 'indeterminate';

        return false;
    }

    onVisibleChange(state: boolean) {
        if (state) {
            this.saveSelectionState();
        } else {
            if (this.needRestore) {
                this.restoreSelectionState();
            }

            this.popover().focus();
        }
    }

    onApply() {
        this.needRestore = false;
        this.popover().hide(0);
        this.popover().focus();
    }

    onSelectionChange(event: KbqListSelectionChange) {
        if (!event.option.selected) {
            this.usersSelectedInOtherGroups.splice(event.option.value, 1);
        }
    }

    private getFilteredUsers(value): ExampleUser[] {
        const searchFilter = value.toLowerCase();

        return searchFilter ? this.users.filter((user) => this.userHasText(user, searchFilter)) : this.users;
    }

    private userHasText(user: ExampleUser, text: string): boolean {
        return (user.lastName + user.firstName + user.middleName + user.login + user.displayName)
            .toLowerCase()
            .includes(text);
    }

    private saveSelectionState() {
        this.savedSelection = [...this.selected];
        this.savedUsersSelectedInOtherGroups = [...this.usersSelectedInOtherGroups];
        this.needRestore = true;
    }

    private restoreSelectionState() {
        this.selected = [...this.savedSelection];
        this.usersSelectedInOtherGroups = [...this.savedUsersSelectedInOtherGroups];
    }
}
```

```html ./list-intermediate-state-example.html
<button
    #kbqPopover="kbqPopover"
    kbq-button
    kbqPopover
    [kbqPopoverSize]="PopUpSizes.Medium"
    [kbqPopoverHeader]="customHeader"
    [kbqPopoverContent]="customContent"
    [kbqPopoverFooter]="customFooter"
    [kbqPopoverArrow]="false"
    [kbqPopoverOffset]="4"
    [kbqPopoverPlacement]="PopUpPlacements.BottomLeft"
    [kbqPopoverPlacementPriority]="PopUpPlacements.BottomLeft"
    [kbqPopoverDefaultPaddings]="false"
    (kbqPopoverVisibleChange)="onVisibleChange($event)"
>
    <i kbq-icon="kbq-user_16"></i>
    Назначить пользователей
    <i kbq-icon="kbq-chevron-down_16"></i>
</button>

<ng-template #customHeader>
    <div style="border-bottom: 1px solid var(--kbq-line-contrast-less)" [style.width.px]="400">
        <kbq-form-field class="layout-padding-vertical-xxs" noBorders>
            <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
            <input kbqInput placeholder="Пользователь" autocomplete="off" [formControl]="searchControl" />
        </kbq-form-field>
    </div>
</ng-template>

<ng-template #customContent>
    <kbq-scrollbar style="height: 374px">
        <kbq-list-selection
            class="layout-padding-horizontal-xxs layout-padding-vertical-xxs"
            multiple="checkbox"
            aria-label="Назначить пользователей"
            [(ngModel)]="selected"
            (selectionChange)="onSelectionChange($event)"
        >
            @for (user of filteredUsers | async; track user) {
                <kbq-list-option [showCheckbox]="false" [value]="user">
                    <kbq-pseudo-checkbox aria-hidden="true" [state]="getCheckboxState(user)" />
                    <kbq-username [userInfo]="user" />
                </kbq-list-option>
            }
        </kbq-list-selection>

        @if (!(filteredUsers | async)?.length) {
            <kbq-empty-state style="margin-top: 132px">
                <span kbq-empty-state-text>Ничего не найдено</span>
            </kbq-empty-state>
        }
    </kbq-scrollbar>
</ng-template>

<ng-template #customFooter>
    <div
        class="layout-row layout-gap-l layout-padding-vertical-l layout-padding-horizontal-xxl"
        style="border-top: 1px solid var(--kbq-line-contrast-less)"
    >
        <button kbq-button [color]="'contrast'" (click)="onApply()">Применить</button>
        <button kbq-button (click)="popover().hide(0)">Отмена</button>
    </div>
</ng-template>
```
