80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useEffect, useRef } from 'react';
|
||
|
|
|
||
|
|
interface DropdownItem {
|
||
|
|
label: string;
|
||
|
|
href: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface DropdownMenuProps {
|
||
|
|
items: DropdownItem[];
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function DropdownMenu({ items, onClose }: DropdownMenuProps) {
|
||
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
// Handle keyboard navigation within dropdown
|
||
|
|
useEffect(() => {
|
||
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||
|
|
if (!menuRef.current) return;
|
||
|
|
|
||
|
|
const focusableElements = menuRef.current.querySelectorAll('a');
|
||
|
|
const currentIndex = Array.from(focusableElements).findIndex(
|
||
|
|
(el) => el === document.activeElement,
|
||
|
|
);
|
||
|
|
|
||
|
|
switch (event.key) {
|
||
|
|
case 'ArrowDown':
|
||
|
|
event.preventDefault();
|
||
|
|
const nextIndex =
|
||
|
|
currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
|
||
|
|
focusableElements[nextIndex]?.focus();
|
||
|
|
break;
|
||
|
|
case 'ArrowUp':
|
||
|
|
event.preventDefault();
|
||
|
|
const prevIndex =
|
||
|
|
currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
|
||
|
|
focusableElements[prevIndex]?.focus();
|
||
|
|
break;
|
||
|
|
case 'Escape':
|
||
|
|
event.preventDefault();
|
||
|
|
onClose();
|
||
|
|
break;
|
||
|
|
case 'Tab':
|
||
|
|
if (event.shiftKey && currentIndex === 0) {
|
||
|
|
onClose();
|
||
|
|
} else if (!event.shiftKey && currentIndex === focusableElements.length - 1) {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
document.addEventListener('keydown', handleKeyDown);
|
||
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||
|
|
}, [onClose]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
ref={menuRef}
|
||
|
|
className="absolute top-full left-0 mt-1 w-64 bg-white rounded-lg shadow-xl border border-amber-200 py-2 z-50 animate-in fade-in-0 zoom-in-95 duration-200"
|
||
|
|
role="menu"
|
||
|
|
aria-orientation="vertical"
|
||
|
|
>
|
||
|
|
{items.map((item, index) => (
|
||
|
|
<a
|
||
|
|
key={index}
|
||
|
|
href={item.href}
|
||
|
|
className="block px-4 py-3 text-sm text-amber-900 hover:bg-amber-50 hover:text-amber-800 transition-colors duration-150 focus:outline-none focus:bg-amber-100 focus:text-amber-800 min-h-[44px] flex items-center"
|
||
|
|
role="menuitem"
|
||
|
|
tabIndex={-1}
|
||
|
|
>
|
||
|
|
{item.label}
|
||
|
|
</a>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|