Select
Випадний список із фільтрацією, повною клавіатурною навігацією та ARIA-combobox.
Єдиний серед чотирьох вихідних проєктів компонент із повноцінним
ARIA-combobox. Решта реалізацій були <div> із випадайкою — візуально
схоже, для скрінрідера не існує.
Приклад
Список звужується під час набору.
API
Props
| Назва | Тип | Типово | Опис |
|---|---|---|---|
options* | SelectOption[] | — | Варіанти вибору. Фільтрація за введеним текстом відбувається локально. |
modelValue | string | number | — | Обране значення. Використовуйте через `v-model`. |
label | string | — | — |
placeholder | string | — | — |
size | FieldSize | "md" | Висота поля. На мобільному кожен розмір вищий за десктопний. |
error | string | — | Текст помилки. Його наявність вмикає стан помилки. |
hint | string | — | Підказка під полем. Ховається, коли показано помилку. |
id | string | — | Стабільний DOM id. `name` використовується лише для форми. |
name | string | — | — |
disabled | boolean | — | — |
required | boolean | — | — |
loading | boolean | — | Показує індикатор замість списку — для серверного пошуку. |
clearable | boolean | — | Дозволяє скинути вибір хрестиком. |
filterable | boolean | true | Дозволяє звужувати список набором тексту. Без фільтра поле поводиться як нативний `<select>`: Enter і пробіл відкривають список. |
mobileSheet | boolean | true | На вузькому екрані (до 768px) список відкривається нижнім sheet'ом на всю ширину замість випадайки під полем. Вимикайте лише там, де sheet недоречний: поле в щільному рядку таблиці чи в іншому sheet'і, що вже займає екран. |
Події
| Назва | Payload | Опис |
|---|---|---|
search | [query: string] | Введений текст. Підключайте для серверної фільтрації. |
update:modelValue | [value: string | number] | Обране значення або `null`, якщо вибір скинуто. |
Слоти
| Назва | Опис |
|---|---|
option | Власний рендер пункту списку. |
Доступно через ref
| Назва | Тип | Опис |
|---|---|---|
focus | () => void | Ставить фокус на поле. |
Коли використовувати
Для вибору одного значення з відомого набору. Фільтр (filterable) типово
ввімкнений: гортати довгий список гірше, ніж набрати три букви. Для короткого
списку його можна вимкнути — тоді поле поводиться як нативний <select>.
Коли НЕ використовувати
Для двох-трьох варіантів беріть радіокнопки: вони показують усі варіанти одразу, без зайвого кліку.
Для довільного тексту з підказками потрібен інший компонент — цей завжди повертає значення зі списку.
Доступність
Поле має role="combobox", aria-autocomplete="list", aria-expanded,
aria-controls і aria-activedescendant. Панель — role="listbox", пункти
— role="option" з aria-selected.
Стрілки пропускають деактивовані пункти, а не зупиняються на них. Escape
викликає stopPropagation — інакше той самий натиск закрив би ще й модалку,
у якій селект стоїть.
З фільтром уже перша літера, набрана в закрите поле, відкриває список і
фільтрує його, а Enter бере знайдений пункт. Без фільтра Enter і пробіл
відкривають список, а пробіл у відкритому списку обирає пункт — як у
нативного <select>.
Атрибути, що не є props (aria-label, data-*), ідуть на саме поле, а
class і style — на обгортку разом із лейблом.
Панель телепортується в body. Базовий шар — 1100, а всередині вкладеного
Modal або Drawer значення автоматично піднімається над найближчим оверлеєм.
На мобільному
До 768px список відкривається не випадайкою під полем, а нижнім sheet'ом
(UiDrawer position="bottom") на всю ширину: рядки по 48px, обраний пункт
посередині екрана, закриття тапом по фону, свайпом униз чи хрестиком. Поле
в цьому режимі — кнопка: readonly, тож клавіатура не вилазить від тапу.
Заголовок sheet'а — label, а без нього aria-label, зовнішній
<label for> поля або плейсхолдер: людина бачить, що саме обирає, хоча
поле лишилося під фоном.
Пошук з'являється лише в списку довшому за 10 пунктів і фокус сам НЕ отримує — клавіатура не накриває список, доки людина не вирішила шукати. З пошуком висота sheet'а фіксована (90dvh): за вмістом він стрибав би вниз із кожною літерою і ховався б під клавіатурою разом із полем пошуку.
Sheet відкривається на click, а не на pointerdown, як випадайка: інакше
фон з'являвся під пальцем ще до того, як дотик завершився. Поворот екрана
з відкритим списком закриває його, а не переносить між режимами.
Вимкнути режим — :mobile-sheet="false": для поля в щільному рядку
таблиці чи всередині іншого sheet'а, що вже займає екран.
Деталі реалізації
Позиція рахується власним кодом, без @floating-ui і без VueUse —
компонент має копіюватись у чужий проєкт без нової залежності.
Слухач прокрутки навішено з capture: true. Подія scroll не спливає, тож
без цього відкрита панель лишалася б висіти на місці, поки список під нею
від'їжджає.
Позиція вимірюється двічі: перший раз із припущенням висоти 240px, другий — коли панель уже отримала реальну. Без другого проходу вибір напрямку відкриття інколи помилявся.
Поки користувач не почав набирати, показується весь список. Інакше після відкриття видно лише один пункт — той, що вже обраний і чия назва стоїть у полі як текст фільтра.
Клік у поле лише відкриває список; закриває його шеврон, клік поза полем або вибір. Поки клік перемикав, клік усередині відкритого поля — просто щоб переставити каретку — закривав список і стирав набраний запит.
Назва в полі стежить не лише за значенням, а й за назвою обраного пункту. Форма редагування зазвичай отримує значення раніше за список опцій, і поле лишалося порожнім, доки користувач не відкривав і не закривав його.