132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
import { forwardRef, useCallback, useState } from "react"
|
|
|
|
// --- Icons ---
|
|
import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon"
|
|
|
|
// --- Hooks ---
|
|
|
|
// --- Tiptap UI ---
|
|
import { HeadingButton } from "@/components/tiptap-ui/heading-button"
|
|
import type { UseHeadingDropdownMenuConfig } from "@/components/tiptap-ui/heading-dropdown-menu"
|
|
import { useHeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu"
|
|
|
|
// --- UI Primitives ---
|
|
import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
|
|
import { Button } from "@/components/tiptap-ui-primitive/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuGroup,
|
|
} from "@/components/tiptap-ui-primitive/dropdown-menu"
|
|
import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
|
|
|
|
export interface HeadingDropdownMenuProps
|
|
extends Omit<ButtonProps, "type">, UseHeadingDropdownMenuConfig {
|
|
/**
|
|
* Callback for when the dropdown opens or closes
|
|
*/
|
|
onOpenChange?: (isOpen: boolean) => void
|
|
/**
|
|
* Whether the dropdown should use a modal
|
|
*/
|
|
modal?: boolean
|
|
}
|
|
|
|
/**
|
|
* Dropdown menu component for selecting heading levels in a Tiptap editor.
|
|
*
|
|
* For custom dropdown implementations, use the `useHeadingDropdownMenu` hook instead.
|
|
*/
|
|
export const HeadingDropdownMenu = forwardRef<
|
|
HTMLButtonElement,
|
|
HeadingDropdownMenuProps
|
|
>(
|
|
(
|
|
{
|
|
editor: providedEditor,
|
|
levels = [1, 2, 3, 4, 5, 6],
|
|
hideWhenUnavailable = false,
|
|
onOpenChange,
|
|
children,
|
|
modal = true,
|
|
...buttonProps
|
|
},
|
|
ref
|
|
) => {
|
|
const { editor } = useTiptapEditor(providedEditor)
|
|
const [isOpen, setIsOpen] = useState<boolean>(false)
|
|
const { isVisible, isActive, canToggle, Icon } = useHeadingDropdownMenu({
|
|
editor,
|
|
levels,
|
|
hideWhenUnavailable,
|
|
})
|
|
|
|
const handleOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
if (!editor || !canToggle) {
|
|
return
|
|
}
|
|
|
|
setIsOpen(open)
|
|
onOpenChange?.(open)
|
|
},
|
|
[canToggle, editor, onOpenChange]
|
|
)
|
|
|
|
if (!isVisible) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu modal={modal} open={isOpen} onOpenChange={handleOpenChange}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
data-active-state={isActive ? "on" : "off"}
|
|
role="button"
|
|
tabIndex={-1}
|
|
disabled={!canToggle}
|
|
data-disabled={!canToggle}
|
|
aria-label="Format text as heading"
|
|
aria-pressed={isActive}
|
|
tooltip="Heading"
|
|
{...buttonProps}
|
|
ref={ref}
|
|
>
|
|
{children ? (
|
|
children
|
|
) : (
|
|
<>
|
|
<Icon className="tiptap-button-icon" />
|
|
<ChevronDownIcon className="tiptap-button-dropdown-small" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="start">
|
|
<DropdownMenuGroup>
|
|
{levels.map((level) => (
|
|
<DropdownMenuItem key={`heading-${level}`} asChild>
|
|
<HeadingButton
|
|
editor={editor}
|
|
level={level}
|
|
text={`Heading ${level}`}
|
|
showTooltip={false}
|
|
/>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
)
|
|
|
|
HeadingDropdownMenu.displayName = "HeadingDropdownMenu"
|
|
|
|
export default HeadingDropdownMenu
|