emoji-picker-react 4.22.0 → 4.22.1

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.
@@ -1,4 +1,11 @@
1
- export declare function useActiveCategoryScrollDetection({ setActiveCategory, setVisibleCategories, }: {
1
+ export declare function useActiveCategoryScrollDetection({ setActiveCategory, setVisibleCategories, categoryIdentitiesKey, }: {
2
2
  setActiveCategory: (category: string) => void;
3
3
  setVisibleCategories: (categories: string[]) => void;
4
+ /**
5
+ * Stable string derived from the rendered category identities
6
+ * (e.g. ids joined). Re-subscribes the observer when sections are
7
+ * added or removed so new elements are observed and detached ones
8
+ * stop contributing; unrelated renders keep the same key.
9
+ */
10
+ categoryIdentitiesKey: string;
4
11
  }): void;
package/llms.txt CHANGED
@@ -236,6 +236,13 @@ automatically (after the standard categories), so grouped emojis always
236
236
  render somewhere. Omit `CUSTOM` from `categories` entirely to hide all
237
237
  customs — explicit `categories` stay an allowlist:
238
238
 
239
+ ### Updating groups at runtime
240
+
241
+ `categories` and `customEmojis` are immutable inputs: replacing either
242
+ array (even with a same-length array) rebuilds sections, tabs, search,
243
+ and lookup. Mutating an array in place is not detected — always provide
244
+ a new reference. Group strings are exact, case-sensitive identifiers.
245
+
239
246
  ```tsx
240
247
  import EmojiPicker, { Categories } from 'emoji-picker-react';
241
248
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.22.0",
2
+ "version": "4.22.1",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -39,7 +39,11 @@ export function useSetConfig(config: PickerConfig) {
39
39
  // not gonna...
40
40
  // eslint-disable-next-line react-hooks/exhaustive-deps
41
41
  }, [
42
- config.customEmojis?.length,
42
+ // New references rebuild merged categories and data even at equal
43
+ // length; compareConfig above still gates no-op updates so inline
44
+ // literals don't loop.
45
+ config.categories,
46
+ config.customEmojis,
43
47
  config.open,
44
48
  config.emojiVersion,
45
49
  config.reactionsDefaultOpen,
@@ -34,9 +34,9 @@ export interface PickerDataContextValue {
34
34
  const PickerDataContext = React.createContext<PickerDataContextValue>({
35
35
  emojiData: {} as EmojiData,
36
36
  allEmojis: [],
37
- allEmojisByUnified: {},
38
- searchIndex: {},
39
- customGroups: {},
37
+ allEmojisByUnified: Object.create(null),
38
+ searchIndex: Object.create(null),
39
+ customGroups: Object.create(null),
40
40
  emojiByUnified,
41
41
  activeVariationFromUnified: () => null,
42
42
  });
@@ -54,7 +54,10 @@ export function PickerDataProvider({
54
54
  // Clone to avoid mutation of shared source
55
55
  const newData: EmojiData = JSON.parse(JSON.stringify(emojiData));
56
56
 
57
- const customGroups: Record<string, DataEmojis> = {};
57
+ // Group names and emoji ids are user-controlled strings: use
58
+ // null-prototype dictionaries so names like `__proto__` behave as
59
+ // ordinary data instead of resolving Object.prototype members.
60
+ const customGroups: Record<string, DataEmojis> = Object.create(null);
58
61
 
59
62
  if (customEmojis && customEmojis.length > 0) {
60
63
  for (const emoji of customEmojis) {
@@ -75,8 +78,10 @@ export function PickerDataProvider({
75
78
  const allEmojis: DataEmojis = Object.values(emojis)
76
79
  .concat(Object.values(customGroups))
77
80
  .flat();
78
- const allEmojisByUnified: Record<string, DataEmoji> = {};
79
- const searchIndex: Record<string, Record<string, DataEmoji>> = {};
81
+ const allEmojisByUnified: Record<string, DataEmoji> =
82
+ Object.create(null);
83
+ const searchIndex: Record<string, Record<string, DataEmoji>> =
84
+ Object.create(null);
80
85
 
81
86
  allEmojis.forEach((emoji) => {
82
87
  const unified = emoji[Keys.unified];
@@ -96,7 +101,7 @@ export function PickerDataProvider({
96
101
  .split('');
97
102
 
98
103
  joinedNameString.forEach((char: string) => {
99
- searchIndex[char] = searchIndex[char] ?? {};
104
+ searchIndex[char] = searchIndex[char] ?? Object.create(null);
100
105
  searchIndex[char][unified] = emoji;
101
106
  });
102
107
  });
@@ -28,7 +28,6 @@ export function CategoryNavigation() {
28
28
  const [activeCategory, setActiveCategory] = useState<string | null>(null);
29
29
  const [, setVisibleCategories] = useVisibleCategoriesState();
30
30
  const scrollCategoryIntoView = useScrollCategoryIntoView();
31
- useActiveCategoryScrollDetection({ setActiveCategory, setVisibleCategories });
32
31
  const isSearchMode = useIsSearchMode();
33
32
 
34
33
  const categoriesConfig = useCategoriesConfig();
@@ -37,6 +36,18 @@ export function CategoryNavigation() {
37
36
  const hideCustomCategory = useShouldHideCustomEmojis();
38
37
  const { customGroups, emojiData } = usePickerDataContext();
39
38
 
39
+ // Rendered section identities. Passed to the scroll-detection hook so
40
+ // added/removed sections are (re-)subscribed; stable across renders
41
+ // unless the effective category list changes.
42
+ const categoryIdentitiesKey = categoriesConfig
43
+ .map(entry => categoryIdFromCategoryConfig(entry))
44
+ .join('|');
45
+ useActiveCategoryScrollDetection({
46
+ setActiveCategory,
47
+ setVisibleCategories,
48
+ categoryIdentitiesKey,
49
+ });
50
+
40
51
  const visibleCategories = categoriesConfig.filter(categoryConfig => {
41
52
  if (isCustomCategory(categoryConfig) && hideCustomCategory) {
42
53
  return false;
@@ -1,4 +1,6 @@
1
+ import { UserCategoryConfig } from './categoryConfig';
1
2
  import { PickerConfig } from './config';
3
+ import { CustomEmoji } from './customEmojiConfig';
2
4
 
3
5
  // eslint-disable-next-line complexity
4
6
  export function compareConfig(prev: PickerConfig, next: PickerConfig) {
@@ -25,6 +27,35 @@ export function compareConfig(prev: PickerConfig, next: PickerConfig) {
25
27
  prev.searchDisabled === next.searchDisabled &&
26
28
  prev.skinTonePickerLocation === next.skinTonePickerLocation &&
27
29
  prevCustomEmojis.length === nextCustomEmojis.length &&
30
+ customEmojisKey(prevCustomEmojis) === customEmojisKey(nextCustomEmojis) &&
31
+ categoriesKey(prev.categories) === categoriesKey(next.categories) &&
28
32
  prev.emojiData === next.emojiData
29
33
  );
30
34
  }
35
+
36
+ /**
37
+ * Content identity for custom emojis. Length alone misses regroups and
38
+ * swaps, which would leave merged categories, search, and sections stale.
39
+ */
40
+ function customEmojisKey(customEmojis: CustomEmoji[]): string {
41
+ return customEmojis
42
+ .map(
43
+ emoji =>
44
+ `${emoji.id}:${emoji.group ?? ''}:${emoji.names.join(',')}:${emoji.imgUrl}`,
45
+ )
46
+ .join('|');
47
+ }
48
+
49
+ /**
50
+ * Content identity for the categories input. Order, labels, icons, and
51
+ * group selection all flow into the merged configuration.
52
+ */
53
+ function categoriesKey(categories: UserCategoryConfig | undefined): string {
54
+ return (categories ?? [])
55
+ .map(entry =>
56
+ typeof entry === 'string'
57
+ ? entry
58
+ : `${entry.category}:${entry.name}:${entry.group ?? ''}:${entry.icon ? '1' : ''}`,
59
+ )
60
+ .join('|');
61
+ }
@@ -7,9 +7,17 @@ import { useBodyRef } from '../components/context/ElementRefContext';
7
7
  export function useActiveCategoryScrollDetection({
8
8
  setActiveCategory,
9
9
  setVisibleCategories,
10
+ categoryIdentitiesKey,
10
11
  }: {
11
12
  setActiveCategory: (category: string) => void;
12
13
  setVisibleCategories: (categories: string[]) => void;
14
+ /**
15
+ * Stable string derived from the rendered category identities
16
+ * (e.g. ids joined). Re-subscribes the observer when sections are
17
+ * added or removed so new elements are observed and detached ones
18
+ * stop contributing; unrelated renders keep the same key.
19
+ */
20
+ categoryIdentitiesKey: string;
13
21
  }) {
14
22
  const BodyRef = useBodyRef();
15
23
 
@@ -67,5 +75,5 @@ export function useActiveCategoryScrollDetection({
67
75
  return () => {
68
76
  observer.disconnect();
69
77
  };
70
- }, [BodyRef, setActiveCategory, setVisibleCategories]);
78
+ }, [BodyRef, categoryIdentitiesKey, setActiveCategory, setVisibleCategories]);
71
79
  }