@quyvt.dev/react-native-native-menu 0.1.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.
- package/LICENSE +20 -0
- package/README.md +109 -0
- package/ios/NativeMenuButtonComponentView.mm +78 -0
- package/ios/NativeMenuButtonView.swift +64 -0
- package/lib/module/NativeMenuButton.ios.js +18 -0
- package/lib/module/NativeMenuButton.ios.js.map +1 -0
- package/lib/module/NativeMenuButton.js +5 -0
- package/lib/module/NativeMenuButton.js.map +1 -0
- package/lib/module/NativeMenuButtonNativeComponent.ts +19 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeMenuButton.d.ts +5 -0
- package/lib/typescript/src/NativeMenuButton.d.ts.map +1 -0
- package/lib/typescript/src/NativeMenuButton.ios.d.ts +15 -0
- package/lib/typescript/src/NativeMenuButton.ios.d.ts.map +1 -0
- package/lib/typescript/src/NativeMenuButtonNativeComponent.d.ts +17 -0
- package/lib/typescript/src/NativeMenuButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +125 -0
- package/react-native-native-menu.podspec +19 -0
- package/src/NativeMenuButton.ios.tsx +34 -0
- package/src/NativeMenuButton.tsx +6 -0
- package/src/NativeMenuButtonNativeComponent.ts +19 -0
- package/src/index.tsx +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quy Vo
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @quyvt.dev/react-native-native-menu
|
|
2
|
+
|
|
3
|
+
Fabric-only native system menus for React Native. iOS uses UIKit `UIMenu`; Android intentionally leaves fallback UI to the consuming app.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- React Native with the New Architecture enabled
|
|
8
|
+
- iOS with CocoaPods
|
|
9
|
+
- iOS 15.1 or newer when using the current React Native scaffold defaults
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @quyvt.dev/react-native-native-menu
|
|
15
|
+
cd ios && pod install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Rebuild the native application after installation.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Render your visual trigger, then place `NativeMenuButton` over it on iOS. Android receives a no-op component, so the app owns its fallback interaction.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Platform, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
26
|
+
import {
|
|
27
|
+
NativeMenuButton,
|
|
28
|
+
type NativeMenuItem,
|
|
29
|
+
} from '@quyvt.dev/react-native-native-menu';
|
|
30
|
+
|
|
31
|
+
const items: NativeMenuItem[] = [
|
|
32
|
+
{ id: 'preview', label: 'Preview' },
|
|
33
|
+
{ id: 'disabled', label: 'Unavailable', disabled: true },
|
|
34
|
+
{ id: 'delete', label: 'Delete', destructive: true },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function MenuTrigger() {
|
|
38
|
+
const handleSelect = (id: string) => {
|
|
39
|
+
console.log(id);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.trigger}>
|
|
44
|
+
<Text>•••</Text>
|
|
45
|
+
{Platform.OS === 'ios' ? (
|
|
46
|
+
<NativeMenuButton
|
|
47
|
+
accessibilityLabel="More options"
|
|
48
|
+
items={items}
|
|
49
|
+
style={StyleSheet.absoluteFill}
|
|
50
|
+
onSelect={handleSelect}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<Pressable
|
|
54
|
+
accessibilityLabel="More options"
|
|
55
|
+
style={StyleSheet.absoluteFill}
|
|
56
|
+
onPress={() => openAndroidMenu(items, handleSelect)}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
trigger: { width: 48, height: 48 },
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
### `NativeMenuButton`
|
|
71
|
+
|
|
72
|
+
A transparent native touch surface. Give it non-zero dimensions and position it over your app-owned visual trigger.
|
|
73
|
+
|
|
74
|
+
### `NativeMenuButtonProps`
|
|
75
|
+
|
|
76
|
+
| Prop | Type | Description |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `accessibilityLabel` | `string?` | Accessibility label applied to the native iOS button. |
|
|
79
|
+
| `items` | `ReadonlyArray<NativeMenuItem>` | Ordered native menu actions. |
|
|
80
|
+
| `onSelect` | `(id: string) => void` | Called immediately with the selected item ID. |
|
|
81
|
+
| `style` | `StyleProp<ViewStyle>?` | Layout for the transparent touch surface. |
|
|
82
|
+
|
|
83
|
+
### `NativeMenuItem`
|
|
84
|
+
|
|
85
|
+
| Field | Type | Description |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `id` | `string` | Stable value delivered to `onSelect`. |
|
|
88
|
+
| `label` | `string` | Native action title. |
|
|
89
|
+
| `destructive` | `boolean?` | Applies UIKit destructive styling. |
|
|
90
|
+
| `disabled` | `boolean?` | Prevents selection. |
|
|
91
|
+
|
|
92
|
+
## Platform behavior
|
|
93
|
+
|
|
94
|
+
- **iOS:** Uses a Fabric component backed by `UIButton`, `UIAction`, and `UIMenu`. Item order is preserved and prop updates rebuild the native menu.
|
|
95
|
+
- **System appearance:** UIKit supplies the native animation and material. Liquid Glass appears automatically on operating systems and SDKs that provide it; this package does not emulate that effect.
|
|
96
|
+
- **Version 0.1.0 theme:** The native view uses dark interface style by default.
|
|
97
|
+
- **Android:** The exported component renders `null`. The consuming app must render its own trigger and fallback menu.
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
- Ensure the React Native New Architecture is enabled; legacy architecture view managers are not included.
|
|
102
|
+
- Run `pod install` in the app's `ios` directory after adding or upgrading the package.
|
|
103
|
+
- Rebuild the native app instead of relying on JavaScript reloads after installation.
|
|
104
|
+
- Give `NativeMenuButton` non-zero width and height, usually with `StyleSheet.absoluteFill` over a sized trigger.
|
|
105
|
+
- If the view is not registered, clean the iOS build and reinstall pods so Codegen can regenerate the Fabric component provider.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <react/renderer/components/VTNativeMenuButtonSpec/ComponentDescriptors.h>
|
|
3
|
+
#import <react/renderer/components/VTNativeMenuButtonSpec/EventEmitters.h>
|
|
4
|
+
#import <react/renderer/components/VTNativeMenuButtonSpec/Props.h>
|
|
5
|
+
#import <react/renderer/components/VTNativeMenuButtonSpec/RCTComponentViewHelpers.h>
|
|
6
|
+
|
|
7
|
+
using namespace facebook::react;
|
|
8
|
+
|
|
9
|
+
@interface VTNativeMenuButtonView : UIView
|
|
10
|
+
@property (nonatomic, copy) NSArray *items;
|
|
11
|
+
@property (nonatomic, copy, nullable) NSString *menuAccessibilityLabel;
|
|
12
|
+
@property (nonatomic, copy, nullable) void (^onSelect)(NSString *itemId);
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@interface VTNativeMenuButton : RCTViewComponentView <RCTVTNativeMenuButtonViewProtocol>
|
|
16
|
+
@end
|
|
17
|
+
|
|
18
|
+
@implementation VTNativeMenuButton {
|
|
19
|
+
VTNativeMenuButtonView *_menuButtonView;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
23
|
+
{
|
|
24
|
+
return concreteComponentDescriptorProvider<VTNativeMenuButtonComponentDescriptor>();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
28
|
+
{
|
|
29
|
+
if (self = [super initWithFrame:frame]) {
|
|
30
|
+
static const auto defaultProps = std::make_shared<const VTNativeMenuButtonProps>();
|
|
31
|
+
_props = defaultProps;
|
|
32
|
+
|
|
33
|
+
_menuButtonView = [[VTNativeMenuButtonView alloc] initWithFrame:frame];
|
|
34
|
+
|
|
35
|
+
__weak VTNativeMenuButton *weakSelf = self;
|
|
36
|
+
_menuButtonView.onSelect = ^(NSString *itemId) {
|
|
37
|
+
VTNativeMenuButton *strongSelf = weakSelf;
|
|
38
|
+
if (!strongSelf || !strongSelf->_eventEmitter) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
auto eventEmitter = std::static_pointer_cast<const VTNativeMenuButtonEventEmitter>(
|
|
43
|
+
strongSelf->_eventEmitter);
|
|
44
|
+
eventEmitter->onSelect(VTNativeMenuButtonEventEmitter::OnSelect{
|
|
45
|
+
.id = std::string(itemId.UTF8String),
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
self.contentView = _menuButtonView;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return self;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
56
|
+
{
|
|
57
|
+
const auto &newMenuProps = *std::static_pointer_cast<const VTNativeMenuButtonProps>(props);
|
|
58
|
+
NSMutableArray<NSDictionary *> *items =
|
|
59
|
+
[NSMutableArray arrayWithCapacity:newMenuProps.items.size()];
|
|
60
|
+
|
|
61
|
+
for (const auto &item : newMenuProps.items) {
|
|
62
|
+
[items addObject:@{
|
|
63
|
+
@"destructive" : @(item.destructive),
|
|
64
|
+
@"disabled" : @(item.disabled),
|
|
65
|
+
@"id" : [NSString stringWithUTF8String:item.id.c_str()],
|
|
66
|
+
@"label" : [NSString stringWithUTF8String:item.label.c_str()],
|
|
67
|
+
}];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_menuButtonView.items = items;
|
|
71
|
+
_menuButtonView.menuAccessibilityLabel = newMenuProps.menuAccessibilityLabel.empty()
|
|
72
|
+
? nil
|
|
73
|
+
: [NSString stringWithUTF8String:newMenuProps.menuAccessibilityLabel.c_str()];
|
|
74
|
+
|
|
75
|
+
[super updateProps:props oldProps:oldProps];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@objc(VTNativeMenuButtonView)
|
|
4
|
+
final class VTNativeMenuButtonView: UIView {
|
|
5
|
+
private let button = UIButton(type: .system)
|
|
6
|
+
|
|
7
|
+
@objc var items: NSArray = [] {
|
|
8
|
+
didSet { updateMenu() }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@objc var menuAccessibilityLabel: NSString? {
|
|
12
|
+
didSet { button.accessibilityLabel = menuAccessibilityLabel as String? }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@objc var onSelect: ((NSString) -> Void)?
|
|
16
|
+
|
|
17
|
+
override init(frame: CGRect) {
|
|
18
|
+
super.init(frame: frame)
|
|
19
|
+
|
|
20
|
+
overrideUserInterfaceStyle = .dark
|
|
21
|
+
button.backgroundColor = .clear
|
|
22
|
+
button.overrideUserInterfaceStyle = .dark
|
|
23
|
+
button.showsMenuAsPrimaryAction = true
|
|
24
|
+
addSubview(button)
|
|
25
|
+
updateMenu()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
required init?(coder: NSCoder) {
|
|
29
|
+
fatalError("init(coder:) has not been implemented")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override func layoutSubviews() {
|
|
33
|
+
super.layoutSubviews()
|
|
34
|
+
button.frame = bounds
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private func updateMenu() {
|
|
38
|
+
let actions = items.compactMap { value -> UIAction? in
|
|
39
|
+
guard
|
|
40
|
+
let item = value as? NSDictionary,
|
|
41
|
+
let id = item["id"] as? String,
|
|
42
|
+
let label = item["label"] as? String
|
|
43
|
+
else {
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var attributes: UIMenuElement.Attributes = []
|
|
48
|
+
|
|
49
|
+
if item["destructive"] as? Bool == true {
|
|
50
|
+
attributes.insert(.destructive)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if item["disabled"] as? Bool == true {
|
|
54
|
+
attributes.insert(.disabled)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return UIAction(title: label, attributes: attributes) { [weak self] _ in
|
|
58
|
+
self?.onSelect?(id as NSString)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
button.menu = UIMenu(children: actions)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import NativeMenuButtonComponent from './NativeMenuButtonNativeComponent';
|
|
4
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
export const getNativeMenuItemId = event => event.nativeEvent.id;
|
|
6
|
+
const NativeMenuButton = ({
|
|
7
|
+
accessibilityLabel,
|
|
8
|
+
items,
|
|
9
|
+
onSelect,
|
|
10
|
+
style
|
|
11
|
+
}) => /*#__PURE__*/_jsx(NativeMenuButtonComponent, {
|
|
12
|
+
items: items,
|
|
13
|
+
menuAccessibilityLabel: accessibilityLabel,
|
|
14
|
+
style: style,
|
|
15
|
+
onSelect: event => onSelect(getNativeMenuItemId(event))
|
|
16
|
+
});
|
|
17
|
+
export default NativeMenuButton;
|
|
18
|
+
//# sourceMappingURL=NativeMenuButton.ios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeMenuButtonComponent","jsx","_jsx","getNativeMenuItemId","event","nativeEvent","id","NativeMenuButton","accessibilityLabel","items","onSelect","style","menuAccessibilityLabel"],"sourceRoot":"../../src","sources":["NativeMenuButton.ios.tsx"],"mappings":";;AAEA,OAAOA,yBAAyB,MAAM,mCAAmC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAY1E,OAAO,MAAMC,mBAAmB,GAAIC,KAA4B,IAC9DA,KAAK,CAACC,WAAW,CAACC,EAAE;AAEtB,MAAMC,gBAAgB,GAAGA,CAAC;EACxBC,kBAAkB;EAClBC,KAAK;EACLC,QAAQ;EACRC;AACqB,CAAC,kBACtBT,IAAA,CAACF,yBAAyB;EACxBS,KAAK,EAAEA,KAAM;EACbG,sBAAsB,EAAEJ,kBAAmB;EAC3CG,KAAK,EAAEA,KAAM;EACbD,QAAQ,EAAGN,KAA4B,IACrCM,QAAQ,CAACP,mBAAmB,CAACC,KAAK,CAAC;AACpC,CACF,CACF;AAED,eAAeG,gBAAgB","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeMenuButton","_props"],"sourceRoot":"../../src","sources":["NativeMenuButton.tsx"],"mappings":";;AAEA,MAAMA,gBAAgB,GAAIC,MAA6B,IAAK,IAAI;AAEhE,eAAeD,gBAAgB","ignoreList":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { codegenNativeComponent } from 'react-native';
|
|
2
|
+
import type { CodegenTypes, HostComponent, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type NativeMenuItem = Readonly<{
|
|
5
|
+
destructive?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export interface NativeProps extends ViewProps {
|
|
12
|
+
items: ReadonlyArray<NativeMenuItem>;
|
|
13
|
+
menuAccessibilityLabel?: string;
|
|
14
|
+
onSelect?: CodegenTypes.BubblingEventHandler<Readonly<{ id: string }>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default codegenNativeComponent<NativeProps>(
|
|
18
|
+
'VTNativeMenuButton'
|
|
19
|
+
) as HostComponent<NativeProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["default","NativeMenuButton"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,OAAO,IAAIC,gBAAgB,QAAQ,oBAAoB","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeMenuButton.d.ts","sourceRoot":"","sources":["../../../src/NativeMenuButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAAwB,CAAC;AAEpE,QAAA,MAAM,gBAAgB,GAAI,QAAQ,qBAAqB,SAAS,CAAC;AAEjE,eAAe,gBAAgB,CAAC;AAChC,YAAY,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { NativeMenuItem } from './NativeMenuButtonNativeComponent';
|
|
3
|
+
type NativeMenuSelectEvent = NativeSyntheticEvent<{
|
|
4
|
+
id: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type NativeMenuButtonProps = {
|
|
7
|
+
accessibilityLabel?: string;
|
|
8
|
+
items: ReadonlyArray<NativeMenuItem>;
|
|
9
|
+
onSelect: (id: string) => void;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
};
|
|
12
|
+
export declare const getNativeMenuItemId: (event: NativeMenuSelectEvent) => string;
|
|
13
|
+
declare const NativeMenuButton: ({ accessibilityLabel, items, onSelect, style, }: NativeMenuButtonProps) => import("react").JSX.Element;
|
|
14
|
+
export default NativeMenuButton;
|
|
15
|
+
//# sourceMappingURL=NativeMenuButton.ios.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeMenuButton.ios.d.ts","sourceRoot":"","sources":["../../../src/NativeMenuButton.ios.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,KAAK,qBAAqB,GAAG,oBAAoB,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAElE,MAAM,MAAM,qBAAqB,GAAG;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACrC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,qBAAqB,WAC1C,CAAC;AAEvB,QAAA,MAAM,gBAAgB,GAAI,iDAKvB,qBAAqB,gCASvB,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CodegenTypes, HostComponent, ViewProps } from 'react-native';
|
|
2
|
+
export type NativeMenuItem = Readonly<{
|
|
3
|
+
destructive?: boolean;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}>;
|
|
8
|
+
export interface NativeProps extends ViewProps {
|
|
9
|
+
items: ReadonlyArray<NativeMenuItem>;
|
|
10
|
+
menuAccessibilityLabel?: string;
|
|
11
|
+
onSelect?: CodegenTypes.BubblingEventHandler<Readonly<{
|
|
12
|
+
id: string;
|
|
13
|
+
}>>;
|
|
14
|
+
}
|
|
15
|
+
declare const _default: HostComponent<NativeProps>;
|
|
16
|
+
export default _default;
|
|
17
|
+
//# sourceMappingURL=NativeMenuButtonNativeComponent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeMenuButtonNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/NativeMenuButtonNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3E,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,QAAQ,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CACxE;wBAII,aAAa,CAAC,WAAW,CAAC;AAF/B,wBAEgC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quyvt.dev/react-native-native-menu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native system menus for React Native using Fabric and UIKit UIMenu",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"react-native-native-menu-source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"!src/__tests__",
|
|
18
|
+
"lib",
|
|
19
|
+
"ios",
|
|
20
|
+
"react-native-native-menu.podspec",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"example": "npm --workspace @quyvt.dev/react-native-native-menu-example run",
|
|
26
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
27
|
+
"prepare": "bob build",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"typecheck": "tsc",
|
|
30
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"react-native",
|
|
34
|
+
"ios",
|
|
35
|
+
"android"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/vtquy98/react-native-native-menu.git"
|
|
40
|
+
},
|
|
41
|
+
"author": "Quy Vo <vtquy98@gmail.com> (https://github.com/vtquy98)",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/vtquy98/react-native-native-menu/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/vtquy98/react-native-native-menu#readme",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/compat": "^2.1.0",
|
|
53
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
54
|
+
"@eslint/js": "^9.39.5",
|
|
55
|
+
"@react-native/babel-preset": "0.85.3",
|
|
56
|
+
"@react-native/eslint-config": "0.85.0",
|
|
57
|
+
"@types/react": "^19.2.0",
|
|
58
|
+
"@types/jest": "^29.5.14",
|
|
59
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
60
|
+
"del-cli": "^7.0.0",
|
|
61
|
+
"eslint": "^9.39.4",
|
|
62
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
64
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
65
|
+
"jest": "^29.7.0",
|
|
66
|
+
"prettier": "^3.8.3",
|
|
67
|
+
"react": "19.2.3",
|
|
68
|
+
"react-native": "0.85.3",
|
|
69
|
+
"react-native-builder-bob": "^0.43.0",
|
|
70
|
+
"react-test-renderer": "19.2.3",
|
|
71
|
+
"turbo": "^2.9.16",
|
|
72
|
+
"typescript": "^6.0.3"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"react": "*",
|
|
76
|
+
"react-native": "*"
|
|
77
|
+
},
|
|
78
|
+
"workspaces": [
|
|
79
|
+
"example"
|
|
80
|
+
],
|
|
81
|
+
"packageManager": "npm@11.4.2",
|
|
82
|
+
"react-native-builder-bob": {
|
|
83
|
+
"source": "src",
|
|
84
|
+
"output": "lib",
|
|
85
|
+
"targets": [
|
|
86
|
+
[
|
|
87
|
+
"module",
|
|
88
|
+
{
|
|
89
|
+
"esm": true
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"typescript",
|
|
94
|
+
{
|
|
95
|
+
"project": "tsconfig.build.json"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"codegenConfig": {
|
|
101
|
+
"name": "VTNativeMenuButtonSpec",
|
|
102
|
+
"type": "components",
|
|
103
|
+
"jsSrcsDir": "src",
|
|
104
|
+
"ios": {
|
|
105
|
+
"componentProvider": {
|
|
106
|
+
"VTNativeMenuButton": "VTNativeMenuButton"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"prettier": {
|
|
111
|
+
"quoteProps": "consistent",
|
|
112
|
+
"singleQuote": true,
|
|
113
|
+
"tabWidth": 2,
|
|
114
|
+
"trailingComma": "es5",
|
|
115
|
+
"useTabs": false
|
|
116
|
+
},
|
|
117
|
+
"create-react-native-library": {
|
|
118
|
+
"type": "fabric-view",
|
|
119
|
+
"languages": "kotlin-objc",
|
|
120
|
+
"tools": [
|
|
121
|
+
"eslint"
|
|
122
|
+
],
|
|
123
|
+
"version": "0.63.0"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-native-menu"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/vtquy98/react-native-native-menu.git", :tag => s.version.to_s }
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
16
|
+
s.swift_version = "5.0"
|
|
17
|
+
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import NativeMenuButtonComponent from './NativeMenuButtonNativeComponent';
|
|
4
|
+
import type { NativeMenuItem } from './NativeMenuButtonNativeComponent';
|
|
5
|
+
|
|
6
|
+
type NativeMenuSelectEvent = NativeSyntheticEvent<{ id: string }>;
|
|
7
|
+
|
|
8
|
+
export type NativeMenuButtonProps = {
|
|
9
|
+
accessibilityLabel?: string;
|
|
10
|
+
items: ReadonlyArray<NativeMenuItem>;
|
|
11
|
+
onSelect: (id: string) => void;
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getNativeMenuItemId = (event: NativeMenuSelectEvent) =>
|
|
16
|
+
event.nativeEvent.id;
|
|
17
|
+
|
|
18
|
+
const NativeMenuButton = ({
|
|
19
|
+
accessibilityLabel,
|
|
20
|
+
items,
|
|
21
|
+
onSelect,
|
|
22
|
+
style,
|
|
23
|
+
}: NativeMenuButtonProps) => (
|
|
24
|
+
<NativeMenuButtonComponent
|
|
25
|
+
items={items}
|
|
26
|
+
menuAccessibilityLabel={accessibilityLabel}
|
|
27
|
+
style={style}
|
|
28
|
+
onSelect={(event: NativeMenuSelectEvent) =>
|
|
29
|
+
onSelect(getNativeMenuItemId(event))
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export default NativeMenuButton;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { codegenNativeComponent } from 'react-native';
|
|
2
|
+
import type { CodegenTypes, HostComponent, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type NativeMenuItem = Readonly<{
|
|
5
|
+
destructive?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export interface NativeProps extends ViewProps {
|
|
12
|
+
items: ReadonlyArray<NativeMenuItem>;
|
|
13
|
+
menuAccessibilityLabel?: string;
|
|
14
|
+
onSelect?: CodegenTypes.BubblingEventHandler<Readonly<{ id: string }>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default codegenNativeComponent<NativeProps>(
|
|
18
|
+
'VTNativeMenuButton'
|
|
19
|
+
) as HostComponent<NativeProps>;
|
package/src/index.tsx
ADDED