EmojiList.tsx raw
1 import Emoji from '@/components/Emoji'
2 import { ScrollArea } from '@/components/ui/scroll-area'
3 import { cn } from '@/lib/utils'
4 import customEmojiService from '@/services/custom-emoji.service'
5 import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
6
7 export interface EmojiListProps {
8 items: string[]
9 command: (params: { name?: string }) => void
10 }
11
12 export interface EmojiListHandler {
13 onKeyDown: (params: { event: KeyboardEvent }) => boolean
14 }
15
16 export const EmojiList = forwardRef<EmojiListHandler, EmojiListProps>((props, ref) => {
17 const [selectedIndex, setSelectedIndex] = useState(0)
18
19 const selectItem = (index: number): void => {
20 const item = props.items[index]
21
22 if (item) {
23 props.command({ name: item })
24 }
25
26 customEmojiService.updateSuggested(item)
27 }
28
29 const upHandler = (): void => {
30 setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
31 }
32
33 const downHandler = (): void => {
34 setSelectedIndex((selectedIndex + 1) % props.items.length)
35 }
36
37 const enterHandler = (): void => {
38 selectItem(selectedIndex)
39 }
40
41 useEffect(() => setSelectedIndex(props.items.length ? 0 : -1), [props.items])
42
43 useImperativeHandle(ref, () => {
44 return {
45 onKeyDown: (x: { event: KeyboardEvent }): boolean => {
46 if (x.event.key === 'ArrowUp') {
47 upHandler()
48 return true
49 }
50
51 if (x.event.key === 'ArrowDown') {
52 downHandler()
53 return true
54 }
55
56 if (x.event.key === 'Enter' && selectedIndex >= 0) {
57 enterHandler()
58 return true
59 }
60
61 return false
62 }
63 }
64 }, [upHandler, downHandler, enterHandler])
65
66 if (!props.items?.length) {
67 return null
68 }
69
70 return (
71 <ScrollArea
72 className="border rounded-lg bg-background z-50 pointer-events-auto flex flex-col max-h-80 overflow-y-auto"
73 onWheel={(e) => e.stopPropagation()}
74 onTouchMove={(e) => e.stopPropagation()}
75 >
76 <div className="p-1">
77 {props.items.map((item, index) => {
78 return (
79 <EmojiListItem
80 key={item}
81 id={item}
82 selectedIndex={selectedIndex}
83 index={index}
84 selectItem={selectItem}
85 setSelectedIndex={setSelectedIndex}
86 />
87 )
88 })}
89 </div>
90 </ScrollArea>
91 )
92 })
93
94 function EmojiListItem({
95 id,
96 selectedIndex,
97 index,
98 selectItem,
99 setSelectedIndex
100 }: {
101 id: string
102 selectedIndex: number
103 index: number
104 selectItem: (index: number) => void
105 setSelectedIndex: (index: number) => void
106 }) {
107 const emoji = useMemo(() => customEmojiService.getEmojiById(id), [id])
108 if (!emoji) return null
109
110 return (
111 <button
112 className={cn(
113 'cursor-pointer w-full p-1 rounded-lg transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
114 selectedIndex === index && 'bg-accent text-accent-foreground'
115 )}
116 onClick={() => selectItem(index)}
117 onMouseEnter={() => setSelectedIndex(index)}
118 >
119 <div className="flex gap-2 items-center truncate pointer-events-none">
120 <Emoji
121 emoji={emoji}
122 classNames={{
123 img: 'size-8 shrink-0 rounded-md',
124 text: 'w-8 text-center shrink-0'
125 }}
126 />
127 <span className="truncate">:{emoji.shortcode}:</span>
128 </div>
129 </button>
130 )
131 }
132