emoji-picker-react 4.20.4 → 4.20.6

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 @@ declare type Props = Readonly<{
6
6
  hidden?: boolean;
7
7
  hiddenOnSearch?: boolean;
8
8
  height?: number;
9
+ emojisPerRow?: number;
9
10
  }>;
10
- export declare function EmojiCategory({ categoryConfig, children, hidden, hiddenOnSearch, height, }: Props): JSX.Element;
11
+ export declare function EmojiCategory({ categoryConfig, children, hidden, hiddenOnSearch, height, emojisPerRow, }: Props): JSX.Element;
11
12
  export {};
@@ -7,8 +7,9 @@ declare type ClickableEmojiProps = Readonly<BaseEmojiProps & {
7
7
  hiddenOnSearch?: boolean;
8
8
  emoji: DataEmoji;
9
9
  className?: string;
10
+ buttonClassName?: string;
10
11
  noBackground?: boolean;
11
12
  style?: React.CSSProperties;
12
13
  }>;
13
- export declare function ClickableEmoji({ emoji, unified, hidden, hiddenOnSearch, emojiStyle, showVariations, size, lazyLoad, getEmojiUrl, className, noBackground, style, }: ClickableEmojiProps): JSX.Element;
14
+ export declare function ClickableEmoji({ emoji, unified, hidden, hiddenOnSearch, emojiStyle, showVariations, size, lazyLoad, getEmojiUrl, className, buttonClassName, noBackground, style, }: ClickableEmojiProps): JSX.Element;
14
15
  export {};
@@ -6,6 +6,7 @@ declare type Props = {
6
6
  isActive: boolean;
7
7
  skinToneVariation: SkinTones;
8
8
  style?: React.CSSProperties;
9
+ tabIndex?: number;
9
10
  };
10
- export declare function BtnSkinToneVariation({ isOpen, onClick, isActive, skinToneVariation, style, }: Props): JSX.Element;
11
+ export declare function BtnSkinToneVariation({ isOpen, onClick, isActive, skinToneVariation, style, tabIndex, }: Props): JSX.Element;
11
12
  export {};
@@ -27900,6 +27900,7 @@ function useKeyboardNavigation() {
27900
27900
  useSearchInputKeyboardEvents();
27901
27901
  useSkinTonePickerKeyboardEvents();
27902
27902
  useCategoryNavigationKeyboardEvents();
27903
+ useReactionsKeyboardEvents();
27903
27904
  useBodyKeyboardEvents();
27904
27905
  }
27905
27906
  function usePickerMainKeyboardEvents() {
@@ -28105,6 +28106,65 @@ function useCategoryNavigationKeyboardEvents() {
28105
28106
  };
28106
28107
  }, [CategoryNavigationRef, BodyRef, onKeyDown]);
28107
28108
  }
28109
+ // Arrow keys move between reaction buttons (and the expand button)
28110
+ // while the reactions bar has focus. The bar lives outside the scroll
28111
+ // body, so the body handler never sees these events.
28112
+ // https://github.com/ealush/emoji-picker-react/issues/411
28113
+ function useReactionsKeyboardEvents() {
28114
+ var ReactionsRef = useReactionsRef();
28115
+ // The reactions bar mounts late when the user collapses the full
28116
+ // picker, so the listener effect must rerun when it opens.
28117
+ var _useReactionsModeStat = useReactionsModeState(),
28118
+ reactionsOpen = _useReactionsModeStat[0];
28119
+ var onKeyDown = React.useMemo(function () {
28120
+ return function onKeyDown(event) {
28121
+ var key = event.key;
28122
+ var current = ReactionsRef.current;
28123
+ if (!current) {
28124
+ return;
28125
+ }
28126
+ var delta = reactionFocusDelta(key);
28127
+ if (delta !== 0 && focusReactionSibling(current, delta)) {
28128
+ event.preventDefault();
28129
+ }
28130
+ };
28131
+ },
28132
+ // reactionsOpen: the bar mounts late on collapse, and the new
28133
+ // callback identity reinstalls the listener effect below.
28134
+ [ReactionsRef, reactionsOpen]);
28135
+ React.useEffect(function () {
28136
+ var current = ReactionsRef.current;
28137
+ if (!current) {
28138
+ return;
28139
+ }
28140
+ current.addEventListener('keydown', onKeyDown);
28141
+ return function () {
28142
+ current.removeEventListener('keydown', onKeyDown);
28143
+ };
28144
+ }, [ReactionsRef, onKeyDown]);
28145
+ }
28146
+ function reactionFocusDelta(key) {
28147
+ switch (key) {
28148
+ case KeyboardEvents.ArrowRight:
28149
+ case KeyboardEvents.ArrowDown:
28150
+ return 1;
28151
+ case KeyboardEvents.ArrowLeft:
28152
+ case KeyboardEvents.ArrowUp:
28153
+ return -1;
28154
+ default:
28155
+ return 0;
28156
+ }
28157
+ }
28158
+ function focusReactionSibling(root, delta) {
28159
+ var buttons = Array.from(root.querySelectorAll('button'));
28160
+ var activeIndex = buttons.indexOf(getActiveElement());
28161
+ var sibling = activeIndex === -1 ? null : buttons[activeIndex + delta];
28162
+ if (!sibling) {
28163
+ return false;
28164
+ }
28165
+ focusElement(sibling);
28166
+ return true;
28167
+ }
28108
28168
  function useBodyKeyboardEvents() {
28109
28169
  var BodyRef = useBodyRef();
28110
28170
  var goUpFromBody = useGoUpFromBody();
@@ -28480,9 +28540,20 @@ var styles = /*#__PURE__*/stylesheet.create({
28480
28540
  });
28481
28541
 
28482
28542
  function elementCountInRow(parent, element) {
28543
+ var _parent$dataset;
28483
28544
  if (!parent || !element) {
28484
28545
  return 0;
28485
28546
  }
28547
+ // The virtualized layout positions emojis with a fixed per-row count that
28548
+ // is rendered onto the category content element. That count is the single
28549
+ // source of truth: deriving it again from fractional rect widths can be
28550
+ // off by one (e.g. category padding shrinking the content box), which
28551
+ // makes arrow-up/arrow-down drift sideways by a column.
28552
+ // https://github.com/ealush/emoji-picker-react/issues/502
28553
+ var declared = Number((_parent$dataset = parent.dataset) == null ? void 0 : _parent$dataset.emojisPerRow);
28554
+ if (Number.isInteger(declared) && declared > 0) {
28555
+ return declared;
28556
+ }
28486
28557
  var parentWidth = parent.getBoundingClientRect().width;
28487
28558
  var elementWidth = element.getBoundingClientRect().width;
28488
28559
  return Math.floor(parentWidth / elementWidth);
@@ -29120,6 +29191,7 @@ function ClickableEmoji(_ref) {
29120
29191
  lazyLoad = _ref.lazyLoad,
29121
29192
  getEmojiUrl = _ref.getEmojiUrl,
29122
29193
  className = _ref.className,
29194
+ buttonClassName = _ref.buttonClassName,
29123
29195
  _ref$noBackground = _ref.noBackground,
29124
29196
  noBackground = _ref$noBackground === void 0 ? false : _ref$noBackground,
29125
29197
  style = _ref.style;
@@ -29132,7 +29204,8 @@ function ClickableEmoji(_ref) {
29132
29204
  emojiNames: emojiNames(emoji),
29133
29205
  unified: unified,
29134
29206
  noBackground: noBackground,
29135
- style: style
29207
+ style: style,
29208
+ className: buttonClassName
29136
29209
  }, React.createElement(ViewOnlyEmoji, {
29137
29210
  unified: unified,
29138
29211
  emoji: emoji,
@@ -29236,6 +29309,7 @@ function Reactions() {
29236
29309
  unified: reaction,
29237
29310
  showVariations: false,
29238
29311
  className: shipstyles.cx(styles$6.emojiButton),
29312
+ buttonClassName: shipstyles.cx(styles$6.reactionButton),
29239
29313
  noBackground: true,
29240
29314
  getEmojiUrl: getEmojiUrl
29241
29315
  }));
@@ -29251,6 +29325,24 @@ var styles$6 = /*#__PURE__*/stylesheet.create({
29251
29325
  alignItems: 'center',
29252
29326
  height: '100%'
29253
29327
  },
29328
+ reactionButton: {
29329
+ // Every picker button sets outline: none, and reaction buttons also
29330
+ // suppress the focus background, so keyboard focus was invisible.
29331
+ // Focus mirrors the hover treatment (scale plus a soft circular
29332
+ // wash, no outline ring): the scale transform on the inner image
29333
+ // alone is clipped by the button's overflow: hidden, and does
29334
+ // nothing on inline native emoji, so the button itself carries the
29335
+ // effect. Scoped to the reactions bar so grid focus treatment is
29336
+ // untouched.
29337
+ // https://github.com/ealush/emoji-picker-react/issues/473
29338
+ ':focus-visible': {
29339
+ transform: 'scale(1.2)',
29340
+ transition: 'background-color 0.2s, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.5)',
29341
+ backgroundColor: 'var(--epr-hover-bg-color-reduced-opacity)',
29342
+ borderRadius: '50%',
29343
+ outline: 'none'
29344
+ }
29345
+ },
29254
29346
  emojiButton: {
29255
29347
  ':hover': {
29256
29348
  transform: 'scale(1.2)'
@@ -29474,12 +29566,14 @@ function EmojiCategory(_ref) {
29474
29566
  children = _ref.children,
29475
29567
  hidden = _ref.hidden,
29476
29568
  hiddenOnSearch = _ref.hiddenOnSearch,
29477
- height = _ref.height;
29569
+ height = _ref.height,
29570
+ emojisPerRow = _ref.emojisPerRow;
29478
29571
  var category = categoryFromCategoryConfig(categoryConfig);
29479
29572
  var categoryName = categoryNameFromCategoryConfig(categoryConfig);
29480
29573
  return React.createElement("li", {
29481
29574
  className: shipstyles.cx(styles$7.category, hidden && commonStyles.hidden, hiddenOnSearch && commonInteractionStyles.hiddenOnSearch),
29482
29575
  "data-name": category,
29576
+ role: "rowgroup",
29483
29577
  "aria-label": categoryName
29484
29578
  }, React.createElement("h2", {
29485
29579
  className: shipstyles.cx(styles$7.label)
@@ -29487,7 +29581,9 @@ function EmojiCategory(_ref) {
29487
29581
  className: shipstyles.cx(styles$7.categoryContent),
29488
29582
  style: {
29489
29583
  height: height
29490
- }
29584
+ },
29585
+ role: "row",
29586
+ "data-emojis-per-row": emojisPerRow
29491
29587
  }, children));
29492
29588
  }
29493
29589
  var styles$7 = /*#__PURE__*/stylesheet.create({
@@ -29589,47 +29685,68 @@ function EmojiList(_ref) {
29589
29685
  var getEmojisByCategory = useGetEmojisByCategory();
29590
29686
  var labelHeight = getLabelHeight(EmojiListRef.current);
29591
29687
  var topOffset = 0;
29592
- return React.createElement("ul", {
29593
- className: shipstyles.cx(styles$8.emojiList),
29594
- ref: EmojiListRef
29595
- }, React.createElement(MeasureEmoji, null), categories.map(function (categoryConfig) {
29596
- var category = categoryFromCategoryConfig(categoryConfig);
29597
- var currentOffset = topOffset;
29598
- var categoryHeight = categoryHeights[category];
29599
- if (categoryHeight) {
29600
- topOffset += categoryHeight + labelHeight;
29601
- }
29602
- return React.createElement(RenderCategory, {
29603
- key: category,
29604
- categoryEmojis: getEmojisByCategory(category),
29605
- categoryConfig: categoryConfig,
29606
- topOffset: currentOffset,
29607
- onHeightReady: function onHeightReady(height) {
29608
- if (categoryHeights[category] !== height) {
29609
- setCategoryHeights(function (prev) {
29610
- var _extends2;
29611
- return _extends({}, prev, (_extends2 = {}, _extends2[category] = height, _extends2));
29612
- });
29613
- }
29614
- },
29615
- scrollTop: scrollTop
29616
- });
29617
- }));
29688
+ // role="grid" makes screen readers switch out of virtual-cursor mode so
29689
+ // arrow keys reach the picker's keyboard handler. Each category is one
29690
+ // rowgroup: visual rows shift under virtualization, so the category is
29691
+ // the only stable row unit. Emoji buttons intentionally keep their
29692
+ // native button role for activation semantics.
29693
+ // https://github.com/ealush/emoji-picker-react/issues/508
29694
+ // The list markup is kept for a backwards-compatible DOM structure;
29695
+ // the grid role override is intentional.
29696
+ return (
29697
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
29698
+ React.createElement("ul", {
29699
+ className: shipstyles.cx(styles$8.emojiList),
29700
+ ref: EmojiListRef,
29701
+ role: "grid"
29702
+ }, React.createElement(MeasureEmoji, null), categories.map(function (categoryConfig, index) {
29703
+ var category = categoryFromCategoryConfig(categoryConfig);
29704
+ var currentOffset = topOffset;
29705
+ var categoryHeight = categoryHeights[category];
29706
+ if (categoryHeight) {
29707
+ topOffset += categoryHeight + labelHeight;
29708
+ }
29709
+ return React.createElement(RenderCategory, {
29710
+ key: category,
29711
+ categoryEmojis: getEmojisByCategory(category),
29712
+ categoryConfig: categoryConfig,
29713
+ topOffset: currentOffset,
29714
+ isFirstCategory: index === 0,
29715
+ onHeightReady: function onHeightReady(height) {
29716
+ if (categoryHeights[category] !== height) {
29717
+ setCategoryHeights(function (prev) {
29718
+ var _extends2;
29719
+ return _extends({}, prev, (_extends2 = {}, _extends2[category] = height, _extends2));
29720
+ });
29721
+ }
29722
+ },
29723
+ scrollTop: scrollTop
29724
+ });
29725
+ }))
29726
+ );
29618
29727
  }
29619
29728
  function RenderCategory(_ref2) {
29620
29729
  var categoryEmojis = _ref2.categoryEmojis,
29621
29730
  categoryConfig = _ref2.categoryConfig,
29622
29731
  topOffset = _ref2.topOffset,
29623
29732
  onHeightReady = _ref2.onHeightReady,
29624
- scrollTop = _ref2.scrollTop;
29733
+ scrollTop = _ref2.scrollTop,
29734
+ isFirstCategory = _ref2.isFirstCategory;
29625
29735
  var _useVisibleCategories = useVisibleCategoriesState(),
29626
29736
  visibleCategories = _useVisibleCategories[0];
29737
+ // The observer is the only writer of visibleCategories. When it never
29738
+ // fires (zero-height container at mount, hidden modal), no category
29739
+ // would ever render. The first category paints optimistically to break
29740
+ // the deadlock; the observer takes over from there.
29741
+ // https://github.com/ealush/emoji-picker-react/issues/469
29742
+ // https://github.com/ealush/emoji-picker-react/issues/475
29743
+ var isCategoryVisible = isFirstCategory || visibleCategories.includes(categoryConfig.category);
29627
29744
  var _useEmojiVirtualizati = useEmojiVirtualization({
29628
29745
  categoryEmojis: categoryEmojis,
29629
29746
  topOffset: topOffset,
29630
29747
  onHeightReady: onHeightReady,
29631
29748
  scrollTop: scrollTop,
29632
- isCategoryVisible: visibleCategories.includes(categoryConfig.category)
29749
+ isCategoryVisible: isCategoryVisible
29633
29750
  }),
29634
29751
  virtualizedCounter = _useEmojiVirtualizati.virtualizedCounter,
29635
29752
  emojis = _useEmojiVirtualizati.emojis,
@@ -29637,6 +29754,7 @@ function RenderCategory(_ref2) {
29637
29754
  return React.createElement(EmojiCategory, {
29638
29755
  categoryConfig: categoryConfig,
29639
29756
  height: dimensions == null ? void 0 : dimensions.categoryHeight,
29757
+ emojisPerRow: dimensions == null ? void 0 : dimensions.emojisPerRow,
29640
29758
  // Indicates that there are no visible emojis
29641
29759
  // Hence, the category should be hidden
29642
29760
  hidden: !emojis.length && virtualizedCounter === 0
@@ -30003,10 +30121,12 @@ function BtnSkinToneVariation(_ref) {
30003
30121
  onClick = _ref.onClick,
30004
30122
  isActive = _ref.isActive,
30005
30123
  skinToneVariation = _ref.skinToneVariation,
30006
- style = _ref.style;
30124
+ style = _ref.style,
30125
+ tabIndex = _ref.tabIndex;
30007
30126
  return React.createElement(Button, {
30008
30127
  style: style,
30009
30128
  onClick: onClick,
30129
+ tabIndex: tabIndex,
30010
30130
  className: shipstyles.cx("epr-tone-" + skinToneVariation, styles$c.tone, !isOpen && styles$c.closedTone, isActive && styles$c.active),
30011
30131
  "aria-pressed": isActive,
30012
30132
  "aria-label": "Skin tone " + skinTonesNamed[skinToneVariation]
@@ -30115,6 +30235,12 @@ function SkinTonePicker(_ref) {
30115
30235
  key: skinToneVariation,
30116
30236
  skinToneVariation: skinToneVariation,
30117
30237
  isOpen: isOpen,
30238
+ // Roving tabindex: when the fan is closed the inactive tones
30239
+ // are invisible (opacity: 0, stacked), so they must not be
30240
+ // reachable via Tab. The active tone stays tabbable to open
30241
+ // the fan from the keyboard.
30242
+ // https://github.com/ealush/emoji-picker-react/issues/492
30243
+ tabIndex: isOpen || active ? 0 : -1,
30118
30244
  style: {
30119
30245
  transform: shipstyles.cx(vertical ? "translateY(-" + i * (isOpen ? ITEM_SIZE : 0) + "px)" : "translateX(-" + i * (isOpen ? ITEM_SIZE : 0) + "px)", isOpen && active && 'scale(1.3)')
30120
30246
  },