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
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";
// types
import { TEditorCommands } from "@/types";
import { EditorStateType } from "./root";
type Props = {
editor: Editor;
onClose: () => void;
editorState: EditorStateType;
};
export const TextAlignmentSelector: React.FC<Props> = (props) => {
const { editor, onClose } = props;
const { editor, editorState } = props;
const menuItem = TextAlignItem(editor);
const textAlignmentOptions: {
@@ -32,10 +32,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({
alignment: "left",
}),
isActive: () =>
menuItem.isActive({
alignment: "left",
}),
isActive: () => editorState.left,
},
{
itemKey: "text-align",
@@ -45,10 +42,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({
alignment: "center",
}),
isActive: () =>
menuItem.isActive({
alignment: "center",
}),
isActive: () => editorState.center,
},
{
itemKey: "text-align",
@@ -58,10 +52,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
menuItem.command({
alignment: "right",
}),
isActive: () =>
menuItem.isActive({
alignment: "right",
}),
isActive: () => editorState.right,
},
];
@@ -74,7 +65,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
onClick={(e) => {
e.stopPropagation();
item.command();
onClose();
}}
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",

View File

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

View File

@@ -1,9 +1,10 @@
import { FC, useEffect, useState } from "react";
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
import { FC, useEffect, useState, useRef } from "react";
// plane utils
import { cn } from "@plane/utils";
// components
import {
BackgroundColorItem,
BoldItem,
BubbleMenuColorSelector,
BubbleMenuLinkSelector,
@@ -11,8 +12,12 @@ import {
CodeItem,
ItalicItem,
StrikeThroughItem,
TextAlignItem,
TextColorItem,
UnderLineItem,
} from "@/components/menus";
// constants
import { COLORS_LIST } from "@/constants/common";
// extensions
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
// local components
@@ -20,16 +25,61 @@ import { TextAlignmentSelector } from "./alignment-selector";
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
// states
export interface EditorStateType {
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 [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false);
const basicFormattingOptions = props.editor.isActive("code")
? [CodeItem(props.editor)]
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)];
const formattingItems = {
code: CodeItem(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 = {
...props,
@@ -51,6 +101,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
},
tippyOptions: {
moveTransition: "transform 0.15s ease-out",
duration: [300, 0],
onHidden: () => {
setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false);
@@ -60,7 +111,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
};
useEffect(() => {
function handleMouseDown() {
function handleMouseDown(e: MouseEvent) {
if (menuRef.current?.contains(e.target as Node)) return;
function handleMouseMove() {
if (!props.editor.state.selection.empty) {
setIsSelecting(true);
@@ -70,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
function handleMouseUp() {
setIsSelecting(false);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
@@ -84,27 +136,28 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
return () => {
document.removeEventListener("mousedown", handleMouseDown);
};
}, []);
}, [props.editor]);
return (
<BubbleMenu {...bubbleMenuProps}>
{!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">
{!props.editor.isActive("table") && (
<BubbleMenuNodeSelector
editor={props.editor!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen((prev) => !prev);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
}}
/>
)}
<BubbleMenuNodeSelector
editor={props.editor!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen((prev) => !prev);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
}}
/>
</div>
<div className="px-2">
{!props.editor.isActive("code") && (
{!editorState.code && (
<div className="px-2">
<BubbleMenuLinkSelector
editor={props.editor}
isOpen={isLinkSelectorOpen}
@@ -114,21 +167,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
setIsColorSelectorOpen(false);
}}
/>
)}
</div>
<div className="px-2">
{!props.editor.isActive("code") && (
</div>
)}
{!editorState.code && (
<div className="px-2">
<BubbleMenuColorSelector
editor={props.editor}
isOpen={isColorSelectorOpen}
editorState={editorState}
setIsOpen={() => {
setIsColorSelectorOpen((prev) => !prev);
setIsNodeSelectorOpen(false);
setIsLinkSelectorOpen(false);
}}
/>
)}
</div>
</div>
)}
<div className="flex gap-0.5 px-2">
{basicFormattingOptions.map((item) => (
<button
@@ -141,7 +195,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
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",
{
"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>
))}
</div>
<TextAlignmentSelector
editor={props.editor}
onClose={() => {
const editor = props.editor as Editor;
if (!editor) return;
const pos = editor.state.selection.to;
editor.commands.setTextSelection(pos ?? 0);
}}
/>
<TextAlignmentSelector editor={props.editor} editorState={editorState} />
</div>
)}
</BubbleMenu>

View File

@@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
icon: UnderlineIcon,
});
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
key: "strikethrough",
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
key: "strike",
name: "Strikethrough",
isActive: () => editor?.isActive("strike"),
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",
renderKey: "bold",
@@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
editors: ["lite", "document"],
},
{
itemKey: "strikethrough",
itemKey: "strike",
renderKey: "strikethrough",
name: "Strikethrough",
icon: Strikethrough,

View File

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