Files
anwar-pi-website/web/src/components/site-menu.tsx
T

94 lines
3.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.
'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>
)
}