@hokkiai/discord-emoji-selector 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +178 -108
  2. package/dist/index.js +180 -110
  3. package/package.json +6 -6
package/dist/index.cjs CHANGED
@@ -57,116 +57,118 @@ var import_react = require("react");
57
57
  function useSkin({ pickerId }) {
58
58
  const [skin, setSkin] = (0, import_react.useState)(0);
59
59
  (0, import_react.useEffect)(() => {
60
- const a = setInterval(() => {
60
+ let raf;
61
+ const loop = () => {
61
62
  const newSkin = window["emojipicker-" + pickerId]?.skin;
62
- const oldSkin = skin;
63
- if (newSkin !== oldSkin) {
64
- setSkin(newSkin);
65
- }
66
- }, 100);
63
+ setSkin(newSkin || 0);
64
+ raf = requestAnimationFrame(loop);
65
+ };
66
+ raf = requestAnimationFrame(loop);
67
67
  return () => {
68
- clearInterval(a);
68
+ cancelAnimationFrame(raf);
69
69
  };
70
- }, [skin]);
70
+ }, [pickerId]);
71
71
  return skin;
72
72
  }
73
73
  function useSearchValue({ pickerId }) {
74
74
  const [searchValue, setSearchValue] = (0, import_react.useState)("");
75
75
  (0, import_react.useEffect)(() => {
76
- const a = setInterval(() => {
77
- const newSearchValue = window["emojipicker-" + pickerId]?.searchValue;
78
- const oldSearchValue = searchValue;
79
- if (newSearchValue !== oldSearchValue) {
80
- setSearchValue(newSearchValue);
81
- }
82
- }, 100);
76
+ let raf;
77
+ const loop = () => {
78
+ const newSearchValue = window["emojipicker-" + pickerId]?.searchValue || "";
79
+ setSearchValue(newSearchValue);
80
+ raf = requestAnimationFrame(loop);
81
+ };
82
+ raf = requestAnimationFrame(loop);
83
83
  return () => {
84
- clearInterval(a);
84
+ cancelAnimationFrame(raf);
85
85
  };
86
- }, [searchValue]);
86
+ }, [pickerId]);
87
87
  return searchValue;
88
88
  }
89
89
 
90
90
  // src/categoryDisplay.tsx
91
91
  var import_jsx_runtime = require("react/jsx-runtime");
92
- function Emoji({
92
+ var Emoji = (0, import_react2.memo)(function Emoji2({
93
93
  pickerId,
94
94
  emoji,
95
95
  onEmojiMouseEnter,
96
96
  onEmojiMouseLeave,
97
- onEmojiSelect,
98
- key
97
+ onEmojiSelect
99
98
  }) {
99
+ const handleMouseEnter = (0, import_react2.useCallback)(() => {
100
+ window["emojipicker-" + pickerId].changeFooterEmoji(emoji);
101
+ window["emojipicker-" + pickerId].changeSearchbarPlaceholder(emoji.name);
102
+ onEmojiMouseEnter(emoji);
103
+ }, [pickerId, emoji, onEmojiMouseEnter]);
104
+ const handleMouseLeave = (0, import_react2.useCallback)(() => {
105
+ onEmojiMouseLeave(emoji);
106
+ }, [emoji, onEmojiMouseLeave]);
107
+ const handleClick = (0, import_react2.useCallback)(() => {
108
+ onEmojiSelect(emoji);
109
+ }, [emoji, onEmojiSelect]);
110
+ const html = (0, import_react2.useMemo)(() => render(emoji.char), [emoji.char]);
100
111
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
101
112
  "div",
102
113
  {
103
- onMouseEnter: () => {
104
- window["emojipicker-" + pickerId].changeFooterEmoji(emoji);
105
- window["emojipicker-" + pickerId].changeSearchbarPlaceholder(
106
- emoji.name
107
- );
108
- onEmojiMouseEnter(emoji);
109
- },
110
- onMouseLeave: () => {
111
- onEmojiMouseLeave(emoji);
112
- },
113
- onClick: () => {
114
- onEmojiSelect(emoji);
115
- },
116
- className: "HOKKIEMOJIPICKER-emoji text-4xl p-1 cursor-pointer hover:bg-white/15 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
117
- dangerouslySetInnerHTML: {
118
- __html: render(emoji.char)
119
- }
114
+ tabIndex: -1,
115
+ onMouseEnter: handleMouseEnter,
116
+ onMouseLeave: handleMouseLeave,
117
+ onClick: handleClick,
118
+ className: "HOKKIEMOJIPICKER-emoji text-4xl p-1 cursor-pointer hover:bg-white/15 focus:bg-white/20 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
119
+ dangerouslySetInnerHTML: { __html: html }
120
120
  }
121
121
  );
122
- }
123
- function SkinEmoji({
122
+ });
123
+ var SkinEmoji = (0, import_react2.memo)(function SkinEmoji2({
124
124
  pickerId,
125
125
  emoji,
126
126
  onEmojiSelect,
127
127
  onEmojiMouseEnter,
128
- onEmojiMouseLeave,
129
- key
128
+ onEmojiMouseLeave
130
129
  }) {
131
130
  const skin = useSkin({ pickerId });
132
- const grabEmoji = () => [1, 2, 3, 4, 5].includes(skin) ? emoji.tones.find((a) => a.tone.find((b) => b === skin) === skin)?.char : emoji.char;
133
- const fakeEmoji = {
134
- ...emoji,
135
- char: grabEmoji(),
136
- preRendered: true,
137
- tones: [
138
- {
139
- name: emoji.tones[0].name.replaceAll("1", "0"),
140
- tone: [0],
141
- char: emoji.char
142
- },
143
- ...emoji.tones
144
- ]
145
- };
131
+ const fakeEmoji = (0, import_react2.useMemo)(() => {
132
+ const charForSkin = [1, 2, 3, 4, 5].includes(skin) ? emoji.tones.find((a) => a.tone.find((b) => b === skin) === skin)?.char : emoji.char;
133
+ return {
134
+ ...emoji,
135
+ char: charForSkin,
136
+ preRendered: true,
137
+ tones: [
138
+ {
139
+ name: emoji.tones[0].name.replaceAll("1", "0"),
140
+ tone: [0],
141
+ char: emoji.char
142
+ },
143
+ ...emoji.tones
144
+ ]
145
+ };
146
+ }, [emoji, skin]);
147
+ const handleMouseEnter = (0, import_react2.useCallback)(() => {
148
+ window["emojipicker-" + pickerId].changeFooterEmoji(fakeEmoji);
149
+ window["emojipicker-" + pickerId].changeSearchbarPlaceholder(emoji.name);
150
+ onEmojiMouseEnter(fakeEmoji);
151
+ }, [pickerId, fakeEmoji, emoji.name, onEmojiMouseEnter]);
152
+ const handleMouseLeave = (0, import_react2.useCallback)(() => {
153
+ onEmojiMouseLeave(fakeEmoji);
154
+ }, [fakeEmoji, onEmojiMouseLeave]);
155
+ const handleClick = (0, import_react2.useCallback)(() => {
156
+ onEmojiSelect(fakeEmoji);
157
+ }, [fakeEmoji, onEmojiSelect]);
158
+ const html = (0, import_react2.useMemo)(() => render(fakeEmoji.char), [fakeEmoji.char]);
146
159
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
147
160
  "div",
148
161
  {
149
- onMouseEnter: () => {
150
- window["emojipicker-" + pickerId].changeFooterEmoji(fakeEmoji);
151
- window["emojipicker-" + pickerId].changeSearchbarPlaceholder(
152
- emoji.name
153
- );
154
- onEmojiMouseEnter(fakeEmoji);
155
- },
156
- onMouseLeave: () => {
157
- onEmojiMouseLeave(fakeEmoji);
158
- },
159
- onClick: () => {
160
- onEmojiSelect(fakeEmoji);
161
- },
162
- className: "HOKKIEMOJIPICKER-skinemoji text-4xl p-1 cursor-pointer hover:bg-white/15 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
163
- dangerouslySetInnerHTML: {
164
- __html: render(fakeEmoji.char)
165
- }
162
+ tabIndex: -1,
163
+ onMouseEnter: handleMouseEnter,
164
+ onMouseLeave: handleMouseLeave,
165
+ onClick: handleClick,
166
+ className: "HOKKIEMOJIPICKER-skinemoji text-4xl p-1 cursor-pointer hover:bg-white/15 focus:bg-white/20 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
167
+ dangerouslySetInnerHTML: { __html: html }
166
168
  }
167
169
  );
168
- }
169
- function CategoryDisplay({
170
+ });
171
+ var CategoryDisplay = (0, import_react2.memo)(function CategoryDisplay2({
170
172
  category,
171
173
  categoryInfo,
172
174
  isToneSelectorEnabled,
@@ -183,13 +185,21 @@ function CategoryDisplay({
183
185
  "hokkiemojipicker-category-" + category.name + "-open"
184
186
  ) || "true") === "true"
185
187
  );
186
- }, []);
187
- if ((searchValue || "").length > 0) {
188
+ }, [category.name]);
189
+ const searchLower = (0, import_react2.useMemo)(
190
+ () => (searchValue || "").toLowerCase().replace(/_/g, " "),
191
+ [searchValue]
192
+ );
193
+ const filteredEmojis = (0, import_react2.useMemo)(() => {
194
+ if (!searchLower) return null;
195
+ return category.emojis.filter(
196
+ (emoji) => emoji.name.toLowerCase().replace(/_/g, " ").includes(searchLower)
197
+ );
198
+ }, [category.emojis, searchLower]);
199
+ if (searchLower) {
188
200
  if (category.name === "recentlyUsed")
189
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-1.5 w-full " });
190
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: category.emojis.map((emoji) => {
191
- if (!emoji.name.toLowerCase().replace(/_/g, " ").includes(searchValue.toLowerCase().replace(/_/g, " ")))
192
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, {});
201
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-1.5 w-full" });
202
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: filteredEmojis.map((emoji) => {
193
203
  if (emoji.hasTone && !emoji.preRendered) {
194
204
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
195
205
  SkinEmoji,
@@ -271,7 +281,8 @@ function CategoryDisplay({
271
281
  );
272
282
  }) })
273
283
  ] });
274
- }
284
+ });
285
+ var categoryDisplay_default = CategoryDisplay;
275
286
 
276
287
  // src/emojilib.json
277
288
  var emojilib_default = [
@@ -17122,7 +17133,6 @@ function SidebarCategory({
17122
17133
  }) {
17123
17134
  const searchValue = useSearchValue({ pickerId: id });
17124
17135
  const [isActive, setIsActive] = (0, import_react5.useState)(false);
17125
- const [scrollTop, setScrollTop] = (0, import_react5.useState)(0);
17126
17136
  (0, import_react5.useEffect)(() => {
17127
17137
  if ((searchValue || "").length > 0) {
17128
17138
  setIsActive(false);
@@ -17136,16 +17146,18 @@ function SidebarCategory({
17136
17146
  );
17137
17147
  if (!header) return;
17138
17148
  const categoryContainer = header.parentElement;
17139
- const scrollStart = categoryContainer.offsetTop - header.offsetHeight * 6;
17140
- const scrollEnd = categoryContainer.offsetTop + categoryContainer.offsetHeight - header.offsetHeight * 5.5;
17141
- const actualScroll = elem.querySelector(
17149
+ const scrollElem = elem.querySelector(
17142
17150
  ".HOKKIEMOJIPICKER-emojidisplay"
17143
- ).scrollTop;
17144
- if (actualScroll > scrollStart && actualScroll < scrollEnd) {
17145
- setIsActive(true);
17146
- } else {
17147
- setIsActive(false);
17148
- }
17151
+ );
17152
+ if (!scrollElem) return;
17153
+ const containerRect = scrollElem.getBoundingClientRect();
17154
+ const categoryRect = categoryContainer.getBoundingClientRect();
17155
+ const headerHeight = header.offsetHeight;
17156
+ const categoryTop = categoryRect.top - containerRect.top + scrollElem.scrollTop;
17157
+ const categoryBottom = categoryTop + categoryContainer.offsetHeight;
17158
+ const sTop = scrollElem.scrollTop;
17159
+ const inView = sTop >= categoryTop - headerHeight && sTop < categoryBottom - headerHeight;
17160
+ setIsActive(inView);
17149
17161
  };
17150
17162
  elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").addEventListener("scroll", updateActive);
17151
17163
  updateActive();
@@ -17153,28 +17165,37 @@ function SidebarCategory({
17153
17165
  elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").removeEventListener("scroll", updateActive);
17154
17166
  };
17155
17167
  }, [picker, searchValue]);
17156
- (0, import_react5.useEffect)(() => {
17157
- const elem = picker.current;
17158
- if (!elem) return;
17159
- const header = elem.querySelector(
17160
- ".HOKKIEMOJIPICKER-categoryHeader." + categoryName
17161
- );
17162
- const categoryContainer = header.parentElement;
17163
- const scrollTop2 = categoryContainer.offsetTop - header.offsetHeight * 5.5;
17164
- setScrollTop(scrollTop2);
17165
- }, []);
17166
17168
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
17167
17169
  "button",
17168
17170
  {
17169
17171
  onClick: () => {
17170
17172
  const elem = picker.current;
17173
+ if (!elem) return;
17174
+ const doScroll = () => {
17175
+ const header = elem.querySelector(
17176
+ ".HOKKIEMOJIPICKER-categoryHeader." + categoryName
17177
+ );
17178
+ if (!header) return;
17179
+ const categoryContainer = header.parentElement;
17180
+ const scrollElem = elem.querySelector(
17181
+ ".HOKKIEMOJIPICKER-emojidisplay"
17182
+ );
17183
+ if (!scrollElem) return;
17184
+ const containerRect = scrollElem.getBoundingClientRect();
17185
+ const categoryRect = categoryContainer.getBoundingClientRect();
17186
+ const headerHeight = header.offsetHeight;
17187
+ const targetTop = categoryRect.top - containerRect.top + scrollElem.scrollTop - headerHeight;
17188
+ scrollElem.scrollTo({
17189
+ top: Math.max(0, targetTop),
17190
+ behavior: "smooth"
17191
+ });
17192
+ };
17171
17193
  if ((searchValue || "").length > 0) {
17172
17194
  window["emojipicker-" + id].setSearchValue("");
17173
- setTimeout(() => {
17174
- elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").scrollTop = scrollTop;
17175
- }, 600);
17195
+ setTimeout(doScroll, 50);
17196
+ } else {
17197
+ doScroll();
17176
17198
  }
17177
- elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").scrollTop = scrollTop;
17178
17199
  },
17179
17200
  "data-active": isActive ? "true" : "false",
17180
17201
  className: "!outline-0 HOKKIEMOJIPICKER-sidebarButton cursor-pointer size-8 data-[active=true]:bg-white/5 hover:bg-white/10 overflow-hidden *:!size-6.5 transition-all hover:*:!opacity-85 data-[active=true]:*:!opacity-100 flex items-center justify-center rounded-sm *:opacity-50",
@@ -17520,9 +17541,55 @@ function EmojiSelector({
17520
17541
  ...emojilib_default
17521
17542
  ].filter((a) => a !== void 0);
17522
17543
  const navHeight = showNav ? Math.floor(height / 7) : 0;
17523
- const id = Math.random().toString(36).substring(2, 15);
17544
+ const id = (0, import_react7.useMemo)(
17545
+ () => Math.random().toString(36).substring(2, 15),
17546
+ []
17547
+ );
17524
17548
  const picker = (0, import_react7.useRef)(null);
17525
- console.log(id);
17549
+ const scrollRef = (0, import_react7.useRef)(null);
17550
+ const [focusIndex, setFocusIndex] = (0, import_react7.useState)(-1);
17551
+ const handleWheel = (0, import_react7.useCallback)((e) => {
17552
+ const el = scrollRef.current;
17553
+ if (!el) return;
17554
+ e.preventDefault();
17555
+ el.scrollTop += e.deltaY;
17556
+ }, []);
17557
+ (0, import_react7.useEffect)(() => {
17558
+ const el = scrollRef.current;
17559
+ if (!el) return;
17560
+ el.addEventListener("wheel", handleWheel, { passive: false });
17561
+ return () => {
17562
+ el.removeEventListener("wheel", handleWheel);
17563
+ };
17564
+ }, [handleWheel]);
17565
+ const handleKeyDown = (0, import_react7.useCallback)(
17566
+ (e) => {
17567
+ const el = scrollRef.current;
17568
+ if (!el) return;
17569
+ const emojiElements = el.querySelectorAll(
17570
+ ".HOKKIEMOJIPICKER-emoji, .HOKKIEMOJIPICKER-skinemoji"
17571
+ );
17572
+ const count = emojiElements.length;
17573
+ if (count === 0) return;
17574
+ if (e.key === "Tab") {
17575
+ e.preventDefault();
17576
+ let next = focusIndex + (e.shiftKey ? -1 : 1);
17577
+ if (next < 0) next = count - 1;
17578
+ if (next >= count) next = 0;
17579
+ setFocusIndex(next);
17580
+ const target = emojiElements[next];
17581
+ if (target) {
17582
+ target.scrollIntoView({ block: "nearest" });
17583
+ target.focus();
17584
+ }
17585
+ } else if (e.key === "Enter" && focusIndex >= 0) {
17586
+ e.preventDefault();
17587
+ const target = emojiElements[focusIndex];
17588
+ if (target) target.click();
17589
+ }
17590
+ },
17591
+ [focusIndex]
17592
+ );
17526
17593
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "HOKKIEMOJIPICKER-emojiContainer", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
17527
17594
  "div",
17528
17595
  {
@@ -17590,6 +17657,9 @@ function EmojiSelector({
17590
17657
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
17591
17658
  "div",
17592
17659
  {
17660
+ ref: scrollRef,
17661
+ tabIndex: 0,
17662
+ onKeyDown: handleKeyDown,
17593
17663
  style: {
17594
17664
  flexBasis: "fit-content"
17595
17665
  },
@@ -17598,7 +17668,7 @@ function EmojiSelector({
17598
17668
  if (!category) return;
17599
17669
  const categoryInfo = categoryData[category.name];
17600
17670
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
17601
- CategoryDisplay,
17671
+ categoryDisplay_default,
17602
17672
  {
17603
17673
  onEmojiMouseEnter,
17604
17674
  onEmojiMouseLeave,
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/index.tsx
2
- import { useRef as useRef2 } from "react";
2
+ import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef2, useState as useState7 } from "react";
3
3
 
4
4
  // src/categoryDisplay.tsx
5
5
  import { ChevronDown } from "lucide-react";
6
- import { useEffect as useEffect2, useState as useState2 } from "react";
6
+ import { memo, useEffect as useEffect2, useState as useState2, useCallback, useMemo } from "react";
7
7
 
8
8
  // src/render.tsx
9
9
  import tw from "@twemoji/api";
@@ -23,116 +23,118 @@ import { useEffect, useState } from "react";
23
23
  function useSkin({ pickerId }) {
24
24
  const [skin, setSkin] = useState(0);
25
25
  useEffect(() => {
26
- const a = setInterval(() => {
26
+ let raf;
27
+ const loop = () => {
27
28
  const newSkin = window["emojipicker-" + pickerId]?.skin;
28
- const oldSkin = skin;
29
- if (newSkin !== oldSkin) {
30
- setSkin(newSkin);
31
- }
32
- }, 100);
29
+ setSkin(newSkin || 0);
30
+ raf = requestAnimationFrame(loop);
31
+ };
32
+ raf = requestAnimationFrame(loop);
33
33
  return () => {
34
- clearInterval(a);
34
+ cancelAnimationFrame(raf);
35
35
  };
36
- }, [skin]);
36
+ }, [pickerId]);
37
37
  return skin;
38
38
  }
39
39
  function useSearchValue({ pickerId }) {
40
40
  const [searchValue, setSearchValue] = useState("");
41
41
  useEffect(() => {
42
- const a = setInterval(() => {
43
- const newSearchValue = window["emojipicker-" + pickerId]?.searchValue;
44
- const oldSearchValue = searchValue;
45
- if (newSearchValue !== oldSearchValue) {
46
- setSearchValue(newSearchValue);
47
- }
48
- }, 100);
42
+ let raf;
43
+ const loop = () => {
44
+ const newSearchValue = window["emojipicker-" + pickerId]?.searchValue || "";
45
+ setSearchValue(newSearchValue);
46
+ raf = requestAnimationFrame(loop);
47
+ };
48
+ raf = requestAnimationFrame(loop);
49
49
  return () => {
50
- clearInterval(a);
50
+ cancelAnimationFrame(raf);
51
51
  };
52
- }, [searchValue]);
52
+ }, [pickerId]);
53
53
  return searchValue;
54
54
  }
55
55
 
56
56
  // src/categoryDisplay.tsx
57
57
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
58
- function Emoji({
58
+ var Emoji = memo(function Emoji2({
59
59
  pickerId,
60
60
  emoji,
61
61
  onEmojiMouseEnter,
62
62
  onEmojiMouseLeave,
63
- onEmojiSelect,
64
- key
63
+ onEmojiSelect
65
64
  }) {
65
+ const handleMouseEnter = useCallback(() => {
66
+ window["emojipicker-" + pickerId].changeFooterEmoji(emoji);
67
+ window["emojipicker-" + pickerId].changeSearchbarPlaceholder(emoji.name);
68
+ onEmojiMouseEnter(emoji);
69
+ }, [pickerId, emoji, onEmojiMouseEnter]);
70
+ const handleMouseLeave = useCallback(() => {
71
+ onEmojiMouseLeave(emoji);
72
+ }, [emoji, onEmojiMouseLeave]);
73
+ const handleClick = useCallback(() => {
74
+ onEmojiSelect(emoji);
75
+ }, [emoji, onEmojiSelect]);
76
+ const html = useMemo(() => render(emoji.char), [emoji.char]);
66
77
  return /* @__PURE__ */ jsx(
67
78
  "div",
68
79
  {
69
- onMouseEnter: () => {
70
- window["emojipicker-" + pickerId].changeFooterEmoji(emoji);
71
- window["emojipicker-" + pickerId].changeSearchbarPlaceholder(
72
- emoji.name
73
- );
74
- onEmojiMouseEnter(emoji);
75
- },
76
- onMouseLeave: () => {
77
- onEmojiMouseLeave(emoji);
78
- },
79
- onClick: () => {
80
- onEmojiSelect(emoji);
81
- },
82
- className: "HOKKIEMOJIPICKER-emoji text-4xl p-1 cursor-pointer hover:bg-white/15 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
83
- dangerouslySetInnerHTML: {
84
- __html: render(emoji.char)
85
- }
80
+ tabIndex: -1,
81
+ onMouseEnter: handleMouseEnter,
82
+ onMouseLeave: handleMouseLeave,
83
+ onClick: handleClick,
84
+ className: "HOKKIEMOJIPICKER-emoji text-4xl p-1 cursor-pointer hover:bg-white/15 focus:bg-white/20 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
85
+ dangerouslySetInnerHTML: { __html: html }
86
86
  }
87
87
  );
88
- }
89
- function SkinEmoji({
88
+ });
89
+ var SkinEmoji = memo(function SkinEmoji2({
90
90
  pickerId,
91
91
  emoji,
92
92
  onEmojiSelect,
93
93
  onEmojiMouseEnter,
94
- onEmojiMouseLeave,
95
- key
94
+ onEmojiMouseLeave
96
95
  }) {
97
96
  const skin = useSkin({ pickerId });
98
- const grabEmoji = () => [1, 2, 3, 4, 5].includes(skin) ? emoji.tones.find((a) => a.tone.find((b) => b === skin) === skin)?.char : emoji.char;
99
- const fakeEmoji = {
100
- ...emoji,
101
- char: grabEmoji(),
102
- preRendered: true,
103
- tones: [
104
- {
105
- name: emoji.tones[0].name.replaceAll("1", "0"),
106
- tone: [0],
107
- char: emoji.char
108
- },
109
- ...emoji.tones
110
- ]
111
- };
97
+ const fakeEmoji = useMemo(() => {
98
+ const charForSkin = [1, 2, 3, 4, 5].includes(skin) ? emoji.tones.find((a) => a.tone.find((b) => b === skin) === skin)?.char : emoji.char;
99
+ return {
100
+ ...emoji,
101
+ char: charForSkin,
102
+ preRendered: true,
103
+ tones: [
104
+ {
105
+ name: emoji.tones[0].name.replaceAll("1", "0"),
106
+ tone: [0],
107
+ char: emoji.char
108
+ },
109
+ ...emoji.tones
110
+ ]
111
+ };
112
+ }, [emoji, skin]);
113
+ const handleMouseEnter = useCallback(() => {
114
+ window["emojipicker-" + pickerId].changeFooterEmoji(fakeEmoji);
115
+ window["emojipicker-" + pickerId].changeSearchbarPlaceholder(emoji.name);
116
+ onEmojiMouseEnter(fakeEmoji);
117
+ }, [pickerId, fakeEmoji, emoji.name, onEmojiMouseEnter]);
118
+ const handleMouseLeave = useCallback(() => {
119
+ onEmojiMouseLeave(fakeEmoji);
120
+ }, [fakeEmoji, onEmojiMouseLeave]);
121
+ const handleClick = useCallback(() => {
122
+ onEmojiSelect(fakeEmoji);
123
+ }, [fakeEmoji, onEmojiSelect]);
124
+ const html = useMemo(() => render(fakeEmoji.char), [fakeEmoji.char]);
112
125
  return /* @__PURE__ */ jsx(
113
126
  "div",
114
127
  {
115
- onMouseEnter: () => {
116
- window["emojipicker-" + pickerId].changeFooterEmoji(fakeEmoji);
117
- window["emojipicker-" + pickerId].changeSearchbarPlaceholder(
118
- emoji.name
119
- );
120
- onEmojiMouseEnter(fakeEmoji);
121
- },
122
- onMouseLeave: () => {
123
- onEmojiMouseLeave(fakeEmoji);
124
- },
125
- onClick: () => {
126
- onEmojiSelect(fakeEmoji);
127
- },
128
- className: "HOKKIEMOJIPICKER-skinemoji text-4xl p-1 cursor-pointer hover:bg-white/15 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
129
- dangerouslySetInnerHTML: {
130
- __html: render(fakeEmoji.char)
131
- }
128
+ tabIndex: -1,
129
+ onMouseEnter: handleMouseEnter,
130
+ onMouseLeave: handleMouseLeave,
131
+ onClick: handleClick,
132
+ className: "HOKKIEMOJIPICKER-skinemoji text-4xl p-1 cursor-pointer hover:bg-white/15 focus:bg-white/20 rounded-sm size-12.5 flex items-center justify-center overflow-hidden",
133
+ dangerouslySetInnerHTML: { __html: html }
132
134
  }
133
135
  );
134
- }
135
- function CategoryDisplay({
136
+ });
137
+ var CategoryDisplay = memo(function CategoryDisplay2({
136
138
  category,
137
139
  categoryInfo,
138
140
  isToneSelectorEnabled,
@@ -149,13 +151,21 @@ function CategoryDisplay({
149
151
  "hokkiemojipicker-category-" + category.name + "-open"
150
152
  ) || "true") === "true"
151
153
  );
152
- }, []);
153
- if ((searchValue || "").length > 0) {
154
+ }, [category.name]);
155
+ const searchLower = useMemo(
156
+ () => (searchValue || "").toLowerCase().replace(/_/g, " "),
157
+ [searchValue]
158
+ );
159
+ const filteredEmojis = useMemo(() => {
160
+ if (!searchLower) return null;
161
+ return category.emojis.filter(
162
+ (emoji) => emoji.name.toLowerCase().replace(/_/g, " ").includes(searchLower)
163
+ );
164
+ }, [category.emojis, searchLower]);
165
+ if (searchLower) {
154
166
  if (category.name === "recentlyUsed")
155
- return /* @__PURE__ */ jsx("div", { className: "h-1.5 w-full " });
156
- return /* @__PURE__ */ jsx(Fragment, { children: category.emojis.map((emoji) => {
157
- if (!emoji.name.toLowerCase().replace(/_/g, " ").includes(searchValue.toLowerCase().replace(/_/g, " ")))
158
- return /* @__PURE__ */ jsx(Fragment, {});
167
+ return /* @__PURE__ */ jsx("div", { className: "h-1.5 w-full" });
168
+ return /* @__PURE__ */ jsx(Fragment, { children: filteredEmojis.map((emoji) => {
159
169
  if (emoji.hasTone && !emoji.preRendered) {
160
170
  return /* @__PURE__ */ jsx(
161
171
  SkinEmoji,
@@ -237,7 +247,8 @@ function CategoryDisplay({
237
247
  );
238
248
  }) })
239
249
  ] });
240
- }
250
+ });
251
+ var categoryDisplay_default = CategoryDisplay;
241
252
 
242
253
  // src/emojilib.json
243
254
  var emojilib_default = [
@@ -17088,7 +17099,6 @@ function SidebarCategory({
17088
17099
  }) {
17089
17100
  const searchValue = useSearchValue({ pickerId: id });
17090
17101
  const [isActive, setIsActive] = useState5(false);
17091
- const [scrollTop, setScrollTop] = useState5(0);
17092
17102
  useEffect5(() => {
17093
17103
  if ((searchValue || "").length > 0) {
17094
17104
  setIsActive(false);
@@ -17102,16 +17112,18 @@ function SidebarCategory({
17102
17112
  );
17103
17113
  if (!header) return;
17104
17114
  const categoryContainer = header.parentElement;
17105
- const scrollStart = categoryContainer.offsetTop - header.offsetHeight * 6;
17106
- const scrollEnd = categoryContainer.offsetTop + categoryContainer.offsetHeight - header.offsetHeight * 5.5;
17107
- const actualScroll = elem.querySelector(
17115
+ const scrollElem = elem.querySelector(
17108
17116
  ".HOKKIEMOJIPICKER-emojidisplay"
17109
- ).scrollTop;
17110
- if (actualScroll > scrollStart && actualScroll < scrollEnd) {
17111
- setIsActive(true);
17112
- } else {
17113
- setIsActive(false);
17114
- }
17117
+ );
17118
+ if (!scrollElem) return;
17119
+ const containerRect = scrollElem.getBoundingClientRect();
17120
+ const categoryRect = categoryContainer.getBoundingClientRect();
17121
+ const headerHeight = header.offsetHeight;
17122
+ const categoryTop = categoryRect.top - containerRect.top + scrollElem.scrollTop;
17123
+ const categoryBottom = categoryTop + categoryContainer.offsetHeight;
17124
+ const sTop = scrollElem.scrollTop;
17125
+ const inView = sTop >= categoryTop - headerHeight && sTop < categoryBottom - headerHeight;
17126
+ setIsActive(inView);
17115
17127
  };
17116
17128
  elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").addEventListener("scroll", updateActive);
17117
17129
  updateActive();
@@ -17119,28 +17131,37 @@ function SidebarCategory({
17119
17131
  elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").removeEventListener("scroll", updateActive);
17120
17132
  };
17121
17133
  }, [picker, searchValue]);
17122
- useEffect5(() => {
17123
- const elem = picker.current;
17124
- if (!elem) return;
17125
- const header = elem.querySelector(
17126
- ".HOKKIEMOJIPICKER-categoryHeader." + categoryName
17127
- );
17128
- const categoryContainer = header.parentElement;
17129
- const scrollTop2 = categoryContainer.offsetTop - header.offsetHeight * 5.5;
17130
- setScrollTop(scrollTop2);
17131
- }, []);
17132
17134
  return /* @__PURE__ */ jsx5(
17133
17135
  "button",
17134
17136
  {
17135
17137
  onClick: () => {
17136
17138
  const elem = picker.current;
17139
+ if (!elem) return;
17140
+ const doScroll = () => {
17141
+ const header = elem.querySelector(
17142
+ ".HOKKIEMOJIPICKER-categoryHeader." + categoryName
17143
+ );
17144
+ if (!header) return;
17145
+ const categoryContainer = header.parentElement;
17146
+ const scrollElem = elem.querySelector(
17147
+ ".HOKKIEMOJIPICKER-emojidisplay"
17148
+ );
17149
+ if (!scrollElem) return;
17150
+ const containerRect = scrollElem.getBoundingClientRect();
17151
+ const categoryRect = categoryContainer.getBoundingClientRect();
17152
+ const headerHeight = header.offsetHeight;
17153
+ const targetTop = categoryRect.top - containerRect.top + scrollElem.scrollTop - headerHeight;
17154
+ scrollElem.scrollTo({
17155
+ top: Math.max(0, targetTop),
17156
+ behavior: "smooth"
17157
+ });
17158
+ };
17137
17159
  if ((searchValue || "").length > 0) {
17138
17160
  window["emojipicker-" + id].setSearchValue("");
17139
- setTimeout(() => {
17140
- elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").scrollTop = scrollTop;
17141
- }, 600);
17161
+ setTimeout(doScroll, 50);
17162
+ } else {
17163
+ doScroll();
17142
17164
  }
17143
- elem.querySelector(".HOKKIEMOJIPICKER-emojidisplay").scrollTop = scrollTop;
17144
17165
  },
17145
17166
  "data-active": isActive ? "true" : "false",
17146
17167
  className: "!outline-0 HOKKIEMOJIPICKER-sidebarButton cursor-pointer size-8 data-[active=true]:bg-white/5 hover:bg-white/10 overflow-hidden *:!size-6.5 transition-all hover:*:!opacity-85 data-[active=true]:*:!opacity-100 flex items-center justify-center rounded-sm *:opacity-50",
@@ -17486,9 +17507,55 @@ function EmojiSelector({
17486
17507
  ...emojilib_default
17487
17508
  ].filter((a) => a !== void 0);
17488
17509
  const navHeight = showNav ? Math.floor(height / 7) : 0;
17489
- const id = Math.random().toString(36).substring(2, 15);
17510
+ const id = useMemo2(
17511
+ () => Math.random().toString(36).substring(2, 15),
17512
+ []
17513
+ );
17490
17514
  const picker = useRef2(null);
17491
- console.log(id);
17515
+ const scrollRef = useRef2(null);
17516
+ const [focusIndex, setFocusIndex] = useState7(-1);
17517
+ const handleWheel = useCallback2((e) => {
17518
+ const el = scrollRef.current;
17519
+ if (!el) return;
17520
+ e.preventDefault();
17521
+ el.scrollTop += e.deltaY;
17522
+ }, []);
17523
+ useEffect7(() => {
17524
+ const el = scrollRef.current;
17525
+ if (!el) return;
17526
+ el.addEventListener("wheel", handleWheel, { passive: false });
17527
+ return () => {
17528
+ el.removeEventListener("wheel", handleWheel);
17529
+ };
17530
+ }, [handleWheel]);
17531
+ const handleKeyDown = useCallback2(
17532
+ (e) => {
17533
+ const el = scrollRef.current;
17534
+ if (!el) return;
17535
+ const emojiElements = el.querySelectorAll(
17536
+ ".HOKKIEMOJIPICKER-emoji, .HOKKIEMOJIPICKER-skinemoji"
17537
+ );
17538
+ const count = emojiElements.length;
17539
+ if (count === 0) return;
17540
+ if (e.key === "Tab") {
17541
+ e.preventDefault();
17542
+ let next = focusIndex + (e.shiftKey ? -1 : 1);
17543
+ if (next < 0) next = count - 1;
17544
+ if (next >= count) next = 0;
17545
+ setFocusIndex(next);
17546
+ const target = emojiElements[next];
17547
+ if (target) {
17548
+ target.scrollIntoView({ block: "nearest" });
17549
+ target.focus();
17550
+ }
17551
+ } else if (e.key === "Enter" && focusIndex >= 0) {
17552
+ e.preventDefault();
17553
+ const target = emojiElements[focusIndex];
17554
+ if (target) target.click();
17555
+ }
17556
+ },
17557
+ [focusIndex]
17558
+ );
17492
17559
  return /* @__PURE__ */ jsx7("div", { className: "HOKKIEMOJIPICKER-emojiContainer", children: /* @__PURE__ */ jsxs6(
17493
17560
  "div",
17494
17561
  {
@@ -17556,6 +17623,9 @@ function EmojiSelector({
17556
17623
  /* @__PURE__ */ jsx7(
17557
17624
  "div",
17558
17625
  {
17626
+ ref: scrollRef,
17627
+ tabIndex: 0,
17628
+ onKeyDown: handleKeyDown,
17559
17629
  style: {
17560
17630
  flexBasis: "fit-content"
17561
17631
  },
@@ -17564,7 +17634,7 @@ function EmojiSelector({
17564
17634
  if (!category) return;
17565
17635
  const categoryInfo = categoryData[category.name];
17566
17636
  return /* @__PURE__ */ jsx7(
17567
- CategoryDisplay,
17637
+ categoryDisplay_default,
17568
17638
  {
17569
17639
  onEmojiMouseEnter,
17570
17640
  onEmojiMouseLeave,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@hokkiai/discord-emoji-selector",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "A lightweight & powerful discord-style emoji picker",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
- "homepage": "https://github.com/shardteam-dev/discord-emoji-selector",
7
+ "homepage": "https://github.com/hokkiai/discord-emoji-selector",
8
8
  "module": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
10
  "keywords": [
@@ -28,9 +28,6 @@
28
28
  "files": [
29
29
  "dist"
30
30
  ],
31
- "scripts": {
32
- "build": "tsup src/index.tsx --dts --format esm,cjs"
33
- },
34
31
  "peerDependencies": {
35
32
  "react": ">=18"
36
33
  },
@@ -44,5 +41,8 @@
44
41
  "@types/react": "^19.1.10",
45
42
  "tsup": "^8.5.0",
46
43
  "typescript": "^5.9.2"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup src/index.tsx --dts --format esm,cjs"
47
47
  }
48
- }
48
+ }