Skip to content

Commit 0d0e710

Browse files
committed
fix: keep suggestion menus open during IME composition
1 parent dac995c commit 0d0e710

7 files changed

Lines changed: 72 additions & 4 deletions

File tree

packages/core/src/extensions/SuggestionMenu/SuggestionMenu.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, it } from "vitest";
2+
import { TextSelection } from "prosemirror-state";
23

34
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
45
import { SuggestionMenu } from "./SuggestionMenu.js";
@@ -188,4 +189,45 @@ describe("SuggestionMenu", () => {
188189

189190
editor._tiptapEditor.destroy();
190191
});
192+
193+
it("should keep suggestion menu open during IME composition selection updates", () => {
194+
const editor = createEditor();
195+
const sm = editor.getExtension(SuggestionMenu)!;
196+
197+
sm.addSuggestionMenu({ triggerCharacter: "@" });
198+
199+
editor.replaceBlocks(editor.document, [
200+
{
201+
id: "paragraph-0",
202+
type: "paragraph",
203+
content: "Hello world",
204+
},
205+
]);
206+
207+
editor.setTextCursorPosition("paragraph-0", "end");
208+
209+
expect(simulateTextInput(editor, "@")).toBe(true);
210+
211+
const view = editor._tiptapEditor.view;
212+
view.dispatch(view.state.tr.insertText("shi"));
213+
214+
expect(getSuggestionPluginState(editor)?.query).toBe("shi");
215+
216+
const cursor = view.state.selection.from;
217+
Object.defineProperty(view, "composing", {
218+
configurable: true,
219+
get: () => true,
220+
});
221+
view.dispatch(
222+
view.state.tr.setSelection(
223+
TextSelection.create(view.state.doc, cursor - 1, cursor),
224+
),
225+
);
226+
227+
expect(getSuggestionPluginState(editor)).toBeDefined();
228+
expect(getSuggestionPluginState(editor)?.composing).toBe(true);
229+
230+
delete (view as any).composing;
231+
editor._tiptapEditor.destroy();
232+
});
191233
});

packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const findBlock = findParentNode((node) => node.type.name === "blockContainer");
1515
export type SuggestionMenuState = UiElementPosition & {
1616
query: string;
1717
ignoreQueryLength?: boolean;
18+
composing?: boolean;
1819
};
1920

2021
class SuggestionMenuView {
@@ -38,6 +39,7 @@ class SuggestionMenuView {
3839
emitUpdate(menuName, {
3940
...this.state,
4041
ignoreQueryLength: this.pluginState?.ignoreQueryLength,
42+
composing: this.pluginState?.composing,
4143
});
4244
};
4345

@@ -146,6 +148,7 @@ type SuggestionPluginState =
146148
query: string;
147149
decorationId: string;
148150
ignoreQueryLength?: boolean;
151+
composing?: boolean;
149152
}
150153
| undefined;
151154

@@ -260,6 +263,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
260263
deleteTriggerCharacter?: boolean;
261264
ignoreQueryLength?: boolean;
262265
} | null = transaction.getMeta(suggestionMenuPluginKey);
266+
const composing = editor._tiptapEditor.view.composing;
263267

264268
if (
265269
typeof suggestionPluginTransactionMeta === "object" &&
@@ -289,6 +293,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
289293
decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
290294
ignoreQueryLength:
291295
suggestionPluginTransactionMeta?.ignoreQueryLength,
296+
composing,
292297
};
293298
}
294299

@@ -297,10 +302,18 @@ export const SuggestionMenu = createExtension(({ editor }) => {
297302
return prev;
298303
}
299304

305+
if (composing) {
306+
return {
307+
...prev,
308+
composing,
309+
};
310+
}
311+
300312
// Checks if the menu should be hidden.
301313
if (
302314
// Highlighting text should hide the menu.
303-
newState.selection.from !== newState.selection.to ||
315+
(!prev.composing &&
316+
newState.selection.from !== newState.selection.to) ||
304317
// Transactions with plugin metadata should hide the menu.
305318
suggestionPluginTransactionMeta === null ||
306319
// Certain mouse events should hide the menu.
@@ -320,6 +333,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
320333
}
321334

322335
const next = { ...prev };
336+
next.composing = composing;
323337

324338
// Updates the current query.
325339
next.query = newState.doc.textBetween(

packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export function GridSuggestionMenuController<
184184
query={state.query}
185185
closeMenu={suggestionMenu.closeMenu}
186186
clearQuery={suggestionMenu.clearQuery}
187+
composing={state.composing}
187188
getItems={getItemsOrDefault}
188189
columns={columns}
189190
gridSuggestionMenuComponent={

packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuWrapper.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function GridSuggestionMenuWrapper<Item>(props: {
1212
query: string;
1313
closeMenu: () => void;
1414
clearQuery: () => void;
15+
composing?: boolean;
1516
getItems: (query: string) => Promise<Item[]>;
1617
columns: number;
1718
onItemClick?: (item: Item) => void;
@@ -31,6 +32,7 @@ export function GridSuggestionMenuWrapper<Item>(props: {
3132
query,
3233
clearQuery,
3334
closeMenu,
35+
composing,
3436
onItemClick,
3537
columns,
3638
} = props;
@@ -49,7 +51,7 @@ export function GridSuggestionMenuWrapper<Item>(props: {
4951
getItems,
5052
);
5153

52-
useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu);
54+
useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu, 3, composing);
5355

5456
const { selectedIndex } = useGridSuggestionMenuKeyboardNavigation(
5557
editor,

packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export function SuggestionMenuController<
177177
query={state.query}
178178
closeMenu={suggestionMenu.closeMenu}
179179
clearQuery={suggestionMenu.clearQuery}
180+
composing={state.composing}
180181
getItems={getItemsOrDefault}
181182
suggestionMenuComponent={
182183
suggestionMenuComponent || SuggestionMenu<ItemType<GetItemsType>>

packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function SuggestionMenuWrapper<Item>(props: {
1212
query: string;
1313
closeMenu: () => void;
1414
clearQuery: () => void;
15+
composing?: boolean;
1516
getItems: (query: string) => Promise<Item[]>;
1617
onItemClick?: (item: Item) => void;
1718
suggestionMenuComponent: FC<SuggestionMenuProps<Item>>;
@@ -30,6 +31,7 @@ export function SuggestionMenuWrapper<Item>(props: {
3031
query,
3132
clearQuery,
3233
closeMenu,
34+
composing,
3335
onItemClick,
3436
} = props;
3537

@@ -47,7 +49,7 @@ export function SuggestionMenuWrapper<Item>(props: {
4749
getItems,
4850
);
4951

50-
useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu);
52+
useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu, 3, composing);
5153

5254
const { selectedIndex } = useSuggestionMenuKeyboardNavigation(
5355
editor,

packages/react/src/components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function useCloseSuggestionMenuNoItems<Item>(
88
usedQuery: string | undefined,
99
closeMenu: () => void,
1010
invalidQueries = 3,
11+
disabled = false,
1112
) {
1213
const lastUsefulQueryLength = useRef(0);
1314

@@ -16,6 +17,11 @@ export function useCloseSuggestionMenuNoItems<Item>(
1617
return;
1718
}
1819

20+
if (disabled) {
21+
lastUsefulQueryLength.current = usedQuery.length;
22+
return;
23+
}
24+
1925
if (items.length > 0) {
2026
lastUsefulQueryLength.current = usedQuery.length;
2127
} else if (
@@ -24,5 +30,5 @@ export function useCloseSuggestionMenuNoItems<Item>(
2430
) {
2531
closeMenu();
2632
}
27-
}, [closeMenu, invalidQueries, items.length, usedQuery]);
33+
}, [closeMenu, disabled, invalidQueries, items.length, usedQuery]);
2834
}

0 commit comments

Comments
 (0)