react-native-tinyui 0.1.1
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 +172 -0
- package/Tinyui.podspec +85 -0
- package/ios/Core/Tinyui.h +8 -0
- package/ios/Core/Tinyui.mm +30 -0
- package/ios/Core/TinyuiComponentClasses.mm +27 -0
- package/ios/Core/TinyuiHostingHelpers.swift +74 -0
- package/ios/Menu/TinyuiMenuProvider.swift +110 -0
- package/ios/Menu/TinyuiMenuView.h +14 -0
- package/ios/Menu/TinyuiMenuView.mm +120 -0
- package/ios/Menu/TinyuiMenuViewSwift.swift +368 -0
- package/ios/Popover/TinyuiPopoverProvider.swift +111 -0
- package/ios/Popover/TinyuiPopoverView.h +14 -0
- package/ios/Popover/TinyuiPopoverView.mm +124 -0
- package/ios/Popover/TinyuiPopoverViewSwift.swift +88 -0
- package/lib/module/Menu.js +43 -0
- package/lib/module/Menu.js.map +1 -0
- package/lib/module/Menu.types.js +4 -0
- package/lib/module/Menu.types.js.map +1 -0
- package/lib/module/MenuNativeComponent.ts +14 -0
- package/lib/module/MenuOptions.js +73 -0
- package/lib/module/MenuOptions.js.map +1 -0
- package/lib/module/NativeTinyui.js +5 -0
- package/lib/module/NativeTinyui.js.map +1 -0
- package/lib/module/Popover.js +71 -0
- package/lib/module/Popover.js.map +1 -0
- package/lib/module/Popover.types.js +4 -0
- package/lib/module/Popover.types.js.map +1 -0
- package/lib/module/PopoverNativeComponent.ts +13 -0
- package/lib/module/PopoverPrimitives.js +33 -0
- package/lib/module/PopoverPrimitives.js.map +1 -0
- package/lib/module/getNativeTinyui.js +30 -0
- package/lib/module/getNativeTinyui.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react-native.d.js +2 -0
- package/lib/module/react-native.d.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Menu.d.ts +4 -0
- package/lib/typescript/src/Menu.d.ts.map +1 -0
- package/lib/typescript/src/Menu.types.d.ts +63 -0
- package/lib/typescript/src/Menu.types.d.ts.map +1 -0
- package/lib/typescript/src/MenuNativeComponent.d.ts +14 -0
- package/lib/typescript/src/MenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/MenuOptions.d.ts +7 -0
- package/lib/typescript/src/MenuOptions.d.ts.map +1 -0
- package/lib/typescript/src/NativeTinyui.d.ts +11 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -0
- package/lib/typescript/src/Popover.d.ts +7 -0
- package/lib/typescript/src/Popover.d.ts.map +1 -0
- package/lib/typescript/src/Popover.types.d.ts +35 -0
- package/lib/typescript/src/Popover.types.d.ts.map +1 -0
- package/lib/typescript/src/PopoverNativeComponent.d.ts +13 -0
- package/lib/typescript/src/PopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/PopoverPrimitives.d.ts +21 -0
- package/lib/typescript/src/PopoverPrimitives.d.ts.map +1 -0
- package/lib/typescript/src/getNativeTinyui.d.ts +13 -0
- package/lib/typescript/src/getNativeTinyui.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +175 -0
- package/src/Menu.tsx +52 -0
- package/src/Menu.types.ts +77 -0
- package/src/MenuNativeComponent.ts +14 -0
- package/src/MenuOptions.ts +146 -0
- package/src/NativeTinyui.ts +12 -0
- package/src/Popover.tsx +92 -0
- package/src/Popover.types.ts +49 -0
- package/src/PopoverNativeComponent.ts +13 -0
- package/src/PopoverPrimitives.tsx +36 -0
- package/src/getNativeTinyui.ts +38 -0
- package/src/index.tsx +23 -0
- package/src/react-native.d.ts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-tinyui",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Dependency-free native iOS Menu and Popover components for React Native",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"react-native-tinyui-source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./menu": {
|
|
14
|
+
"react-native-tinyui-source": "./src/Menu.tsx",
|
|
15
|
+
"types": "./lib/typescript/src/Menu.d.ts",
|
|
16
|
+
"default": "./lib/module/Menu.js"
|
|
17
|
+
},
|
|
18
|
+
"./popover": {
|
|
19
|
+
"react-native-tinyui-source": "./src/Popover.tsx",
|
|
20
|
+
"types": "./lib/typescript/src/Popover.d.ts",
|
|
21
|
+
"default": "./lib/module/Popover.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"lib",
|
|
28
|
+
"ios",
|
|
29
|
+
"*.podspec",
|
|
30
|
+
"!ios/build",
|
|
31
|
+
"!**/__tests__",
|
|
32
|
+
"!**/__fixtures__",
|
|
33
|
+
"!**/__mocks__",
|
|
34
|
+
"!**/.*"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"example": "yarn workspace react-native-tinyui-example",
|
|
38
|
+
"clean": "del-cli example/ios/build lib",
|
|
39
|
+
"prepare": "bob build",
|
|
40
|
+
"typecheck": "tsc",
|
|
41
|
+
"release": "release-it --only-version",
|
|
42
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
43
|
+
"test": "jest"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"react-native",
|
|
47
|
+
"ios",
|
|
48
|
+
"menu",
|
|
49
|
+
"popover",
|
|
50
|
+
"fabric"
|
|
51
|
+
],
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/luoxuhai/react-native-tinyui.git"
|
|
55
|
+
},
|
|
56
|
+
"author": "Darkce <15186743693@163.com> (https://github.com/luoxuhai)",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/luoxuhai/react-native-tinyui/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/luoxuhai/react-native-tinyui#readme",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"registry": "https://registry.npmjs.org/"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@eslint/compat": "^2.1.0",
|
|
67
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
68
|
+
"@eslint/js": "^10.0.1",
|
|
69
|
+
"@jest/globals": "^29.7.0",
|
|
70
|
+
"@react-native/babel-preset": "0.85.0",
|
|
71
|
+
"@react-native/eslint-config": "0.85.0",
|
|
72
|
+
"@react-native/jest-preset": "0.85.0",
|
|
73
|
+
"@release-it/conventional-changelog": "^11.0.1",
|
|
74
|
+
"@types/react": "^19.2.0",
|
|
75
|
+
"del-cli": "^7.0.0",
|
|
76
|
+
"eslint": "^9.39.4",
|
|
77
|
+
"eslint-config-prettier": "^10.1.8",
|
|
78
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
79
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
80
|
+
"jest": "^29.7.0",
|
|
81
|
+
"prettier": "^3.8.3",
|
|
82
|
+
"react": "19.2.3",
|
|
83
|
+
"react-native": "0.85.0",
|
|
84
|
+
"react-native-builder-bob": "^0.43.0",
|
|
85
|
+
"release-it": "^20.2.0",
|
|
86
|
+
"turbo": "^2.9.16",
|
|
87
|
+
"typescript": "^6.0.3"
|
|
88
|
+
},
|
|
89
|
+
"peerDependencies": {
|
|
90
|
+
"react": "*",
|
|
91
|
+
"react-native": "*"
|
|
92
|
+
},
|
|
93
|
+
"workspaces": [
|
|
94
|
+
"example"
|
|
95
|
+
],
|
|
96
|
+
"packageManager": "yarn@4.11.0",
|
|
97
|
+
"react-native-builder-bob": {
|
|
98
|
+
"source": "src",
|
|
99
|
+
"output": "lib",
|
|
100
|
+
"targets": [
|
|
101
|
+
[
|
|
102
|
+
"module",
|
|
103
|
+
{
|
|
104
|
+
"esm": true
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"typescript",
|
|
109
|
+
{
|
|
110
|
+
"project": "tsconfig.build.json"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"codegenConfig": {
|
|
116
|
+
"name": "TinyuiSpec",
|
|
117
|
+
"type": "all",
|
|
118
|
+
"jsSrcsDir": "src",
|
|
119
|
+
"ios": {
|
|
120
|
+
"modules": {
|
|
121
|
+
"Tinyui": {
|
|
122
|
+
"className": "Tinyui"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"components": {
|
|
126
|
+
"TinyuiMenuView": {
|
|
127
|
+
"className": "TinyuiMenuView"
|
|
128
|
+
},
|
|
129
|
+
"TinyuiPopoverView": {
|
|
130
|
+
"className": "TinyuiPopoverView"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"release-it": {
|
|
136
|
+
"git": {
|
|
137
|
+
"commitMessage": "chore: release ${version}",
|
|
138
|
+
"tagName": "v${version}"
|
|
139
|
+
},
|
|
140
|
+
"npm": {
|
|
141
|
+
"publish": true
|
|
142
|
+
},
|
|
143
|
+
"github": {
|
|
144
|
+
"release": true
|
|
145
|
+
},
|
|
146
|
+
"plugins": {
|
|
147
|
+
"@release-it/conventional-changelog": {
|
|
148
|
+
"preset": {
|
|
149
|
+
"name": "angular"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"prettier": {
|
|
155
|
+
"quoteProps": "consistent",
|
|
156
|
+
"singleQuote": true,
|
|
157
|
+
"tabWidth": 2,
|
|
158
|
+
"trailingComma": "es5",
|
|
159
|
+
"useTabs": false
|
|
160
|
+
},
|
|
161
|
+
"jest": {
|
|
162
|
+
"preset": "@react-native/jest-preset",
|
|
163
|
+
"testEnvironmentOptions": {
|
|
164
|
+
"customExportConditions": [
|
|
165
|
+
"require",
|
|
166
|
+
"react-native",
|
|
167
|
+
"<%- project.sourceCondition -%>"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
"modulePathIgnorePatterns": [
|
|
171
|
+
"<rootDir>/example/node_modules",
|
|
172
|
+
"<rootDir>/lib/"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/Menu.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { forwardRef, type ComponentRef } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { assertComponentEnabled } from './getNativeTinyui';
|
|
5
|
+
import type { MenuComponent, MenuProps } from './Menu.types';
|
|
6
|
+
import NativeMenuView from './MenuNativeComponent';
|
|
7
|
+
import { serializeMenuOptions } from './MenuOptions';
|
|
8
|
+
|
|
9
|
+
const MenuRoot = forwardRef<ComponentRef<typeof View>, MenuProps>(function Menu(
|
|
10
|
+
{
|
|
11
|
+
accessibilityLabel,
|
|
12
|
+
children,
|
|
13
|
+
disabled,
|
|
14
|
+
onPrimaryAction,
|
|
15
|
+
options,
|
|
16
|
+
testID,
|
|
17
|
+
...viewProps
|
|
18
|
+
},
|
|
19
|
+
ref
|
|
20
|
+
) {
|
|
21
|
+
assertComponentEnabled('Menu');
|
|
22
|
+
|
|
23
|
+
const { config, callbacks } = serializeMenuOptions(options);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View {...viewProps} ref={ref} collapsable={false}>
|
|
27
|
+
<NativeMenuView
|
|
28
|
+
accessibilityLabel={accessibilityLabel}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
hasPrimaryAction={onPrimaryAction != null}
|
|
31
|
+
menuConfig={config}
|
|
32
|
+
onItemPress={(event) => callbacks.get(event.nativeEvent.id)?.()}
|
|
33
|
+
onPrimaryAction={onPrimaryAction}
|
|
34
|
+
testID={testID}
|
|
35
|
+
>
|
|
36
|
+
<View collapsable={false}>{children}</View>
|
|
37
|
+
</NativeMenuView>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const Menu = MenuRoot as MenuComponent;
|
|
43
|
+
|
|
44
|
+
export type {
|
|
45
|
+
MenuActionOption,
|
|
46
|
+
MenuDividerOption,
|
|
47
|
+
MenuItemState,
|
|
48
|
+
MenuOption,
|
|
49
|
+
MenuProps,
|
|
50
|
+
MenuSectionOption,
|
|
51
|
+
MenuSubmenuOption,
|
|
52
|
+
} from './Menu.types';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentRef,
|
|
3
|
+
ForwardRefExoticComponent,
|
|
4
|
+
ReactNode,
|
|
5
|
+
RefAttributes,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import type { ColorValue, View, ViewProps } from 'react-native';
|
|
8
|
+
|
|
9
|
+
export type MenuItemState = 'off' | 'on' | 'mixed';
|
|
10
|
+
|
|
11
|
+
interface MenuOptionLabel {
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
/** SF Symbol name. */
|
|
15
|
+
systemImage?: string;
|
|
16
|
+
/** Image name from the containing app's asset catalog. Takes precedence over `systemImage`. */
|
|
17
|
+
icon?: string;
|
|
18
|
+
/** Tint applied to `icon` or `systemImage`. */
|
|
19
|
+
iconColor?: ColorValue;
|
|
20
|
+
/** Foreground color applied to the option title. */
|
|
21
|
+
titleColor?: ColorValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MenuActionOption extends MenuOptionLabel {
|
|
25
|
+
/** Actions are the default option type, so this can be omitted. */
|
|
26
|
+
type?: 'action';
|
|
27
|
+
/** Stable identifier returned by the native selection event. */
|
|
28
|
+
id?: string;
|
|
29
|
+
state?: MenuItemState;
|
|
30
|
+
destructive?: boolean;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
hidden?: boolean;
|
|
33
|
+
/** Keep the menu visible after selection on iOS 16 and newer. */
|
|
34
|
+
keepOpen?: boolean;
|
|
35
|
+
onSelect?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MenuSubmenuOption extends MenuOptionLabel {
|
|
39
|
+
type: 'submenu';
|
|
40
|
+
/** Marks the submenu and its children as destructive. */
|
|
41
|
+
destructive?: boolean;
|
|
42
|
+
/** Prevents the submenu from being opened. */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Removes the submenu from the rendered menu. */
|
|
45
|
+
hidden?: boolean;
|
|
46
|
+
/** Displays the children inline instead of opening a nested menu. */
|
|
47
|
+
displayInline?: boolean;
|
|
48
|
+
options: readonly MenuOption[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MenuSectionOption {
|
|
52
|
+
type: 'section';
|
|
53
|
+
title?: string;
|
|
54
|
+
options: readonly MenuOption[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MenuDividerOption {
|
|
58
|
+
type: 'divider';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type MenuOption =
|
|
62
|
+
MenuActionOption | MenuSubmenuOption | MenuSectionOption | MenuDividerOption;
|
|
63
|
+
|
|
64
|
+
export interface MenuProps extends Omit<ViewProps, 'children'> {
|
|
65
|
+
/** The trigger element. */
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
/** Native menu actions, submenus, sections, and dividers. */
|
|
68
|
+
options: readonly MenuOption[];
|
|
69
|
+
/** Prevents the trigger from opening the menu. */
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
/** Called on a normal tap. When set, a long press opens the menu. */
|
|
72
|
+
onPrimaryAction?: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MenuComponent extends ForwardRefExoticComponent<
|
|
76
|
+
MenuProps & RefAttributes<ComponentRef<typeof View>>
|
|
77
|
+
> {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { codegenNativeComponent, type ViewProps } from 'react-native';
|
|
2
|
+
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
|
|
4
|
+
export interface NativeMenuProps extends ViewProps {
|
|
5
|
+
menuConfig: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
hasPrimaryAction?: boolean;
|
|
8
|
+
onItemPress?: DirectEventHandler<Readonly<{ id: string }>>;
|
|
9
|
+
onPrimaryAction?: DirectEventHandler<null>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default codegenNativeComponent<NativeMenuProps>('TinyuiMenuView', {
|
|
13
|
+
excludedPlatforms: ['android'],
|
|
14
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { processColor, type ColorValue } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { MenuOption } from './Menu.types';
|
|
4
|
+
|
|
5
|
+
type NativeMenuColor = ReturnType<typeof processColor>;
|
|
6
|
+
|
|
7
|
+
interface NativeMenuLabel {
|
|
8
|
+
title: string;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
systemImage?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
iconColor?: NativeMenuColor;
|
|
13
|
+
titleColor?: NativeMenuColor;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NativeMenuAction extends NativeMenuLabel {
|
|
17
|
+
type: 'action';
|
|
18
|
+
id: string;
|
|
19
|
+
state: 'off' | 'on' | 'mixed';
|
|
20
|
+
destructive: boolean;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
hidden: boolean;
|
|
23
|
+
keepOpen: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NativeMenuSubmenu extends NativeMenuLabel {
|
|
27
|
+
type: 'submenu';
|
|
28
|
+
destructive: boolean;
|
|
29
|
+
disabled: boolean;
|
|
30
|
+
hidden: boolean;
|
|
31
|
+
displayInline: boolean;
|
|
32
|
+
children: NativeMenuElement[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NativeMenuSection {
|
|
36
|
+
type: 'section';
|
|
37
|
+
title?: string;
|
|
38
|
+
children: NativeMenuElement[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface NativeMenuSeparator {
|
|
42
|
+
type: 'separator';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type NativeMenuElement =
|
|
46
|
+
| NativeMenuAction
|
|
47
|
+
| NativeMenuSubmenu
|
|
48
|
+
| NativeMenuSection
|
|
49
|
+
| NativeMenuSeparator;
|
|
50
|
+
|
|
51
|
+
export interface SerializedMenuOptions {
|
|
52
|
+
config: string;
|
|
53
|
+
callbacks: Map<string, () => void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serializeColor(color: ColorValue | undefined): NativeMenuColor {
|
|
57
|
+
return color === undefined ? undefined : processColor(color);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function serializeMenuOptions(
|
|
61
|
+
options: readonly MenuOption[]
|
|
62
|
+
): SerializedMenuOptions {
|
|
63
|
+
const callbacks = new Map<string, () => void>();
|
|
64
|
+
const items = serializeOptions(options, callbacks, new Set(), 'root');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
callbacks,
|
|
68
|
+
config: JSON.stringify({ items }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function serializeOptions(
|
|
73
|
+
options: readonly MenuOption[],
|
|
74
|
+
callbacks: Map<string, () => void>,
|
|
75
|
+
usedIds: Set<string>,
|
|
76
|
+
path: string
|
|
77
|
+
): NativeMenuElement[] {
|
|
78
|
+
return options.map((option, index) => {
|
|
79
|
+
const optionPath = `${path}.${index}`;
|
|
80
|
+
|
|
81
|
+
if (option.type === 'divider') {
|
|
82
|
+
return { type: 'separator' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (option.type === 'submenu') {
|
|
86
|
+
return {
|
|
87
|
+
type: 'submenu',
|
|
88
|
+
title: option.title,
|
|
89
|
+
subtitle: option.subtitle,
|
|
90
|
+
systemImage: option.systemImage,
|
|
91
|
+
icon: option.icon,
|
|
92
|
+
iconColor: serializeColor(option.iconColor),
|
|
93
|
+
titleColor: serializeColor(option.titleColor),
|
|
94
|
+
destructive: option.destructive ?? false,
|
|
95
|
+
disabled: option.disabled ?? false,
|
|
96
|
+
hidden: option.hidden ?? false,
|
|
97
|
+
displayInline: option.displayInline ?? false,
|
|
98
|
+
children: serializeOptions(
|
|
99
|
+
option.options,
|
|
100
|
+
callbacks,
|
|
101
|
+
usedIds,
|
|
102
|
+
`${optionPath}.submenu`
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (option.type === 'section') {
|
|
108
|
+
return {
|
|
109
|
+
type: 'section',
|
|
110
|
+
title: option.title,
|
|
111
|
+
children: serializeOptions(
|
|
112
|
+
option.options,
|
|
113
|
+
callbacks,
|
|
114
|
+
usedIds,
|
|
115
|
+
`${optionPath}.section`
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const id = option.id ?? `item.${optionPath}`;
|
|
121
|
+
if (usedIds.has(id)) {
|
|
122
|
+
throw new Error(`Menu option id "${id}" must be unique within a menu.`);
|
|
123
|
+
}
|
|
124
|
+
usedIds.add(id);
|
|
125
|
+
|
|
126
|
+
if (option.onSelect) {
|
|
127
|
+
callbacks.set(id, option.onSelect);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
type: 'action',
|
|
132
|
+
id,
|
|
133
|
+
title: option.title,
|
|
134
|
+
subtitle: option.subtitle,
|
|
135
|
+
systemImage: option.systemImage,
|
|
136
|
+
icon: option.icon,
|
|
137
|
+
iconColor: serializeColor(option.iconColor),
|
|
138
|
+
titleColor: serializeColor(option.titleColor),
|
|
139
|
+
state: option.state ?? 'off',
|
|
140
|
+
destructive: option.destructive ?? false,
|
|
141
|
+
disabled: option.disabled ?? false,
|
|
142
|
+
hidden: option.hidden ?? false,
|
|
143
|
+
keepOpen: option.keepOpen ?? false,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
/**
|
|
6
|
+
* Returns the names of the native components compiled into the app,
|
|
7
|
+
* based on the `react-native-tinyui.components` array in package.json.
|
|
8
|
+
*/
|
|
9
|
+
getEnabledComponents(): ReadonlyArray<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default TurboModuleRegistry.get<Spec>('Tinyui');
|
package/src/Popover.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useState, type ComponentRef } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { PlatformColor, Pressable, StyleSheet, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { assertComponentEnabled } from './getNativeTinyui';
|
|
6
|
+
import { PopoverClose } from './PopoverPrimitives';
|
|
7
|
+
import type {
|
|
8
|
+
PopoverComponent,
|
|
9
|
+
PopoverContentRenderArgs,
|
|
10
|
+
PopoverContent as PopoverContentType,
|
|
11
|
+
PopoverProps,
|
|
12
|
+
} from './Popover.types';
|
|
13
|
+
import NativePopoverView from './PopoverNativeComponent';
|
|
14
|
+
|
|
15
|
+
const PopoverRoot = forwardRef<ComponentRef<typeof View>, PopoverProps>(
|
|
16
|
+
function Popover(
|
|
17
|
+
{
|
|
18
|
+
children,
|
|
19
|
+
content,
|
|
20
|
+
open,
|
|
21
|
+
defaultOpen = false,
|
|
22
|
+
onOpenChange,
|
|
23
|
+
attachmentAnchor = 'center',
|
|
24
|
+
arrowEdge = 'none',
|
|
25
|
+
...viewProps
|
|
26
|
+
},
|
|
27
|
+
ref
|
|
28
|
+
) {
|
|
29
|
+
assertComponentEnabled('Popover');
|
|
30
|
+
|
|
31
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
32
|
+
const isControlled = open !== undefined;
|
|
33
|
+
const isOpen = isControlled ? open : uncontrolledOpen;
|
|
34
|
+
|
|
35
|
+
const setOpen = useCallback(
|
|
36
|
+
(nextOpen: boolean) => {
|
|
37
|
+
if (!isControlled) {
|
|
38
|
+
setUncontrolledOpen(nextOpen);
|
|
39
|
+
}
|
|
40
|
+
onOpenChange?.(nextOpen);
|
|
41
|
+
},
|
|
42
|
+
[isControlled, onOpenChange]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
46
|
+
|
|
47
|
+
const renderedContent = renderContent(content, { close });
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View {...viewProps} ref={ref} collapsable={false}>
|
|
51
|
+
<NativePopoverView
|
|
52
|
+
attachmentAnchor={attachmentAnchor}
|
|
53
|
+
arrowEdge={arrowEdge}
|
|
54
|
+
isPresented={isOpen}
|
|
55
|
+
onIsPresentedChange={(event) => {
|
|
56
|
+
if (!event.nativeEvent.isPresented && isOpen) {
|
|
57
|
+
setOpen(false);
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<Pressable onPress={() => setOpen(true)}>{children}</Pressable>
|
|
62
|
+
<View style={styles.content} pointerEvents="box-none">
|
|
63
|
+
{renderedContent}
|
|
64
|
+
</View>
|
|
65
|
+
</NativePopoverView>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
function renderContent(
|
|
72
|
+
content: PopoverContentType,
|
|
73
|
+
args: PopoverContentRenderArgs
|
|
74
|
+
): ReactNode {
|
|
75
|
+
return typeof content === 'function' ? content(args) : content;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const styles = StyleSheet.create({
|
|
79
|
+
content: {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
backgroundColor: PlatformColor('systemBackground'),
|
|
82
|
+
overflow: 'hidden',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const Popover = Object.assign(PopoverRoot, {
|
|
87
|
+
Close: PopoverClose,
|
|
88
|
+
}) as PopoverComponent & {
|
|
89
|
+
Close: typeof PopoverClose;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { PopoverClose };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentRef,
|
|
3
|
+
ForwardRefExoticComponent,
|
|
4
|
+
RefAttributes,
|
|
5
|
+
} from 'react';
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import type { PressableProps, View, ViewProps } from 'react-native';
|
|
8
|
+
|
|
9
|
+
export type PopoverAttachmentAnchor =
|
|
10
|
+
'leading' | 'trailing' | 'center' | 'top' | 'bottom';
|
|
11
|
+
|
|
12
|
+
export type PopoverArrowEdge =
|
|
13
|
+
'leading' | 'trailing' | 'top' | 'bottom' | 'none';
|
|
14
|
+
|
|
15
|
+
/** Arguments passed to a render-function `content`. */
|
|
16
|
+
export interface PopoverContentRenderArgs {
|
|
17
|
+
/** Dismisses the popover. */
|
|
18
|
+
close: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PopoverContent =
|
|
22
|
+
ReactNode | ((args: PopoverContentRenderArgs) => ReactNode);
|
|
23
|
+
|
|
24
|
+
export interface PopoverProps extends Omit<ViewProps, 'children'> {
|
|
25
|
+
/** The trigger element. Tapping it opens the popover. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Content shown inside the popover. Pass a node, or a render function that
|
|
29
|
+
* receives `{ close }` to dismiss the popover from within the content.
|
|
30
|
+
*/
|
|
31
|
+
content: PopoverContent;
|
|
32
|
+
/** Controlled presentation state. */
|
|
33
|
+
open?: boolean;
|
|
34
|
+
/** Initial state when `open` is not controlled. */
|
|
35
|
+
defaultOpen?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
attachmentAnchor?: PopoverAttachmentAnchor;
|
|
38
|
+
arrowEdge?: PopoverArrowEdge;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PopoverComponent extends ForwardRefExoticComponent<
|
|
42
|
+
PopoverProps & RefAttributes<ComponentRef<typeof View>>
|
|
43
|
+
> {}
|
|
44
|
+
|
|
45
|
+
/** Props for the `Popover.Close` helper button. */
|
|
46
|
+
export interface PopoverCloseProps extends PressableProps {
|
|
47
|
+
/** Dismisses the popover. Pass the `close` callback from the content render args. */
|
|
48
|
+
close: () => void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { codegenNativeComponent, type ViewProps } from 'react-native';
|
|
2
|
+
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
|
|
4
|
+
export interface NativePopoverProps extends ViewProps {
|
|
5
|
+
isPresented?: boolean;
|
|
6
|
+
attachmentAnchor?: string;
|
|
7
|
+
arrowEdge?: string;
|
|
8
|
+
onIsPresentedChange?: DirectEventHandler<Readonly<{ isPresented: boolean }>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default codegenNativeComponent<NativePopoverProps>('TinyuiPopoverView', {
|
|
12
|
+
excludedPlatforms: ['android'],
|
|
13
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { PopoverCloseProps } from './Popover.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A button that dismisses the popover. Render it inside the `content` prop
|
|
8
|
+
* and pass the `close` callback from the render-function arguments:
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Popover
|
|
12
|
+
* content={({ close }) => (
|
|
13
|
+
* <Popover.Close close={close}>Done</Popover.Close>
|
|
14
|
+
* )}
|
|
15
|
+
* >
|
|
16
|
+
* <Button title="Open" />
|
|
17
|
+
* </Popover>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function PopoverClose({
|
|
21
|
+
close,
|
|
22
|
+
onPress,
|
|
23
|
+
...props
|
|
24
|
+
}: PopoverCloseProps): ReactNode {
|
|
25
|
+
return (
|
|
26
|
+
<Pressable
|
|
27
|
+
{...props}
|
|
28
|
+
onPress={(event) => {
|
|
29
|
+
onPress?.(event);
|
|
30
|
+
close();
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
PopoverClose.displayName = 'Popover.Close';
|