Fixing more issues with popovers

This commit is contained in:
Ronan Berder
2025-06-18 20:55:32 +08:00
parent 46bf212f41
commit 9b59f43a67
20 changed files with 148 additions and 111 deletions
+7 -3
View File
@@ -514,12 +514,16 @@
[role='menuitemcheckbox'],
[role='menuitemradio'] {
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate;
@apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
&:not([aria-disabled='true']) {
@apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
}
&.active {
@apply bg-accent text-accent-foreground;
}
}
[role='menu'] [role="presentation"] {
[role='menu'] [role='heading'] {
@apply flex px-2 py-1.5 text-sm font-medium;
}
[role='separator'] {
@@ -734,7 +738,7 @@
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
}
[role='listbox'] [role="presentation"] {
[role='listbox'] [role='heading'] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
}
[role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
+1 -1
View File
@@ -84,7 +84,7 @@
{% endfor %}
{% endif %}
>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_dropdown_items(item.items, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
+1 -1
View File
@@ -151,7 +151,7 @@
{% endfor %}
{% endif %}
>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, selected, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
+31 -18
View File
@@ -16,19 +16,30 @@
let menuItems = [];
let activeIndex = -1;
const closeMenu = () => {
const closePopover = (focusOnTrigger = true) => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
popover.setAttribute('aria-hidden', 'true');
trigger.focus();
if (focusOnTrigger) {
trigger.focus();
}
setActiveItem(-1);
};
const openMenu = () => {
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: dropdownMenuComponent }
}));
trigger.setAttribute('aria-expanded', 'true');
popover.setAttribute('aria-hidden', 'false');
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]')).filter(item =>
!item.hasAttribute('disabled') &&
item.getAttribute('aria-disabled') !== 'true'
);
if (menuItems.length > 0) {
setActiveItem(0);
}
@@ -51,9 +62,9 @@
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closeMenu();
closePopover();
} else {
openMenu();
openPopover();
}
});
@@ -61,14 +72,14 @@
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (event.key === 'Escape') {
if (isExpanded) closeMenu();
if (isExpanded) closePopover();
return;
}
if (!isExpanded) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
event.preventDefault();
openMenu();
openPopover();
}
return;
}
@@ -80,15 +91,11 @@
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
if (activeIndex < menuItems.length - 1) {
nextIndex = activeIndex + 1;
}
break;
nextIndex = Math.min(activeIndex + 1, menuItems.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
if (activeIndex > 0) {
nextIndex = activeIndex - 1;
}
nextIndex = Math.max(activeIndex - 1, 0);
break;
case 'Home':
event.preventDefault();
@@ -102,7 +109,7 @@
case ' ':
event.preventDefault();
menuItems[activeIndex]?.click();
closeMenu();
closePopover();
return;
}
@@ -113,13 +120,19 @@
menu.addEventListener('click', (event) => {
if (event.target.closest('[role^="menuitem"]')) {
closeMenu();
closePopover();
}
});
document.addEventListener('click', (event) => {
if (!dropdownMenuComponent.contains(event.target)) {
closeMenu();
closePopover();
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== dropdownMenuComponent) {
closePopover(false);
}
});
+14 -2
View File
@@ -11,14 +11,20 @@
return;
}
const closePopover = () => {
const closePopover = (focusOnTrigger = true) => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
trigger.focus();
if (focusOnTrigger) {
trigger.focus();
}
};
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: popoverComponent }
}));
const elementToFocus = content.querySelector('[autofocus]');
if (elementToFocus) {
content.addEventListener('transitionend', () => {
@@ -51,6 +57,12 @@
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== popoverComponent) {
closePopover(false);
}
});
popoverComponent.dataset.popoverInitialized = true;
};
+40 -27
View File
@@ -148,7 +148,7 @@
if (currentVisibleIndex > 0) {
nextVisibleIndex = currentVisibleIndex - 1;
} else if (currentVisibleIndex === -1) {
nextVisibleIndex = 0; // Start from top if nothing is active
nextVisibleIndex = 0;
}
break;
case 'Home':
@@ -180,37 +180,44 @@
filter.addEventListener('keydown', handleKeyNavigation);
}
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: selectComponent }
}));
if (filter) {
if (hasTransition()) {
popover.addEventListener('transitionend', () => {
filter.focus();
}, { once: true });
} else {
filter.focus();
}
}
popover.setAttribute('aria-hidden', 'false');
trigger.setAttribute('aria-expanded', 'true');
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' });
}
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
if (filter) {
if (hasTransition()) {
popover.addEventListener('transitionend', () => {
filter.focus();
}, { once: true });
} else {
filter.focus();
}
}
popover.setAttribute('aria-hidden', 'false');
trigger.setAttribute('aria-expanded', 'true');
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' });
}
openPopover();
}
});
@@ -227,6 +234,12 @@
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== selectComponent) {
closePopover(false);
}
});
popover.setAttribute('aria-hidden', 'true');
selectComponent.dataset.selectInitialized = true;
};
+1 -1
View File
@@ -166,7 +166,7 @@
const initialToaster = document.getElementById('toaster');
if (initialToaster) initToaster(initialToaster);
window.addEventListener('basecoat:toast', (event) => {
document.addEventListener('basecoat:toast', (event) => {
if (!toaster) {
console.error('Cannot create toast: toaster container not found on page.');
return;
+1 -1
View File
@@ -84,7 +84,7 @@
{% endfor %}
{% endif %}
>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_dropdown_items(item.items, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
+1 -1
View File
@@ -151,7 +151,7 @@
{% endfor %}
{% endif %}
>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, selected, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}