@scania/tegel
Version:
Tegel Design System
191 lines (190 loc) • 8.78 kB
JavaScript
// ============================================================================
// Dropdown Keyboard Navigation
// Handles arrow keys, Enter, Space, Tab, and Escape for all dropdown variants
// ============================================================================
let initialized = false;
export function initDropdownKeyboard() {
if (initialized)
return;
initialized = true;
// ============================================================================
// Handler for when dropdown list is open
// ============================================================================
const handleOpenListKeydown = (e) => {
const openTrigger = document.querySelector('.tl-dropdown__button[aria-expanded="true"], .tl-dropdown__input[aria-expanded="true"]');
if (!openTrigger)
return;
const root = openTrigger.closest('.tl-dropdown');
const openList = root === null || root === void 0 ? void 0 : root.querySelector('.tl-dropdown__list');
if (!openList)
return;
const isDropUp = root === null || root === void 0 ? void 0 : root.classList.contains('tl-dropdown--dropup');
const isMultiSelect = openList.getAttribute('aria-multiselectable') === 'true';
const isFilterDropdown = !!(root === null || root === void 0 ? void 0 : root.querySelector('.tl-dropdown__input'));
const options = Array.from(openList.querySelectorAll('.tl-dropdown__option')).filter((option) => {
const el = option;
return (!el.classList.contains('tl-dropdown__option--disabled') &&
!el.classList.contains('tl-dropdown__option--no-result') &&
el.style.display !== 'none');
});
if (!options.length)
return;
const { activeElement } = document;
const currentIndex = options.findIndex((option) => option === activeElement);
const focusOption = (index) => {
if (index < 0 || index >= options.length)
return;
options[index].focus();
};
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (isFilterDropdown && activeElement === openTrigger) {
const isArrowDown = e.key === 'ArrowDown';
const firstIndex = isArrowDown
? isDropUp
? options.length - 1
: 0
: isDropUp
? 0
: options.length - 1;
focusOption(firstIndex);
return;
}
const direction = e.key === 'ArrowDown' ? 1 : -1;
let nextIndex = currentIndex;
if (currentIndex === -1) {
const isArrowDown = e.key === 'ArrowDown';
nextIndex = isArrowDown
? isDropUp
? options.length - 1
: 0
: isDropUp
? 0
: options.length - 1;
}
else {
nextIndex = currentIndex + (isDropUp ? -direction : direction);
}
if (nextIndex >= options.length)
nextIndex = 0;
if (nextIndex < 0)
nextIndex = options.length - 1;
focusOption(nextIndex);
return;
}
if (e.key === 'Enter' || e.key === ' ') {
const focusedOption = options[currentIndex];
if (!focusedOption)
return;
if (isMultiSelect)
return;
e.preventDefault();
focusedOption.click();
if (openTrigger)
openTrigger.setAttribute('aria-expanded', 'false');
if (!isFilterDropdown) {
openTrigger === null || openTrigger === void 0 ? void 0 : openTrigger.focus();
}
return;
}
if (e.key === 'Tab') {
if (isFilterDropdown) {
const inputWrapper = openTrigger.parentElement;
const clearButton = inputWrapper === null || inputWrapper === void 0 ? void 0 : inputWrapper.querySelector('.tl-dropdown__input-clear');
const clearButtonTabindex = clearButton === null || clearButton === void 0 ? void 0 : clearButton.getAttribute('tabindex');
const canFocusClearButton = clearButton && clearButtonTabindex === '0';
if (currentIndex >= 0) {
if (!e.shiftKey && canFocusClearButton) {
e.preventDefault();
clearButton === null || clearButton === void 0 ? void 0 : clearButton.focus();
return;
}
return;
}
return;
}
e.preventDefault();
const movingForward = isDropUp ? e.shiftKey : !e.shiftKey;
const nextIndex = movingForward ? currentIndex + 1 : currentIndex - 1;
if (nextIndex >= options.length || nextIndex < 0) {
if (openTrigger)
openTrigger.setAttribute('aria-expanded', 'false');
openTrigger === null || openTrigger === void 0 ? void 0 : openTrigger.focus();
return;
}
focusOption(nextIndex);
return;
}
if (e.key === 'Escape') {
e.preventDefault();
if (openTrigger)
openTrigger.setAttribute('aria-expanded', 'false');
openTrigger === null || openTrigger === void 0 ? void 0 : openTrigger.focus();
}
};
const handleTriggerKeydown = (e) => {
var _a;
const trigger = (_a = e.target) === null || _a === void 0 ? void 0 : _a.closest('.tl-dropdown__button, .tl-dropdown__input');
if (!trigger)
return;
const root = trigger.closest('.tl-dropdown');
const list = root === null || root === void 0 ? void 0 : root.querySelector('.tl-dropdown__list');
if (!list)
return;
const isDropUp = root === null || root === void 0 ? void 0 : root.classList.contains('tl-dropdown--dropup');
const isOpen = trigger.getAttribute('aria-expanded') === 'true';
const options = Array.from(list.querySelectorAll('.tl-dropdown__option')).filter((option) => {
const el = option;
return !el.classList.contains('tl-dropdown__option--disabled') && el.style.display !== 'none';
});
if (!options.length)
return;
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !isOpen) {
e.preventDefault();
trigger.setAttribute('aria-expanded', 'true');
const firstIndex = isDropUp ? options.length - 1 : 0;
options[firstIndex].focus();
}
};
const handleClearButtonKeydown = (e) => {
const target = e.target;
if (!target.classList.contains('tl-dropdown__input-clear'))
return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
target.click();
}
if (e.key === 'Tab' && e.shiftKey) {
const root = target.closest('.tl-dropdown');
const list = root === null || root === void 0 ? void 0 : root.querySelector('.tl-dropdown__list');
if (list) {
const options = Array.from(list.querySelectorAll('.tl-dropdown__option')).filter((option) => {
const el = option;
return (!el.classList.contains('tl-dropdown__option--disabled') &&
!el.classList.contains('tl-dropdown__option--no-result') &&
el.style.display !== 'none');
});
if (options.length > 0) {
e.preventDefault();
options[options.length - 1].focus();
}
}
}
};
const handleClearButtonFocus = (e) => {
var _a;
const target = e.target;
if (!target.classList.contains('tl-dropdown__input-clear'))
return;
const input = (_a = target
.closest('.tl-dropdown__input-wrapper')) === null || _a === void 0 ? void 0 : _a.querySelector('.tl-dropdown__input');
if (input) {
input.setAttribute('aria-expanded', 'true');
}
};
document.addEventListener('keydown', handleOpenListKeydown);
document.addEventListener('keydown', handleTriggerKeydown);
document.addEventListener('keydown', handleClearButtonKeydown);
document.addEventListener('focus', handleClearButtonFocus, true);
}