emoji-picker-react 4.22.0 → 4.22.2

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
+ import { CategoriesConfig } from '../config/categoryConfig';
2
+ export declare function useActiveCategoryScrollDetection({ setActiveCategory, setVisibleCategories, categories, }: {
2
3
  setActiveCategory: (category: string) => void;
3
4
  setVisibleCategories: (categories: string[]) => void;
5
+ /**
6
+ * Effective category list. The merged array reference only changes when
7
+ * the configuration is re-merged, so the observer re-subscribes exactly
8
+ * when sections are added or removed — no serialization involved.
9
+ */
10
+ categories: CategoriesConfig;
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.2",
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,14 @@ export function CategoryNavigation() {
37
36
  const hideCustomCategory = useShouldHideCustomEmojis();
38
37
  const { customGroups, emojiData } = usePickerDataContext();
39
38
 
39
+ // The observer re-subscribes when the merged categories reference
40
+ // changes (sections added/removed); the reference is stable otherwise.
41
+ useActiveCategoryScrollDetection({
42
+ setActiveCategory,
43
+ setVisibleCategories,
44
+ categories: categoriesConfig,
45
+ });
46
+
40
47
  const visibleCategories = categoriesConfig.filter(categoryConfig => {
41
48
  if (isCustomCategory(categoryConfig) && hideCustomCategory) {
42
49
  return false;
@@ -1,4 +1,8 @@
1
+ import { Categories, CategoryConfig } from '../types/exposedTypes';
2
+
3
+ import { UserCategoryConfig } from './categoryConfig';
1
4
  import { PickerConfig } from './config';
5
+ import { CustomEmoji } from './customEmojiConfig';
2
6
 
3
7
  // eslint-disable-next-line complexity
4
8
  export function compareConfig(prev: PickerConfig, next: PickerConfig) {
@@ -25,6 +29,74 @@ export function compareConfig(prev: PickerConfig, next: PickerConfig) {
25
29
  prev.searchDisabled === next.searchDisabled &&
26
30
  prev.skinTonePickerLocation === next.skinTonePickerLocation &&
27
31
  prevCustomEmojis.length === nextCustomEmojis.length &&
32
+ customEmojisEqual(prevCustomEmojis, nextCustomEmojis) &&
33
+ categoriesEqual(prev.categories, next.categories) &&
28
34
  prev.emojiData === next.emojiData
29
35
  );
30
36
  }
37
+
38
+ /**
39
+ * Structural content equality for custom emojis. Length alone misses
40
+ * regroups and swaps, which would leave merged categories, search, and
41
+ * sections stale. Fields are compared element-wise (never serialized
42
+ * with delimiters) so user-controlled values cannot collide.
43
+ */
44
+ function customEmojisEqual(
45
+ prev: CustomEmoji[],
46
+ next: CustomEmoji[],
47
+ ): boolean {
48
+ if (prev.length !== next.length) {
49
+ return false;
50
+ }
51
+ return prev.every((emoji, index) => {
52
+ const other = next[index];
53
+ return (
54
+ emoji.id === other.id &&
55
+ (emoji.group ?? '') === (other.group ?? '') &&
56
+ emoji.imgUrl === other.imgUrl &&
57
+ emoji.names.length === other.names.length &&
58
+ emoji.names.every((name, nameIndex) => name === other.names[nameIndex])
59
+ );
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Structural content equality for the categories input. Order, labels,
65
+ * group selection, and icon identity all flow into the merged
66
+ * configuration. Icons compare by reference: swapping one icon element
67
+ * for another must rebuild, while a stable reference stays cheap.
68
+ */
69
+ function categoriesEqual(
70
+ prev: UserCategoryConfig | undefined,
71
+ next: UserCategoryConfig | undefined,
72
+ ): boolean {
73
+ const prevEntries = prev ?? [];
74
+ const nextEntries = next ?? [];
75
+ if (prevEntries.length !== nextEntries.length) {
76
+ return false;
77
+ }
78
+ return prevEntries.every((entry, index) =>
79
+ categoryEntriesEqual(entry, nextEntries[index]),
80
+ );
81
+ }
82
+
83
+ /**
84
+ * One categories entry compared structurally. Icons compare by reference:
85
+ * swapping one icon element for another rebuilds, a stable reference stays
86
+ * cheap. Delimited serialization is avoided so user-controlled values
87
+ * cannot collide.
88
+ */
89
+ function categoryEntriesEqual(
90
+ entry: Categories | CategoryConfig,
91
+ other: Categories | CategoryConfig,
92
+ ): boolean {
93
+ if (typeof entry === 'string' || typeof other === 'string') {
94
+ return entry === other;
95
+ }
96
+ return (
97
+ entry.category === other.category &&
98
+ entry.name === other.name &&
99
+ (entry.group ?? '') === (other.group ?? '') &&
100
+ entry.icon === other.icon
101
+ );
102
+ }
@@ -3,13 +3,21 @@ import { useEffect } from 'react';
3
3
  import { categoryNameFromDom } from '../DomUtils/categoryNameFromDom';
4
4
  import { asSelectors, ClassNames } from '../DomUtils/classNames';
5
5
  import { useBodyRef } from '../components/context/ElementRefContext';
6
+ import { CategoriesConfig } from '../config/categoryConfig';
6
7
 
7
8
  export function useActiveCategoryScrollDetection({
8
9
  setActiveCategory,
9
10
  setVisibleCategories,
11
+ categories,
10
12
  }: {
11
13
  setActiveCategory: (category: string) => void;
12
14
  setVisibleCategories: (categories: string[]) => void;
15
+ /**
16
+ * Effective category list. The merged array reference only changes when
17
+ * the configuration is re-merged, so the observer re-subscribes exactly
18
+ * when sections are added or removed — no serialization involved.
19
+ */
20
+ categories: CategoriesConfig;
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, categories, setActiveCategory, setVisibleCategories]);
71
79
  }