emoji-picker-react 4.21.0 → 4.22.0

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.
@@ -321,6 +321,34 @@ var SuggestedRecent = {
321
321
  name: 'Recently Used',
322
322
  category: Categories.SUGGESTED
323
323
  };
324
+ /**
325
+ * Stable identity for a category entry. Custom groups are namespaced so
326
+ * duplicate `{ category: CUSTOM }` configs no longer collide in React
327
+ * keys, scroll targets, and active-tab detection.
328
+ */
329
+ function categoryIdFromCategoryConfig(categoryConfig) {
330
+ var category = categoryFromCategoryConfig(categoryConfig);
331
+ if (isCustomGroupConfig(categoryConfig)) {
332
+ return category + ":" + categoryConfig.group;
333
+ }
334
+ return category;
335
+ }
336
+ function isCustomGroupConfig(categoryConfig) {
337
+ if (categoryConfig.category !== Categories.CUSTOM) {
338
+ return false;
339
+ }
340
+ var group = categoryConfig.group;
341
+ return typeof group === 'string' && group.length > 0;
342
+ }
343
+ /**
344
+ * The `CustomEmoji` group rendered by a category entry, if any.
345
+ */
346
+ function customGroupFromCategoryConfig(categoryConfig) {
347
+ if (isCustomGroupConfig(categoryConfig)) {
348
+ return categoryConfig.group;
349
+ }
350
+ return undefined;
351
+ }
324
352
  var configByCategory = (_configByCategory = {}, _configByCategory[Categories.SUGGESTED] = {
325
353
  category: Categories.SUGGESTED,
326
354
  name: 'Frequently Used'
@@ -363,14 +391,17 @@ function categoryFromCategoryConfig(category) {
363
391
  function categoryNameFromCategoryConfig(category) {
364
392
  return category.name;
365
393
  }
366
- function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData) {
367
- var _userCategoriesConfig;
394
+ function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData, customEmojis) {
395
+ var _userCategoriesConfig, _userCategoriesConfig2;
368
396
  if (userCategoriesConfig === void 0) {
369
397
  userCategoriesConfig = [];
370
398
  }
371
399
  if (modifiers === void 0) {
372
400
  modifiers = {};
373
401
  }
402
+ if (customEmojis === void 0) {
403
+ customEmojis = [];
404
+ }
374
405
  var extra = function () {
375
406
  // 1. Start with localized categories from emojiData
376
407
  var fromData = emojiData != null && emojiData.categories ? Object.fromEntries(Object.entries(emojiData.categories).filter(function (_ref) {
@@ -389,15 +420,50 @@ function mergeCategoriesConfig(userCategoriesConfig, modifiers, emojiData) {
389
420
  return fromData;
390
421
  }();
391
422
  var base = baseCategoriesConfig(extra);
392
- if (!((_userCategoriesConfig = userCategoriesConfig) != null && _userCategoriesConfig.length)) {
393
- return base;
394
- }
395
- return userCategoriesConfig.map(function (category) {
423
+ var explicit = (_userCategoriesConfig = userCategoriesConfig) != null && _userCategoriesConfig.length ? userCategoriesConfig.map(mapUserEntry) : base;
424
+ // Groups without a `categories` entry get an appended section so their
425
+ // emojis always render somewhere — unless the user opted out of customs
426
+ // entirely by listing `categories` without CUSTOM. `categories` remains
427
+ // the override for order, display name, and icon.
428
+ // https://github.com/ealush/emoji-picker-react/issues/510
429
+ var customInPlay = !((_userCategoriesConfig2 = userCategoriesConfig) != null && _userCategoriesConfig2.length) || userCategoriesConfig.some(function (entry) {
430
+ return entry === Categories.CUSTOM || typeof entry !== 'string' && entry.category === Categories.CUSTOM;
431
+ });
432
+ var listedGroups = new Set(explicit.map(function (entry) {
433
+ return customGroupFromCategoryConfig(entry);
434
+ }).filter(function (group) {
435
+ return !!group;
436
+ }));
437
+ var appended = !customInPlay ? [] : Array.from(new Set(customEmojis.map(function (emoji) {
438
+ return emoji.group;
439
+ }).filter(function (group) {
440
+ return !!group;
441
+ }))).filter(function (group) {
442
+ return !listedGroups.has(group);
443
+ }).map(function (group) {
444
+ return {
445
+ category: Categories.CUSTOM,
446
+ name: group,
447
+ group: group
448
+ };
449
+ });
450
+ // Collapse duplicate entries (same identity twice renders the same
451
+ // section twice under one React key); first occurrence wins.
452
+ var seen = new Set();
453
+ return explicit.concat(appended).filter(function (entry) {
454
+ var id = categoryIdFromCategoryConfig(entry);
455
+ if (seen.has(id)) {
456
+ return false;
457
+ }
458
+ seen.add(id);
459
+ return true;
460
+ });
461
+ function mapUserEntry(category) {
396
462
  if (typeof category === 'string') {
397
463
  return getBaseConfigByCategory(category, extra[category]);
398
464
  }
399
465
  return _extends({}, getBaseConfigByCategory(category.category, extra[category.category]), category);
400
- });
466
+ }
401
467
  }
402
468
  function getBaseConfigByCategory(category, modifier) {
403
469
  if (modifier === void 0) {
@@ -428,7 +494,7 @@ function mergeConfig(userConfig) {
428
494
  var config = Object.assign(base, userConfig);
429
495
  var categories = mergeCategoriesConfig(userConfig.categories, {
430
496
  suggestionMode: config.suggestedEmojisMode
431
- }, userConfig.emojiData);
497
+ }, userConfig.emojiData, userConfig.customEmojis);
432
498
  config.hiddenEmojis.forEach(function (emoji) {
433
499
  config.unicodeToHide.add(emoji);
434
500
  });
@@ -26862,6 +26928,7 @@ var PickerDataContext = /*#__PURE__*/React__default.createContext({
26862
26928
  allEmojis: [],
26863
26929
  allEmojisByUnified: {},
26864
26930
  searchIndex: {},
26931
+ customGroups: {},
26865
26932
  emojiByUnified: emojiByUnified,
26866
26933
  activeVariationFromUnified: function activeVariationFromUnified() {
26867
26934
  return null;
@@ -26876,11 +26943,23 @@ function PickerDataProvider(_ref) {
26876
26943
  var emojiData = genericEmojiData || defaultEmojiData;
26877
26944
  // Clone to avoid mutation of shared source
26878
26945
  var newData = JSON.parse(JSON.stringify(emojiData));
26946
+ var customGroups = {};
26879
26947
  if (customEmojis && customEmojis.length > 0) {
26880
- newData.emojis[Categories.CUSTOM] = customEmojis.map(customToRegularEmoji);
26948
+ for (var _iterator = _createForOfIteratorHelperLoose(customEmojis), _step; !(_step = _iterator()).done;) {
26949
+ var _customGroups$emoji$g;
26950
+ var emoji = _step.value;
26951
+ if (!emoji.group) {
26952
+ continue;
26953
+ }
26954
+ customGroups[emoji.group] = (_customGroups$emoji$g = customGroups[emoji.group]) != null ? _customGroups$emoji$g : [];
26955
+ customGroups[emoji.group].push(customToRegularEmoji(emoji));
26956
+ }
26957
+ newData.emojis[Categories.CUSTOM] = customEmojis.filter(function (emoji) {
26958
+ return !emoji.group;
26959
+ }).map(customToRegularEmoji);
26881
26960
  }
26882
26961
  var emojis = newData.emojis || {};
26883
- var allEmojis = Object.values(emojis).flat();
26962
+ var allEmojis = Object.values(emojis).concat(Object.values(customGroups)).flat();
26884
26963
  var allEmojisByUnified = {};
26885
26964
  var searchIndex = {};
26886
26965
  allEmojis.forEach(function (emoji) {
@@ -26905,7 +26984,8 @@ function PickerDataProvider(_ref) {
26905
26984
  emojiData: newData,
26906
26985
  allEmojis: allEmojis,
26907
26986
  allEmojisByUnified: allEmojisByUnified,
26908
- searchIndex: searchIndex
26987
+ searchIndex: searchIndex,
26988
+ customGroups: customGroups
26909
26989
  };
26910
26990
  }, [genericEmojiData, customEmojis]);
26911
26991
  var emojiByUnified = React__default.useCallback(function (unified) {
@@ -26927,7 +27007,8 @@ function usePickerDataContext() {
26927
27007
  function useGetEmojisByCategory() {
26928
27008
  var _usePickerDataContext = usePickerDataContext(),
26929
27009
  emojiData = _usePickerDataContext.emojiData,
26930
- emojiByUnified = _usePickerDataContext.emojiByUnified;
27010
+ emojiByUnified = _usePickerDataContext.emojiByUnified,
27011
+ customGroups = _usePickerDataContext.customGroups;
26931
27012
  var suggestedEmojisModeConfig = useSuggestedEmojisModeConfig();
26932
27013
  var _useUpdateSuggested = useUpdateSuggested(),
26933
27014
  suggestedUpdated = _useUpdateSuggested[0];
@@ -26948,11 +27029,15 @@ function useGetEmojisByCategory() {
26948
27029
  }).filter(Boolean);
26949
27030
  // eslint-disable-next-line react-hooks/exhaustive-deps
26950
27031
  }, [isMounted, suggestedUpdated, suggestedEmojisModeConfig, emojiByUnified]);
26951
- return function getEmojisByCategory(category) {
27032
+ return function getEmojisByCategory(category, group) {
26952
27033
  var _emojiData$emojis$cat, _emojiData$emojis;
26953
27034
  if (category === Categories.SUGGESTED) {
26954
27035
  return suggested;
26955
27036
  }
27037
+ if (category === Categories.CUSTOM && group) {
27038
+ var _customGroups$group;
27039
+ return (_customGroups$group = customGroups[group]) != null ? _customGroups$group : [];
27040
+ }
26956
27041
  return (_emojiData$emojis$cat = (_emojiData$emojis = emojiData.emojis) == null ? void 0 : _emojiData$emojis[category]) != null ? _emojiData$emojis$cat : [];
26957
27042
  };
26958
27043
  }
@@ -29586,11 +29671,10 @@ function EmojiCategory(_ref) {
29586
29671
  hiddenOnSearch = _ref.hiddenOnSearch,
29587
29672
  height = _ref.height,
29588
29673
  emojisPerRow = _ref.emojisPerRow;
29589
- var category = categoryFromCategoryConfig(categoryConfig);
29590
29674
  var categoryName = categoryNameFromCategoryConfig(categoryConfig);
29591
29675
  return createElement("li", {
29592
29676
  className: cx(styles$7.category, hidden && commonStyles.hidden, hiddenOnSearch && commonInteractionStyles.hiddenOnSearch),
29593
- "data-name": category,
29677
+ "data-name": categoryIdFromCategoryConfig(categoryConfig),
29594
29678
  role: "rowgroup",
29595
29679
  "aria-label": categoryName
29596
29680
  }, createElement("h2", {
@@ -29665,7 +29749,7 @@ function MeasureEmoji() {
29665
29749
  return null;
29666
29750
  }
29667
29751
  var firstCategory = categories[0];
29668
- var dummyEmoji = getEmojisByCategory(categoryFromCategoryConfig(firstCategory))[0];
29752
+ var dummyEmoji = firstCategory ? getEmojisByCategory(categoryFromCategoryConfig(firstCategory), customGroupFromCategoryConfig(firstCategory))[0] : undefined;
29669
29753
  var unified = dummyEmoji ? emojiUnified(dummyEmoji, activeSkinTone) : '';
29670
29754
  if (!dummyEmoji) {
29671
29755
  return null;
@@ -29719,22 +29803,24 @@ function EmojiList(_ref) {
29719
29803
  role: "grid"
29720
29804
  }, createElement(MeasureEmoji, null), categories.map(function (categoryConfig, index) {
29721
29805
  var category = categoryFromCategoryConfig(categoryConfig);
29806
+ var categoryId = categoryIdFromCategoryConfig(categoryConfig);
29807
+ var group = customGroupFromCategoryConfig(categoryConfig);
29722
29808
  var currentOffset = topOffset;
29723
- var categoryHeight = categoryHeights[category];
29809
+ var categoryHeight = categoryHeights[categoryId];
29724
29810
  if (categoryHeight) {
29725
29811
  topOffset += categoryHeight + labelHeight;
29726
29812
  }
29727
29813
  return createElement(RenderCategory, {
29728
- key: category,
29729
- categoryEmojis: getEmojisByCategory(category),
29814
+ key: categoryId,
29815
+ categoryEmojis: getEmojisByCategory(category, group),
29730
29816
  categoryConfig: categoryConfig,
29731
29817
  topOffset: currentOffset,
29732
29818
  isFirstCategory: index === 0,
29733
29819
  onHeightReady: function onHeightReady(height) {
29734
- if (categoryHeights[category] !== height) {
29820
+ if (categoryHeights[categoryId] !== height) {
29735
29821
  setCategoryHeights(function (prev) {
29736
29822
  var _extends2;
29737
- return _extends({}, prev, (_extends2 = {}, _extends2[category] = height, _extends2));
29823
+ return _extends({}, prev, (_extends2 = {}, _extends2[categoryId] = height, _extends2));
29738
29824
  });
29739
29825
  }
29740
29826
  },
@@ -29758,7 +29844,7 @@ function RenderCategory(_ref2) {
29758
29844
  // the deadlock; the observer takes over from there.
29759
29845
  // https://github.com/ealush/emoji-picker-react/issues/469
29760
29846
  // https://github.com/ealush/emoji-picker-react/issues/475
29761
- var isCategoryVisible = isFirstCategory || visibleCategories.includes(categoryConfig.category);
29847
+ var isCategoryVisible = isFirstCategory || visibleCategories.includes(categoryIdFromCategoryConfig(categoryConfig));
29762
29848
  var _useEmojiVirtualizati = useEmojiVirtualization({
29763
29849
  categoryEmojis: categoryEmojis,
29764
29850
  topOffset: topOffset,
@@ -29973,6 +30059,16 @@ var styles$a = /*#__PURE__*/stylesheet.create({
29973
30059
  }
29974
30060
  });
29975
30061
 
30062
+ function detectEmojyPartiallyBelowFold(button, bodyRef) {
30063
+ if (!button || !bodyRef) {
30064
+ return 0;
30065
+ }
30066
+ var buttonRect = button.getBoundingClientRect();
30067
+ var bodyRect = bodyRef.getBoundingClientRect();
30068
+ // If the element is obscured by at least half of its size
30069
+ return bodyRect.height - (buttonRect.y - bodyRect.y);
30070
+ }
30071
+
29976
30072
  function useEmojiPreviewEvents(allow, setPreviewEmoji) {
29977
30073
  var BodyRef = useBodyRef();
29978
30074
  var PickerMainRef = usePickerMainRef();
@@ -30045,12 +30141,19 @@ function useEmojiPreviewEvents(allow, setPreviewEmoji) {
30045
30141
  // Pulling focus out of an external element steals the caret from
30046
30142
  // host-app inputs (issue #320). The guard runs again inside the
30047
30143
  // deferred callback in case focus moved out after this event.
30048
- if (!isExternalElementFocused()) {
30144
+ // Partially visible buttons are never focused: focusing would make
30145
+ // the browser scroll them into view, yanking the scroll position
30146
+ // while the user is browsing (see also PR #509).
30147
+ if (!isExternalElementFocused() && !isPartiallyBelowFold(button, bodyRef)) {
30049
30148
  focusElement(button, function () {
30050
30149
  return !isExternalElementFocused();
30051
30150
  });
30052
30151
  }
30053
30152
  }
30153
+ function isPartiallyBelowFold(button, body) {
30154
+ var belowFoldByPx = detectEmojyPartiallyBelowFold(button, body);
30155
+ return belowFoldByPx < button.getBoundingClientRect().height;
30156
+ }
30054
30157
  function isExternalElementFocused() {
30055
30158
  var _PickerMainRef$curren;
30056
30159
  var active = document.activeElement;
@@ -30066,7 +30169,7 @@ function useEmojiPreviewEvents(allow, setPreviewEmoji) {
30066
30169
  bodyRef == null ? void 0 : bodyRef.removeEventListener('blur', onLeave, true);
30067
30170
  bodyRef == null ? void 0 : bodyRef.removeEventListener('keydown', onEscape);
30068
30171
  };
30069
- }, [BodyRef, allow, setPreviewEmoji, isMouseDisallowed, allowMouseMove]);
30172
+ }, [BodyRef, PickerMainRef, allow, setPreviewEmoji, isMouseDisallowed, allowMouseMove]);
30070
30173
  }
30071
30174
 
30072
30175
  var _stylesheet$create;
@@ -30466,7 +30569,8 @@ function useScrollCategoryIntoView() {
30466
30569
  if (!BodyRef.current) {
30467
30570
  return;
30468
30571
  }
30469
- var $category = (_BodyRef$current = BodyRef.current) == null ? void 0 : _BodyRef$current.querySelector("[data-name=\"" + category + "\"]");
30572
+ // Group names are user-controlled; escape for the attribute selector.
30573
+ var $category = (_BodyRef$current = BodyRef.current) == null ? void 0 : _BodyRef$current.querySelector("[data-name=\"" + CSS.escape(category) + "\"]");
30470
30574
  if (!$category) {
30471
30575
  return;
30472
30576
  }
@@ -30849,8 +30953,24 @@ function CategoryNavigation() {
30849
30953
  var categoryIcons = useCategoryIconsConfig();
30850
30954
  var CategoryNavigationRef = useCategoryNavigationRef();
30851
30955
  var hideCustomCategory = useShouldHideCustomEmojis();
30956
+ var _usePickerDataContext = usePickerDataContext(),
30957
+ customGroups = _usePickerDataContext.customGroups,
30958
+ emojiData = _usePickerDataContext.emojiData;
30852
30959
  var visibleCategories = categoriesConfig.filter(function (categoryConfig) {
30853
- return !(isCustomCategory(categoryConfig) && hideCustomCategory);
30960
+ if (isCustomCategory(categoryConfig) && hideCustomCategory) {
30961
+ return false;
30962
+ }
30963
+ // Tabs navigating to an empty (hidden) section are dead weight.
30964
+ var group = customGroupFromCategoryConfig(categoryConfig);
30965
+ if (group) {
30966
+ var _customGroups$group$l, _customGroups$group;
30967
+ return ((_customGroups$group$l = (_customGroups$group = customGroups[group]) == null ? void 0 : _customGroups$group.length) != null ? _customGroups$group$l : 0) > 0;
30968
+ }
30969
+ if (isCustomCategory(categoryConfig)) {
30970
+ var _emojiData$emojis$Cat, _emojiData$emojis, _emojiData$emojis$Cat2;
30971
+ return ((_emojiData$emojis$Cat = (_emojiData$emojis = emojiData.emojis) == null ? void 0 : (_emojiData$emojis$Cat2 = _emojiData$emojis[Categories.CUSTOM]) == null ? void 0 : _emojiData$emojis$Cat2.length) != null ? _emojiData$emojis$Cat : 0) > 0;
30972
+ }
30973
+ return true;
30854
30974
  });
30855
30975
  // A single tab navigates nowhere — hide the bar to reclaim its space.
30856
30976
  // https://github.com/ealush/emoji-picker-react/issues/396
@@ -30865,19 +30985,20 @@ function CategoryNavigation() {
30865
30985
  ref: CategoryNavigationRef
30866
30986
  }, visibleCategories.map(function (categoryConfig) {
30867
30987
  var category = categoryFromCategoryConfig(categoryConfig);
30868
- var isActiveCategory = category === activeCategory;
30988
+ var categoryId = categoryIdFromCategoryConfig(categoryConfig);
30989
+ var isActiveCategory = categoryId === activeCategory;
30869
30990
  var allowNavigation = !isSearchMode && !isActiveCategory;
30870
30991
  return createElement(CategoryButton, {
30871
- key: category,
30992
+ key: categoryId,
30872
30993
  category: category,
30873
30994
  isActiveCategory: isActiveCategory,
30874
30995
  allowNavigation: allowNavigation,
30875
30996
  categoryConfig: categoryConfig,
30876
30997
  customIcon: categoryIcons[category],
30877
30998
  onClick: function onClick() {
30878
- scrollCategoryIntoView(category);
30999
+ scrollCategoryIntoView(categoryId);
30879
31000
  setTimeout(function () {
30880
- setActiveCategory(category);
31001
+ setActiveCategory(categoryId);
30881
31002
  }, 10);
30882
31003
  }
30883
31004
  });