fix: bubble menu weird flickering fixed (#6591)

This commit is contained in:
M. Palanikannan 2025-02-19 02:09:27 +05:30 committed by GitHub
parent d3af913ec7
commit 126575d22a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 105 additions and 67 deletions

View file

@ -6,15 +6,15 @@ import { cn } from "@plane/utils";
import { TextAlignItem } from "@/components/menus"; import { TextAlignItem } from "@/components/menus";
// types // types
import { TEditorCommands } from "@/types"; import { TEditorCommands } from "@/types";
import { EditorStateType } from "./root";
type Props = { type Props = {
editor: Editor; editor: Editor;
onClose: () => void; editorState: EditorStateType;
}; };
export const TextAlignmentSelector: React.FC<Props> = (props) => { export const TextAlignmentSelector: React.FC<Props> = (props) => {
const { editor, onClose } = props; const { editor, editorState } = props;
const menuItem = TextAlignItem(editor); const menuItem = TextAlignItem(editor);
const textAlignmentOptions: { const textAlignmentOptions: {
@ -32,10 +32,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({ menuItem.command({
alignment: "left", alignment: "left",
}), }),
isActive: () => isActive: () => editorState.left,
menuItem.isActive({
alignment: "left",
}),
}, },
{ {
itemKey: "text-align", itemKey: "text-align",
@ -45,10 +42,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({ menuItem.command({
alignment: "center", alignment: "center",
}), }),
isActive: () => isActive: () => editorState.center,
menuItem.isActive({
alignment: "center",
}),
}, },
{ {
itemKey: "text-align", itemKey: "text-align",
@ -58,10 +52,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({ menuItem.command({
alignment: "right", alignment: "right",
}), }),
isActive: () => isActive: () => editorState.right,
menuItem.isActive({
alignment: "right",
}),
}, },
]; ];
@ -74,7 +65,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
item.command(); item.command();
onClose();
}} }}
className={cn( className={cn(
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors", "size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",

View file

@ -1,24 +1,26 @@
import { Dispatch, FC, SetStateAction } from "react";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { ALargeSmall, Ban } from "lucide-react"; import { ALargeSmall, Ban } from "lucide-react";
import { Dispatch, FC, SetStateAction } from "react";
// plane utils // plane utils
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// constants // constants
import { COLORS_LIST } from "@/constants/common"; import { COLORS_LIST } from "@/constants/common";
// helpers // helpers
import { BackgroundColorItem, TextColorItem } from "../menu-items"; import { BackgroundColorItem, TextColorItem } from "../menu-items";
import { EditorStateType } from "./root";
type Props = { type Props = {
editor: Editor; editor: Editor;
isOpen: boolean; isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
editorState: EditorStateType;
}; };
export const BubbleMenuColorSelector: FC<Props> = (props) => { export const BubbleMenuColorSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props; const { editor, isOpen, setIsOpen, editorState } = props;
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })); const activeTextColor = editorState.color;
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })); const activeBackgroundColor = editorState.backgroundColor;
return ( return (
<div className="relative h-full"> <div className="relative h-full">

View file

@ -1,9 +1,10 @@
import { FC, useEffect, useState } from "react"; import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react"; import { FC, useEffect, useState, useRef } from "react";
// plane utils // plane utils
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// components // components
import { import {
BackgroundColorItem,
BoldItem, BoldItem,
BubbleMenuColorSelector, BubbleMenuColorSelector,
BubbleMenuLinkSelector, BubbleMenuLinkSelector,
@ -11,8 +12,12 @@ import {
CodeItem, CodeItem,
ItalicItem, ItalicItem,
StrikeThroughItem, StrikeThroughItem,
TextAlignItem,
TextColorItem,
UnderLineItem, UnderLineItem,
} from "@/components/menus"; } from "@/components/menus";
// constants
import { COLORS_LIST } from "@/constants/common";
// extensions // extensions
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
// local components // local components
@ -20,16 +25,61 @@ import { TextAlignmentSelector } from "./alignment-selector";
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">; type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => { export interface EditorStateType {
// states code: boolean;
bold: boolean;
italic: boolean;
underline: boolean;
strike: boolean;
left: boolean;
right: boolean;
center: boolean;
color: { key: string; label: string; textColor: string; backgroundColor: string } | undefined;
backgroundColor:
| {
key: string;
label: string;
textColor: string;
backgroundColor: string;
}
| undefined;
}
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Editor }) => {
const menuRef = useRef<HTMLDivElement>(null);
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const basicFormattingOptions = props.editor.isActive("code") const formattingItems = {
? [CodeItem(props.editor)] code: CodeItem(props.editor),
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)]; bold: BoldItem(props.editor),
italic: ItalicItem(props.editor),
underline: UnderLineItem(props.editor),
strike: StrikeThroughItem(props.editor),
textAlign: TextAlignItem(props.editor),
};
const editorState: EditorStateType = useEditorState({
editor: props.editor,
selector: ({ editor }: { editor: Editor }) => ({
code: formattingItems.code.isActive(),
bold: formattingItems.bold.isActive(),
italic: formattingItems.italic.isActive(),
underline: formattingItems.underline.isActive(),
strike: formattingItems.strike.isActive(),
left: formattingItems.textAlign.isActive({ alignment: "left" }),
right: formattingItems.textAlign.isActive({ alignment: "right" }),
center: formattingItems.textAlign.isActive({ alignment: "center" }),
color: COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })),
backgroundColor: COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })),
}),
});
const basicFormattingOptions = editorState.code
? [formattingItems.code]
: [formattingItems.bold, formattingItems.italic, formattingItems.underline, formattingItems.strike];
const bubbleMenuProps: EditorBubbleMenuProps = { const bubbleMenuProps: EditorBubbleMenuProps = {
...props, ...props,
@ -51,6 +101,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
}, },
tippyOptions: { tippyOptions: {
moveTransition: "transform 0.15s ease-out", moveTransition: "transform 0.15s ease-out",
duration: [300, 0],
onHidden: () => { onHidden: () => {
setIsNodeSelectorOpen(false); setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false); setIsLinkSelectorOpen(false);
@ -60,7 +111,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
}; };
useEffect(() => { useEffect(() => {
function handleMouseDown() { function handleMouseDown(e: MouseEvent) {
if (menuRef.current?.contains(e.target as Node)) return;
function handleMouseMove() { function handleMouseMove() {
if (!props.editor.state.selection.empty) { if (!props.editor.state.selection.empty) {
setIsSelecting(true); setIsSelecting(true);
@ -70,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
function handleMouseUp() { function handleMouseUp() {
setIsSelecting(false); setIsSelecting(false);
document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("mouseup", handleMouseUp);
} }
@ -84,14 +136,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
return () => { return () => {
document.removeEventListener("mousedown", handleMouseDown); document.removeEventListener("mousedown", handleMouseDown);
}; };
}, []); }, [props.editor]);
return ( return (
<BubbleMenu {...bubbleMenuProps}> <BubbleMenu {...bubbleMenuProps}>
{!isSelecting && ( {!isSelecting && (
<div className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"> <div
ref={menuRef}
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"
>
<div className="px-2"> <div className="px-2">
{!props.editor.isActive("table") && (
<BubbleMenuNodeSelector <BubbleMenuNodeSelector
editor={props.editor!} editor={props.editor!}
isOpen={isNodeSelectorOpen} isOpen={isNodeSelectorOpen}
@ -101,10 +155,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
setIsColorSelectorOpen(false); setIsColorSelectorOpen(false);
}} }}
/> />
)}
</div> </div>
{!editorState.code && (
<div className="px-2"> <div className="px-2">
{!props.editor.isActive("code") && (
<BubbleMenuLinkSelector <BubbleMenuLinkSelector
editor={props.editor} editor={props.editor}
isOpen={isLinkSelectorOpen} isOpen={isLinkSelectorOpen}
@ -114,21 +167,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
setIsColorSelectorOpen(false); setIsColorSelectorOpen(false);
}} }}
/> />
)}
</div> </div>
)}
{!editorState.code && (
<div className="px-2"> <div className="px-2">
{!props.editor.isActive("code") && (
<BubbleMenuColorSelector <BubbleMenuColorSelector
editor={props.editor} editor={props.editor}
isOpen={isColorSelectorOpen} isOpen={isColorSelectorOpen}
editorState={editorState}
setIsOpen={() => { setIsOpen={() => {
setIsColorSelectorOpen((prev) => !prev); setIsColorSelectorOpen((prev) => !prev);
setIsNodeSelectorOpen(false); setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false); setIsLinkSelectorOpen(false);
}} }}
/> />
)}
</div> </div>
)}
<div className="flex gap-0.5 px-2"> <div className="flex gap-0.5 px-2">
{basicFormattingOptions.map((item) => ( {basicFormattingOptions.map((item) => (
<button <button
@ -141,7 +195,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
className={cn( className={cn(
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors", "size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",
{ {
"bg-custom-background-80 text-custom-text-100": item.isActive(""), "bg-custom-background-80 text-custom-text-100": editorState[item.key],
} }
)} )}
> >
@ -149,15 +203,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
</button> </button>
))} ))}
</div> </div>
<TextAlignmentSelector <TextAlignmentSelector editor={props.editor} editorState={editorState} />
editor={props.editor}
onClose={() => {
const editor = props.editor as Editor;
if (!editor) return;
const pos = editor.state.selection.to;
editor.commands.setTextSelection(pos ?? 0);
}}
/>
</div> </div>
)} )}
</BubbleMenu> </BubbleMenu>

View file

@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
icon: UnderlineIcon, icon: UnderlineIcon,
}); });
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
key: "strikethrough", key: "strike",
name: "Strikethrough", name: "Strikethrough",
isActive: () => editor?.isActive("strike"), isActive: () => editor?.isActive("strike"),
command: () => toggleStrike(editor), command: () => toggleStrike(editor),

View file

@ -87,7 +87,7 @@ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
}, },
]; ];
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strike">[] = [
{ {
itemKey: "bold", itemKey: "bold",
renderKey: "bold", renderKey: "bold",
@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
itemKey: "strikethrough", itemKey: "strike",
renderKey: "strikethrough", renderKey: "strikethrough",
name: "Strikethrough", name: "Strikethrough",
icon: Strikethrough, icon: Strikethrough,

View file

@ -31,7 +31,7 @@ export type TEditorCommands =
| "bold" | "bold"
| "italic" | "italic"
| "underline" | "underline"
| "strikethrough" | "strike"
| "bulleted-list" | "bulleted-list"
| "numbered-list" | "numbered-list"
| "to-do-list" | "to-do-list"

View file

@ -93,7 +93,7 @@ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
}, },
]; ];
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strike">[] = [
{ {
itemKey: "bold", itemKey: "bold",
renderKey: "bold", renderKey: "bold",
@ -119,7 +119,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
itemKey: "strikethrough", itemKey: "strike",
renderKey: "strikethrough", renderKey: "strikethrough",
name: "Strikethrough", name: "Strikethrough",
icon: Strikethrough, icon: Strikethrough,