Список показывает набор связанных элементов и позволяет выбрать один или несколько. Для выбора одного значения из закрытого набора в форме используйте Select, а для иерархических данных — Tree.

По умолчанию клик выбирает ровно одну опцию: autoSelect предварительно снимает выбор с остальных. Задайте autoSelect="false", чтобы клик переключал только ту опцию, по которой попали, не трогая соседние.

noUnselectLast не даёт списку остаться пустым: Ctrl + клик по единственной выбранной опции игнорируется. Со значением false снять выбор со всех можно.

Оба свойства включены по умолчанию и оба автоматически выключаются в режиме multiple="checkbox", где от клика ждут переключения одной строки.

multiple принимает режим значением. multiple="checkbox" рисует чекбокс в каждой опции, поэтому выбор читается без фокуса:

multiple="keyboard" обходится без чекбоксов: обычный клик по-прежнему переносит выбор на одну опцию, а расширяют его Ctrl + кликом или Shift + кликом.

Голый multiple, как и любое другое значение, понимается как checkbox.

Свойство selectAll добавляет элемент для выбора всех доступных опций в режиме multiple="checkbox". Элемент не отображается в горизонтальном списке, при виртуальном скролле и в режиме multiple="keyboard".

Значения из форм-контрола список сопоставляет со значениями опций по идентичности. Если в опциях лежат объекты, передайте функцию compareWith, чтобы значение из модели нашло свою опцию:

<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 решает и то, когда опция сохраняет выбор: замена объектов за [value] на равные — например, после повторного запроса или пересоздания массива — выбор не сбрасывает. Передавайте стабильную ссылку: выражение, создающее новую функцию на каждом цикле проверки, заставляет список заново сопоставлять значения на каждом из них.

horizontal выкладывает опции в строку и переносит навигацию на стрелки влево и вправо.

<kbq-list-selection horizontal aria-label="Выравнивание">
    <kbq-list-option [value]="'left'">По левому краю</kbq-list-option>
    <kbq-list-option [value]="'right'">По правому краю</kbq-list-option>
</kbq-list-selection>

Список — одна точка табуляции. Переход в список ставит фокус на первую выбранную опцию, а когда не выбрано ничего — на первый элемент списка. Внутри стрелки переключают активную опцию, Home и End переводят к краям, PageUp и PageDown листают страницами, Space и Enter переключают активную опцию, а набор букв переносит к опции, которая с них начинается.

Ctrl/Cmd + A выделяет все опции, кроме отключённых, и работает только в режиме множественного выбора. По умолчанию повторное нажатие оставляет их выделенными; с selectAllToggle оно снимает выделение. Пока строка selectAll на экране, сочетание всегда работает в обе стороны, чтобы оно и мастер-чекбокс не расходились; в режимах, где строка не отображается, остаётся поведение «только выделить». О пачке сообщает событие onSelectAll — в нём приходят опции, на которые сочетание могло подействовать, а selectionChange на пачку не приходит.

Поведение целиком заменяется свойством selectAllHandler. В него приходят событие клавиатуры и сам список, и это должна быть функция — на любом другом значении будет брошено исключение. Учтите, что обработчик заменяет только сочетание клавиш: мастер-чекбокс продолжает вызывать встроенное переключение, так что с selectAll вы отвечаете за оба пути.

Ctrl/Cmd + C копирует активную опцию. Подпишитесь на onCopy, чтобы решить, что попадёт в буфер: в событии приходят список, опция и исходное событие клавиатуры, а сам список больше ничего не делает. Без подписчика он кладёт в буфер String(value) активной опции.

Для включения возможности перестановки опций необходимо установить свойство draggable для kbq-list-selection.

Отдельную опцию можно исключить, задав ей draggable="false". Взять её нельзя ни указателем, ни с клавиатуры, но, в отличие от отключённой, она по-прежнему получает фокус и выбирается. Остальные опции свободно перемещаются вокруг неё, поэтому её собственный индекс всё равно может сместиться.

Во время перетаскивания список не раздвигается: соседние опции остаются на местах, перетаскиваемая сохраняет своё место в виде приглушённой строки, а позицию, куда она встанет, показывает линия. Пока указатель над местом, которое опция уже занимает, линия не показывается — отпускание там ничего не изменит.

Больше во время перетаскивания ничего не реагирует. Опции перестают отзываться на наведение, потому что дроп принимает список, а не строка под указателем, а курсор становится no-drop, как только указатель уходит со всех списков, готовых принять опцию.

За указателем едет плашка с текстом самой опции: подпись, а под ней, отдельной строкой, caption. Что не помещается в её максимальную ширину, обрезается многоточием. Свойство dragPreview="full" у kbq-list-selection возвращает прежнее поведение — за указателем едет копия всей строки, вместе с чекбоксом, иконками и кнопкой действия.

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

Перетаскиваемая строка сохраняет тот курсор, который был бы у неё и так: она по-прежнему реагирует на клик и выбирается, и то, что её можно перетащить, — не повод перестать выглядеть обычной строкой. Свойство dragCursor="grab" включает курсор grab там, где вся строка задумана как ручка:

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

Список никогда не изменяет данные сам — он сообщает о перемещении через событие dropped, а применяете его вы, обычно с помощью moveItemInArray из @angular/cdk/drag-drop. Отслеживайте опции по идентификатору (track item.id): при позиционном ключе вроде track $index опция на своём месте сохраняется и получает новое значение, а смена значения сбрасывает её выбор.

Опцию можно брать не за всю строку, а за отдельную область: спроецируйте в неё элемент с cdkDragHandle — перетаскивание начинает только он. Ручка всегда показывает курсор grab — она для того и нужна, поэтому dragCursor ей не требуется, а строка вокруг неё сохраняет свой курсор, о чём бы ни просил список. Директива реэкспортируется из KbqListModule, отдельно импортировать ничего не нужно.

Опции можно переносить и в другой список. Передайте соседний kbq-list-selection в connectedTo у обоих списков и примените перемещение через transferArrayItem. В списке-приёмнике опция появляется невыбранной, если её значения ещё нет в его модели.

Опции нумеруются сквозным образом по всему списку, поэтому kbq-optgroup перетаскиванию не мешает, а опция, перенесённая через его границу, меняет группу. А вот внутри cdk-virtual-scroll-viewport перетаскивание не поддерживается: индексы в событии dropped учитывают только отрисованные опции, поэтому применение перемещения к массиву данных незаметно затронет не тот элемент. В режиме разработки этот случай выводит предупреждение.

Перестановка работает только указателем: клавиатурного эквивалента нет, поэтому тем, кто не может перетаскивать, она недоступна.

В списках, связанных по id, а не по ссылке на компонент, индикатор не показывается: по id нельзя получить экземпляр списка, который должен его нарисовать.

kbq-list-selection объявляется как listbox, а каждый kbq-list-option — как option с собственным aria-selected. Строка «Выбрать все» тоже остаётся option — checkbox не может быть дочерним элементом listbox — и сообщает состояние пачки через aria-checked, включая mixed для частичного выбора; собственного aria-selected у неё нет. С multiple список дополнительно помечается aria-multiselectable, а с horizontal сообщает aria-orientation="horizontal" и переключает активный элемент стрелками влево/вправо. Список — одна точка табуляции, внутри него фокус перемещается roving-навигацией. Отключённые список и элемент сообщаются через aria-disabled. Встроенный псевдочекбокс декоративен и в дерево доступности не попадает; если вы проецируете свой kbq-pseudo-checkbox (externalPseudoCheckbox), тоже пометьте его aria-hidden="true" — состояние выбора уже передаёт aria-selected самого элемента.

Listbox должен иметь доступное имя, поэтому задайте его через aria-label или aria-labelledby:

<kbq-list-selection aria-label="Почтовые ящики">
    <kbq-list-option [value]="'inbox'">Входящие</kbq-list-option>
    <kbq-list-option [value]="'starred'">Помеченные</kbq-list-option>
</kbq-list-selection>

У kbq-list и kbq-list-item роли намеренно нет: это обычный контейнер, который используется и для семантических списков, и для чисто визуальной группировки. Если содержимое — настоящий список, добавьте role="list" / role="listitem" самостоятельно.

  • Давайте каждому kbq-list-selection доступное имя. Он объявляется как listbox, а по listbox без имени скринридер не может сказать, что именно выбирают.
  • Предпочитайте multiple="checkbox", когда выбор должен читаться с одного взгляда. multiple="keyboard" оставляет строки компактными, но прячет состояние от того, кто не знает, что искать подсветку.
  • Берите виртуальную прокрутку, когда список становится достаточно длинным, чтобы его пришлось долго листать, — но помните, что с перетаскиванием она не сочетается.
  • Держите подпись опции такой, чтобы она умещалась в строку. Список — это выбор, а не место для абзацев: подробности выносите в caption или в тултип.
Предложения по улучшению
Если вы нашли ошибку или хотите доработать статью, создайте запрос на GitHub.