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