SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.1 KB · 78 lines tsx
Raw Blame History
1'use client';2import { ChevronDown } from 'lucide-react';3import Link from 'next/link';4import { usePathname } from 'next/navigation';5import { useEffect, useId, useRef, useState } from 'react';6import { t } from '@/i18n';7import { cn } from '@/lib/cn';89interface Item {10  href: string;11  label: string;12  group: 'explore' | 'reference';13}1415/** "More ▾" disclosure in the desktop header: two columns (Explore · Reference), keyboard + outside-click closing. */16export function MoreMenu({ items }: { items: Item[] }) {17  const [open, setOpen] = useState(false);18  const id = useId();19  const ref = useRef<HTMLDivElement>(null);20  const pathname = usePathname();21  const active = items.some((it) => pathname === it.href || pathname.startsWith(`${it.href}/`));2223  useEffect(() => {24    setOpen(false);25  }, [pathname]);26  useEffect(() => {27    if (!open) return;28    const onDoc = (e: PointerEvent) => {29      if (!ref.current?.contains(e.target as Node)) setOpen(false);30    };31    const onKey = (e: KeyboardEvent) => {32      if (e.key === 'Escape') setOpen(false);33    };34    document.addEventListener('pointerdown', onDoc);35    document.addEventListener('keydown', onKey);36    return () => {37      document.removeEventListener('pointerdown', onDoc);38      document.removeEventListener('keydown', onKey);39    };40  }, [open]);4142  const groups: Array<{ key: Item['group']; label: string }> = [43    { key: 'explore', label: t('site.footer.explore') },44    { key: 'reference', label: t('site.footer.reference') },45  ];46  return (47    <div ref={ref} className="relative">48      <button type="button" onClick={() => setOpen((o) => !o)} aria-expanded={open} aria-controls={id} aria-haspopup="menu" className={cn('inline-flex h-9 items-center gap-1 rounded-sm px-2.5 text-sm', active ? 'font-medium text-ink' : 'text-ink-2 hover:bg-surface-2 hover:text-ink')}>49        {t('nav.more')}50        <ChevronDown size={14} aria-hidden className={cn('transition-transform', open && 'rotate-180')} />51      </button>52      {open ? (53        <div id={id} role="menu" aria-label={t('nav.moreMenu')} className="animate-fade absolute left-0 top-full z-40 mt-1 grid w-[26rem] grid-cols-2 gap-x-6 rounded-sm border border-rule bg-surface p-3 shadow-pop">54          {groups.map((g) => (55            <div key={g.key}>56              <div className="eyebrow mb-1 px-2">{g.label}</div>57              <ul>58                {items59                  .filter((it) => it.group === g.key)60                  .map((it) => {61                    const cur = pathname === it.href || pathname.startsWith(`${it.href}/`);62                    return (63                      <li key={it.href}>64                        <Link href={it.href} role="menuitem" aria-current={cur ? 'page' : undefined} className={cn('flex h-9 items-center rounded-sm px-2 text-sm', cur ? 'font-medium text-ink' : 'text-ink-2 hover:bg-surface-2 hover:text-ink')}>65                          {it.label}66                        </Link>67                      </li>68                    );69                  })}70              </ul>71            </div>72          ))}73        </div>74      ) : null}75    </div>76  );77}78