2026-01-13 23:46:43 +01:00

146 lines
3.4 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 (
<>
<MobileMenuButton
isOpen={isOpen}
onClick={() => setIsOpen(!isOpen)}
ariaLabel={ariaLabel ?? 'Toggle menu'}
/>
{isOpen && (
<MobileMenuDrawer
isOpen={isOpen}
onClose={() => setIsOpen(false)}
ariaLabel={ariaLabel ?? 'Mobile menu'}
>
{children}
</MobileMenuDrawer>
)}
</>
)
}
function MobileMenuButton({
isOpen,
onClick,
ariaLabel,
}: {
isOpen: boolean
onClick: () => void
ariaLabel: string
}): React.ReactElement {
return (
<button
onClick={onClick}
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}
aria-expanded={isOpen}
aria-controls="mobile-menu"
>
<HamburgerIcon isOpen={isOpen} />
</button>
)
}
function MobileMenuDrawer({
isOpen,
onClose,
ariaLabel,
children,
}: {
isOpen: boolean
onClose: () => void
ariaLabel: string
children: ReactNode
}): React.ReactElement {
return (
<>
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
onClick={onClose}
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}
>
<div className="p-4">
<div className="flex justify-end mb-4">
<button
onClick={onClose}
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>
</>
)
}