fix: prevent editor bubble menu update loop
This commit is contained in:
1 parent
e6f68378d0
commit
d0c124993c
4 files changed
+73
-6
No files matched your search
@@ -76,6 +76,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { EditorToolbar } from "./EditorToolbar";
|
||||
import { EditorOutline } from "./EditorOutline";
|
||||
import { useAiBubbleMenu } from "./editor/useAiBubbleMenu";
|
||||
import { WeChatIcon } from "./WeChatIcon";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
@@ -703,6 +704,7 @@ const RichEditorPane = ({
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
const [aiAssistantOpen, setAiAssistantOpen] = useState(false);
|
||||
const aiBubbleMenu = useAiBubbleMenu(aiAssistantOpen);
|
||||
const [aiSelection, setAiSelection] = useState<AiSelectionContext | null>(null);
|
||||
const [systemInfoOpen, setSystemInfoOpen] = useState(false);
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
@@ -4133,10 +4135,8 @@ const RichEditorPane = ({
|
||||
>
|
||||
<BubbleMenu
|
||||
editor={editor}
|
||||
shouldShow={({ editor: activeEditor }) =>
|
||||
activeEditor.isEditable && !activeEditor.state.selection.empty && !aiAssistantOpen
|
||||
}
|
||||
options={{ placement: "top" }}
|
||||
shouldShow={aiBubbleMenu.shouldShow}
|
||||
options={aiBubbleMenu.options}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useCallback } from "react";
|
||||
import type { BubbleMenuProps } from "@tiptap/react/menus";
|
||||
|
||||
type BubbleMenuShouldShow = NonNullable<BubbleMenuProps["shouldShow"]>;
|
||||
type BubbleMenuOptions = NonNullable<BubbleMenuProps["options"]>;
|
||||
|
||||
const AI_BUBBLE_MENU_OPTIONS: BubbleMenuOptions = { placement: "top" };
|
||||
|
||||
export const useAiBubbleMenu = (assistantOpen: boolean) => {
|
||||
// BubbleMenu dispatches an options-update transaction whenever these prop
|
||||
// references change. Keep them stable so the editor's transaction-driven
|
||||
// toolbar refresh cannot feed back into another BubbleMenu update.
|
||||
const shouldShow = useCallback<BubbleMenuShouldShow>(
|
||||
({ editor }) => editor.isEditable && !editor.state.selection.empty && !assistantOpen,
|
||||
[assistantOpen],
|
||||
);
|
||||
|
||||
return {
|
||||
options: AI_BUBBLE_MENU_OPTIONS,
|
||||
shouldShow,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import { BubbleMenu } from "@tiptap/react/menus";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAiBubbleMenu } from "@/components/editor/useAiBubbleMenu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -15,8 +19,49 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const BubbleMenuTransactionRegression = () => {
|
||||
const [transactionCount, setTransactionCount] = useState(0);
|
||||
const aiBubbleMenu = useAiBubbleMenu(false);
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: "<p>Bubble menu regression</p>",
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const handleTransaction = () => setTransactionCount((count) => count + 1);
|
||||
editor.on("transaction", handleTransaction);
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
editor.commands.setTextSelection({ from: 1, to: 7 });
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frame);
|
||||
editor.off("transaction", handleTransaction);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-bubble-menu-regression-ready={transactionCount > 0 ? "true" : "false"}
|
||||
data-bubble-menu-transaction-count={transactionCount}
|
||||
>
|
||||
<BubbleMenu
|
||||
editor={editor}
|
||||
shouldShow={aiBubbleMenu.shouldShow}
|
||||
options={aiBubbleMenu.options}
|
||||
>
|
||||
<span>Selection action</span>
|
||||
</BubbleMenu>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopRendererTest = () => (
|
||||
<main data-desktop-renderer-test-ready>
|
||||
<BubbleMenuTransactionRegression />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" title="More actions">
|
||||
|
||||
@@ -55,7 +55,7 @@ try {
|
||||
});
|
||||
await verifyEntry({
|
||||
filePath: componentTestPath,
|
||||
readySelector: "[data-desktop-renderer-test-ready]",
|
||||
readySelector: '[data-desktop-renderer-test-ready] [data-bubble-menu-regression-ready="true"]',
|
||||
label: "Desktop renderer component test",
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user