emoji-picker-react 4.21.1 → 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.
@@ -6,6 +6,7 @@ export interface PickerDataContextValue {
6
6
  allEmojis: DataEmojis;
7
7
  allEmojisByUnified: Record<string, DataEmoji>;
8
8
  searchIndex: Record<string, Record<string, DataEmoji>>;
9
+ customGroups: Record<string, DataEmojis>;
9
10
  emojiByUnified: (unified?: string) => DataEmoji | undefined;
10
11
  activeVariationFromUnified: (unified: string) => SkinTones | null;
11
12
  }
@@ -13,4 +14,4 @@ export declare function PickerDataProvider({ children, }: {
13
14
  children: React.ReactNode;
14
15
  }): JSX.Element;
15
16
  export declare function usePickerDataContext(): PickerDataContextValue;
16
- export declare function useGetEmojisByCategory(): (category: Categories) => DataEmojis;
17
+ export declare function useGetEmojisByCategory(): (category: Categories, group?: string | undefined) => DataEmojis;
@@ -1,16 +1,34 @@
1
1
  import { Categories, CategoryConfig, EmojiData, SuggestionMode } from '../types/exposedTypes';
2
+ import { CustomEmoji } from './customEmojiConfig';
2
3
  export { Categories };
3
4
  export declare const SuggestedRecent: CategoryConfig;
4
5
  export declare type CustomCategoryConfig = {
5
6
  category: Categories.CUSTOM;
6
7
  name: string;
8
+ /**
9
+ * Selects the `CustomEmoji` group rendered in this section. Each entry
10
+ * may carry its own `name` and `icon`, so multiple custom sections can
11
+ * coexist — each placed wherever its entry sits in `categories`.
12
+ * Entries without a group share the ungrouped customs bucket.
13
+ */
14
+ group?: string;
7
15
  };
16
+ /**
17
+ * Stable identity for a category entry. Custom groups are namespaced so
18
+ * duplicate `{ category: CUSTOM }` configs no longer collide in React
19
+ * keys, scroll targets, and active-tab detection.
20
+ */
21
+ export declare function categoryIdFromCategoryConfig(categoryConfig: CategoryConfig): string;
22
+ /**
23
+ * The `CustomEmoji` group rendered by a category entry, if any.
24
+ */
25
+ export declare function customGroupFromCategoryConfig(categoryConfig: CategoryConfig): string | undefined;
8
26
  export declare function baseCategoriesConfig(modifiers?: Record<Categories, CategoryConfig>): CategoriesConfig;
9
27
  export declare function categoryFromCategoryConfig(category: CategoryConfig): Categories;
10
28
  export declare function categoryNameFromCategoryConfig(category: CategoryConfig): string;
11
29
  export declare type CategoriesConfig = CategoryConfig[];
12
30
  export declare type UserCategoryConfig = Array<Categories | CategoryConfig>;
13
- export declare function mergeCategoriesConfig(userCategoriesConfig?: UserCategoryConfig, modifiers?: CategoryConfigModifiers, emojiData?: EmojiData): CategoriesConfig;
31
+ export declare function mergeCategoriesConfig(userCategoriesConfig?: UserCategoryConfig, modifiers?: CategoryConfigModifiers, emojiData?: EmojiData, customEmojis?: CustomEmoji[]): CategoriesConfig;
14
32
  declare type CategoryConfigModifiers = {
15
33
  suggestionMode?: SuggestionMode;
16
34
  };
@@ -3,4 +3,12 @@ export declare type CustomEmoji = {
3
3
  names: string[];
4
4
  [EmojiProperties.imgUrl]: string;
5
5
  id: string;
6
+ /**
7
+ * Optional group name. Customs sharing a group render as their own
8
+ * category section; ungrouped customs keep the single shared bucket.
9
+ * Reference the group from a `{ category: Categories.CUSTOM, group }`
10
+ * entry in the `categories` prop to place and name the section.
11
+ * https://github.com/ealush/emoji-picker-react/issues/510
12
+ */
13
+ group?: string;
6
14
  };
@@ -179,7 +179,27 @@ function compareConfig(prev, next) {
179
179
  var _prev$customEmojis, _next$customEmojis;
180
180
  var prevCustomEmojis = (_prev$customEmojis = prev.customEmojis) != null ? _prev$customEmojis : [];
181
181
  var nextCustomEmojis = (_next$customEmojis = next.customEmojis) != null ? _next$customEmojis : [];
182
- return prev.open === next.open && prev.emojiVersion === next.emojiVersion && prev.reactionsDefaultOpen === next.reactionsDefaultOpen && prev.searchPlaceHolder === next.searchPlaceHolder && prev.searchPlaceholder === next.searchPlaceholder && prev.searchClearButtonLabel === next.searchClearButtonLabel && prev.defaultSkinTone === next.defaultSkinTone && prev.skinTonesDisabled === next.skinTonesDisabled && prev.autoFocusSearch === next.autoFocusSearch && prev.emojiStyle === next.emojiStyle && prev.theme === next.theme && prev.suggestedEmojisMode === next.suggestedEmojisMode && prev.lazyLoadEmojis === next.lazyLoadEmojis && prev.className === next.className && prev.height === next.height && prev.width === next.width && prev.style === next.style && prev.searchDisabled === next.searchDisabled && prev.skinTonePickerLocation === next.skinTonePickerLocation && prevCustomEmojis.length === nextCustomEmojis.length && prev.emojiData === next.emojiData;
182
+ return prev.open === next.open && prev.emojiVersion === next.emojiVersion && prev.reactionsDefaultOpen === next.reactionsDefaultOpen && prev.searchPlaceHolder === next.searchPlaceHolder && prev.searchPlaceholder === next.searchPlaceholder && prev.searchClearButtonLabel === next.searchClearButtonLabel && prev.defaultSkinTone === next.defaultSkinTone && prev.skinTonesDisabled === next.skinTonesDisabled && prev.autoFocusSearch === next.autoFocusSearch && prev.emojiStyle === next.emojiStyle && prev.theme === next.theme && prev.suggestedEmojisMode === next.suggestedEmojisMode && prev.lazyLoadEmojis === next.lazyLoadEmojis && prev.className === next.className && prev.height === next.height && prev.width === next.width && prev.style === next.style && prev.searchDisabled === next.searchDisabled && prev.skinTonePickerLocation === next.skinTonePickerLocation && prevCustomEmojis.length === nextCustomEmojis.length && customEmojisKey(prevCustomEmojis) === customEmojisKey(nextCustomEmojis) && categoriesKey(prev.categories) === categoriesKey(next.categories) && prev.emojiData === next.emojiData;
183
+ }
184
+ /**
185
+ * Content identity for custom emojis. Length alone misses regroups and
186
+ * swaps, which would leave merged categories, search, and sections stale.
187
+ */
188
+ function customEmojisKey(customEmojis) {
189
+ return customEmojis.map(function (emoji) {
190
+ var _emoji$group;
191
+ return emoji.id + ":" + ((_emoji$group = emoji.group) != null ? _emoji$group : '') + ":" + emoji.names.join(',') + ":" + emoji.imgUrl;
192
+ }).join('|');
193
+ }
194
+ /**
195
+ * Content identity for the categories input. Order, labels, icons, and
196
+ * group selection all flow into the merged configuration.
197
+ */
198
+ function categoriesKey(categories) {
199
+ return (categories != null ? categories : []).map(function (entry) {
200
+ var _entry$group;
201
+ return typeof entry === 'string' ? entry : entry.category + ":" + entry.name + ":" + ((_entry$group = entry.group) != null ? _entry$group : '') + ":" + (entry.icon ? '1' : '');
202
+ }).join('|');
183
203
  }
184
204
 
185
205
  var DEFAULT_REACTIONS = ['1f44d', '2764-fe0f', '1f603', '1f622', '1f64f', '1f44e', '1f621'];
@@ -322,6 +342,34 @@ var SuggestedRecent = {
322
342
  name: 'Recently Used',
323
343
  category: exports.Categories.SUGGESTED
324
344
  };
345
+ /**
346
+ * Stable identity for a category entry. Custom groups are namespaced so
347
+ * duplicate `{ category: CUSTOM }` configs no longer collide in React
348
+ * keys, scroll targets, and active-tab detection.
349
+ */
350
+ function categoryIdFromCategoryConfig(categoryConfig) {
351
+ var category = categoryFromCategoryConfig(categoryConfig);
352
+ if (isCustomGroupConfig(categoryConfig)) {
353
+ return category + ":" + categoryConfig.group;
354
+ }
355
+ return category;
356
+ }
357
+ function isCustomGroupConfig(categoryConfig) {
358
+ if (categoryConfig.category !== exports.Categories.CUSTOM) {
359
+ return false;
360
+ }
361
+ var group = categoryConfig.group;
362
+ return typeof group === 'string' && group.length > 0;
363
+ }
364
+ /**
365
+ * The `CustomEmoji` group rendered by a category entry, if any.
366
+ */
367
+ function customGroupFromCategoryConfig(categoryConfig) {
368
+ if (isCustomGroupConfig(categoryConfig)) {
369
+ return categoryConfig.group;
370
+ }
371
+ return undefined;
372
+ }
325
373
  var configByCategory = (_configByCategory = {}, _configByCategory[exports.Categories.SUGGESTED] = {
326
374
  category: exports.Categories.SUGGESTED,
327
375
  name: 'Frequently Used'
@@ -364,14 +412,17 @@ function categoryFromCategoryConfig(category) {
364
412
  function categoryNameFromCategoryConfig(category) {
365
413
  return category.name;
366
414
  }
367
- function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData) {
368
- var _userCategoriesConfig;
415
+ function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData, customEmojis) {
416
+ var _userCategoriesConfig, _userCategoriesConfig2;
369
417
  if (userCategoriesConfig === void 0) {
370
418
  userCategoriesConfig = [];
371
419
  }
372
420
  if (modifiers === void 0) {
373
421
  modifiers = {};
374
422
  }
423
+ if (customEmojis === void 0) {
424
+ customEmojis = [];
425
+ }
375
426
  var extra = function () {
376
427
  // 1. Start with localized categories from emojiData
377
428
  var fromData = emojiData != null && emojiData.categories ? Object.fromEntries(Object.entries(emojiData.categories).filter(function (_ref) {
@@ -390,15 +441,50 @@ function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData) {
390
441
  return fromData;
391
442
  }();
392
443
  var base = baseCategoriesConfig(extra);
393
- if (!((_userCategoriesConfig = userCategoriesConfig) != null && _userCategoriesConfig.length)) {
394
- return base;
395
- }
396
- return userCategoriesConfig.map(function (category) {
444
+ var explicit = (_userCategoriesConfig = userCategoriesConfig) != null && _userCategoriesConfig.length ? userCategoriesConfig.map(mapUserEntry) : base;
445
+ // Groups without a `categories` entry get an appended section so their
446
+ // emojis always render somewhere — unless the user opted out of customs
447
+ // entirely by listing `categories` without CUSTOM. `categories` remains
448
+ // the override for order, display name, and icon.
449
+ // https://github.com/ealush/emoji-picker-react/issues/510
450
+ var customInPlay = !((_userCategoriesConfig2 = userCategoriesConfig) != null && _userCategoriesConfig2.length) || userCategoriesConfig.some(function (entry) {
451
+ return entry === exports.Categories.CUSTOM || typeof entry !== 'string' && entry.category === exports.Categories.CUSTOM;
452
+ });
453
+ var listedGroups = new Set(explicit.map(function (entry) {
454
+ return customGroupFromCategoryConfig(entry);
455
+ }).filter(function (group) {
456
+ return !!group;
457
+ }));
458
+ var appended = !customInPlay ? [] : Array.from(new Set(customEmojis.map(function (emoji) {
459
+ return emoji.group;
460
+ }).filter(function (group) {
461
+ return !!group;
462
+ }))).filter(function (group) {
463
+ return !listedGroups.has(group);
464
+ }).map(function (group) {
465
+ return {
466
+ category: exports.Categories.CUSTOM,
467
+ name: group,
468
+ group: group
469
+ };
470
+ });
471
+ // Collapse duplicate entries (same identity twice renders the same
472
+ // section twice under one React key); first occurrence wins.
473
+ var seen = new Set();
474
+ return explicit.concat(appended).filter(function (entry) {
475
+ var id = categoryIdFromCategoryConfig(entry);
476
+ if (seen.has(id)) {
477
+ return false;
478
+ }
479
+ seen.add(id);
480
+ return true;
481
+ });
482
+ function mapUserEntry(category) {
397
483
  if (typeof category === 'string') {
398
484
  return getBaseConfigByCategory(category, extra[category]);
399
485
  }
400
486
  return _extends({}, getBaseConfigByCategory(category.category, extra[category.category]), category);
401
- });
487
+ }
402
488
  }
403
489
  function getBaseConfigByCategory(category, modifier) {
404
490
  if (modifier === void 0) {
@@ -429,7 +515,7 @@ function mergeConfig(userConfig) {
429
515
  var config = Object.assign(base, userConfig);
430
516
  var categories = mergeCategoriesConfig(userConfig.categories, {
431
517
  suggestionMode: config.suggestedEmojisMode
432
- }, userConfig.emojiData);
518
+ }, userConfig.emojiData, userConfig.customEmojis);
433
519
  config.hiddenEmojis.forEach(function (emoji) {
434
520
  config.unicodeToHide.add(emoji);
435
521
  });
@@ -491,7 +577,6 @@ function PickerConfigProvider(_ref) {
491
577
  }, children);
492
578
  }
493
579
  function useSetConfig(config) {
494
- var _config$customEmojis;
495
580
  var _React$useState = React.useState(function () {
496
581
  return mergeConfig(config);
497
582
  }),
@@ -504,7 +589,11 @@ function useSetConfig(config) {
504
589
  setMergedConfig(mergeConfig(config));
505
590
  // not gonna...
506
591
  // eslint-disable-next-line react-hooks/exhaustive-deps
507
- }, [(_config$customEmojis = config.customEmojis) == null ? void 0 : _config$customEmojis.length, config.open, config.emojiVersion, config.reactionsDefaultOpen, config.searchPlaceHolder, config.searchPlaceholder, config.searchClearButtonLabel, config.defaultSkinTone, config.skinTonesDisabled, config.autoFocusSearch, config.emojiStyle, config.theme, config.suggestedEmojisMode, config.lazyLoadEmojis, config.className, config.height, config.width, config.searchDisabled, config.skinTonePickerLocation, config.allowExpandReactions, config.emojiData]);
592
+ }, [
593
+ // New references rebuild merged categories and data even at equal
594
+ // length; compareConfig above still gates no-op updates so inline
595
+ // literals don't loop.
596
+ config.categories, config.customEmojis, config.open, config.emojiVersion, config.reactionsDefaultOpen, config.searchPlaceHolder, config.searchPlaceholder, config.searchClearButtonLabel, config.defaultSkinTone, config.skinTonesDisabled, config.autoFocusSearch, config.emojiStyle, config.theme, config.suggestedEmojisMode, config.lazyLoadEmojis, config.className, config.height, config.width, config.searchDisabled, config.skinTonePickerLocation, config.allowExpandReactions, config.emojiData]);
508
597
  return mergedConfig;
509
598
  }
510
599
  function usePickerConfig() {
@@ -26861,8 +26950,9 @@ function useIsMounted() {
26861
26950
  var PickerDataContext = /*#__PURE__*/React__default.createContext({
26862
26951
  emojiData: {},
26863
26952
  allEmojis: [],
26864
- allEmojisByUnified: {},
26865
- searchIndex: {},
26953
+ allEmojisByUnified: /*#__PURE__*/Object.create(null),
26954
+ searchIndex: /*#__PURE__*/Object.create(null),
26955
+ customGroups: /*#__PURE__*/Object.create(null),
26866
26956
  emojiByUnified: emojiByUnified,
26867
26957
  activeVariationFromUnified: function activeVariationFromUnified() {
26868
26958
  return null;
@@ -26877,13 +26967,28 @@ function PickerDataProvider(_ref) {
26877
26967
  var emojiData = genericEmojiData || defaultEmojiData;
26878
26968
  // Clone to avoid mutation of shared source
26879
26969
  var newData = JSON.parse(JSON.stringify(emojiData));
26970
+ // Group names and emoji ids are user-controlled strings: use
26971
+ // null-prototype dictionaries so names like `__proto__` behave as
26972
+ // ordinary data instead of resolving Object.prototype members.
26973
+ var customGroups = Object.create(null);
26880
26974
  if (customEmojis && customEmojis.length > 0) {
26881
- newData.emojis[exports.Categories.CUSTOM] = customEmojis.map(customToRegularEmoji);
26975
+ for (var _iterator = _createForOfIteratorHelperLoose(customEmojis), _step; !(_step = _iterator()).done;) {
26976
+ var _customGroups$emoji$g;
26977
+ var emoji = _step.value;
26978
+ if (!emoji.group) {
26979
+ continue;
26980
+ }
26981
+ customGroups[emoji.group] = (_customGroups$emoji$g = customGroups[emoji.group]) != null ? _customGroups$emoji$g : [];
26982
+ customGroups[emoji.group].push(customToRegularEmoji(emoji));
26983
+ }
26984
+ newData.emojis[exports.Categories.CUSTOM] = customEmojis.filter(function (emoji) {
26985
+ return !emoji.group;
26986
+ }).map(customToRegularEmoji);
26882
26987
  }
26883
26988
  var emojis = newData.emojis || {};
26884
- var allEmojis = Object.values(emojis).flat();
26885
- var allEmojisByUnified = {};
26886
- var searchIndex = {};
26989
+ var allEmojis = Object.values(emojis).concat(Object.values(customGroups)).flat();
26990
+ var allEmojisByUnified = Object.create(null);
26991
+ var searchIndex = Object.create(null);
26887
26992
  allEmojis.forEach(function (emoji) {
26888
26993
  var unified = emoji[EmojiProperties.unified];
26889
26994
  allEmojisByUnified[unified] = emoji;
@@ -26898,7 +27003,7 @@ function PickerDataProvider(_ref) {
26898
27003
  var joinedNameString = (emoji[EmojiProperties.name] || []).join('').toLowerCase().split('');
26899
27004
  joinedNameString.forEach(function (_char) {
26900
27005
  var _searchIndex$_char;
26901
- searchIndex[_char] = (_searchIndex$_char = searchIndex[_char]) != null ? _searchIndex$_char : {};
27006
+ searchIndex[_char] = (_searchIndex$_char = searchIndex[_char]) != null ? _searchIndex$_char : Object.create(null);
26902
27007
  searchIndex[_char][unified] = emoji;
26903
27008
  });
26904
27009
  });
@@ -26906,7 +27011,8 @@ function PickerDataProvider(_ref) {
26906
27011
  emojiData: newData,
26907
27012
  allEmojis: allEmojis,
26908
27013
  allEmojisByUnified: allEmojisByUnified,
26909
- searchIndex: searchIndex
27014
+ searchIndex: searchIndex,
27015
+ customGroups: customGroups
26910
27016
  };
26911
27017
  }, [genericEmojiData, customEmojis]);
26912
27018
  var emojiByUnified = React__default.useCallback(function (unified) {
@@ -26928,7 +27034,8 @@ function usePickerDataContext() {
26928
27034
  function useGetEmojisByCategory() {
26929
27035
  var _usePickerDataContext = usePickerDataContext(),
26930
27036
  emojiData = _usePickerDataContext.emojiData,
26931
- emojiByUnified = _usePickerDataContext.emojiByUnified;
27037
+ emojiByUnified = _usePickerDataContext.emojiByUnified,
27038
+ customGroups = _usePickerDataContext.customGroups;
26932
27039
  var suggestedEmojisModeConfig = useSuggestedEmojisModeConfig();
26933
27040
  var _useUpdateSuggested = useUpdateSuggested(),
26934
27041
  suggestedUpdated = _useUpdateSuggested[0];
@@ -26949,11 +27056,15 @@ function useGetEmojisByCategory() {
26949
27056
  }).filter(Boolean);
26950
27057
  // eslint-disable-next-line react-hooks/exhaustive-deps
26951
27058
  }, [isMounted, suggestedUpdated, suggestedEmojisModeConfig, emojiByUnified]);
26952
- return function getEmojisByCategory(category) {
27059
+ return function getEmojisByCategory(category, group) {
26953
27060
  var _emojiData$emojis$cat, _emojiData$emojis;
26954
27061
  if (category === exports.Categories.SUGGESTED) {
26955
27062
  return suggested;
26956
27063
  }
27064
+ if (category === exports.Categories.CUSTOM && group) {
27065
+ var _customGroups$group;
27066
+ return (_customGroups$group = customGroups[group]) != null ? _customGroups$group : [];
27067
+ }
26957
27068
  return (_emojiData$emojis$cat = (_emojiData$emojis = emojiData.emojis) == null ? void 0 : _emojiData$emojis[category]) != null ? _emojiData$emojis$cat : [];
26958
27069
  };
26959
27070
  }
@@ -29587,11 +29698,10 @@ function EmojiCategory(_ref) {
29587
29698
  hiddenOnSearch = _ref.hiddenOnSearch,
29588
29699
  height = _ref.height,
29589
29700
  emojisPerRow = _ref.emojisPerRow;
29590
- var category = categoryFromCategoryConfig(categoryConfig);
29591
29701
  var categoryName = categoryNameFromCategoryConfig(categoryConfig);
29592
29702
  return React.createElement("li", {
29593
29703
  className: shipstyles.cx(styles$7.category, hidden && commonStyles.hidden, hiddenOnSearch && commonInteractionStyles.hiddenOnSearch),
29594
- "data-name": category,
29704
+ "data-name": categoryIdFromCategoryConfig(categoryConfig),
29595
29705
  role: "rowgroup",
29596
29706
  "aria-label": categoryName
29597
29707
  }, React.createElement("h2", {
@@ -29666,7 +29776,7 @@ function MeasureEmoji() {
29666
29776
  return null;
29667
29777
  }
29668
29778
  var firstCategory = categories[0];
29669
- var dummyEmoji = getEmojisByCategory(categoryFromCategoryConfig(firstCategory))[0];
29779
+ var dummyEmoji = firstCategory ? getEmojisByCategory(categoryFromCategoryConfig(firstCategory), customGroupFromCategoryConfig(firstCategory))[0] : undefined;
29670
29780
  var unified = dummyEmoji ? emojiUnified(dummyEmoji, activeSkinTone) : '';
29671
29781
  if (!dummyEmoji) {
29672
29782
  return null;
@@ -29720,22 +29830,24 @@ function EmojiList(_ref) {
29720
29830
  role: "grid"
29721
29831
  }, React.createElement(MeasureEmoji, null), categories.map(function (categoryConfig, index) {
29722
29832
  var category = categoryFromCategoryConfig(categoryConfig);
29833
+ var categoryId = categoryIdFromCategoryConfig(categoryConfig);
29834
+ var group = customGroupFromCategoryConfig(categoryConfig);
29723
29835
  var currentOffset = topOffset;
29724
- var categoryHeight = categoryHeights[category];
29836
+ var categoryHeight = categoryHeights[categoryId];
29725
29837
  if (categoryHeight) {
29726
29838
  topOffset += categoryHeight + labelHeight;
29727
29839
  }
29728
29840
  return React.createElement(RenderCategory, {
29729
- key: category,
29730
- categoryEmojis: getEmojisByCategory(category),
29841
+ key: categoryId,
29842
+ categoryEmojis: getEmojisByCategory(category, group),
29731
29843
  categoryConfig: categoryConfig,
29732
29844
  topOffset: currentOffset,
29733
29845
  isFirstCategory: index === 0,
29734
29846
  onHeightReady: function onHeightReady(height) {
29735
- if (categoryHeights[category] !== height) {
29847
+ if (categoryHeights[categoryId] !== height) {
29736
29848
  setCategoryHeights(function (prev) {
29737
29849
  var _extends2;
29738
- return _extends({}, prev, (_extends2 = {}, _extends2[category] = height, _extends2));
29850
+ return _extends({}, prev, (_extends2 = {}, _extends2[categoryId] = height, _extends2));
29739
29851
  });
29740
29852
  }
29741
29853
  },
@@ -29759,7 +29871,7 @@ function RenderCategory(_ref2) {
29759
29871
  // the deadlock; the observer takes over from there.
29760
29872
  // https://github.com/ealush/emoji-picker-react/issues/469
29761
29873
  // https://github.com/ealush/emoji-picker-react/issues/475
29762
- var isCategoryVisible = isFirstCategory || visibleCategories.includes(categoryConfig.category);
29874
+ var isCategoryVisible = isFirstCategory || visibleCategories.includes(categoryIdFromCategoryConfig(categoryConfig));
29763
29875
  var _useEmojiVirtualizati = useEmojiVirtualization({
29764
29876
  categoryEmojis: categoryEmojis,
29765
29877
  topOffset: topOffset,
@@ -30421,7 +30533,8 @@ function categoryNameFromDom($category) {
30421
30533
 
30422
30534
  function useActiveCategoryScrollDetection(_ref) {
30423
30535
  var setActiveCategory = _ref.setActiveCategory,
30424
- setVisibleCategories = _ref.setVisibleCategories;
30536
+ setVisibleCategories = _ref.setVisibleCategories,
30537
+ categoryIdentitiesKey = _ref.categoryIdentitiesKey;
30425
30538
  var BodyRef = useBodyRef();
30426
30539
  React.useEffect(function () {
30427
30540
  var visibleCategories = new Map();
@@ -30473,7 +30586,7 @@ function useActiveCategoryScrollDetection(_ref) {
30473
30586
  return function () {
30474
30587
  observer.disconnect();
30475
30588
  };
30476
- }, [BodyRef, setActiveCategory, setVisibleCategories]);
30589
+ }, [BodyRef, categoryIdentitiesKey, setActiveCategory, setVisibleCategories]);
30477
30590
  }
30478
30591
 
30479
30592
  function useScrollCategoryIntoView() {
@@ -30484,7 +30597,8 @@ function useScrollCategoryIntoView() {
30484
30597
  if (!BodyRef.current) {
30485
30598
  return;
30486
30599
  }
30487
- var $category = (_BodyRef$current = BodyRef.current) == null ? void 0 : _BodyRef$current.querySelector("[data-name=\"" + category + "\"]");
30600
+ // Group names are user-controlled; escape for the attribute selector.
30601
+ var $category = (_BodyRef$current = BodyRef.current) == null ? void 0 : _BodyRef$current.querySelector("[data-name=\"" + CSS.escape(category) + "\"]");
30488
30602
  if (!$category) {
30489
30603
  return;
30490
30604
  }
@@ -30858,17 +30972,40 @@ function CategoryNavigation() {
30858
30972
  var _useVisibleCategories = useVisibleCategoriesState(),
30859
30973
  setVisibleCategories = _useVisibleCategories[1];
30860
30974
  var scrollCategoryIntoView = useScrollCategoryIntoView();
30861
- useActiveCategoryScrollDetection({
30862
- setActiveCategory: setActiveCategory,
30863
- setVisibleCategories: setVisibleCategories
30864
- });
30865
30975
  var isSearchMode = useIsSearchMode();
30866
30976
  var categoriesConfig = useCategoriesConfig();
30867
30977
  var categoryIcons = useCategoryIconsConfig();
30868
30978
  var CategoryNavigationRef = useCategoryNavigationRef();
30869
30979
  var hideCustomCategory = useShouldHideCustomEmojis();
30980
+ var _usePickerDataContext = usePickerDataContext(),
30981
+ customGroups = _usePickerDataContext.customGroups,
30982
+ emojiData = _usePickerDataContext.emojiData;
30983
+ // Rendered section identities. Passed to the scroll-detection hook so
30984
+ // added/removed sections are (re-)subscribed; stable across renders
30985
+ // unless the effective category list changes.
30986
+ var categoryIdentitiesKey = categoriesConfig.map(function (entry) {
30987
+ return categoryIdFromCategoryConfig(entry);
30988
+ }).join('|');
30989
+ useActiveCategoryScrollDetection({
30990
+ setActiveCategory: setActiveCategory,
30991
+ setVisibleCategories: setVisibleCategories,
30992
+ categoryIdentitiesKey: categoryIdentitiesKey
30993
+ });
30870
30994
  var visibleCategories = categoriesConfig.filter(function (categoryConfig) {
30871
- return !(isCustomCategory(categoryConfig) && hideCustomCategory);
30995
+ if (isCustomCategory(categoryConfig) && hideCustomCategory) {
30996
+ return false;
30997
+ }
30998
+ // Tabs navigating to an empty (hidden) section are dead weight.
30999
+ var group = customGroupFromCategoryConfig(categoryConfig);
31000
+ if (group) {
31001
+ var _customGroups$group$l, _customGroups$group;
31002
+ return ((_customGroups$group$l = (_customGroups$group = customGroups[group]) == null ? void 0 : _customGroups$group.length) != null ? _customGroups$group$l : 0) > 0;
31003
+ }
31004
+ if (isCustomCategory(categoryConfig)) {
31005
+ var _emojiData$emojis$Cat, _emojiData$emojis, _emojiData$emojis$Cat2;
31006
+ return ((_emojiData$emojis$Cat = (_emojiData$emojis = emojiData.emojis) == null ? void 0 : (_emojiData$emojis$Cat2 = _emojiData$emojis[exports.Categories.CUSTOM]) == null ? void 0 : _emojiData$emojis$Cat2.length) != null ? _emojiData$emojis$Cat : 0) > 0;
31007
+ }
31008
+ return true;
30872
31009
  });
30873
31010
  // A single tab navigates nowhere — hide the bar to reclaim its space.
30874
31011
  // https://github.com/ealush/emoji-picker-react/issues/396
@@ -30883,19 +31020,20 @@ function CategoryNavigation() {
30883
31020
  ref: CategoryNavigationRef
30884
31021
  }, visibleCategories.map(function (categoryConfig) {
30885
31022
  var category = categoryFromCategoryConfig(categoryConfig);
30886
- var isActiveCategory = category === activeCategory;
31023
+ var categoryId = categoryIdFromCategoryConfig(categoryConfig);
31024
+ var isActiveCategory = categoryId === activeCategory;
30887
31025
  var allowNavigation = !isSearchMode && !isActiveCategory;
30888
31026
  return React.createElement(CategoryButton, {
30889
- key: category,
31027
+ key: categoryId,
30890
31028
  category: category,
30891
31029
  isActiveCategory: isActiveCategory,
30892
31030
  allowNavigation: allowNavigation,
30893
31031
  categoryConfig: categoryConfig,
30894
31032
  customIcon: categoryIcons[category],
30895
31033
  onClick: function onClick() {
30896
- scrollCategoryIntoView(category);
31034
+ scrollCategoryIntoView(categoryId);
30897
31035
  setTimeout(function () {
30898
- setActiveCategory(category);
31036
+ setActiveCategory(categoryId);
30899
31037
  }, 10);
30900
31038
  }
30901
31039
  });