Files
wr.do/components/shared/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx
2025-10-20 11:34:05 +08:00

128 lines
3.5 KiB
TypeScript

"use client";
import * as React from "react";
import { Separator } from "@/components/shared/tiptap/tiptap-ui-primitive/separator";
import "@/components/shared/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss";
import { cn } from "@/lib/tiptap-utils";
import { useComposedRef } from "@/hooks/use-composed-ref";
import { useMenuNavigation } from "@/hooks/use-menu-navigation";
type BaseProps = React.HTMLAttributes<HTMLDivElement>;
interface ToolbarProps extends BaseProps {
variant?: "floating" | "fixed";
}
const useToolbarNavigation = (
toolbarRef: React.RefObject<HTMLDivElement | null>,
) => {
const [items, setItems] = React.useState<HTMLElement[]>([]);
const collectItems = React.useCallback(() => {
if (!toolbarRef.current) return [];
return Array.from(
toolbarRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [role="button"]:not([disabled]), [tabindex="0"]:not([disabled])',
),
);
}, [toolbarRef]);
React.useEffect(() => {
const toolbar = toolbarRef.current;
if (!toolbar) return;
const updateItems = () => setItems(collectItems());
updateItems();
const observer = new MutationObserver(updateItems);
observer.observe(toolbar, { childList: true, subtree: true });
return () => observer.disconnect();
}, [collectItems, toolbarRef]);
const { selectedIndex } = useMenuNavigation<HTMLElement>({
containerRef: toolbarRef,
items,
orientation: "horizontal",
onSelect: (el) => el.click(),
autoSelectFirstItem: false,
});
React.useEffect(() => {
const toolbar = toolbarRef.current;
if (!toolbar) return;
const handleFocus = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (toolbar.contains(target))
target.setAttribute("data-focus-visible", "true");
};
const handleBlur = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (toolbar.contains(target))
target.removeAttribute("data-focus-visible");
};
toolbar.addEventListener("focus", handleFocus, true);
toolbar.addEventListener("blur", handleBlur, true);
return () => {
toolbar.removeEventListener("focus", handleFocus, true);
toolbar.removeEventListener("blur", handleBlur, true);
};
}, [toolbarRef]);
React.useEffect(() => {
if (selectedIndex !== undefined && items[selectedIndex]) {
items[selectedIndex].focus();
}
}, [selectedIndex, items]);
};
export const Toolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
({ children, className, variant = "fixed", ...props }, ref) => {
const toolbarRef = React.useRef<HTMLDivElement>(null);
const composedRef = useComposedRef(toolbarRef, ref);
useToolbarNavigation(toolbarRef);
return (
<div
ref={composedRef}
role="toolbar"
aria-label="toolbar"
data-variant={variant}
className={cn("tiptap-toolbar", className)}
{...props}
>
{children}
</div>
);
},
);
Toolbar.displayName = "Toolbar";
export const ToolbarGroup = React.forwardRef<HTMLDivElement, BaseProps>(
({ children, className, ...props }, ref) => (
<div
ref={ref}
role="group"
className={cn("tiptap-toolbar-group", className)}
{...props}
>
{children}
</div>
),
);
ToolbarGroup.displayName = "ToolbarGroup";
export const ToolbarSeparator = React.forwardRef<HTMLDivElement, BaseProps>(
({ ...props }, ref) => (
<Separator ref={ref} orientation="vertical" decorative {...props} />
),
);
ToolbarSeparator.displayName = "ToolbarSeparator";