emoji-picker-react 4.22.1 → 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,11 +1,11 @@
1
- export declare function useActiveCategoryScrollDetection({ setActiveCategory, setVisibleCategories, categoryIdentitiesKey, }: {
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;
4
5
  /**
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.
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
9
  */
10
- categoryIdentitiesKey: string;
10
+ categories: CategoriesConfig;
11
11
  }): void;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.22.1",
2
+ "version": "4.22.2",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -36,16 +36,12 @@ export function CategoryNavigation() {
36
36
  const hideCustomCategory = useShouldHideCustomEmojis();
37
37
  const { customGroups, emojiData } = usePickerDataContext();
38
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('|');
39
+ // The observer re-subscribes when the merged categories reference
40
+ // changes (sections added/removed); the reference is stable otherwise.
45
41
  useActiveCategoryScrollDetection({
46
42
  setActiveCategory,
47
43
  setVisibleCategories,
48
- categoryIdentitiesKey,
44
+ categories: categoriesConfig,
49
45
  });
50
46
 
51
47
  const visibleCategories = categoriesConfig.filter(categoryConfig => {
@@ -1,3 +1,5 @@
1
+ import { Categories, CategoryConfig } from '../types/exposedTypes';
2
+
1
3
  import { UserCategoryConfig } from './categoryConfig';
2
4
  import { PickerConfig } from './config';
3
5
  import { CustomEmoji } from './customEmojiConfig';
@@ -27,35 +29,74 @@ export function compareConfig(prev: PickerConfig, next: PickerConfig) {
27
29
  prev.searchDisabled === next.searchDisabled &&
28
30
  prev.skinTonePickerLocation === next.skinTonePickerLocation &&
29
31
  prevCustomEmojis.length === nextCustomEmojis.length &&
30
- customEmojisKey(prevCustomEmojis) === customEmojisKey(nextCustomEmojis) &&
31
- categoriesKey(prev.categories) === categoriesKey(next.categories) &&
32
+ customEmojisEqual(prevCustomEmojis, nextCustomEmojis) &&
33
+ categoriesEqual(prev.categories, next.categories) &&
32
34
  prev.emojiData === next.emojiData
33
35
  );
34
36
  }
35
37
 
36
38
  /**
37
- * Content identity for custom emojis. Length alone misses regroups and
38
- * swaps, which would leave merged categories, search, and sections stale.
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.
39
68
  */
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('|');
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
+ );
47
81
  }
48
82
 
49
83
  /**
50
- * Content identity for the categories input. Order, labels, icons, and
51
- * group selection all flow into the merged configuration.
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.
52
88
  */
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('|');
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
+ );
61
102
  }
@@ -3,21 +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,
10
- categoryIdentitiesKey,
11
+ categories,
11
12
  }: {
12
13
  setActiveCategory: (category: string) => void;
13
14
  setVisibleCategories: (categories: string[]) => void;
14
15
  /**
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.
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
19
  */
20
- categoryIdentitiesKey: string;
20
+ categories: CategoriesConfig;
21
21
  }) {
22
22
  const BodyRef = useBodyRef();
23
23
 
@@ -75,5 +75,5 @@ export function useActiveCategoryScrollDetection({
75
75
  return () => {
76
76
  observer.disconnect();
77
77
  };
78
- }, [BodyRef, categoryIdentitiesKey, setActiveCategory, setVisibleCategories]);
78
+ }, [BodyRef, categories, setActiveCategory, setVisibleCategories]);
79
79
  }