Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
TableIcon,
TypeIcon,
Undo2Icon,
Link2,
} from "lucide-react";

/**
Expand Down Expand Up @@ -264,6 +265,12 @@ export const COMMANDS: Record<TYPE_COMMANDS_KEYS, EditorCommand> = {
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;
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const COMMANDS_KEYS = [
"redo",
"codeblock",
"horizontal_rule",
"link",
] as const;

export type TYPE_COMMANDS_KEYS = (typeof COMMANDS_KEYS)[number];
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<BubbleMenu editor={editor} shouldShow={shouldShow}>
<div className="p-2 w-72 flex items-center gap-2 bg-surface-white shadow-xl rounded">
<div className="w-full">
<TextInput
type="text"
placeholder="https://example.com"
variant="subtle"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
<div className="shrink-0 flex items-center gap-1.5 ml-auto">
<Button
aria-label="Confirm Link"
icon={() => <Check className="w-4 h-4" />}
variant="subtle"
onClick={() => {
setLink(value ?? "");
close();
}}
/>
<Button
aria-label="Reset Link"
icon={() => <X className="w-4 h-4" />}
variant="subtle"
onClick={() => {
unsetLink();
close();
}}
/>
</div>
</div>
</BubbleMenu>
);
};

export default LinkBubbleMenu;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const DEFAULT_COMMANDS: Array<
"bold",
"italic",
"strike",
"link",
"font_color",
"separator",
"bullet_list",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="m-2 w-[550px]">
<TextEditor {...args} />
</div>
);
},
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");
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -59,6 +60,9 @@ const TextEditor = ({
strike: false,
blockquote: false,
horizontalRule: false,
link: {
openOnClick: false,
},
...starterkitOptions,
}),
Placeholder.configure({
Expand Down Expand Up @@ -114,6 +118,7 @@ const TextEditor = ({

return (
<EditorContext.Provider value={{ editor }}>
<LinkBubbleMenu />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When clicking on the link, it opens directly. This behavior is different from frappe-ui, where a popup appears with options to click or edit the link. I think that approach is better, as accidentally clicking the link while editing opens a new tab. Overall, I don’t think the current UX is good.

{Top && <Top />}
{fixedMenu && <FixedMenu />}
{Editor ? <Editor editor={editor} /> : <EditorContent editor={editor} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
value={inputValue}
required={rest.required}
onChange={handleChange}
data-testid="text-input"
className={`appearance-none ${inputClasses}`}
{...rest}
/>
Expand Down
1 change: 1 addition & 0 deletions packages/frappe-ui-react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"outDir": "dist",
"declaration": true,
"declarationDir": "dist-types",
"moduleResolution": "bundler",
"sourceMap": true,
"typeRoots": ["./typings", "./node_modules/@types"],
"noUncheckedSideEffectImports": true,
Expand Down