interface HandleDropdownParams { toggle: HTMLElement; menu: HTMLElement; showOnHover?: boolean, onOpen?: Function | undefined; onClose?: Function | undefined; showAside?: boolean; } function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) { const toggleRect = toggle.getBoundingClientRect(); const menuBounds = menu.getBoundingClientRect(); menu.style.position = 'fixed'; if (showAside) { let targetLeft = toggleRect.right; const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth; if (isRightOOB) { targetLeft = Math.max(toggleRect.left - menuBounds.width, 0); } menu.style.top = toggleRect.top + 'px'; menu.style.left = targetLeft + 'px'; } else { const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth; let targetLeft = toggleRect.left; if (isRightOOB) { targetLeft = Math.max(toggleRect.right - menuBounds.width, 0); } menu.style.top = toggleRect.bottom + 'px'; menu.style.left = targetLeft + 'px'; } } export function handleDropdown(options: HandleDropdownParams) { const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options; let clickListener: Function|null = null; const hide = () => { menu.hidden = true; menu.style.removeProperty('position'); menu.style.removeProperty('left'); menu.style.removeProperty('top'); if (clickListener) { window.removeEventListener('click', clickListener as EventListener); } if (onClose) { onClose(); } }; const show = () => { menu.hidden = false positionMenu(menu, toggle, Boolean(showAside)); clickListener = (event: MouseEvent) => { if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) { hide(); } } window.addEventListener('click', clickListener as EventListener); if (onOpen) { onOpen(); } }; const toggleShowing = (event: MouseEvent) => { menu.hasAttribute('hidden') ? show() : hide(); }; toggle.addEventListener('click', toggleShowing); if (showOnHover) { toggle.addEventListener('mouseenter', toggleShowing); } menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => { // Prevent mouseleave hiding if withing the same bounds of the toggle. // Avoids hiding in the event the mouse is interrupted by a high z-index // item like a browser scrollbar. const toggleBounds = toggle.getBoundingClientRect(); const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left; const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top; const withinToggle = withinX && withinY; if (!withinToggle) { hide(); } }); }