2026-01-13 23:45:28 +01:00

106 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import type { ReactNode } from 'react'
interface MobileMenuProps {
children: ReactNode
'aria-label'?: string
}
function HamburgerIcon({ isOpen }: { isOpen: boolean }): React.ReactElement {
return (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
{isOpen ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
)
}
export function MobileMenu({ children, 'aria-label': ariaLabel }: MobileMenuProps): React.ReactElement {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
useEffect(() => {
const handleEscape = (e: KeyboardEvent): void => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false)
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [isOpen])
return (
<>
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden text-cyber-accent hover:text-neon-cyan transition-colors focus:outline-none focus:ring-2 focus:ring-neon-cyan rounded p-2"
aria-label={ariaLabel ?? 'Toggle menu'}
aria-expanded={isOpen}
aria-controls="mobile-menu"
>
<HamburgerIcon isOpen={isOpen} />
</button>
{isOpen && (
<>
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
<div
id="mobile-menu"
className="fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-cyber-dark border-l border-neon-cyan/30 shadow-glow-cyan z-50 transform transition-transform md:hidden overflow-y-auto"
role="dialog"
aria-modal="true"
aria-label={ariaLabel ?? 'Mobile menu'}
>
<div className="p-4">
<div className="flex justify-end mb-4">
<button
onClick={() => setIsOpen(false)}
className="text-cyber-accent hover:text-neon-cyan text-2xl transition-colors focus:outline-none focus:ring-2 focus:ring-neon-cyan rounded"
aria-label="Close menu"
>
×
</button>
</div>
<div className="space-y-4">{children}</div>
</div>
</div>
</>
)}
</>
)
}