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.
Files changed (74) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +172 -0
  3. package/Tinyui.podspec +85 -0
  4. package/ios/Core/Tinyui.h +8 -0
  5. package/ios/Core/Tinyui.mm +30 -0
  6. package/ios/Core/TinyuiComponentClasses.mm +27 -0
  7. package/ios/Core/TinyuiHostingHelpers.swift +74 -0
  8. package/ios/Menu/TinyuiMenuProvider.swift +110 -0
  9. package/ios/Menu/TinyuiMenuView.h +14 -0
  10. package/ios/Menu/TinyuiMenuView.mm +120 -0
  11. package/ios/Menu/TinyuiMenuViewSwift.swift +368 -0
  12. package/ios/Popover/TinyuiPopoverProvider.swift +111 -0
  13. package/ios/Popover/TinyuiPopoverView.h +14 -0
  14. package/ios/Popover/TinyuiPopoverView.mm +124 -0
  15. package/ios/Popover/TinyuiPopoverViewSwift.swift +88 -0
  16. package/lib/module/Menu.js +43 -0
  17. package/lib/module/Menu.js.map +1 -0
  18. package/lib/module/Menu.types.js +4 -0
  19. package/lib/module/Menu.types.js.map +1 -0
  20. package/lib/module/MenuNativeComponent.ts +14 -0
  21. package/lib/module/MenuOptions.js +73 -0
  22. package/lib/module/MenuOptions.js.map +1 -0
  23. package/lib/module/NativeTinyui.js +5 -0
  24. package/lib/module/NativeTinyui.js.map +1 -0
  25. package/lib/module/Popover.js +71 -0
  26. package/lib/module/Popover.js.map +1 -0
  27. package/lib/module/Popover.types.js +4 -0
  28. package/lib/module/Popover.types.js.map +1 -0
  29. package/lib/module/PopoverNativeComponent.ts +13 -0
  30. package/lib/module/PopoverPrimitives.js +33 -0
  31. package/lib/module/PopoverPrimitives.js.map +1 -0
  32. package/lib/module/getNativeTinyui.js +30 -0
  33. package/lib/module/getNativeTinyui.js.map +1 -0
  34. package/lib/module/index.js +6 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/package.json +1 -0
  37. package/lib/module/react-native.d.js +2 -0
  38. package/lib/module/react-native.d.js.map +1 -0
  39. package/lib/typescript/package.json +1 -0
  40. package/lib/typescript/src/Menu.d.ts +4 -0
  41. package/lib/typescript/src/Menu.d.ts.map +1 -0
  42. package/lib/typescript/src/Menu.types.d.ts +63 -0
  43. package/lib/typescript/src/Menu.types.d.ts.map +1 -0
  44. package/lib/typescript/src/MenuNativeComponent.d.ts +14 -0
  45. package/lib/typescript/src/MenuNativeComponent.d.ts.map +1 -0
  46. package/lib/typescript/src/MenuOptions.d.ts +7 -0
  47. package/lib/typescript/src/MenuOptions.d.ts.map +1 -0
  48. package/lib/typescript/src/NativeTinyui.d.ts +11 -0
  49. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -0
  50. package/lib/typescript/src/Popover.d.ts +7 -0
  51. package/lib/typescript/src/Popover.d.ts.map +1 -0
  52. package/lib/typescript/src/Popover.types.d.ts +35 -0
  53. package/lib/typescript/src/Popover.types.d.ts.map +1 -0
  54. package/lib/typescript/src/PopoverNativeComponent.d.ts +13 -0
  55. package/lib/typescript/src/PopoverNativeComponent.d.ts.map +1 -0
  56. package/lib/typescript/src/PopoverPrimitives.d.ts +21 -0
  57. package/lib/typescript/src/PopoverPrimitives.d.ts.map +1 -0
  58. package/lib/typescript/src/getNativeTinyui.d.ts +13 -0
  59. package/lib/typescript/src/getNativeTinyui.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +7 -0
  61. package/lib/typescript/src/index.d.ts.map +1 -0
  62. package/package.json +175 -0
  63. package/src/Menu.tsx +52 -0
  64. package/src/Menu.types.ts +77 -0
  65. package/src/MenuNativeComponent.ts +14 -0
  66. package/src/MenuOptions.ts +146 -0
  67. package/src/NativeTinyui.ts +12 -0
  68. package/src/Popover.tsx +92 -0
  69. package/src/Popover.types.ts +49 -0
  70. package/src/PopoverNativeComponent.ts +13 -0
  71. package/src/PopoverPrimitives.tsx +36 -0
  72. package/src/getNativeTinyui.ts +38 -0
  73. package/src/index.tsx +23 -0
  74. 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');
@@ -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';