В интерфейсных компонентах на основе нативного
<select> поведение фокуса определяется браузером, но
при замене элемента на кастомный компонент управление фокусом полностью
переходит к JavaScript. В Slim Select оригинальный
<select> скрывается, а вместо него создаётся
DOM-структура, имитирующая поведение выпадающего списка. Это приводит к
необходимости отдельно учитывать:
tabindex у контейнеров;Фокусная модель строится вокруг двух ключевых элементов: скрытого
<select> и визуального контейнера Slim Select.
При инициализации экземпляра создаётся обёртка:
const select = document.querySelector('#mySelect');
const slim = new SlimSelect({
select: '#mySelect'
});
После инициализации:
<select> получает
display: none или аналогичное скрытие;.ss-main;Фокус по умолчанию остаётся на скрытом <select>,
но пользователь взаимодействует уже с .ss-main.
Ключевая проблема кастомных селектов — участие в последовательности Tab.
В Slim Select используется стратегия:
.ss-main получает
tabindex="0";<select> исключается из таб-цепочки
(tabindex="-1" или display:none);Пример итоговой структуры:
<div class="ss-main" tabindex="0">
...
</div>
<select id="mySelect" tabindex="-1"></select>
Tab → перенос фокуса на .ss-mainShift + Tab → возврат к предыдущему элементу формыTab → переход дальше по формеВажно, что внутренние элементы dropdown не участвуют в Tab-последовательности.
Фокус на .ss-main сам по себе не обязан открывать
список. Поведение зависит от конфигурации и реализации событий:
focus → только активация компонента;keydown (Enter/Space/ArrowDown) → открытие списка;click → открытие списка и перенос фокуса внутрь
UI.Типовой сценарий:
document.querySelector('.ss-main').addEventListener('focus', () => {
// визуальная активация
});
Инициация открытия чаще привязана к клавиатуре:
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
slim.open();
}
});
После открытия списка фокус не переносится на каждый элемент списка как на отдельный tab-stop. Вместо этого используется:
aria-selected.Структура поведения:
ArrowDown → следующий элемент;ArrowUp → предыдущий;Enter → выбор текущего;Escape → закрытие и возврат фокуса на
.ss-main.Пример обработчика:
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
slim.moveDown();
break;
case 'ArrowUp':
slim.moveUp();
break;
case 'Enter':
slim.selectHighlighted();
break;
case 'Escape':
slim.close();
document.querySelector('.ss-main').focus();
break;
}
});
После выбора значения поведение фокуса критично для форм:
.ss-main;Это предотвращает «потерю фокуса» при последовательной навигации по форме.
Типовой сценарий:
slim.setSelected('value');
document.querySelector('.ss-main').focus();
Blur в кастомных селектах часто вызывает проблемы, если не учитывать структуру DOM.
В Slim Select blur может происходить:
Рекомендуемая логика:
.ss-main не всегда означает закрытие
dropdown;Пример:
document.addEventListener('click', (e) => {
if (!e.target.closest('.ss-main')) {
slim.close();
}
});
При наличии нескольких селектов важно сохранять линейную табуляцию:
<input type="text">
<select id="a"></select>
<select id="b"></select>
<input type="text">
Поведение:
.ss-main (a) → Tab.ss-main (b) → TabКаждый .ss-main становится единым таб-узлом.
Особенность: внутренние элементы dropdown никогда не становятся частью tab order, что предотвращает «застревание» внутри списка.
В ряде сценариев требуется ручное управление фокусом:
document.querySelector('#mySelect + .ss-main').focus();
slim.open();
document.querySelector('.ss-main').focus();
if (!value) {
const el = document.querySelector('.ss-main');
el.classList.add('error');
el.focus();
}
В контексте фокуса возникают типовые конфликты:
При фокусе на .ss-main клавиши стрелок могут
прокручивать страницу. Решение:
document.addEventListener('keydown', (e) => {
if (document.activeElement.classList.contains('ss-main')) {
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
e.preventDefault();
}
}
});
Enter может вызывать submit формы вместо выбора элемента:
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && slim.isOpen()) {
e.preventDefault();
slim.selectHighlighted();
}
});
Когда селект отключён:
.ss-main не должен получать фокус;tabindex удаляется или устанавливается в
-1;Пример:
slim.disable();
const el = document.querySelector('.ss-main');
el.setAttribute('tabindex', '-1');
Фокус в Slim Select тесно связан с ARIA-состояниями:
aria-expanded → состояние dropdown;aria-activedescendant → активный элемент;aria-selected → выбранные значения.Фокус всегда должен коррелировать с aria-expanded:
aria-expanded="true"aria-expanded="false"При обновлении данных:
.ss-main теряет tabindex;Решение:
slim.setData(newData);
document.querySelector('.ss-main').focus();
Возникает при одновременной обработке click и focus:
el.addEventListener('focus', slim.open);
el.addEventListener('click', slim.open);
Решение — централизовать открытие:
el.addEventListener('click', () => {
if (!slim.isOpen()) slim.open();
});
Появляется, если внутренние обработчики блокируют blur. Решение:
preventDefault() без условия.