94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect } from 'react'
|
||
import Link from 'next/link'
|
||
import BrandLogo from './brand-logo'
|
||
import { usePathname } from 'next/navigation'
|
||
import { SITE_LINKS } from './site-links'
|
||
import AccountLink from './account-link'
|
||
|
||
/* Shared mobile navigation, including keyboard focus and background locking. */
|
||
export default function SiteMenu({
|
||
menuId = 'siteMenu',
|
||
viewer,
|
||
}: {
|
||
menuId?: string
|
||
viewer?: { email: string } | null
|
||
}) {
|
||
const pathname = usePathname()
|
||
|
||
useEffect(() => {
|
||
const menu = document.getElementById(menuId)
|
||
const trigger = document.getElementById('menuTrigger')
|
||
if (!menu || !trigger) return
|
||
const background = [...document.querySelectorAll<HTMLElement>('#site > *')]
|
||
.filter(element => element !== menu)
|
||
const originalInert = background.map(element => element.inert)
|
||
|
||
function setOpen(open: boolean) {
|
||
menu!.classList.toggle('open', open)
|
||
menu!.setAttribute('aria-hidden', String(!open))
|
||
trigger!.setAttribute('aria-expanded', String(open))
|
||
document.body.classList.toggle('locked', open)
|
||
background.forEach((element, index) => { element.inert = open || originalInert[index] })
|
||
if (open) (menu!.querySelector('.menu-head [data-menu-close]') as HTMLElement | null)?.focus()
|
||
else trigger!.focus()
|
||
}
|
||
|
||
const onTrigger = () => setOpen(true)
|
||
const onScrim = (e: Event) => {
|
||
if ((e.target as HTMLElement).closest('[data-menu-close]')) setOpen(false)
|
||
}
|
||
const onLink = () => setOpen(false)
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape' && menu!.classList.contains('open')) setOpen(false)
|
||
if (e.key === 'Tab' && menu!.classList.contains('open')) {
|
||
const focusable = [...menu!.querySelectorAll<HTMLElement>('a[href], button:not([disabled])')]
|
||
.filter(element => element.getClientRects().length && !element.classList.contains('menu-scrim'))
|
||
const first = focusable[0]
|
||
const last = focusable[focusable.length - 1]
|
||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus() }
|
||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus() }
|
||
}
|
||
}
|
||
|
||
trigger.addEventListener('click', onTrigger)
|
||
menu.addEventListener('click', onScrim)
|
||
menu.querySelectorAll('a').forEach((a) => a.addEventListener('click', onLink))
|
||
document.addEventListener('keydown', onKey)
|
||
return () => {
|
||
trigger.removeEventListener('click', onTrigger)
|
||
menu.removeEventListener('click', onScrim)
|
||
menu.querySelectorAll('a').forEach(a => a.removeEventListener('click', onLink))
|
||
document.removeEventListener('keydown', onKey)
|
||
document.body.classList.remove('locked')
|
||
background.forEach((element, index) => { element.inert = originalInert[index] })
|
||
}
|
||
}, [menuId])
|
||
|
||
return (
|
||
<aside className="menu-panel" id={menuId} aria-hidden="true" data-od-id="site-menu">
|
||
<button className="menu-scrim" type="button" aria-label="إغلاق القائمة" data-menu-close></button>
|
||
<div className="menu-drawer" role="dialog" aria-modal="true" aria-label="قائمة أنور پآي">
|
||
<div className="menu-head">
|
||
<Link className="logo" href="/">
|
||
<BrandLogo />
|
||
</Link>
|
||
<button className="icon-btn" type="button" aria-label="إغلاق القائمة" data-menu-close>×</button>
|
||
</div>
|
||
<nav className="menu-links" aria-label="قائمة الموقع">
|
||
{SITE_LINKS.map((l) => (
|
||
<Link key={l.href} href={l.href} aria-current={pathname === l.href ? 'page' : undefined}>
|
||
{l.label} <span>←</span>
|
||
</Link>
|
||
))}
|
||
</nav>
|
||
<div className="menu-foot">
|
||
<AccountLink viewer={viewer} />
|
||
<p>محتوى عربي عملي، بلا ضجيج.</p>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
)
|
||
}
|