50 lines
2.3 KiB
TypeScript
50 lines
2.3 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||
import { IconButton } from "./Button.tsx";
|
||
import { IconMore } from "./icons.tsx";
|
||
|
||
// выпадающий список: кнопка открывает, щелчок вне или Escape закрывает (строки 897–904)
|
||
export function Dropdown({ button, className = "", children, onOpen }: {
|
||
button: (p: { open: boolean; toggle(): void }) => ReactNode; className?: string;
|
||
children: (close: () => void) => ReactNode; onOpen?: () => void;
|
||
}) {
|
||
const [open, setOpen] = useState(false);
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const away = (e: PointerEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); };
|
||
document.addEventListener("pointerdown", away);
|
||
return () => document.removeEventListener("pointerdown", away);
|
||
}, [open]);
|
||
const toggle = () => { if (!open) onOpen?.(); setOpen(!open); }; // T12.1: побочное действие вне updater
|
||
return (
|
||
<div className={"dd " + className} ref={ref} onKeyDown={(e) => { if (e.key === "Escape") setOpen(false); }}>
|
||
{button({ open, toggle })}
|
||
{open && <div className={"dd-menu" + (className.includes("more") ? " menu" : "")}>{children(() => setOpen(false))}</div>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// меню «Ещё»: Dropdown с кнопкой-иконкой; пункты — кнопки (aria-pressed для переключателей)
|
||
export function Menu({ label, children, align = "end" }: {
|
||
label: string; children: (close: () => void) => ReactNode; align?: "start" | "end";
|
||
}) {
|
||
return (
|
||
<Dropdown className={"more" + (align === "start" ? " dd--start" : "")}
|
||
button={({ open, toggle }) => <IconButton label={label} icon={<IconMore />} aria-haspopup="menu" aria-expanded={open} onClick={toggle} />}>
|
||
{children}
|
||
</Dropdown>
|
||
);
|
||
}
|
||
|
||
export function MenuItem({ onSelect, pressed, danger, icon, children }: {
|
||
onSelect(): void; pressed?: boolean; danger?: boolean; icon?: ReactNode; children: ReactNode;
|
||
}) {
|
||
return (
|
||
<button type="button" className={danger ? "danger" : undefined} aria-pressed={pressed === undefined ? undefined : pressed} onClick={onSelect}>
|
||
{icon}{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
export function MenuSeparator() { return <hr />; }
|