Skip to content
File

Blob: src/client/components/editor/controllers/emoji/suggestion-panel.tsx

typescript102 lines
1import { useId, useImperativeHandle, useRef, type Ref } from "react";
2import { FloatingPortal } from "@floating-ui/react";
3import type { EmojiItem } from "@tiptap/extension-emoji";
4import { useMenuNavigation } from "../menu/navigation";
5import { preserveEditorSelectionOnMouseDown, useEditorRectPopover } from "../menu/popover";
6 
7export interface EmojiSuggestionPanelProps {
8 items: EmojiItem[];
9 command: (item: EmojiItem) => void;
10 clientRect: (() => DOMRect | null) | null;
11 contextElement?: HTMLElement | null;
12 onClose?: () => void;
13 ref?: Ref<EmojiSuggestionPanelHandle>;
14}
15 
16export interface EmojiSuggestionPanelHandle {
17 onKeyDown: (event: KeyboardEvent) => boolean;
18}
19 
20export function EmojiSuggestionPanel({
21 items,
22 command,
23 clientRect,
24 contextElement,
25 onClose,
26 ref,
27}: EmojiSuggestionPanelProps) {
28 const panelRef = useRef<HTMLDivElement>(null);
29 const listboxId = useId();
30 const navigation = useMenuNavigation({
31 items,
32 listRef: panelRef,
33 onSelect: command,
34 });
35 const { floatingStyles, setFloating } = useEditorRectPopover({
36 open: true,
37 onClose,
38 getAnchorRect: () => clientRect?.() ?? null,
39 contextElement,
40 maxHeight: true,
41 deferOutsidePress: true,
42 });
43 
44 useImperativeHandle(ref, () => ({
45 onKeyDown: (event: KeyboardEvent) => navigation.onKeyDown(event),
46 }));
47 
48 return (
49 <FloatingPortal>
50 <div
51 ref={(node) => {
52 setFloating(node);
53 panelRef.current = node;
54 }}
55 className="tiptap-slash-menu"
56 style={{ ...floatingStyles, zIndex: 80 }}
57 onMouseDownCapture={(event) => preserveEditorSelectionOnMouseDown(event)}
58 >
59 <div className="tiptap-slash-menu-label" id={`${listboxId}-label`}>
60 Emoji
61 </div>
62 <div id={listboxId} role="listbox" aria-labelledby={`${listboxId}-label`}>
63 {items.length === 0 ? (
64 <div className="tiptap-slash-menu-item" role="option" aria-disabled aria-selected="false">
65 <span className="tiptap-emoji-suggestion-glyph">๐Ÿ™‚</span>
66 <span className="tiptap-emoji-suggestion-meta">No emoji found</span>
67 </div>
68 ) : (
69 items.map((item, index) => (
70 <button
71 key={`${item.name}-${index}`}
72 id={`${listboxId}-option-${index}`}
73 type="button"
74 role="option"
75 data-menu-index={index}
76 className="tiptap-slash-menu-item"
77 aria-selected={index === navigation.selectedIndex}
78 onMouseEnter={() => navigation.setSelectedIndex(index)}
79 onMouseDown={(event) => {
80 event.preventDefault();
81 command(item);
82 }}
83 >
84 {item.fallbackImage ? (
85 <img
86 src={item.fallbackImage}
87 alt=""
88 className="tiptap-emoji-suggestion-glyph tiptap-emoji-suggestion-glyph-image"
89 />
90 ) : (
91 <span className="tiptap-emoji-suggestion-glyph">{item.emoji ?? "๐Ÿ™‚"}</span>
92 )}
93 <span className="tiptap-emoji-suggestion-meta">:{item.shortcodes[0] ?? item.name}:</span>
94 </button>
95 ))
96 )}
97 </div>
98 </div>
99 </FloatingPortal>
100 );
101}