Getting ready to clean up the package and release 0.2.0

This commit is contained in:
Ronan Berder
2025-06-16 17:58:01 +08:00
parent f51a03da79
commit 17a18c1871
34 changed files with 789 additions and 707 deletions
+60 -45
View File
@@ -1,13 +1,35 @@
(() => {
const initDropdownMenu = (dropdownMenuComponent) => {
const trigger = dropdownMenuComponent.querySelector('[popovertarget]');
const popover = dropdownMenuComponent.querySelector('[popover]');
const menu = popover?.querySelector('[role="menu"]');
if (!trigger || !popover || !menu) return;
const trigger = dropdownMenuComponent.querySelector(':scope > button');
const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]');
const menu = popover.querySelector('[role="menu"]');
if (!trigger || !menu || !popover) {
console.error('Dropdown menu component is missing a trigger, a menu, or a popover content element.', dropdownMenuComponent);
return;
}
let menuItems = [];
let activeIndex = -1;
const closeMenu = () => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
popover.setAttribute('aria-hidden', 'true');
trigger.focus();
activeIndex = -1;
};
const openMenu = () => {
trigger.setAttribute('aria-expanded', 'true');
popover.setAttribute('aria-hidden', 'false');
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
if (menuItems.length > 0) {
setActiveItem(0);
}
};
const setActiveItem = (index) => {
if (activeIndex > -1 && menuItems[activeIndex]) {
menuItems[activeIndex].classList.remove('active');
@@ -17,36 +39,48 @@
const activeItem = menuItems[activeIndex];
activeItem.classList.add('active');
trigger.setAttribute('aria-activedescendant', activeItem.id);
activeItem.scrollIntoView({ block: 'nearest' });
} else {
trigger.removeAttribute('aria-activedescendant');
}
};
const handleKeyDown = (e) => {
if (!popover.matches(':popover-open')) {
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closeMenu();
} else {
openMenu();
}
});
dropdownMenuComponent.addEventListener('keydown', (e) => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (e.key === 'Escape') {
if (isExpanded) closeMenu();
return;
}
if (!isExpanded) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
e.preventDefault();
trigger.click();
openMenu();
}
return;
}
let nextIndex = activeIndex;
if (menuItems.length === 0) return;
let nextIndex = activeIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (activeIndex < menuItems.length - 1) {
nextIndex = activeIndex + 1;
}
nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0;
break;
case 'ArrowUp':
e.preventDefault();
if (activeIndex > 0) {
nextIndex = activeIndex - 1;
}
nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1;
break;
case 'Home':
e.preventDefault();
@@ -60,42 +94,24 @@
case ' ':
e.preventDefault();
menuItems[activeIndex]?.click();
break;
closeMenu();
return;
}
if (nextIndex !== activeIndex) {
setActiveItem(nextIndex);
}
};
trigger.addEventListener('keydown', handleKeyDown);
popover.addEventListener('toggle', (e) => {
trigger.setAttribute('aria-expanded', e.newState === 'open');
if (e.newState === 'open') {
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
menuItems.forEach((item, index) => {
if (!item.id) item.id = `${menu.id}-item-${index}`;
});
setActiveItem(0);
} else {
setActiveItem(-1);
}
});
menu.addEventListener('click', (e) => {
if (e.target.closest('[role^="menuitem"]')) {
popover.hidePopover();
closeMenu();
}
});
menu.addEventListener('mouseover', (e) => {
const item = e.target.closest('[role^="menuitem"]:not([disabled])');
if (item) {
const index = menuItems.indexOf(item);
if (index > -1 && index !== activeIndex) {
setActiveItem(index);
}
document.addEventListener('click', (e) => {
if (!dropdownMenuComponent.contains(e.target)) {
closeMenu();
}
});
@@ -107,15 +123,14 @@
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
+69
View File
@@ -0,0 +1,69 @@
(() => {
const initPopover = (popoverComponent) => {
const trigger = popoverComponent.querySelector(':scope > button');
const content = popoverComponent.querySelector(':scope > [data-popover]');
if (!trigger || !content) {
console.error('Popover component is missing a trigger button or a content element.', popoverComponent);
return;
}
const closePopover = () => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
trigger.focus();
};
const openPopover = () => {
const elementToFocus = content.querySelector('[autofocus]');
if (elementToFocus) {
content.addEventListener('transitionend', () => {
elementToFocus.focus();
}, { once: true });
}
trigger.setAttribute('aria-expanded', 'true');
content.setAttribute('aria-hidden', 'false');
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
});
popoverComponent.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closePopover();
}
});
document.addEventListener('click', (e) => {
if (!popoverComponent.contains(e.target)) {
closePopover();
}
});
popoverComponent.dataset.popoverInitialized = true;
};
document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.popover:not([data-popover-initialized])')) {
initPopover(node);
}
node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
+59 -49
View File
@@ -1,8 +1,8 @@
(() => {
const initSelect = (selectComponent) => {
const trigger = selectComponent.querySelector(':scope > [popovertarget]');
const trigger = selectComponent.querySelector(':scope > button');
const selectedValue = trigger.querySelector(':scope > span');
const popover = selectComponent.querySelector(':scope > [popover]');
const popover = selectComponent.querySelector(':scope > [data-popover]');
const listbox = popover.querySelector('[role="listbox"]');
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
const filter = selectComponent.querySelector('header input[type="text"]');
@@ -21,15 +21,26 @@
}
};
const closePopover = () => {
popover.setAttribute('aria-hidden', 'true');
trigger.setAttribute('aria-expanded', 'false');
if (filter) {
filter.value = '';
visibleOptions = [...options];
options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
}
trigger.removeAttribute('aria-activedescendant');
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
activeIndex = -1;
}
const selectOption = (option) => {
if (!option) return;
updateValue(option);
trigger.removeAttribute('aria-activedescendant');
options.forEach(opt => opt.classList.remove('active'));
activeIndex = -1;
popover.hidePopover();
if (option.dataset.value) {
updateValue(option);
}
closePopover();
};
if (filter) {
@@ -62,12 +73,14 @@
updateValue(initialOption);
const handleKeyNavigation = (e) => {
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(e.key)) {
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) {
return;
}
if (!popover.matches(':popover-open')) {
if (e.currentTarget === trigger && e.key !== 'Enter') {
if (!isPopoverOpen) {
if (e.key !== 'Enter' && e.key !== 'Escape') {
e.preventDefault();
trigger.click();
}
@@ -76,9 +89,14 @@
e.preventDefault();
if (e.key === 'Escape') {
closePopover();
return;
}
if (e.key === 'Enter') {
if (activeIndex > -1) {
selectOption(options[activeIndex]);
selectOption(visibleOptions[activeIndex]);
}
return;
}
@@ -130,6 +148,31 @@
filter.addEventListener('keydown', handleKeyNavigation);
}
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
popover.setAttribute('aria-hidden', 'false');
trigger.setAttribute('aria-expanded', 'true');
if (filter) filter.focus();
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
if (selectedOption) {
if (activeIndex > -1) {
options[activeIndex]?.classList.remove('active');
}
activeIndex = options.indexOf(selectedOption);
selectedOption.classList.add('active');
if (selectedOption.id) {
trigger.setAttribute('aria-activedescendant', selectedOption.id);
}
selectedOption.scrollIntoView({ block: 'nearest' });
}
}
});
listbox.addEventListener('click', (e) => {
const clickedOption = e.target.closest('[role="option"]');
if (clickedOption) {
@@ -137,46 +180,13 @@
}
});
popover.addEventListener('toggle', (e) => {
trigger.setAttribute('aria-expanded', e.newState === 'open');
if (e.newState === 'open') {
if (filter) filter.focus();
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
let startingOption = null;
if (selectedOption && visibleOptions.includes(selectedOption)) {
startingOption = selectedOption;
} else if (visibleOptions.length > 0) {
startingOption = visibleOptions[0];
}
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
if (startingOption) {
activeIndex = options.indexOf(startingOption);
startingOption.classList.add('active');
if (startingOption.id) {
trigger.setAttribute('aria-activedescendant', startingOption.id);
}
startingOption.scrollIntoView({ block: 'nearest' });
} else {
activeIndex = -1;
}
} else if (e.newState === 'closed') {
if (filter) {
filter.value = '';
visibleOptions = [...options];
options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
}
trigger.removeAttribute('aria-activedescendant');
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
activeIndex = -1;
document.addEventListener('click', (e) => {
if (!selectComponent.contains(e.target)) {
closePopover();
}
});
popover.setAttribute('aria-hidden', 'true');
selectComponent.dataset.selectInitialized = true;
};