Skip to content

Commit dad85d8

Browse files
fix: bubble menu weird flickering fixed
1 parent 6aa139a commit dad85d8

File tree

7 files changed

+104
-66
lines changed

7 files changed

+104
-66
lines changed

packages/editor/src/core/components/menus/bubble-menu/alignment-selector.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import { cn } from "@plane/utils";
66
import { TextAlignItem } from "@/components/menus";
77
// types
88
import { TEditorCommands } from "@/types";
9+
import { EditorStateType } from "./root";
910

1011
type Props = {
1112
editor: Editor;
12-
onClose: () => void;
13+
editorState: EditorStateType;
1314
};
1415

1516
export const TextAlignmentSelector: React.FC<Props> = (props) => {
16-
const { editor, onClose } = props;
17-
17+
const { editor, editorState } = props;
1818
const menuItem = TextAlignItem(editor);
1919

2020
const textAlignmentOptions: {
@@ -32,10 +32,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
3232
menuItem.command({
3333
alignment: "left",
3434
}),
35-
isActive: () =>
36-
menuItem.isActive({
37-
alignment: "left",
38-
}),
35+
isActive: () => editorState.left,
3936
},
4037
{
4138
itemKey: "text-align",
@@ -45,10 +42,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
4542
menuItem.command({
4643
alignment: "center",
4744
}),
48-
isActive: () =>
49-
menuItem.isActive({
50-
alignment: "center",
51-
}),
45+
isActive: () => editorState.center,
5246
},
5347
{
5448
itemKey: "text-align",
@@ -58,10 +52,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
5852
menuItem.command({
5953
alignment: "right",
6054
}),
61-
isActive: () =>
62-
menuItem.isActive({
63-
alignment: "right",
64-
}),
55+
isActive: () => editorState.right,
6556
},
6657
];
6758

@@ -74,7 +65,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
7465
onClick={(e) => {
7566
e.stopPropagation();
7667
item.command();
77-
onClose();
7868
}}
7969
className={cn(
8070
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",

packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import { Dispatch, FC, SetStateAction } from "react";
21
import { Editor } from "@tiptap/react";
32
import { ALargeSmall, Ban } from "lucide-react";
3+
import { Dispatch, FC, SetStateAction } from "react";
44
// plane utils
55
import { cn } from "@plane/utils";
66
// constants
77
import { COLORS_LIST } from "@/constants/common";
88
// helpers
99
import { BackgroundColorItem, TextColorItem } from "../menu-items";
10+
import { EditorStateType } from "./root";
1011

1112
type Props = {
1213
editor: Editor;
1314
isOpen: boolean;
1415
setIsOpen: Dispatch<SetStateAction<boolean>>;
16+
editorState: EditorStateType;
1517
};
1618

1719
export const BubbleMenuColorSelector: FC<Props> = (props) => {
18-
const { editor, isOpen, setIsOpen } = props;
20+
const { editor, isOpen, setIsOpen, editorState } = props;
1921

20-
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key }));
21-
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key }));
22+
const activeTextColor = editorState.color;
23+
const activeBackgroundColor = editorState.backgroundColor;
2224

2325
return (
2426
<div className="relative h-full">

packages/editor/src/core/components/menus/bubble-menu/root.tsx

Lines changed: 86 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,85 @@
1-
import { FC, useEffect, useState } from "react";
2-
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
1+
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
2+
import { FC, useEffect, useState, useRef } from "react";
33
// plane utils
44
import { cn } from "@plane/utils";
55
// components
66
import {
7+
BackgroundColorItem,
78
BoldItem,
89
BubbleMenuColorSelector,
910
BubbleMenuLinkSelector,
1011
BubbleMenuNodeSelector,
1112
CodeItem,
1213
ItalicItem,
1314
StrikeThroughItem,
15+
TextAlignItem,
16+
TextColorItem,
1417
UnderLineItem,
1518
} from "@/components/menus";
19+
// constants
20+
import { COLORS_LIST } from "@/constants/common";
1621
// extensions
1722
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
1823
// local components
1924
import { TextAlignmentSelector } from "./alignment-selector";
2025

2126
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
2227

23-
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
24-
// states
28+
export interface EditorStateType {
29+
code: boolean;
30+
bold: boolean;
31+
italic: boolean;
32+
underline: boolean;
33+
strike: boolean;
34+
left: boolean;
35+
right: boolean;
36+
center: boolean;
37+
color: { key: string; label: string; textColor: string; backgroundColor: string } | undefined;
38+
backgroundColor:
39+
| {
40+
key: string;
41+
label: string;
42+
textColor: string;
43+
backgroundColor: string;
44+
}
45+
| undefined;
46+
}
47+
48+
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Editor }) => {
49+
const menuRef = useRef<HTMLDivElement>(null);
2550
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
2651
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
2752
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
2853
const [isSelecting, setIsSelecting] = useState(false);
2954

30-
const basicFormattingOptions = props.editor.isActive("code")
31-
? [CodeItem(props.editor)]
32-
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)];
55+
const formattingItems = {
56+
code: CodeItem(props.editor),
57+
bold: BoldItem(props.editor),
58+
italic: ItalicItem(props.editor),
59+
underline: UnderLineItem(props.editor),
60+
strike: StrikeThroughItem(props.editor),
61+
textAlign: TextAlignItem(props.editor),
62+
};
63+
64+
const editorState: EditorStateType = useEditorState({
65+
editor: props.editor,
66+
selector: ({ editor }: { editor: Editor }) => ({
67+
code: formattingItems.code.isActive(),
68+
bold: formattingItems.bold.isActive(),
69+
italic: formattingItems.italic.isActive(),
70+
underline: formattingItems.underline.isActive(),
71+
strike: formattingItems.strike.isActive(),
72+
left: formattingItems.textAlign.isActive({ alignment: "left" }),
73+
right: formattingItems.textAlign.isActive({ alignment: "right" }),
74+
center: formattingItems.textAlign.isActive({ alignment: "center" }),
75+
color: COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })),
76+
backgroundColor: COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })),
77+
}),
78+
});
79+
80+
const basicFormattingOptions = editorState.code
81+
? [formattingItems.code]
82+
: [formattingItems.bold, formattingItems.italic, formattingItems.underline, formattingItems.strike];
3383

3484
const bubbleMenuProps: EditorBubbleMenuProps = {
3585
...props,
@@ -51,6 +101,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
51101
},
52102
tippyOptions: {
53103
moveTransition: "transform 0.15s ease-out",
104+
duration: [300, 0],
54105
onHidden: () => {
55106
setIsNodeSelectorOpen(false);
56107
setIsLinkSelectorOpen(false);
@@ -60,7 +111,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
60111
};
61112

62113
useEffect(() => {
63-
function handleMouseDown() {
114+
function handleMouseDown(e: MouseEvent) {
115+
if (menuRef.current?.contains(e.target as Node)) return;
116+
64117
function handleMouseMove() {
65118
if (!props.editor.state.selection.empty) {
66119
setIsSelecting(true);
@@ -70,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
70123

71124
function handleMouseUp() {
72125
setIsSelecting(false);
73-
74126
document.removeEventListener("mousemove", handleMouseMove);
75127
document.removeEventListener("mouseup", handleMouseUp);
76128
}
@@ -84,27 +136,28 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
84136
return () => {
85137
document.removeEventListener("mousedown", handleMouseDown);
86138
};
87-
}, []);
139+
}, [props.editor]);
88140

89141
return (
90142
<BubbleMenu {...bubbleMenuProps}>
91143
{!isSelecting && (
92-
<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">
144+
<div
145+
ref={menuRef}
146+
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"
147+
>
93148
<div className="px-2">
94-
{!props.editor.isActive("table") && (
95-
<BubbleMenuNodeSelector
96-
editor={props.editor!}
97-
isOpen={isNodeSelectorOpen}
98-
setIsOpen={() => {
99-
setIsNodeSelectorOpen((prev) => !prev);
100-
setIsLinkSelectorOpen(false);
101-
setIsColorSelectorOpen(false);
102-
}}
103-
/>
104-
)}
149+
<BubbleMenuNodeSelector
150+
editor={props.editor!}
151+
isOpen={isNodeSelectorOpen}
152+
setIsOpen={() => {
153+
setIsNodeSelectorOpen((prev) => !prev);
154+
setIsLinkSelectorOpen(false);
155+
setIsColorSelectorOpen(false);
156+
}}
157+
/>
105158
</div>
106-
<div className="px-2">
107-
{!props.editor.isActive("code") && (
159+
{!editorState.code && (
160+
<div className="px-2">
108161
<BubbleMenuLinkSelector
109162
editor={props.editor}
110163
isOpen={isLinkSelectorOpen}
@@ -114,21 +167,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
114167
setIsColorSelectorOpen(false);
115168
}}
116169
/>
117-
)}
118-
</div>
119-
<div className="px-2">
120-
{!props.editor.isActive("code") && (
170+
</div>
171+
)}
172+
{!editorState.code && (
173+
<div className="px-2">
121174
<BubbleMenuColorSelector
122175
editor={props.editor}
123176
isOpen={isColorSelectorOpen}
177+
editorState={editorState}
124178
setIsOpen={() => {
125179
setIsColorSelectorOpen((prev) => !prev);
126180
setIsNodeSelectorOpen(false);
127181
setIsLinkSelectorOpen(false);
128182
}}
129183
/>
130-
)}
131-
</div>
184+
</div>
185+
)}
132186
<div className="flex gap-0.5 px-2">
133187
{basicFormattingOptions.map((item) => (
134188
<button
@@ -141,23 +195,15 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
141195
className={cn(
142196
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",
143197
{
144-
"bg-custom-background-80 text-custom-text-100": item.isActive(""),
198+
"bg-custom-background-80 text-custom-text-100": editorState[item.key],
145199
}
146200
)}
147201
>
148202
<item.icon className="size-4" />
149203
</button>
150204
))}
151205
</div>
152-
<TextAlignmentSelector
153-
editor={props.editor}
154-
onClose={() => {
155-
const editor = props.editor as Editor;
156-
if (!editor) return;
157-
const pos = editor.state.selection.to;
158-
editor.commands.setTextSelection(pos ?? 0);
159-
}}
160-
/>
206+
<TextAlignmentSelector editor={props.editor} editorState={editorState} />
161207
</div>
162208
)}
163209
</BubbleMenu>

packages/editor/src/core/components/menus/menu-items.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
142142
icon: UnderlineIcon,
143143
});
144144

145-
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
146-
key: "strikethrough",
145+
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
146+
key: "strike",
147147
name: "Strikethrough",
148148
isActive: () => editor?.isActive("strike"),
149149
command: () => toggleStrike(editor),

packages/editor/src/core/constants/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
8787
},
8888
];
8989

90-
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [
90+
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strike">[] = [
9191
{
9292
itemKey: "bold",
9393
renderKey: "bold",
@@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
113113
editors: ["lite", "document"],
114114
},
115115
{
116-
itemKey: "strikethrough",
116+
itemKey: "strike",
117117
renderKey: "strikethrough",
118118
name: "Strikethrough",
119119
icon: Strikethrough,

packages/editor/src/core/types/editor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type TEditorCommands =
3131
| "bold"
3232
| "italic"
3333
| "underline"
34-
| "strikethrough"
34+
| "strike"
3535
| "bulleted-list"
3636
| "numbered-list"
3737
| "to-do-list"

web/core/constants/editor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
119119
editors: ["lite", "document"],
120120
},
121121
{
122-
itemKey: "strikethrough",
122+
itemKey: "strike",
123123
renderKey: "strikethrough",
124124
name: "Strikethrough",
125125
icon: Strikethrough,

0 commit comments

Comments
 (0)