mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-14 22:18:19 +00:00
Getting ready to clean up the package and release 0.2.0
This commit is contained in:
+60
-45
@@ -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 });
|
||||
})();
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user