index.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { cn } from '@/lib/utils'
3 import { parseEmojiPickerUnified } from '@/lib/utils'
4 import { TEmoji } from '@/types'
5 import { getSuggested } from 'emoji-picker-react/src/dataUtils/suggested'
6 import { MoreHorizontal } from 'lucide-react'
7 import { useCallback, useEffect, useRef, useState } from 'react'
8 import Emoji from '../Emoji'
9
10 const DEFAULT_SUGGESTED_EMOJIS = ['👍', '❤️', '😂', '🥲', '👀', '🫡', '🫂']
11
12 export default function SuggestedEmojis({
13 onEmojiClick,
14 onMoreButtonClick,
15 onClose
16 }: {
17 onEmojiClick: (emoji: string | TEmoji) => void
18 onMoreButtonClick: () => void
19 onClose?: () => void
20 }) {
21 const [suggestedEmojis, setSuggestedEmojis] =
22 useState<(string | TEmoji)[]>(DEFAULT_SUGGESTED_EMOJIS)
23 const [selectedIndex, setSelectedIndex] = useState(0)
24 const containerRef = useRef<HTMLDivElement>(null)
25
26 // Total items: 1 (plus) + suggestedEmojis.length + 1 (more button)
27 const totalItems = 1 + suggestedEmojis.length + 1
28
29 useEffect(() => {
30 try {
31 const suggested = getSuggested()
32 const emojiSet = new Set<string>()
33 const suggestEmojis = (
34 suggested
35 .sort((a, b) => b.count - a.count)
36 .map((item) => parseEmojiPickerUnified(item.unified))
37 .filter(Boolean) as (string | TEmoji)[]
38 )
39 .concat(DEFAULT_SUGGESTED_EMOJIS)
40 .filter((emoji) => {
41 if (typeof emoji !== 'string') return true
42 if (emojiSet.has(emoji)) return false
43 emojiSet.add(emoji)
44 return true
45 })
46 setSuggestedEmojis(suggestEmojis.slice(0, 9))
47 } catch {
48 // ignore
49 }
50 }, [])
51
52 // Focus container on mount for keyboard events
53 useEffect(() => {
54 containerRef.current?.focus()
55 }, [])
56
57 const handleSelect = useCallback(() => {
58 if (selectedIndex === 0) {
59 // Plus button
60 onEmojiClick('+')
61 } else if (selectedIndex <= suggestedEmojis.length) {
62 // Emoji
63 onEmojiClick(suggestedEmojis[selectedIndex - 1])
64 } else {
65 // More button
66 onMoreButtonClick()
67 }
68 }, [selectedIndex, suggestedEmojis, onEmojiClick, onMoreButtonClick])
69
70 const handleKeyDown = useCallback(
71 (e: React.KeyboardEvent) => {
72 switch (e.key) {
73 case 'ArrowLeft':
74 e.preventDefault()
75 setSelectedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1))
76 break
77 case 'ArrowRight':
78 e.preventDefault()
79 setSelectedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0))
80 break
81 case 'ArrowUp':
82 e.preventDefault()
83 // Jump to first item
84 setSelectedIndex(0)
85 break
86 case 'ArrowDown':
87 e.preventDefault()
88 // Jump to last item (more button)
89 setSelectedIndex(totalItems - 1)
90 break
91 case 'Enter':
92 case ' ':
93 e.preventDefault()
94 handleSelect()
95 break
96 case 'Escape':
97 e.preventDefault()
98 onClose?.()
99 break
100 }
101 },
102 [totalItems, handleSelect, onClose]
103 )
104
105 return (
106 <div
107 ref={containerRef}
108 className="flex gap-1 p-1 outline-none"
109 onClick={(e) => e.stopPropagation()}
110 onKeyDown={handleKeyDown}
111 tabIndex={0}
112 >
113 <div
114 className={cn(
115 'w-8 h-8 rounded-lg clickable flex justify-center items-center text-xl',
116 selectedIndex === 0 && 'ring-2 ring-primary'
117 )}
118 onClick={() => onEmojiClick('+')}
119 >
120 <Emoji emoji="+" />
121 </div>
122 {suggestedEmojis.map((emoji, index) =>
123 typeof emoji === 'string' ? (
124 <div
125 key={index}
126 className={cn(
127 'w-8 h-8 rounded-lg clickable flex justify-center items-center text-xl',
128 selectedIndex === index + 1 && 'ring-2 ring-primary'
129 )}
130 onClick={() => onEmojiClick(emoji)}
131 >
132 {emoji}
133 </div>
134 ) : (
135 <div
136 className={cn(
137 'flex flex-col items-center justify-center p-1 rounded-lg clickable',
138 selectedIndex === index + 1 && 'ring-2 ring-primary'
139 )}
140 key={index}
141 onClick={() => onEmojiClick(emoji)}
142 >
143 <Emoji emoji={emoji} classNames={{ img: 'size-6 rounded-md' }} />
144 </div>
145 )
146 )}
147 <Button
148 variant="ghost"
149 className={cn(
150 'w-8 h-8 text-muted-foreground',
151 selectedIndex === totalItems - 1 && 'ring-2 ring-primary'
152 )}
153 onClick={onMoreButtonClick}
154 >
155 <MoreHorizontal size={24} />
156 </Button>
157 </div>
158 )
159 }
160