@thiagobueno/rn-selectable-text 1.0.1 → 1.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
@@ -1,23 +1,26 @@
1
- Disclaimer -
2
-
3
- I tested this code in my own projects, but this code has been with heavy assistance from Claude Code. If you see a problem - submit a ticket!
4
-
5
1
  # rn-selectable-text
6
2
 
7
- A React Native library for custom text selection menus, redesigned from the ground up for React Native 0.81.1 with full support for the new architecture (Fabric).
3
+ [![npm version](https://badge.fury.io/js/@thiagobueno%2Frn-selectable-text.svg)](https://badge.fury.io/js/@thiagobueno%2Frn-selectable-text)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A highly stable, Fabric-compatible React Native library for custom text selection menus. Redesigned from the ground up for **React Native 0.81+** and the **New Architecture**.
8
7
 
9
- The `SelectableTextView` component wraps your text content and provides custom menu options that appear when users select text. It supports nested text styling and cross-platform event handling.
8
+ The `SelectableTextView` component wraps your text content and provides custom native menu options that appear when users select text. It elegantly handles nested text styling, cross-platform event handling, and modern OS requirements.
10
9
 
11
- ## Features
10
+ ## 🚀 Why use this package? (Key Fixes)
12
11
 
13
- - Cross-platform support (iOS & Android)
14
- - Support for nested text with different styles
15
- - Custom menu options with callback handling
12
+ This library was heavily refactored to solve critical issues present in older selectable text libraries:
13
+ - **True Fabric Support:** Fully compatible with React Native's New Architecture (C++ Codegen).
14
+ - **iOS 16+ Stability:** Replaces the deprecated and crash-prone `UIMenuController` with the modern `UIEditMenuInteraction` API.
15
+ - **Bypasses iOS Menu Suppression:** Safely mocks copy selectors to prevent the modern iOS engine from suppressing your custom menu.
16
+ - **View Recycling Poisoning Fix:** Solves the severe Fabric bug where sibling UI elements (like icons and buttons) would randomly disappear from the screen due to improper native state cleanups.
16
17
 
17
- ## Installation
18
+ ## 📦 Installation
18
19
 
19
20
  ```sh
20
21
  yarn add @thiagobueno/rn-selectable-text
22
+ # or
23
+ npm install @thiagobueno/rn-selectable-text
21
24
  ```
22
25
 
23
26
  For iOS, run pod install:
@@ -25,12 +28,12 @@ For iOS, run pod install:
25
28
  cd ios && pod install
26
29
  ```
27
30
 
28
- ## Usage
31
+ ## 🛠 Usage
29
32
 
30
33
  ### Basic Example
31
34
 
32
35
  ```tsx
33
- import React, { useState } from 'react';
36
+ import React from 'react';
34
37
  import { View, Text, Alert } from 'react-native';
35
38
  import { SelectableTextView } from '@thiagobueno/rn-selectable-text';
36
39
 
@@ -45,48 +48,78 @@ export default function App() {
45
48
  };
46
49
 
47
50
  return (
48
- <View>
49
-
51
+ <View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
50
52
  <SelectableTextView
51
- menuOptions={['look up', 'copy', 'share']}
53
+ menuOptions={['Save', 'Share', 'Copy']}
52
54
  onSelection={handleSelection}
53
- style={{ margin: 20 }}
54
55
  >
55
- <Text>This is simple selectable text</Text>
56
+ <Text style={{ fontSize: 18, color: '#333' }}>
57
+ Highlight this text to see the custom native menu!
58
+ </Text>
56
59
  </SelectableTextView>
57
60
  </View>
58
61
  );
59
62
  }
60
63
  ```
61
64
 
62
- ### Advanced Example with Nested Text Styling
65
+ ### Advanced Example (Nested Text & Index Mapping)
66
+
67
+ When dealing with internationalization (i18n) or dynamic menus, it's highly recommended to map your selections by index rather than relying on the translated string.
63
68
 
64
69
  ```jsx
65
- <SelectableTextView
66
- menuOptions={['Action 1', 'Action 2', 'Custom Action']}
67
- onSelection={handleSelection}
68
- style={{ marginHorizontal: 20 }}
69
- >
70
- <Text style={{ color: 'black', fontSize: 16 }}>
71
- This text is black{' '}
72
- <Text style={{ textDecorationLine: 'underline', color: 'red' }}>
73
- this part is underlined and red
74
- </Text>{' '}
75
- and this is black again. All of it is selectable with custom menu options!
76
- </Text>
77
- </SelectableTextView>
70
+ import React from 'react';
71
+ import { View, Text } from 'react-native';
72
+ import { SelectableTextView } from '@thiagobueno/rn-selectable-text';
73
+
74
+ const MENU_OPTIONS = ['Save Note', 'Edit Text', 'Highlight Content'];
75
+
76
+ export default function AdvancedApp() {
77
+
78
+ const handleSelection = ({ chosenOption, highlightedText }) => {
79
+ const actionIndex = MENU_OPTIONS.indexOf(chosenOption);
80
+
81
+ switch(actionIndex) {
82
+ case 0:
83
+ console.log('Action: Save Note - Text:', highlightedText);
84
+ break;
85
+ case 1:
86
+ console.log('Action: Edit Text - Text:', highlightedText);
87
+ break;
88
+ case 2:
89
+ console.log('Action: Highlight Content - Text:', highlightedText);
90
+ break;
91
+ }
92
+ };
93
+
94
+ return (
95
+ <View style={{ padding: 20 }}>
96
+ <SelectableTextView
97
+ menuOptions={MENU_OPTIONS}
98
+ onSelection={handleSelection}
99
+ >
100
+ <Text style={{ color: 'black', fontSize: 16 }}>
101
+ This text is black, but{' '}
102
+ <Text style={{ fontWeight: 'bold', color: 'blue' }}>
103
+ this part is bold and blue
104
+ </Text>{' '}
105
+ and this is black again. The entire block is selectable!
106
+ </Text>
107
+ </SelectableTextView>
108
+ </View>
109
+ );
110
+ }
78
111
  ```
79
112
 
80
- ## API Reference
113
+ ## 📖 API Reference
81
114
 
82
115
  ### SelectableTextView Props
83
116
 
84
117
  | Prop | Type | Required | Description |
85
118
  | ------------- | --------------------------------- | -------- | ------------------------------------------- |
86
119
  | `children` | `React.ReactNode` | Yes | Text components to make selectable |
87
- | `menuOptions` | `string[]` | Yes | Array of menu option strings |
88
- | `onSelection` | `(event: SelectionEvent) => void` | No | Callback fired when menu option is selected |
89
- | `style` | `ViewStyle` | No | Style object for the container |
120
+ | `menuOptions` | `string[]` | Yes | Array of custom menu option strings |
121
+ | `onSelection` | `(event: SelectionEvent) => void` | No | Callback fired when a menu option is tapped |
122
+ | `style` | `ViewStyle` | No | Style object for the native container |
90
123
 
91
124
  ### SelectionEvent
92
125
 
@@ -94,26 +127,23 @@ The `onSelection` callback receives an event object with:
94
127
 
95
128
  ```typescript
96
129
  interface SelectionEvent {
97
- chosenOption: string; // The menu option that was selected
98
- highlightedText: string; // The text that was highlighted by the user
130
+ chosenOption: string; // The exact string of the menu option selected
131
+ highlightedText: string; // The specific text highlighted by the user
99
132
  }
100
133
  ```
101
134
 
102
- ## Requirements
135
+ ## ⚙️ Requirements
103
136
 
104
137
  - React Native 0.81.1+
105
- - iOS 11.0+
138
+ - iOS 15.1+ (Optimized for modern APIs)
106
139
  - Android API level 21+
107
- - React Native's new architecture (Fabric) enabled
108
-
109
- ## Platform Differences
110
-
111
- The library handles platform differences internally:
112
- - **iOS**: Uses direct event handlers for optimal performance
113
- - **Android**: Uses DeviceEventEmitter for reliable event delivery
140
+ - React Native's New Architecture (Fabric) enabled
114
141
 
115
- Both platforms provide the same API and functionality.
142
+ ## 🔄 Platform Differences
116
143
 
117
- ## License
144
+ The library handles platform differences internally, providing the same API and functionality for both:
145
+ - **iOS**: Uses direct event handlers and the modern `UIEditMenuInteraction` API for optimal performance.
146
+ - **Android**: Uses `DeviceEventEmitter` for reliable event delivery and bridges native selection to the JS thread.
118
147
 
148
+ ## ⚖️ License
119
149
  MIT
@@ -20,6 +20,12 @@ using namespace facebook::react;
20
20
 
21
21
  - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
22
22
  {
23
+ // FIX: We explicitly tell iOS that we can perform standard actions.
24
+ // This prevents the "[UIKitCore] The edit menu did not have performable commands" warning and crash.
25
+ if (action == @selector(copy:) || action == @selector(selectAll:)) {
26
+ return YES;
27
+ }
28
+
23
29
  if (self.parentSelectableTextView) {
24
30
  return [self.parentSelectableTextView canPerformAction:action withSender:sender];
25
31
  }
@@ -46,10 +52,10 @@ using namespace facebook::react;
46
52
  }
47
53
  }
48
54
 
49
- // Intercepta o "Copiar" de teclados físicos e anula a ação
55
+ // Intercepts physical keyboard "Copy" and nullifies the action to keep it custom
50
56
  - (void)copy:(id)sender
51
57
  {
52
- // Bloqueado silenciosamente
58
+ // Silently blocked
53
59
  }
54
60
 
55
61
  @end
@@ -222,9 +228,13 @@ using namespace facebook::react;
222
228
  #pragma mark - UITextViewDelegate
223
229
 
224
230
  // ====================================================================
225
- // A NOVA API DO IOS 16+
231
+ // THE NEW IOS 16+ API
226
232
  // ====================================================================
227
233
  - (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0)) {
234
+
235
+ // FORCING THE FOCUS: ensures the system doesn't dismiss our menu unexpectedly
236
+ [textView becomeFirstResponder];
237
+
228
238
  NSMutableArray<UIMenuElement *> *customActions = [[NSMutableArray alloc] init];
229
239
 
230
240
  for (NSString *option in _menuOptions) {
@@ -234,7 +244,7 @@ using namespace facebook::react;
234
244
  [customActions addObject:action];
235
245
  }
236
246
 
237
- // Retorna apenas nosso menu. A ação "Copiar" nativa exigida pelo sistema é engolida e não aparece.
247
+ // Returns only our custom menu. The native "Copy" action required by the system is swallowed and doesn't appear.
238
248
  return [UIMenu menuWithTitle:@"" children:customActions];
239
249
  }
240
250
 
@@ -291,7 +301,7 @@ using namespace facebook::react;
291
301
  }
292
302
 
293
303
  // ====================================================================
294
- // O TRUQUE PARA FORÇAR O MENU A ABRIR
304
+ // THE TRICK TO FORCE THE MENU TO OPEN AND PREVENT UIKIT WARNINGS
295
305
  // ====================================================================
296
306
  - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
297
307
  {
@@ -300,11 +310,10 @@ using namespace facebook::react;
300
310
  return YES;
301
311
  }
302
312
 
303
- if (@available(iOS 16.0, *)) {
304
- // Dizemos para o iOS que podemos copiar. Isso convence o sistema a não abortar o menu.
305
- if (action == @selector(copy:)) {
306
- return YES;
307
- }
313
+ // We tell iOS that we can perform standard actions.
314
+ // This convinces the system not to abort the menu rendering.
315
+ if (action == @selector(copy:) || action == @selector(selectAll:)) {
316
+ return YES;
308
317
  }
309
318
 
310
319
  return NO;
package/package.json CHANGED
@@ -1,170 +1,167 @@
1
1
  {
2
- "name": "@thiagobueno/rn-selectable-text",
3
- "version": "1.0.1",
4
- "type": "module",
5
- "description": "A library for custom text selection menus",
6
- "main": "./lib/module/index.js",
7
- "types": "./lib/typescript/src/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "source": "./src/index.tsx",
11
- "types": "./lib/typescript/src/index.d.ts",
12
- "default": "./lib/module/index.js"
2
+ "name": "@thiagobueno/rn-selectable-text",
3
+ "version": "1.0.3",
4
+ "type": "module",
5
+ "description": "A library for custom text selection menus",
6
+ "main": "./lib/module/index.js",
7
+ "types": "./lib/typescript/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "source": "./src/index.tsx",
11
+ "types": "./lib/typescript/src/index.d.ts",
12
+ "default": "./lib/module/index.js"
13
+ },
14
+ "./package.json": "./package.json"
13
15
  },
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "src",
18
- "lib",
19
- "android",
20
- "ios",
21
- "cpp",
22
- "*.podspec",
23
- "react-native.config.js",
24
- "!ios/build",
25
- "!android/build",
26
- "!android/gradle",
27
- "!android/gradlew",
28
- "!android/gradlew.bat",
29
- "!android/local.properties",
30
- "!**/__tests__",
31
- "!**/__fixtures__",
32
- "!**/__mocks__",
33
- "!**/.*"
34
- ],
35
- "scripts": {
36
- "example": "yarn workspace rn-selectable-text-example",
37
- "test": "jest",
38
- "typecheck": "tsc",
39
- "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
- "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
41
- "prepare": "bob build",
42
- "release": "release-it --only-version"
43
- },
44
- "keywords": [
45
- "react-native",
46
- "ios",
47
- "android"
48
- ],
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/thiagobueno/rn-selectable-text.git"
52
- },
53
- "author": "Thiago Bueno (https://github.com/thiagobueno)",
54
- "license": "MIT",
55
- "bugs": {
56
- "url": "https://github.com/thiagobueno/rn-selectable-text/issues"
57
- },
58
- "homepage": "https://github.com/thiagobueno/rn-selectable-text#readme",
59
- "publishConfig": {
60
- "registry": "https://registry.npmjs.org/"
61
- },
62
- "devDependencies": {
63
- "@commitlint/config-conventional": "^19.8.1",
64
- "@eslint/compat": "^1.3.2",
65
- "@eslint/eslintrc": "^3.3.1",
66
- "@eslint/js": "^9.35.0",
67
- "@evilmartians/lefthook": "^1.12.3",
68
- "@react-native-community/cli": "20.0.1",
69
- "@react-native/babel-preset": "0.81.1",
70
- "@react-native/eslint-config": "^0.81.1",
71
- "@release-it/conventional-changelog": "^10.0.1",
72
- "@types/jest": "^29.5.14",
73
- "@types/react": "^19.1.0",
74
- "commitlint": "^19.8.1",
75
- "del-cli": "^6.0.0",
76
- "eslint": "^9.35.0",
77
- "eslint-config-prettier": "^10.1.8",
78
- "eslint-plugin-prettier": "^5.5.4",
79
- "jest": "^29.7.0",
80
- "prettier": "^3.6.2",
81
- "react": "19.1.0",
82
- "react-native": "0.81.1",
83
- "react-native-builder-bob": "^0.40.13",
84
- "release-it": "^19.0.4",
85
- "turbo": "^2.5.6",
86
- "typescript": "^5.9.2"
87
- },
88
- "peerDependencies": {
89
- "react": "*",
90
- "react-native": "*"
91
- },
92
- "workspaces": [
93
- "example"
94
- ],
95
- "packageManager": "yarn@3.6.1",
96
- "jest": {
97
- "preset": "react-native",
98
- "modulePathIgnorePatterns": [
99
- "<rootDir>/example/node_modules",
100
- "<rootDir>/lib/"
101
- ]
102
- },
103
- "commitlint": {
104
- "extends": [
105
- "@commitlint/config-conventional"
106
- ]
107
- },
108
- "release-it": {
109
- "git": {
110
- "commitMessage": "chore: release ${version}",
111
- "tagName": "v${version}"
16
+ "files": [
17
+ "src",
18
+ "lib",
19
+ "android",
20
+ "ios",
21
+ "cpp",
22
+ "*.podspec",
23
+ "react-native.config.js",
24
+ "!ios/build",
25
+ "!android/build",
26
+ "!android/gradle",
27
+ "!android/gradlew",
28
+ "!android/gradlew.bat",
29
+ "!android/local.properties",
30
+ "!**/__tests__",
31
+ "!**/__fixtures__",
32
+ "!**/__mocks__",
33
+ "!**/.*"
34
+ ],
35
+ "scripts": {
36
+ "example": "yarn workspace rn-selectable-text-example",
37
+ "test": "jest",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
41
+ "prepare": "bob build",
42
+ "release": "release-it --only-version"
112
43
  },
113
- "npm": {
114
- "publish": true
44
+ "keywords": [
45
+ "react-native",
46
+ "ios",
47
+ "android"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/thiagobueno/rn-selectable-text.git"
115
52
  },
116
- "github": {
117
- "release": true
53
+ "author": "Thiago Bueno (https://github.com/thiagobueno)",
54
+ "license": "MIT",
55
+ "bugs": {
56
+ "url": "https://github.com/thiagobueno/rn-selectable-text/issues"
118
57
  },
119
- "plugins": {
120
- "@release-it/conventional-changelog": {
121
- "preset": {
122
- "name": "angular"
123
- }
124
- }
125
- }
126
- },
127
- "prettier": {
128
- "quoteProps": "consistent",
129
- "singleQuote": true,
130
- "tabWidth": 2,
131
- "trailingComma": "es5",
132
- "useTabs": false
133
- },
134
- "react-native-builder-bob": {
135
- "source": "src",
136
- "output": "lib",
137
- "targets": [
138
- [
139
- "module",
140
- {
141
- "esm": true
58
+ "homepage": "https://github.com/thiagobueno/rn-selectable-text#readme",
59
+ "devDependencies": {
60
+ "@commitlint/config-conventional": "^19.8.1",
61
+ "@eslint/compat": "^1.3.2",
62
+ "@eslint/eslintrc": "^3.3.1",
63
+ "@eslint/js": "^9.35.0",
64
+ "@evilmartians/lefthook": "^1.12.3",
65
+ "@react-native-community/cli": "20.0.1",
66
+ "@react-native/babel-preset": "0.81.1",
67
+ "@react-native/eslint-config": "^0.81.1",
68
+ "@release-it/conventional-changelog": "^10.0.1",
69
+ "@types/jest": "^29.5.14",
70
+ "@types/react": "^19.1.0",
71
+ "commitlint": "^19.8.1",
72
+ "del-cli": "^6.0.0",
73
+ "eslint": "^9.35.0",
74
+ "eslint-config-prettier": "^10.1.8",
75
+ "eslint-plugin-prettier": "^5.5.4",
76
+ "jest": "^29.7.0",
77
+ "prettier": "^3.6.2",
78
+ "react": "19.1.0",
79
+ "react-native": "0.81.1",
80
+ "react-native-builder-bob": "^0.40.13",
81
+ "release-it": "^19.0.4",
82
+ "turbo": "^2.5.6",
83
+ "typescript": "^5.9.2"
84
+ },
85
+ "peerDependencies": {
86
+ "react": "*",
87
+ "react-native": "*"
88
+ },
89
+ "workspaces": [
90
+ "example"
91
+ ],
92
+ "packageManager": "yarn@3.6.1",
93
+ "jest": {
94
+ "preset": "react-native",
95
+ "modulePathIgnorePatterns": [
96
+ "<rootDir>/example/node_modules",
97
+ "<rootDir>/lib/"
98
+ ]
99
+ },
100
+ "commitlint": {
101
+ "extends": [
102
+ "@commitlint/config-conventional"
103
+ ]
104
+ },
105
+ "release-it": {
106
+ "git": {
107
+ "commitMessage": "chore: release ${version}",
108
+ "tagName": "v${version}"
109
+ },
110
+ "npm": {
111
+ "publish": true
112
+ },
113
+ "github": {
114
+ "release": true
115
+ },
116
+ "plugins": {
117
+ "@release-it/conventional-changelog": {
118
+ "preset": {
119
+ "name": "angular"
120
+ }
121
+ }
142
122
  }
143
- ],
144
- [
145
- "typescript",
146
- {
147
- "project": "tsconfig.build.json"
123
+ },
124
+ "prettier": {
125
+ "quoteProps": "consistent",
126
+ "singleQuote": true,
127
+ "tabWidth": 2,
128
+ "trailingComma": "es5",
129
+ "useTabs": false
130
+ },
131
+ "react-native-builder-bob": {
132
+ "source": "src",
133
+ "output": "lib",
134
+ "targets": [
135
+ [
136
+ "module",
137
+ {
138
+ "esm": true
139
+ }
140
+ ],
141
+ [
142
+ "typescript",
143
+ {
144
+ "project": "tsconfig.build.json"
145
+ }
146
+ ]
147
+ ]
148
+ },
149
+ "codegenConfig": {
150
+ "name": "SelectableTextViewSpec",
151
+ "type": "all",
152
+ "jsSrcsDir": "src",
153
+ "android": {
154
+ "javaPackageName": "com.selectabletext"
155
+ },
156
+ "ios": {
157
+ "componentProvider": {
158
+ "SelectableTextView": "SelectableTextView"
159
+ }
148
160
  }
149
- ]
150
- ]
151
- },
152
- "codegenConfig": {
153
- "name": "SelectableTextViewSpec",
154
- "type": "all",
155
- "jsSrcsDir": "src",
156
- "android": {
157
- "javaPackageName": "com.selectabletext"
158
161
  },
159
- "ios": {
160
- "componentProvider": {
161
- "SelectableTextView": "SelectableTextView"
162
- }
162
+ "create-react-native-library": {
163
+ "languages": "kotlin-objc",
164
+ "type": "fabric-view",
165
+ "version": "0.54.2"
163
166
  }
164
- },
165
- "create-react-native-library": {
166
- "languages": "kotlin-objc",
167
- "type": "fabric-view",
168
- "version": "0.54.2"
169
- }
170
- }
167
+ }