Skip to content

Commit 4083cd9

Browse files
committed
Added keyboard navigation to AI button
1 parent 78924bb commit 4083cd9

File tree

4 files changed

+99
-50
lines changed

4 files changed

+99
-50
lines changed

packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
import {
88
DefaultReactSuggestionItem,
99
useComponentsContext,
10+
useSuggestionMenuKeyboardHandler,
1011
} from "@blocknote/react";
1112
import {
1213
ChangeEvent,
1314
KeyboardEvent,
1415
useCallback,
16+
useEffect,
1517
useMemo,
1618
useState,
1719
} from "react";
@@ -69,12 +71,45 @@ export const AIButton = () => {
6971
title: "Make Longer",
7072
onItemClick: () => runAIEdit("Make longer"),
7173
},
74+
{
75+
name: "make-shorter",
76+
title: "Make Shorter",
77+
onItemClick: () => runAIEdit("Make shorter"),
78+
},
79+
{
80+
name: "summarize",
81+
title: "Summarize",
82+
onItemClick: () => runAIEdit("Summarize"),
83+
},
7284
],
7385
currentEditingPrompt
7486
),
7587
[currentEditingPrompt, runAIEdit]
7688
);
7789

90+
const { selectedIndex, setSelectedIndex, handler } =
91+
useSuggestionMenuKeyboardHandler(items, (item) => item.onItemClick());
92+
93+
const handleKeyDown = useCallback(
94+
(event: KeyboardEvent) => {
95+
if (event.key === "Enter") {
96+
if (items.length > 0) {
97+
handler(event);
98+
} else {
99+
handleEnter(event);
100+
}
101+
} else {
102+
handler(event);
103+
}
104+
},
105+
[handleEnter, handler, items.length]
106+
);
107+
108+
// Resets index when items change
109+
useEffect(() => {
110+
setSelectedIndex(0);
111+
}, [currentEditingPrompt, setSelectedIndex]);
112+
78113
if (!editor.isEditable) {
79114
return null;
80115
}
@@ -99,20 +134,19 @@ export const AIButton = () => {
99134
value={currentEditingPrompt || ""}
100135
autoFocus={true}
101136
placeholder={dict.formatting_toolbar.ai.input_placeholder}
102-
onKeyDown={handleEnter}
137+
onKeyDown={handleKeyDown}
103138
onChange={handleChange}
104139
/>
105140
</Components.Generic.Form.Root>
106-
107141
<Components.SuggestionMenu.Root
108142
className={"bn-ai-menu"}
109143
id={"ai-suggestion-menu"}>
110-
{items.map((item) => (
144+
{items.map((item, index) => (
111145
<Components.SuggestionMenu.Item
112146
key={item.name}
113147
className={"bn-suggestion-menu-item"}
114148
id={item.name}
115-
isSelected={false}
149+
isSelected={index === selectedIndex}
116150
onClick={item.onItemClick}
117151
item={item}
118152
/>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useState } from "react";
2+
3+
// Hook which returns a handler for keyboard navigation of a suggestion menu. Up
4+
// & down arrow keys are used to select an item, enter is used to execute it.
5+
export function useSuggestionMenuKeyboardHandler<Item>(
6+
items: Item[],
7+
onItemClick?: (item: Item) => void
8+
) {
9+
const [selectedIndex, setSelectedIndex] = useState<number>(0);
10+
11+
return {
12+
selectedIndex,
13+
setSelectedIndex,
14+
handler: (event: { key: string; preventDefault: () => void }) => {
15+
if (event.key === "ArrowUp") {
16+
event.preventDefault();
17+
18+
if (items.length) {
19+
setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length);
20+
}
21+
22+
return true;
23+
}
24+
25+
if (event.key === "ArrowDown") {
26+
// debugger;
27+
event.preventDefault();
28+
29+
if (items.length) {
30+
setSelectedIndex((selectedIndex + 1) % items!.length);
31+
}
32+
33+
return true;
34+
}
35+
36+
if (event.key === "Enter") {
37+
event.preventDefault();
38+
39+
if (items.length) {
40+
onItemClick?.(items[selectedIndex]);
41+
}
42+
43+
return true;
44+
}
45+
46+
return false;
47+
},
48+
};
49+
}

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

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,35 @@
11
import { BlockNoteEditor } from "@blocknote/core";
2-
import { useEffect, useState } from "react";
2+
import { useEffect } from "react";
3+
import { useSuggestionMenuKeyboardHandler } from "./useSuggestionMenuKeyboardHandler";
34

45
// Hook which handles keyboard navigation of a suggestion menu. Up & down arrow
56
// keys are used to select a menu item, enter is used to execute it.
67
export function useSuggestionMenuKeyboardNavigation<Item>(
78
editor: BlockNoteEditor<any, any, any>,
89
query: string,
910
items: Item[],
10-
onItemClick?: (item: Item) => void
11+
onItemClick?: (item: Item) => void,
12+
element?: HTMLElement
1113
) {
12-
const [selectedIndex, setSelectedIndex] = useState<number>(0);
14+
const { selectedIndex, setSelectedIndex, handler } =
15+
useSuggestionMenuKeyboardHandler(items, onItemClick);
1316

1417
useEffect(() => {
15-
const handleMenuNavigationKeys = (event: KeyboardEvent) => {
16-
if (event.key === "ArrowUp") {
17-
event.preventDefault();
18-
19-
if (items.length) {
20-
setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length);
21-
}
22-
23-
return true;
24-
}
25-
26-
if (event.key === "ArrowDown") {
27-
event.preventDefault();
28-
29-
if (items.length) {
30-
setSelectedIndex((selectedIndex + 1) % items!.length);
31-
}
32-
33-
return true;
34-
}
35-
36-
if (event.key === "Enter") {
37-
event.preventDefault();
38-
39-
if (items.length) {
40-
onItemClick?.(items[selectedIndex]);
41-
}
42-
43-
return true;
44-
}
45-
46-
return false;
47-
};
48-
49-
editor.domElement.addEventListener(
50-
"keydown",
51-
handleMenuNavigationKeys,
52-
true
53-
);
18+
(element || editor.domElement).addEventListener("keydown", handler, true);
5419

5520
return () => {
56-
editor.domElement.removeEventListener(
21+
(element || editor.domElement).removeEventListener(
5722
"keydown",
58-
handleMenuNavigationKeys,
23+
handler,
5924
true
6025
);
6126
};
62-
}, [editor.domElement, items, selectedIndex, onItemClick]);
27+
}, [editor.domElement, items, selectedIndex, onItemClick, element]);
6328

6429
// Resets index when items change
6530
useEffect(() => {
6631
setSelectedIndex(0);
67-
}, [query]);
32+
}, [query, setSelectedIndex]);
6833

6934
return {
7035
selectedIndex: items.length === 0 ? undefined : selectedIndex,

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export * from "./components/SuggestionMenu/SuggestionMenuWrapper";
4848
export * from "./components/SuggestionMenu/getDefaultReactSlashMenuItems";
4949
export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems";
5050
export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems";
51+
export * from "./components/SuggestionMenu/hooks/useSuggestionMenuKeyboardHandler";
5152
export * from "./components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation";
5253
export * from "./components/SuggestionMenu/types";
5354

0 commit comments

Comments
 (0)