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.
- package/dist/emoji-picker-react.cjs.development.js +73 -16
- package/dist/emoji-picker-react.cjs.development.js.map +1 -1
- package/dist/emoji-picker-react.cjs.production.min.js +1 -1
- package/dist/emoji-picker-react.cjs.production.min.js.map +1 -1
- package/dist/emoji-picker-react.esm.js +73 -16
- package/dist/emoji-picker-react.esm.js.map +1 -1
- package/dist/hooks/useActiveCategoryScrollDetection.d.ts +8 -1
- package/llms.txt +7 -0
- package/package.json +1 -1
- package/src/components/context/PickerConfigContext.tsx +5 -1
- package/src/components/context/PickerDataContext.tsx +12 -7
- package/src/components/navigation/CategoryNavigation.tsx +8 -1
- package/src/config/compareConfig.ts +72 -0
- package/src/hooks/useActiveCategoryScrollDetection.ts +9 -1
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|