expo-native-sheet-emojis 2.0.1 → 2.0.3

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/README.md CHANGED
@@ -6,7 +6,7 @@ A fully native emoji picker bottom sheet for React Native. Built entirely in Swi
6
6
 
7
7
  ## Highlights
8
8
 
9
- - **1900+ emojis** across 9 categories (Unicode Emoji up to v16.0), rendered at 60+ FPS
9
+ - **1900+ emojis** across 9 categories (Unicode Emoji up to v16.0)
10
10
  - **60+ FPS everywhere** -- native UICollectionView (iOS) and RecyclerView (Android) with no JS bridge involvement during scrolling, searching, or animations
11
11
  - Fully native on both platforms -- no JavaScript emoji rendering, no web views, no React re-renders
12
12
  - Search across 21 languages powered by [Unicode CLDR](https://cldr.unicode.org/) -- the industry-standard source for emoji annotations used by iOS, Android, and major platforms. Runs on a background thread to keep the UI thread free
@@ -431,6 +431,41 @@ await EmojiSheetModule.present({
431
431
  | gestureEnabled | `boolean` | `true` | Swipe to dismiss |
432
432
  | backdropOpacity | `number` | `0.22` | Backdrop opacity |
433
433
 
434
+ ## Companion: expo-native-emojis-popup
435
+
436
+ For quick emoji reactions (like message reaction trays), the companion module [expo-native-emojis-popup](https://github.com/efstathiosntonas/expo-native-emojis-popup) provides a fully native emoji reaction popup with long-press drag-to-select, hover labels, and spring animations.
437
+
438
+ Together they form a complete reaction system:
439
+
440
+ 1. User long-presses a message/post -> `expo-native-emojis-popup` shows the quick reaction tray
441
+ 2. User taps the plus button -> your app presents `expo-native-sheet-emojis` for the full emoji catalog
442
+ 3. Selected emoji flows back into your reaction system
443
+
444
+ ```typescript
445
+ import { EmojisPopupModule } from 'expo-native-emojis-popup';
446
+ import { EmojiSheetModule } from 'expo-native-sheet-emojis';
447
+
448
+ const result = await EmojisPopupModule.show({
449
+ anchorId: 'message:42',
450
+ items: [
451
+ { emoji: '❤️', emoji_name: 'Red Heart', id: 'heart' },
452
+ { emoji: '👍', emoji_name: 'Thumbs Up', id: 'thumbsup' },
453
+ { emoji: '😂', emoji_name: 'Face with Tears of Joy', id: 'laugh' },
454
+ ],
455
+ plusEnabled: true,
456
+ });
457
+
458
+ if (result.type === 'plus') {
459
+ // Open the full emoji picker
460
+ const sheetResult = await EmojiSheetModule.present({ theme: 'dark' });
461
+ if (!sheetResult.cancelled) {
462
+ handleReaction(sheetResult.emoji);
463
+ }
464
+ } else if (result.type === 'select') {
465
+ handleReaction(result.id);
466
+ }
467
+ ```
468
+
434
469
  ## LLM / AI Agent Reference
435
470
 
436
471
  If you're an AI agent or using an LLM to integrate this module, see [llms.txt](https://raw.githubusercontent.com/efstathiosntonas/expo-native-sheet-emojis/refs/heads/main/llms.txt) for a concise, structured reference with all types, APIs, and usage patterns.
@@ -111,12 +111,16 @@ class EmojiSkinTonePicker(
111
111
  }
112
112
  }
113
113
 
114
- // Position above anchor
115
- val location = IntArray(2)
116
- anchorView.getLocationOnScreen(location)
117
- val xOff = -(totalWidth / 2) + (anchorView.width / 2)
118
- val yOff = -totalHeight - (4 * density).toInt()
114
+ // Use window-relative coordinates to avoid RTL xOff flip and status bar offset
115
+ val anchorLocation = IntArray(2)
116
+ anchorView.getLocationInWindow(anchorLocation)
117
+ val windowWidth = anchorView.rootView.width
118
+ val edgePadding = (8 * density).toInt()
119
119
 
120
- popup.showAsDropDown(anchorView, xOff, yOff)
120
+ val idealLeft = anchorLocation[0] + anchorView.width / 2 - totalWidth / 2
121
+ val clampedLeft = idealLeft.coerceIn(edgePadding, windowWidth - totalWidth - edgePadding)
122
+ val popupTop = anchorLocation[1] - totalHeight - (4 * density).toInt()
123
+
124
+ popup.showAtLocation(anchorView.rootView, Gravity.NO_GRAVITY, clampedLeft, popupTop)
121
125
  }
122
126
  }
@@ -72,36 +72,62 @@ class EmojiCategoryStrip: UIView, UICollectionViewDataSource, UICollectionViewDe
72
72
  func updateCategories(_ keys: [String]) {
73
73
  categoryKeys = keys
74
74
  selectedIndex = 0
75
- collectionView.reloadData()
75
+ reloadCategories()
76
76
  }
77
77
 
78
78
  func applyTheme(_ theme: EmojiSheetTheme) {
79
79
  currentTheme = theme
80
80
  backgroundColor = theme.categoryBarBackgroundColor
81
81
  dividerLine.backgroundColor = theme.dividerColor
82
- collectionView.reloadData()
82
+ reloadCategories()
83
83
  }
84
84
 
85
85
  func applyLayoutDirection(_ attribute: UISemanticContentAttribute) {
86
- semanticContentAttribute = attribute
87
- collectionView.semanticContentAttribute = attribute
88
- collectionView.collectionViewLayout.invalidateLayout()
86
+ UIView.performWithoutAnimation {
87
+ semanticContentAttribute = attribute
88
+ collectionView.semanticContentAttribute = attribute
89
+ collectionView.collectionViewLayout.invalidateLayout()
90
+ collectionView.layoutIfNeeded()
91
+ }
89
92
  }
90
93
 
91
94
  func selectCategory(at index: Int) {
92
95
  guard !isSearchActive, index != selectedIndex, index >= 0, index < categoryKeys.count else { return }
93
96
  selectedIndex = index
94
- collectionView.reloadData()
95
- collectionView.scrollToItem(
96
- at: IndexPath(item: index, section: 0),
97
- at: .centeredHorizontally,
98
- animated: true
99
- )
97
+ reloadCategories()
98
+ scrollToCategoryIfNeeded(at: index)
100
99
  }
101
100
 
102
101
  func setSearchActive(_ active: Bool) {
103
102
  isSearchActive = active
104
- collectionView.reloadData()
103
+ reloadCategories()
104
+ }
105
+
106
+ private func reloadCategories() {
107
+ UIView.performWithoutAnimation {
108
+ collectionView.reloadData()
109
+ collectionView.layoutIfNeeded()
110
+ }
111
+ }
112
+
113
+ private func scrollToCategoryIfNeeded(at index: Int) {
114
+ guard index >= 0, index < categoryKeys.count else { return }
115
+
116
+ let indexPath = IndexPath(item: index, section: 0)
117
+ guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else {
118
+ return
119
+ }
120
+
121
+ let visibleBounds = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
122
+ guard !visibleBounds.contains(attributes.frame) else {
123
+ return
124
+ }
125
+
126
+ collectionView.scrollToItem(
127
+ at: indexPath,
128
+ at: .centeredHorizontally,
129
+ animated: false
130
+ )
105
131
  }
106
132
 
107
133
  // MARK: - UICollectionViewDataSource
@@ -134,7 +160,7 @@ class EmojiCategoryStrip: UIView, UICollectionViewDataSource, UICollectionViewDe
134
160
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
135
161
  selectedIndex = indexPath.item
136
162
  isSearchActive = false
137
- collectionView.reloadData()
163
+ reloadCategories()
138
164
  delegate?.categoryStrip(self, didSelectCategoryAt: indexPath.item)
139
165
  }
140
166
  }
@@ -357,7 +357,8 @@ class EmojiGridView: UIView, UICollectionViewDataSource, UICollectionViewDelegat
357
357
  dismissSkinTonePicker()
358
358
 
359
359
  guard let cell = collectionView.cellForItem(at: indexPath) else { return }
360
- let cellFrameInSelf = collectionView.convert(cell.frame, to: self)
360
+ let targetView = superview ?? self
361
+ let cellFrameInTarget = collectionView.convert(cell.frame, to: targetView)
361
362
 
362
363
  if enableHaptics {
363
364
  impactFeedbackMedium.impactOccurred()
@@ -381,19 +382,19 @@ class EmojiGridView: UIView, UICollectionViewDataSource, UICollectionViewDelegat
381
382
  self.dismissSkinTonePicker()
382
383
  }
383
384
 
384
- addSubview(picker)
385
+ targetView.addSubview(picker)
385
386
  picker.translatesAutoresizingMaskIntoConstraints = false
386
387
 
387
388
  let pickerWidth: CGFloat = 6 * 48 + 16
388
389
  let pickerHeight: CGFloat = 56
389
390
 
390
- var centerX = cellFrameInSelf.midX
391
+ var centerX = cellFrameInTarget.midX
391
392
  let halfWidth = pickerWidth / 2
392
- centerX = max(halfWidth + 4, min(bounds.width - halfWidth - 4, centerX))
393
+ centerX = max(halfWidth + 8, min(targetView.bounds.width - halfWidth - 8, centerX))
393
394
 
394
395
  NSLayoutConstraint.activate([
395
- picker.centerXAnchor.constraint(equalTo: leadingAnchor, constant: centerX),
396
- picker.bottomAnchor.constraint(equalTo: topAnchor, constant: cellFrameInSelf.minY - 4),
396
+ picker.centerXAnchor.constraint(equalTo: targetView.leftAnchor, constant: centerX),
397
+ picker.bottomAnchor.constraint(equalTo: targetView.topAnchor, constant: cellFrameInTarget.minY - 4),
397
398
  picker.widthAnchor.constraint(equalToConstant: pickerWidth),
398
399
  picker.heightAnchor.constraint(equalToConstant: pickerHeight),
399
400
  ])
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'EmojiSheetModule'
3
- s.version = '2.0.1'
3
+ s.version = '2.0.3'
4
4
  s.summary = 'Native emoji picker bottom sheet for React Native'
5
5
  s.description = 'A fully native iOS/Android emoji picker presented in a bottom sheet with search, skin tones, and theming support.'
6
6
  s.author = ''
@@ -96,6 +96,15 @@ public class EmojiSheetModule: Module {
96
96
  // MARK: - Presentation via dedicated UIWindow
97
97
 
98
98
  private func presentSheet(options: [String: Any], promise: Promise) {
99
+ guard currentPromise == nil, sheetViewController == nil else {
100
+ promise.resolve(["cancelled": true])
101
+ return
102
+ }
103
+
104
+ if overlayWindow != nil {
105
+ tearDownWindow()
106
+ }
107
+
99
108
  guard let windowScene = UIApplication.shared.connectedScenes
100
109
  .compactMap({ $0 as? UIWindowScene })
101
110
  .first(where: { $0.activationState == .foregroundActive })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-native-sheet-emojis",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "A fully native emoji picker bottom sheet for React Native. Built with Swift and Kotlin for maximum performance. Features search with multilingual keywords, skin tones, frequently used tracking, theming, and configurable layout.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -32,7 +32,7 @@
32
32
  "input"
33
33
  ],
34
34
  "license": "MIT",
35
- "author": "Efstathios Ntonas <efstathiosntonas@gmail.com> (https://github.com/efstathiosntonas)",
35
+ "author": "Efstathios Ntonas",
36
36
  "homepage": "https://github.com/efstathiosntonas/expo-native-sheet-emojis#readme",
37
37
  "bugs": {
38
38
  "url": "https://github.com/efstathiosntonas/expo-native-sheet-emojis/issues"