Skip to content

Commit 7097db9

Browse files
feat: add custom emoji category support
Amp-Thread-ID: https://ampcode.com/threads/T-019cf809-1bbe-7158-a32f-d7f240e4c1ce Co-authored-by: Amp <amp@ampcode.com>
1 parent b16c81c commit 7097db9

File tree

4 files changed

+109
-5
lines changed

4 files changed

+109
-5
lines changed

src/components/emoji-picker.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ import { useStableCallback } from "../utils/use-stable-callback";
6666
function EmojiPickerDataHandler({
6767
emojiVersion,
6868
emojibaseUrl,
69-
}: Pick<EmojiPickerRootProps, "emojiVersion" | "emojibaseUrl">) {
69+
custom,
70+
}: Pick<EmojiPickerRootProps, "emojiVersion" | "emojibaseUrl" | "custom">) {
7071
const [emojiData, setEmojiData] = useState<EmojiData | undefined>(undefined);
7172
const store = useEmojiPickerStore();
7273
const locale = useSelectorKey(store, "locale");
@@ -103,12 +104,12 @@ function EmojiPickerDataHandler({
103104
store
104105
.get()
105106
.onDataChange(
106-
getEmojiPickerData(emojiData, columns, skinTone, search),
107+
getEmojiPickerData(emojiData, columns, skinTone, search, custom),
107108
);
108109
},
109110
{ timeout: 100 },
110111
);
111-
}, [emojiData, columns, skinTone, search]);
112+
}, [emojiData, columns, skinTone, search, custom]);
112113

113114
return null;
114115
}
@@ -143,6 +144,7 @@ const EmojiPickerRoot = forwardRef<HTMLDivElement, EmojiPickerRootProps>(
143144
columns = 9,
144145
skinTone = "none",
145146
onEmojiSelect = noop,
147+
custom,
146148
emojiVersion,
147149
emojibaseUrl,
148150
onFocusCapture,
@@ -472,6 +474,7 @@ const EmojiPickerRoot = forwardRef<HTMLDivElement, EmojiPickerRootProps>(
472474
<EmojiPickerDataHandler
473475
emojibaseUrl={emojibaseUrl}
474476
emojiVersion={emojiVersion}
477+
custom={custom}
475478
/>
476479
{children}
477480
</EmojiPickerStoreProvider>
@@ -1048,7 +1051,11 @@ function DefaultEmojiPickerListEmoji({
10481051
}: EmojiPickerListEmojiProps) {
10491052
return (
10501053
<button type="button" {...props}>
1051-
{emoji.emoji}
1054+
{emoji.url ? (
1055+
<img src={emoji.url} alt={emoji.label} width="1em" height="1em" />
1056+
) : (
1057+
emoji.emoji
1058+
)}
10521059
</button>
10531060
);
10541061
}

src/data/emoji-picker.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
CustomCategory,
23
Emoji,
34
EmojiData,
45
EmojiDataEmoji,
@@ -48,6 +49,7 @@ export function getEmojiPickerData(
4849
columns: number,
4950
skinTone: SkinTone | undefined,
5051
search: string,
52+
custom?: CustomCategory[],
5153
): EmojiPickerData {
5254
const emojis = searchEmojis(data.emojis, search);
5355
const rows: EmojiPickerDataRow[] = [];
@@ -98,8 +100,81 @@ export function getEmojiPickerData(
98100
startRowIndex += categoryRows.length;
99101
}
100102

103+
let customCount = 0;
104+
105+
if (custom) {
106+
const searchText = search?.toLowerCase().trim();
107+
108+
for (const customCategory of custom) {
109+
let customEmojis: EmojiPickerEmoji[] = customCategory.emojis.map(
110+
(ce) => ({
111+
emoji: "",
112+
label: ce.label,
113+
url: ce.url,
114+
id: ce.id,
115+
}),
116+
);
117+
118+
if (searchText) {
119+
const scores = new Map<string, number>();
120+
121+
customEmojis = customEmojis.filter((ce) => {
122+
let score = 0;
123+
124+
if (ce.label.toLowerCase().includes(searchText)) {
125+
score += 10;
126+
}
127+
128+
const original = customCategory.emojis.find((e) => e.id === ce.id);
129+
130+
if (original?.tags) {
131+
for (const tag of original.tags) {
132+
if (tag.toLowerCase().includes(searchText)) {
133+
score += 1;
134+
}
135+
}
136+
}
137+
138+
if (score > 0) {
139+
scores.set(ce.id!, score);
140+
return true;
141+
}
142+
143+
return false;
144+
});
145+
146+
customEmojis.sort(
147+
(a, b) => (scores.get(b.id!) ?? 0) - (scores.get(a.id!) ?? 0),
148+
);
149+
}
150+
151+
if (customEmojis.length === 0) {
152+
continue;
153+
}
154+
155+
customCount += customEmojis.length;
156+
157+
const categoryRows = chunk(customEmojis, columns).map((emojis) => ({
158+
categoryIndex,
159+
emojis,
160+
}));
161+
162+
rows.push(...categoryRows);
163+
categories.push({
164+
label: customCategory.label,
165+
rowsCount: categoryRows.length,
166+
startRowIndex,
167+
});
168+
169+
categoriesStartRowIndices.push(startRowIndex);
170+
171+
categoryIndex++;
172+
startRowIndex += categoryRows.length;
173+
}
174+
}
175+
101176
return {
102-
count: emojis.length,
177+
count: emojis.length + customCount,
103178
categories,
104179
categoriesStartRowIndices,
105180
rows,

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export * as EmojiPicker from "./components/emoji-picker";
22
export { useActiveEmoji, useSkinTone } from "./hooks";
33
export type {
44
Category,
5+
CustomCategory,
6+
CustomEmoji,
57
Emoji,
68
EmojiPickerActiveEmojiProps,
79
EmojiPickerEmptyProps,

src/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,24 @@ export type EmojiData = {
6262
skinTones: Record<Exclude<SkinTone, "none">, string>;
6363
};
6464

65+
export type CustomEmoji = {
66+
id: string;
67+
label: string;
68+
url: string;
69+
tags?: string[];
70+
};
71+
72+
export type CustomCategory = {
73+
id: string;
74+
label: string;
75+
emojis: CustomEmoji[];
76+
};
77+
6578
export type EmojiPickerEmoji = {
6679
emoji: string;
6780
label: string;
81+
url?: string;
82+
id?: string;
6883
};
6984

7085
export type EmojiPickerCategory = {
@@ -143,6 +158,11 @@ export interface EmojiPickerListProps extends ComponentProps<"div"> {
143158
}
144159

145160
export interface EmojiPickerRootProps extends ComponentProps<"div"> {
161+
/**
162+
* Custom emoji categories to append to the picker.
163+
*/
164+
custom?: CustomCategory[];
165+
146166
/**
147167
* A callback invoked when an emoji is selected.
148168
*/

0 commit comments

Comments
 (0)