SHA256
140 lines
4.4 KiB
JavaScript
140 lines
4.4 KiB
JavaScript
export function createDropdownMenu({
|
|
anchorEl,
|
|
items = [],
|
|
className = '',
|
|
minWidth = 210,
|
|
offset = 7,
|
|
align = 'right',
|
|
leftShift = 0,
|
|
onOpen = null,
|
|
onClose = null,
|
|
} = {}) {
|
|
let portal = null;
|
|
|
|
const close = () => {
|
|
if (!portal) return;
|
|
portal.remove();
|
|
portal = null;
|
|
anchorEl?.setAttribute('aria-expanded', 'false');
|
|
onClose?.();
|
|
};
|
|
|
|
const position = () => {
|
|
if (!portal || !anchorEl) return;
|
|
const rect = anchorEl.getBoundingClientRect();
|
|
const margin = 10;
|
|
const menuWidth = portal.offsetWidth || minWidth;
|
|
const baseLeft = align === 'left' ? rect.left : rect.right - menuWidth;
|
|
const desiredLeft = baseLeft - Number(leftShift || 0);
|
|
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, desiredLeft));
|
|
let top = rect.bottom + offset;
|
|
const menuHeight = portal.offsetHeight || 180;
|
|
if (top + menuHeight > window.innerHeight - margin) {
|
|
top = Math.max(margin, rect.top - menuHeight - offset);
|
|
}
|
|
portal.style.left = `${Math.round(left)}px`;
|
|
portal.style.top = `${Math.round(top)}px`;
|
|
};
|
|
|
|
const open = () => {
|
|
if (!anchorEl || portal) return;
|
|
document.dispatchEvent(new CustomEvent('shine:dropdown-open', { detail: { owner: anchorEl } }));
|
|
const menu = document.createElement('div');
|
|
menu.className = `dm-head-menu dm-head-menu--portal shared-dropdown-menu ${className}`.trim();
|
|
menu.setAttribute('role', 'menu');
|
|
menu.style.minWidth = `${minWidth}px`;
|
|
|
|
items.forEach((item) => {
|
|
if (item?.divider) {
|
|
const divider = document.createElement('div');
|
|
divider.className = 'channel-menu-divider';
|
|
menu.append(divider);
|
|
return;
|
|
}
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = `dm-head-menu-item shared-dropdown-menu__item${item?.selected ? ' is-selected' : ''}${item?.danger ? ' destructive' : ''}`;
|
|
btn.setAttribute('role', 'menuitem');
|
|
if (item?.iconHtml) {
|
|
const icon = document.createElement('span');
|
|
icon.className = 'shared-dropdown-menu__icon';
|
|
icon.innerHTML = item.iconHtml;
|
|
btn.append(icon);
|
|
} else if (item?.iconSrc) {
|
|
const icon = document.createElement('img');
|
|
icon.src = item.iconSrc;
|
|
icon.alt = '';
|
|
icon.setAttribute('aria-hidden', 'true');
|
|
btn.append(icon);
|
|
}
|
|
const label = document.createElement('span');
|
|
label.textContent = String(item?.label || '');
|
|
btn.append(label);
|
|
btn.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
close();
|
|
item?.action?.();
|
|
});
|
|
menu.append(btn);
|
|
});
|
|
|
|
menu.addEventListener('click', (event) => event.stopPropagation());
|
|
document.body.append(menu);
|
|
portal = menu;
|
|
anchorEl.setAttribute('aria-expanded', 'true');
|
|
onOpen?.();
|
|
position();
|
|
};
|
|
|
|
const toggle = () => {
|
|
if (portal) close();
|
|
else open();
|
|
};
|
|
|
|
const onAnchorClick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
toggle();
|
|
};
|
|
const onOutsideClick = (event) => {
|
|
if (!portal) return;
|
|
if (portal.contains(event.target) || anchorEl?.contains(event.target)) return;
|
|
close();
|
|
};
|
|
const onPeerDropdownOpen = (event) => {
|
|
if (!portal || event?.detail?.owner === anchorEl) return;
|
|
close();
|
|
};
|
|
const onKeydown = (event) => {
|
|
if (event.key !== 'Escape' || !portal) return;
|
|
close();
|
|
anchorEl?.focus();
|
|
};
|
|
|
|
anchorEl?.setAttribute('aria-haspopup', 'menu');
|
|
anchorEl?.setAttribute('aria-expanded', 'false');
|
|
anchorEl?.addEventListener('click', onAnchorClick);
|
|
document.addEventListener('click', onOutsideClick);
|
|
document.addEventListener('keydown', onKeydown);
|
|
document.addEventListener('shine:dropdown-open', onPeerDropdownOpen);
|
|
window.addEventListener('resize', position, { passive: true });
|
|
window.addEventListener('scroll', position, { passive: true, capture: true });
|
|
|
|
return {
|
|
open,
|
|
close,
|
|
toggle,
|
|
position,
|
|
destroy() {
|
|
close();
|
|
anchorEl?.removeEventListener('click', onAnchorClick);
|
|
document.removeEventListener('click', onOutsideClick);
|
|
document.removeEventListener('keydown', onKeydown);
|
|
document.removeEventListener('shine:dropdown-open', onPeerDropdownOpen);
|
|
window.removeEventListener('resize', position);
|
|
window.removeEventListener('scroll', position, true);
|
|
},
|
|
};
|
|
}
|