diff --git a/packages/frappe-ui-react/src/components/textEditor/menu/commands/index.ts b/packages/frappe-ui-react/src/components/textEditor/menu/commands/index.ts index 214e0eeb..0e36a1aa 100644 --- a/packages/frappe-ui-react/src/components/textEditor/menu/commands/index.ts +++ b/packages/frappe-ui-react/src/components/textEditor/menu/commands/index.ts @@ -25,6 +25,7 @@ import { TableIcon, TypeIcon, Undo2Icon, + Link2, } from "lucide-react"; /** @@ -264,6 +265,12 @@ export const COMMANDS: Record = { isDisabled: (editor) => !editor.can().redo(), isActive: () => false, }, + link: { + label: "Link", + icon: Link2, + action: (editor) => editor.chain().focus().setLink({ href: "" }).run(), + isActive: (editor) => editor.isActive("link"), + }, }; export default COMMANDS; diff --git a/packages/frappe-ui-react/src/components/textEditor/menu/commands/types.ts b/packages/frappe-ui-react/src/components/textEditor/menu/commands/types.ts index e2b621a3..c46ceff1 100644 --- a/packages/frappe-ui-react/src/components/textEditor/menu/commands/types.ts +++ b/packages/frappe-ui-react/src/components/textEditor/menu/commands/types.ts @@ -39,6 +39,7 @@ export const COMMANDS_KEYS = [ "redo", "codeblock", "horizontal_rule", + "link", ] as const; export type TYPE_COMMANDS_KEYS = (typeof COMMANDS_KEYS)[number]; diff --git a/packages/frappe-ui-react/src/components/textEditor/menu/linkBubbleMenu.tsx b/packages/frappe-ui-react/src/components/textEditor/menu/linkBubbleMenu.tsx new file mode 100644 index 00000000..f297996c --- /dev/null +++ b/packages/frappe-ui-react/src/components/textEditor/menu/linkBubbleMenu.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies. + */ +import { useCurrentEditor, useEditorState } from "@tiptap/react"; +import { BubbleMenu, type BubbleMenuProps } from "@tiptap/react/menus"; +import { Check, X } from "lucide-react"; +import { useEffect, useState } from "react"; + +/** + * Internal dependencies. + */ +import { TextInput } from "../../textInput"; +import { Button } from "../../button"; + +const LinkBubbleMenu = () => { + const { editor } = useCurrentEditor(); + + const state = useEditorState({ + editor, + selector: ({ editor }) => ({ + currentLink: (editor?.getAttributes("link").href || "") as string, + from: editor?.state.selection.from, + }), + }); + + const [value, setValue] = useState(state?.currentLink); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setValue(state?.currentLink ?? ""); + }, [state?.currentLink]); + + if (!editor) { + return null; + } + + const shouldShow: BubbleMenuProps["shouldShow"] = ({ editor, from, to }) => { + return editor.isActive("link") && from < to; + }; + + const unsetLink = () => { + editor.chain().focus().unsetLink().run(); + }; + + const setLink = (href: string) => { + editor.chain().setLink({ href }).run(); + setValue(""); + }; + + const close = () => { + if (state && state.from) { + editor.commands.setTextSelection(state.from); + } + }; + + return ( + +
+
+ setValue(e.target.value)} + /> +
+
+
+
+
+ ); +}; + +export default LinkBubbleMenu; diff --git a/packages/frappe-ui-react/src/components/textEditor/menu/menu.tsx b/packages/frappe-ui-react/src/components/textEditor/menu/menu.tsx index b333c0e2..793e4d42 100644 --- a/packages/frappe-ui-react/src/components/textEditor/menu/menu.tsx +++ b/packages/frappe-ui-react/src/components/textEditor/menu/menu.tsx @@ -32,6 +32,7 @@ const DEFAULT_COMMANDS: Array< "bold", "italic", "strike", + "link", "font_color", "separator", "bullet_list", diff --git a/packages/frappe-ui-react/src/components/textEditor/textEditor.interactions.stories.tsx b/packages/frappe-ui-react/src/components/textEditor/textEditor.interactions.stories.tsx index 417155e6..b8950dea 100644 --- a/packages/frappe-ui-react/src/components/textEditor/textEditor.interactions.stories.tsx +++ b/packages/frappe-ui-react/src/components/textEditor/textEditor.interactions.stories.tsx @@ -255,3 +255,40 @@ export const EditorFontColor: Story = { expect(newText).toHaveStyle("background-color: #ffe7e7"); }, }; + +export const EditorLink: Story = { + args: { + content: CONTENT, + editorClass: "prose-sm min-h-[4rem] border rounded-b-lg border-t-0 p-2", + fixedMenu: true, + }, + render: function BasicRender(args) { + return ( +
+ +
+ ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const text = canvas.getByText((content) => { + return content.includes("paragraph"); + }); + + await userEvent.tripleClick(text); + + const linkButton = await screen.findByTitle("Link"); + await userEvent.click(linkButton); + + const linkInput = await screen.findByPlaceholderText(/example.com/i); + await userEvent.type(linkInput, "https://test-link.com"); + + const confirmButton = await screen.findByRole("button", { + name: /confirm link/i, + }); + await userEvent.click(confirmButton); + + const linkElement = canvas.getByRole("link"); + expect(linkElement).toHaveAttribute("href", "https://test-link.com"); + }, +}; diff --git a/packages/frappe-ui-react/src/components/textEditor/textEditor.tsx b/packages/frappe-ui-react/src/components/textEditor/textEditor.tsx index c07bf08f..2d9ce4b5 100644 --- a/packages/frappe-ui-react/src/components/textEditor/textEditor.tsx +++ b/packages/frappe-ui-react/src/components/textEditor/textEditor.tsx @@ -22,6 +22,7 @@ import { normalizeClasses } from "../../utils"; import type { TextEditorProps } from "./types"; import FixedMenu from "./menu/fixedMenu"; import { ExtendedCodeBlock } from "./extension/codeBlock"; +import LinkBubbleMenu from "./menu/linkBubbleMenu"; const TextEditor = ({ content, @@ -59,6 +60,9 @@ const TextEditor = ({ strike: false, blockquote: false, horizontalRule: false, + link: { + openOnClick: false, + }, ...starterkitOptions, }), Placeholder.configure({ @@ -114,6 +118,7 @@ const TextEditor = ({ return ( + {Top && } {fixedMenu && } {Editor ? : } diff --git a/packages/frappe-ui-react/src/components/textInput/textInput.tsx b/packages/frappe-ui-react/src/components/textInput/textInput.tsx index bcbe4465..1d0da59e 100644 --- a/packages/frappe-ui-react/src/components/textInput/textInput.tsx +++ b/packages/frappe-ui-react/src/components/textInput/textInput.tsx @@ -149,7 +149,6 @@ const TextInput = forwardRef( value={inputValue} required={rest.required} onChange={handleChange} - data-testid="text-input" className={`appearance-none ${inputClasses}`} {...rest} /> diff --git a/packages/frappe-ui-react/tsconfig.json b/packages/frappe-ui-react/tsconfig.json index 9676ca63..46d74f90 100644 --- a/packages/frappe-ui-react/tsconfig.json +++ b/packages/frappe-ui-react/tsconfig.json @@ -6,6 +6,7 @@ "outDir": "dist", "declaration": true, "declarationDir": "dist-types", + "moduleResolution": "bundler", "sourceMap": true, "typeRoots": ["./typings", "./node_modules/@types"], "noUncheckedSideEffectImports": true,