master @ 82 LINES
[ HISTORY ] [ UP ]
┌─ ASTRO ────────────────────────────────────────────────────────────────────┐
│ --- │
│ // Dropdown menu: [ LABEL ↓ ] trigger over a framed popover of actions. │
│ // ↓ instead of ? - it's the arrow the frame font ships (see Select). │
│ import Pre from "../Pre/Pre.astro"; │
│ import { G, pad, rule, checkGrid } from "../../lib/ascii"; │
│ import { menuCells, type Props } from "./Menu"; │
│ import "./Menu.scss"; │
│ │
│ const { label, items, cells } = Astro.props as Props; │
│ const inner = cells ?? menuCells(items); │
│ const ctx = `Menu(${label})`; │
│ checkGrid(items.map((it) => it.label).join("\n"), inner, ctx); │
│ --- │
│ <span class="menu"> │
│ <button type="button" class="menu-trigger" aria-haspopup="menu" aria-expanded="f │
│ alse">[ {label} ↓ ]</button> │
│ <div class="menu-pop" role="menu" hidden> │
│ <Pre>{G.TL}{rule(inner + 2)}{G.TR}</Pre> │
│ {items.map((it) => ( │
│ <Pre>{G.V}{" "}{it.href │
│ ? <a class="menu-item" role="menuitem" href={it.href}>{pad(it.label, inner │
│ )}</a> │
│ : <button type="button" class="menu-item" role="menuitem">{pad(it.label, i │
│ nner)}</button> │
│ }{" "}{G.V}</Pre> │
│ ))} │
│ <Pre>{G.BL}{rule(inner + 2)}{G.BR}</Pre> │
│ </div> │
│ </span> │
│ │
│ <script> │
│ function close(menu: HTMLElement) { │
│ menu.querySelector<HTMLElement>(".menu-pop")!.hidden = true; │
│ menu.querySelector(".menu-trigger")!.setAttribute("aria-expanded", "false"); │
│ } │
│ │
│ function closeAll(except?: HTMLElement) { │
│ document.querySelectorAll<HTMLElement>(".menu").forEach((m) => { │
│ if (m !== except) close(m); │
│ }); │
│ } │
│ │
│ document.addEventListener("click", (e) => { │
│ const target = e.target as HTMLElement; │
│ const trigger = target.closest<HTMLElement>(".menu-trigger"); │
│ if (trigger) { │
│ const menu = trigger.closest<HTMLElement>(".menu")!; │
│ const pop = menu.querySelector<HTMLElement>(".menu-pop")!; │
│ closeAll(menu); │
│ pop.hidden = !pop.hidden; │
│ trigger.setAttribute("aria-expanded", String(!pop.hidden)); │
│ if (!pop.hidden) pop.querySelector<HTMLElement>(".menu-item")?.focus(); │
│ return; │
│ } │
│ if (target.closest(".menu-item")) { │
│ const menu = target.closest<HTMLElement>(".menu"); │
│ if (menu) close(menu); │
│ return; │
│ } │
│ closeAll(); │
│ }); │
│ │
│ document.addEventListener("keydown", (e) => { │
│ const menu = (e.target as HTMLElement).closest?.<HTMLElement>(".menu"); │
│ if (!menu) return; │
│ const pop = menu.querySelector<HTMLElement>(".menu-pop")!; │
│ if (pop.hidden) return; │
│ │
│ if (e.key === "Escape") { │
│ e.preventDefault(); │
│ close(menu); │
│ menu.querySelector<HTMLElement>(".menu-trigger")?.focus(); │
│ return; │
│ } │
│ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; │
│ e.preventDefault(); │
│ const menuItems = Array.from(pop.querySelectorAll<HTMLElement>(".menu-item")); │
│ const idx = menuItems.indexOf(document.activeElement as HTMLElement); │
│ const next = e.key === "ArrowDown" │
│ ? Math.min(menuItems.length - 1, idx + 1) │
│ : Math.max(0, idx - 1); │
│ menuItems[next]?.focus(); │
│ }); │
│ </script> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ASTRO ──────────────────────────────┐
│ --- │
│ // Dropdown menu: [ LABEL ↓ ] trigger over a │
│ framed popover of actions. │
│ // ↓ instead of ? - it's the arrow the frame │
│ font ships (see Select). │
│ import Pre from "../Pre/Pre.astro"; │
│ import { G, pad, rule, checkGrid } from "../ │
│ ../lib/ascii"; │
│ import { menuCells, type Props } from "./Men │
│ u"; │
│ import "./Menu.scss"; │
│ │
│ const { label, items, cells } = Astro.props │
│ as Props; │
│ const inner = cells ?? menuCells(items); │
│ const ctx = `Menu(${label})`; │
│ checkGrid(items.map((it) => it.label).join(" │
│ \n"), inner, ctx); │
│ --- │
│ <span class="menu"> │
│ <button type="button" class="menu-trigger" │
│ aria-haspopup="menu" aria-expanded="false"> │
│ [ {label} ↓ ]</button> │
│ <div class="menu-pop" role="menu" hidden> │
│ <Pre>{G.TL}{rule(inner + 2)}{G.TR}</Pre> │
│ {items.map((it) => ( │
│ <Pre>{G.V}{" "}{it.href │
│ ? <a class="menu-item" role="menuite │
│ m" href={it.href}>{pad(it.label, inner)}</a> │
│ : <button type="button" class="menu- │
│ item" role="menuitem">{pad(it.label, inner)} │
│ </button> │
│ }{" "}{G.V}</Pre> │
│ ))} │
│ <Pre>{G.BL}{rule(inner + 2)}{G.BR}</Pre> │
│ </div> │
│ </span> │
│ │
│ <script> │
│ function close(menu: HTMLElement) { │
│ menu.querySelector<HTMLElement>(".menu-p │
│ op")!.hidden = true; │
│ menu.querySelector(".menu-trigger")!.set │
│ Attribute("aria-expanded", "false"); │
│ } │
│ │
│ function closeAll(except?: HTMLElement) { │
│ document.querySelectorAll<HTMLElement>(" │
│ .menu").forEach((m) => { │
│ if (m !== except) close(m); │
│ }); │
│ } │
│ │
│ document.addEventListener("click", (e) => │
│ { │
│ const target = e.target as HTMLElement; │
│ const trigger = target.closest<HTMLEleme │
│ nt>(".menu-trigger"); │
│ if (trigger) { │
│ const menu = trigger.closest<HTMLEleme │
│ nt>(".menu")!; │
│ const pop = menu.querySelector<HTMLEle │
│ ment>(".menu-pop")!; │
│ closeAll(menu); │
│ pop.hidden = !pop.hidden; │
│ trigger.setAttribute("aria-expanded", │
│ String(!pop.hidden)); │
│ if (!pop.hidden) pop.querySelector<HTM │
│ LElement>(".menu-item")?.focus(); │
│ return; │
│ } │
│ if (target.closest(".menu-item")) { │
│ const menu = target.closest<HTMLElemen │
│ t>(".menu"); │
│ if (menu) close(menu); │
│ return; │
│ } │
│ closeAll(); │
│ }); │
│ │
│ document.addEventListener("keydown", (e) = │
│ > { │
│ const menu = (e.target as HTMLElement).c │
│ losest?.<HTMLElement>(".menu"); │
│ if (!menu) return; │
│ const pop = menu.querySelector<HTMLEleme │
│ nt>(".menu-pop")!; │
│ if (pop.hidden) return; │
│ │
│ if (e.key === "Escape") { │
│ e.preventDefault(); │
│ close(menu); │
│ menu.querySelector<HTMLElement>(".menu │
│ -trigger")?.focus(); │
│ return; │
│ } │
│ if (e.key !== "ArrowDown" && e.key !== " │
│ ArrowUp") return; │
│ e.preventDefault(); │
│ const menuItems = Array.from(pop.querySe │
│ lectorAll<HTMLElement>(".menu-item")); │
│ const idx = menuItems.indexOf(document.a │
│ ctiveElement as HTMLElement); │
│ const next = e.key === "ArrowDown" │
│ ? Math.min(menuItems.length - 1, idx + │
│ 1) │
│ : Math.max(0, idx - 1); │
│ menuItems[next]?.focus(); │
│ }); │
│ </script> │
└──────────────────────────────────────────────┘
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET