Список показывает набор связанных элементов и позволяет выбрать один или несколько. Для выбора одного значения из закрытого набора в форме используйте 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"
оставляет строки компактными, но прячет состояние от того, кто не знает, что искать подсветку.