Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { expect, it } from "vitest";
import { act } from "react";
import { createRoot, Root } from "react-dom/client";
import { afterEach, expect, it, vi } from "vitest";
import { SuggestionMenuController } from "./SuggestionMenuController.js";
import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems.js";

(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;

let root: Root | undefined;

afterEach(() => {
act(() => {
root?.unmount();
});
root = undefined;
});

it("has good typing", () => {
// invalid, because DefaultSuggestionItem doesn't have a title property, so the default MantineSuggestionMenu doesn't wrok
Expand Down Expand Up @@ -40,3 +54,55 @@ it("has good typing", () => {

expect(menu).toBeDefined();
});

it("keeps the suggestion menu open while an IME query is composing", async () => {
const closeMenu = vi.fn();
const container = document.createElement("div");

function TestHook(props: { isComposing: boolean; usedQuery: string }) {
useCloseSuggestionMenuNoItems(
[],
props.usedQuery,
closeMenu,
3,
props.isComposing,
);
return null;
}

root = createRoot(container);

await act(async () => {
root!.render(<TestHook isComposing={true} usedQuery="nihao" />);
});

expect(closeMenu).not.toHaveBeenCalled();
});

it("keeps the suggestion menu open when composition ends before the committed query is loaded", async () => {
const closeMenu = vi.fn();
const container = document.createElement("div");

function TestHook(props: { isComposing: boolean; usedQuery: string }) {
useCloseSuggestionMenuNoItems(
[],
props.usedQuery,
closeMenu,
3,
props.isComposing,
);
return null;
}

root = createRoot(container);

await act(async () => {
root!.render(<TestHook isComposing={true} usedQuery="nihao" />);
});

await act(async () => {
root!.render(<TestHook isComposing={false} usedQuery="nihao" />);
});

expect(closeMenu).not.toHaveBeenCalled();
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core";
import { FC, useCallback, useEffect } from "react";
import { FC, useCallback, useEffect, useState } from "react";

import { useBlockNoteContext } from "../../editor/BlockNoteContext.js";
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
Expand Down Expand Up @@ -32,6 +32,7 @@ export function SuggestionMenuWrapper<Item>(props: {
closeMenu,
onItemClick,
} = props;
const [isComposing, setIsComposing] = useState(false);

const onItemClickCloseMenu = useCallback(
(item: Item) => {
Expand All @@ -47,7 +48,7 @@ export function SuggestionMenuWrapper<Item>(props: {
getItems,
);

useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu);
useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu, 3, isComposing);

const { selectedIndex } = useSuggestionMenuKeyboardNavigation(
editor,
Expand All @@ -72,6 +73,57 @@ export function SuggestionMenuWrapper<Item>(props: {
};
}, [setContentEditableProps]);

useEffect(() => {
let previousCompositionStart:
| ((event: CompositionEvent) => void)
| undefined;
let previousCompositionEnd: ((event: CompositionEvent) => void) | undefined;

const handleCompositionStart = (event: CompositionEvent) => {
previousCompositionStart?.(event);
setIsComposing(true);
};
const handleCompositionEnd = (event: CompositionEvent) => {
previousCompositionEnd?.(event);
setIsComposing(false);
};

setContentEditableProps((p = {}) => {
previousCompositionStart = p.onCompositionStart;
previousCompositionEnd = p.onCompositionEnd;

return {
...p,
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
};
});

return () => {
setContentEditableProps((p = {}) => {
const next = { ...p };

if (next.onCompositionStart === handleCompositionStart) {
if (previousCompositionStart) {
next.onCompositionStart = previousCompositionStart;
} else {
delete next.onCompositionStart;
}
}

if (next.onCompositionEnd === handleCompositionEnd) {
if (previousCompositionEnd) {
next.onCompositionEnd = previousCompositionEnd;
} else {
delete next.onCompositionEnd;
}
}

return next;
});
};
}, [setContentEditableProps]);

// set selected item (activedescendent) attributes when selected item changes
useEffect(() => {
setContentEditableProps((p) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@ export function useCloseSuggestionMenuNoItems<Item>(
usedQuery: string | undefined,
closeMenu: () => void,
invalidQueries = 3,
isComposing = false,
) {
const lastUsefulQueryLength = useRef(0);
const composingQuery = useRef<string | undefined>(undefined);

useEffect(() => {
if (usedQuery === undefined) {
return;
}

if (isComposing) {
composingQuery.current = usedQuery;
return;
}

if (composingQuery.current === usedQuery) {
return;
}

composingQuery.current = undefined;

if (items.length > 0) {
lastUsefulQueryLength.current = usedQuery.length;
} else if (
Expand All @@ -24,5 +37,5 @@ export function useCloseSuggestionMenuNoItems<Item>(
) {
closeMenu();
}
}, [closeMenu, invalidQueries, items.length, usedQuery]);
}, [closeMenu, invalidQueries, isComposing, items.length, usedQuery]);
}