Примитивы
15

MultiSelect

Выбор нескольких значений. Каждый выбранный становится чипом внутри поля, а само поле — текстовый input, которым список и фильтруется: выбор и поиск происходят в одном месте, а не в двух. role="combobox" стоит на этом input, а не на коробке вокруг, — владельцем списка должно объявляться то, где находится курсор. Чипы — это Chip размера sm. Чего этот паттерн не даёт ни в одном из четырёх пакетов: скринридер на инпуте не зачитывает чипы обратно. У кнопки удаления каждого чипа есть собственное имя («Удалить Apple») — это доступная часть, и у паттерна multiselect в APG форма та же. Записано как есть, а не залатано через aria-describedby, который быстро устареет.

чистая разметка — библиотеки на странице нет
отрендерил aura-react
отрендерил aura-vue
отрендерил aura-svelte
собрано из классов самого aura-angular
Apple
Apple
Apple
Apple
Apple

Код

index.html
<div class="aura-field-group">
  <label class="aura-label" id="m-label" for="m">Tags</label>
  <div class="aura-combobox aura-combobox--multi" data-state="closed">
    <span class="aura-chip aura-chip--sm">Apple
      <button type="button" class="aura-chip__remove" aria-label="Remove Apple">
        <svg viewBox="0 0 8 8" aria-hidden="true"><path d="M1 1l6 6M7 1l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>
      </button>
    </span>
    <input id="m" type="text" class="aura-combobox__input" role="combobox" aria-expanded="false"
           aria-controls="m-listbox" aria-autocomplete="list" aria-labelledby="m-label" autocomplete="off" value="" />
    <span class="aura-combobox__chevron" aria-hidden="true"></span>
  </div>
</div>
Demo.tsx
import { MultiSelect } from '@ilomee/aura-react';

const OPTIONS = [
  { value: 'apple', label: 'Apple' },
  { value: 'pear', label: 'Pear' },
  { value: 'cherry', label: 'Cherry' },
];

/**
 * Повторяет html-сниппет `multi-select`, с которым сравнивают все стеки, — поэтому подпись и чип
 * здесь его, а не подобранные покрасивее. Закрытый и без обёртки для раскладки: открытый список
 * портализуется в `document.body`, где его не видит ничто, читающее панель превью, а обёртка была
 * бы узлом, которого в сниппете нет.
 */
export default function Demo() {
  return <MultiSelect label="Tags" options={OPTIONS} defaultValue={['apple']} />;
}
Demo.vue
import { MultiSelect } from '@ilomee/aura-vue';
Demo.svelte
import { MultiSelect } from '@ilomee/aura-svelte';

Используется как aura-multi-select.

demo.component.ts
import { AuraMultiSelect } from '@ilomee/aura-angular';

Пропсы

У чистой разметки нет пропсов — классы ниже и есть весь API. Четыре пакета не делают ничего сверх того, что проставляют вам те же самые классы.
ИмяТипПо умолчаниюПримечания
optionsSelectOption[] | null
valueдвустороннийstring[]
defaultValuestring[][]
onValueChangeсобытие(value: string[]) => void
hintstringПостоянная подсказка под контролом, показывается всегда.
errorstringЧто не так прямо сейчас. Ставит контролу aria-invalid и добавляет сообщение в его aria-describedby — после подсказки. Без живого региона: role="alert" объявляет себя при монтировании, поэтому форма с тремя ошибками, отрисованная на сервере, перебила бы читателя трижды ещё до первого поля.
searchablebooleantrueВыключено — весь контрол превращается в ряд чипов: input остаётся read-only, поэтому клавиатура продолжает работать.
maxnumberНа пределе невыбранные опции получают aria-disabled, а не исчезают: список, который сжимается по мере выбора, не даёт увидеть, что вы пропустили.
removeLabelstring'Remove {label}'{label} заменяется на подпись опции. Именно шаблон: в разных языках существительное стоит в разных местах.
renderOption(option, state) => ReactNode
ИмяТипПо умолчаниюПримечания
modelValueдвустороннийstring[]
update:modelValueсобытие(value: string[]) => void
hintstringПостоянная подсказка под контролом, показывается всегда.
errorstringЧто не так прямо сейчас. Ставит контролу aria-invalid и добавляет сообщение в его aria-describedby — после подсказки. Без живого региона: role="alert" объявляет себя при монтировании, поэтому форма с тремя ошибками, отрисованная на сервере, перебила бы читателя трижды ещё до первого поля.
searchablebooleantrue
maxnumber
optionслотslot({ option, index, active, selected, disabled })
ИмяТипПо умолчаниюПримечания
valueдвустороннийstring[]
onchangeсобытие(value: string[]) => void
hintstringПостоянная подсказка под контролом, показывается всегда.
errorstringЧто не так прямо сейчас. Ставит контролу aria-invalid и добавляет сообщение в его aria-describedby — после подсказки. Без живого региона: role="alert" объявляет себя при монтировании, поэтому форма с тремя ошибками, отрисованная на сервере, перебила бы читателя трижды ещё до первого поля.
searchablebooleantrue
maxnumber
optionслотSnippet<[SelectOption, AuraOptionRenderState]>
ИмяТипПо умолчаниюПримечания
valueдвустороннийstring[]
valueChangeсобытие(value: string[]) => void
hintstringПостоянная подсказка под контролом, показывается всегда.
errorstringЧто не так прямо сейчас. Ставит контролу aria-invalid и добавляет сообщение в его aria-describedby — после подсказки. Без живого региона: role="alert" объявляет себя при монтировании, поэтому форма с тремя ошибками, отрисованная на сервере, перебила бы читателя трижды ещё до первого поля.
searchablebooleantrue
maxnumber
optionTemplateTemplateRef

Какие классы отдаёт

Все фреймворки отдают одни и те же классы — именно поэтому четыре библиотеки выглядят одинаково. Их можно использовать напрямую, вообще без библиотеки компонентов.

КлассРольПримечания
.aura-comboboxбазаОбязательный. Без него модификатор не работает.
.aura-combobox--multiмодификатор
.aura-combobox__inputэлемент
.aura-chipбазаОбязательный. Без него модификатор не работает.
.aura-chip--smмодификатор
.aura-listboxбазаОбязательный. Без него модификатор не работает.

Токены и правила

components.css
ряд чипов
calc((var(--control-h) - 26px) / 2 - 1px) — one row lands on --control-h, a second grows the box
чипы
.aura-chip--sm — 26px, the "editable tag lists" variant
слой панели
var(--z-dropdown)