@tamagui/create-menu 2.0.0-rc.8 → 2.0.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.
Files changed (148) hide show
  1. package/dist/cjs/MenuPredefined.cjs +159 -157
  2. package/dist/cjs/MenuPredefined.native.js +159 -157
  3. package/dist/cjs/MenuPredefined.native.js.map +1 -1
  4. package/dist/cjs/createBaseMenu.cjs +1150 -941
  5. package/dist/cjs/createBaseMenu.native.js +1280 -1108
  6. package/dist/cjs/createBaseMenu.native.js.map +1 -1
  7. package/dist/cjs/createNativeMenu/createNativeMenu.cjs +318 -159
  8. package/dist/cjs/createNativeMenu/createNativeMenu.native.js +430 -267
  9. package/dist/cjs/createNativeMenu/createNativeMenu.native.js.map +1 -1
  10. package/dist/cjs/createNativeMenu/createNativeMenuTypes.cjs +7 -5
  11. package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js +7 -5
  12. package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js.map +1 -1
  13. package/dist/cjs/createNativeMenu/utils.cjs +85 -42
  14. package/dist/cjs/createNativeMenu/utils.native.js +83 -58
  15. package/dist/cjs/createNativeMenu/utils.native.js.map +1 -1
  16. package/dist/cjs/createNativeMenu/withNativeMenu.cjs +27 -17
  17. package/dist/cjs/createNativeMenu/withNativeMenu.native.js +22 -14
  18. package/dist/cjs/createNativeMenu/withNativeMenu.native.js.map +1 -1
  19. package/dist/cjs/index.cjs +15 -12
  20. package/dist/cjs/index.native.js +15 -12
  21. package/dist/cjs/index.native.js.map +1 -1
  22. package/dist/esm/MenuPredefined.mjs +144 -144
  23. package/dist/esm/MenuPredefined.mjs.map +1 -1
  24. package/dist/esm/MenuPredefined.native.js +144 -144
  25. package/dist/esm/MenuPredefined.native.js.map +1 -1
  26. package/dist/esm/createBaseMenu.mjs +1110 -903
  27. package/dist/esm/createBaseMenu.mjs.map +1 -1
  28. package/dist/esm/createBaseMenu.native.js +1240 -1070
  29. package/dist/esm/createBaseMenu.native.js.map +1 -1
  30. package/dist/esm/createNativeMenu/createNativeMenu.mjs +291 -134
  31. package/dist/esm/createNativeMenu/createNativeMenu.mjs.map +1 -1
  32. package/dist/esm/createNativeMenu/createNativeMenu.native.js +377 -216
  33. package/dist/esm/createNativeMenu/createNativeMenu.native.js.map +1 -1
  34. package/dist/esm/createNativeMenu/utils.mjs +58 -17
  35. package/dist/esm/createNativeMenu/utils.mjs.map +1 -1
  36. package/dist/esm/createNativeMenu/utils.native.js +57 -34
  37. package/dist/esm/createNativeMenu/utils.native.js.map +1 -1
  38. package/dist/esm/createNativeMenu/withNativeMenu.mjs +13 -5
  39. package/dist/esm/createNativeMenu/withNativeMenu.mjs.map +1 -1
  40. package/dist/esm/createNativeMenu/withNativeMenu.native.js +8 -2
  41. package/dist/esm/createNativeMenu/withNativeMenu.native.js.map +1 -1
  42. package/dist/esm/index.js +5 -6
  43. package/dist/esm/index.js.map +1 -6
  44. package/dist/esm/index.mjs +2 -1
  45. package/dist/esm/index.mjs.map +1 -1
  46. package/dist/esm/index.native.js +2 -1
  47. package/dist/esm/index.native.js.map +1 -1
  48. package/dist/jsx/MenuPredefined.mjs +144 -144
  49. package/dist/jsx/MenuPredefined.mjs.map +1 -1
  50. package/dist/jsx/MenuPredefined.native.js +159 -157
  51. package/dist/jsx/MenuPredefined.native.js.map +1 -1
  52. package/dist/jsx/createBaseMenu.mjs +1110 -903
  53. package/dist/jsx/createBaseMenu.mjs.map +1 -1
  54. package/dist/jsx/createBaseMenu.native.js +1280 -1108
  55. package/dist/jsx/createBaseMenu.native.js.map +1 -1
  56. package/dist/jsx/createNativeMenu/createNativeMenu.mjs +291 -134
  57. package/dist/jsx/createNativeMenu/createNativeMenu.mjs.map +1 -1
  58. package/dist/jsx/createNativeMenu/createNativeMenu.native.js +430 -267
  59. package/dist/jsx/createNativeMenu/createNativeMenu.native.js.map +1 -1
  60. package/dist/jsx/createNativeMenu/createNativeMenuTypes.native.js +7 -5
  61. package/dist/jsx/createNativeMenu/utils.mjs +58 -17
  62. package/dist/jsx/createNativeMenu/utils.mjs.map +1 -1
  63. package/dist/jsx/createNativeMenu/utils.native.js +83 -58
  64. package/dist/jsx/createNativeMenu/utils.native.js.map +1 -1
  65. package/dist/jsx/createNativeMenu/withNativeMenu.mjs +13 -5
  66. package/dist/jsx/createNativeMenu/withNativeMenu.mjs.map +1 -1
  67. package/dist/jsx/createNativeMenu/withNativeMenu.native.js +22 -14
  68. package/dist/jsx/createNativeMenu/withNativeMenu.native.js.map +1 -1
  69. package/dist/jsx/index.js +5 -6
  70. package/dist/jsx/index.js.map +1 -6
  71. package/dist/jsx/index.mjs +2 -1
  72. package/dist/jsx/index.mjs.map +1 -1
  73. package/dist/jsx/index.native.js +15 -12
  74. package/dist/jsx/index.native.js.map +1 -1
  75. package/package.json +26 -29
  76. package/src/createBaseMenu.tsx +367 -266
  77. package/src/createNativeMenu/createNativeMenu.tsx +448 -220
  78. package/src/createNativeMenu/createNativeMenuTypes.ts +20 -20
  79. package/src/createNativeMenu/withNativeMenu.tsx +5 -3
  80. package/src/index.tsx +3 -5
  81. package/types/createBaseMenu.d.ts +117 -31
  82. package/types/createBaseMenu.d.ts.map +1 -1
  83. package/types/createNativeMenu/createNativeMenu.d.ts +21 -21
  84. package/types/createNativeMenu/createNativeMenu.d.ts.map +1 -1
  85. package/types/createNativeMenu/createNativeMenuTypes.d.ts +20 -20
  86. package/types/createNativeMenu/createNativeMenuTypes.d.ts.map +1 -1
  87. package/types/createNativeMenu/withNativeMenu.d.ts +3 -3
  88. package/types/createNativeMenu/withNativeMenu.d.ts.map +1 -1
  89. package/types/index.d.ts +3 -2
  90. package/types/index.d.ts.map +1 -1
  91. package/dist/cjs/MenuPredefined.js +0 -168
  92. package/dist/cjs/MenuPredefined.js.map +0 -6
  93. package/dist/cjs/createBaseMenu.js +0 -843
  94. package/dist/cjs/createBaseMenu.js.map +0 -6
  95. package/dist/cjs/createNativeMenu/createNativeMenu.js +0 -177
  96. package/dist/cjs/createNativeMenu/createNativeMenu.js.map +0 -6
  97. package/dist/cjs/createNativeMenu/createNativeMenuTypes.js +0 -14
  98. package/dist/cjs/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  99. package/dist/cjs/createNativeMenu/index.cjs +0 -19
  100. package/dist/cjs/createNativeMenu/index.js +0 -16
  101. package/dist/cjs/createNativeMenu/index.js.map +0 -6
  102. package/dist/cjs/createNativeMenu/index.native.js +0 -22
  103. package/dist/cjs/createNativeMenu/index.native.js.map +0 -1
  104. package/dist/cjs/createNativeMenu/utils.js +0 -66
  105. package/dist/cjs/createNativeMenu/utils.js.map +0 -6
  106. package/dist/cjs/createNativeMenu/withNativeMenu.js +0 -30
  107. package/dist/cjs/createNativeMenu/withNativeMenu.js.map +0 -6
  108. package/dist/cjs/index.js +0 -23
  109. package/dist/cjs/index.js.map +0 -6
  110. package/dist/esm/MenuPredefined.js +0 -154
  111. package/dist/esm/MenuPredefined.js.map +0 -6
  112. package/dist/esm/createBaseMenu.js +0 -849
  113. package/dist/esm/createBaseMenu.js.map +0 -6
  114. package/dist/esm/createNativeMenu/createNativeMenu.js +0 -156
  115. package/dist/esm/createNativeMenu/createNativeMenu.js.map +0 -6
  116. package/dist/esm/createNativeMenu/createNativeMenuTypes.js +0 -1
  117. package/dist/esm/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  118. package/dist/esm/createNativeMenu/index.js +0 -3
  119. package/dist/esm/createNativeMenu/index.js.map +0 -6
  120. package/dist/esm/createNativeMenu/index.mjs +0 -3
  121. package/dist/esm/createNativeMenu/index.mjs.map +0 -1
  122. package/dist/esm/createNativeMenu/index.native.js +0 -3
  123. package/dist/esm/createNativeMenu/index.native.js.map +0 -1
  124. package/dist/esm/createNativeMenu/utils.js +0 -47
  125. package/dist/esm/createNativeMenu/utils.js.map +0 -6
  126. package/dist/esm/createNativeMenu/withNativeMenu.js +0 -15
  127. package/dist/esm/createNativeMenu/withNativeMenu.js.map +0 -6
  128. package/dist/jsx/MenuPredefined.js +0 -154
  129. package/dist/jsx/MenuPredefined.js.map +0 -6
  130. package/dist/jsx/createBaseMenu.js +0 -849
  131. package/dist/jsx/createBaseMenu.js.map +0 -6
  132. package/dist/jsx/createNativeMenu/createNativeMenu.js +0 -156
  133. package/dist/jsx/createNativeMenu/createNativeMenu.js.map +0 -6
  134. package/dist/jsx/createNativeMenu/createNativeMenuTypes.js +0 -1
  135. package/dist/jsx/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  136. package/dist/jsx/createNativeMenu/index.js +0 -3
  137. package/dist/jsx/createNativeMenu/index.js.map +0 -6
  138. package/dist/jsx/createNativeMenu/index.mjs +0 -3
  139. package/dist/jsx/createNativeMenu/index.mjs.map +0 -1
  140. package/dist/jsx/createNativeMenu/index.native.js +0 -22
  141. package/dist/jsx/createNativeMenu/index.native.js.map +0 -1
  142. package/dist/jsx/createNativeMenu/utils.js +0 -47
  143. package/dist/jsx/createNativeMenu/utils.js.map +0 -6
  144. package/dist/jsx/createNativeMenu/withNativeMenu.js +0 -15
  145. package/dist/jsx/createNativeMenu/withNativeMenu.js.map +0 -6
  146. package/src/createNativeMenu/index.tsx +0 -7
  147. package/types/createNativeMenu/index.d.ts +0 -4
  148. package/types/createNativeMenu/index.d.ts.map +0 -1
@@ -1,194 +1,260 @@
1
1
  /**
2
- * createNativeMenu - provides native menu implementation for React Native
2
+ * createNativeMenu - native menu implementation for React Native
3
3
  *
4
- * On Web: Returns empty stub components (withNativeMenu will use the web components instead)
5
- * On Native: Uses Zeego for native menus (Credit to nandorojo/Zeego)
4
+ * Web: returns empty stub components (withNativeMenu uses the web components instead)
5
+ * Native: lazily resolves Zeego at render time so importing the package doesn't warn/error
6
6
  */
7
7
 
8
- import { getZeego } from '@tamagui/native'
8
+ import {
9
+ getZeego,
10
+ NativeMenuContext,
11
+ unstable_claimExternalPressOwnership,
12
+ unstable_releaseExternalPressOwnership,
13
+ } from '@tamagui/native'
9
14
  import { isWeb, withStaticProperties, isIos } from '@tamagui/web'
10
15
  import type { FC } from 'react'
11
16
  import React from 'react'
12
17
  import type {
13
18
  ContextMenuPreviewProps,
14
- MenuArrowProps,
15
- MenuCheckboxItemProps,
16
- MenuContentProps,
17
- MenuGroupProps,
18
- MenuItemIconProps,
19
- MenuItemImageProps,
20
- MenuItemIndicatorProps,
21
- MenuItemProps,
22
- MenuItemSubtitleProps,
23
- MenuItemTitleProps,
24
- MenuLabelProps,
25
- MenuProps,
26
- MenuSeparatorProps,
27
- MenuSubContentProps,
28
- MenuSubProps,
29
- MenuSubTriggerProps,
19
+ NativeContextMenuAuxiliaryProps,
20
+ NativeMenuArrowProps,
21
+ NativeMenuCheckboxItemProps,
22
+ NativeMenuContentProps,
23
+ NativeMenuGroupProps,
24
+ NativeMenuItemIconProps,
25
+ NativeMenuItemImageProps,
26
+ NativeMenuItemIndicatorProps,
27
+ NativeMenuItemProps,
28
+ NativeMenuItemSubtitleProps,
29
+ NativeMenuItemTitleProps,
30
+ NativeMenuLabelProps,
31
+ NativeMenuProps,
32
+ NativeMenuSeparatorProps,
33
+ NativeMenuSubContentProps,
34
+ NativeMenuSubProps,
35
+ NativeMenuSubTriggerProps,
30
36
  MenuTriggerProps,
31
37
  } from './createNativeMenuTypes'
32
38
 
39
+ // zeego module shape (DropdownMenu / ContextMenu both share this)
40
+ type ZeegoMenuModule = {
41
+ Root: FC<Record<string, unknown>>
42
+ Trigger: FC<MenuTriggerProps>
43
+ Content: FC<NativeMenuContentProps>
44
+ Item: FC<NativeMenuItemProps>
45
+ ItemTitle: FC<NativeMenuItemTitleProps>
46
+ ItemSubtitle: FC<NativeMenuItemSubtitleProps>
47
+ ItemIcon: FC<NativeMenuItemIconProps>
48
+ ItemImage: FC<NativeMenuItemImageProps>
49
+ ItemIndicator: FC<NativeMenuItemIndicatorProps>
50
+ Group: FC<NativeMenuGroupProps>
51
+ Label: FC<NativeMenuLabelProps>
52
+ Separator: FC<NativeMenuSeparatorProps>
53
+ Sub: FC<NativeMenuSubProps>
54
+ SubTrigger: FC<NativeMenuSubTriggerProps>
55
+ SubContent: FC<NativeMenuSubContentProps>
56
+ CheckboxItem: FC<NativeMenuCheckboxItemProps>
57
+ Preview: FC<ContextMenuPreviewProps>
58
+ Auxiliary: FC<NativeContextMenuAuxiliaryProps>
59
+ }
60
+
61
+ // component types we recognize via displayName matching
62
+ type MappedComponentType =
63
+ | 'SubContent'
64
+ | 'SubTrigger'
65
+ | 'Content'
66
+ | 'Sub'
67
+ | 'Group'
68
+ | 'CheckboxItem'
69
+
70
+ const MAPPED_TYPES: MappedComponentType[] = [
71
+ 'SubContent',
72
+ 'SubTrigger',
73
+ 'Content',
74
+ 'Sub',
75
+ 'Group',
76
+ 'CheckboxItem',
77
+ ]
78
+
79
+ // types whose children get recursively transformed
80
+ const CONTAINER_TYPES: MappedComponentType[] = ['SubContent', 'Content', 'Sub', 'Group']
81
+
82
+ type ComponentMap = Pick<
83
+ ZeegoMenuModule,
84
+ 'SubContent' | 'Content' | 'Sub' | 'Group' | 'SubTrigger'
85
+ >
86
+
87
+ type TriggerPressBoundaryHandlers = {
88
+ claim(debugName?: string | null): void
89
+ release(debugName?: string | null): void
90
+ }
91
+
33
92
  export type NativeMenuComponents = {
34
- Menu: FC<MenuProps> & {
93
+ Menu: FC<NativeMenuProps> & {
35
94
  Trigger: FC<MenuTriggerProps>
36
- Content: FC<MenuContentProps>
37
- Item: FC<MenuItemProps>
38
- ItemTitle: FC<MenuItemTitleProps>
39
- ItemSubtitle: FC<MenuItemSubtitleProps>
40
- SubTrigger: FC<MenuSubTriggerProps>
41
- Group: FC<MenuGroupProps>
42
- ItemIcon: FC<MenuItemIconProps>
43
- Separator: FC<MenuSeparatorProps>
44
- CheckboxItem: FC<MenuCheckboxItemProps>
45
- ItemIndicator: FC<MenuItemIndicatorProps>
46
- ItemImage: FC<MenuItemImageProps>
47
- Label: FC<MenuLabelProps>
48
- Arrow: FC<MenuArrowProps>
49
- Sub: FC<MenuSubProps>
50
- SubContent: FC<MenuSubContentProps>
95
+ Content: FC<NativeMenuContentProps>
96
+ Item: FC<NativeMenuItemProps>
97
+ ItemTitle: FC<NativeMenuItemTitleProps>
98
+ ItemSubtitle: FC<NativeMenuItemSubtitleProps>
99
+ SubTrigger: FC<NativeMenuSubTriggerProps>
100
+ Group: FC<NativeMenuGroupProps>
101
+ ItemIcon: FC<NativeMenuItemIconProps>
102
+ Separator: FC<NativeMenuSeparatorProps>
103
+ CheckboxItem: FC<NativeMenuCheckboxItemProps>
104
+ ItemIndicator: FC<NativeMenuItemIndicatorProps>
105
+ ItemImage: FC<NativeMenuItemImageProps>
106
+ Label: FC<NativeMenuLabelProps>
107
+ Arrow: FC<NativeMenuArrowProps>
108
+ Sub: FC<NativeMenuSubProps>
109
+ SubContent: FC<NativeMenuSubContentProps>
51
110
  Preview: FC<ContextMenuPreviewProps>
52
111
  Portal: FC<{ children: React.ReactNode }>
53
112
  RadioGroup: FC<{ children: React.ReactNode }>
54
113
  RadioItem: FC<{ children: React.ReactNode }>
55
- Auxiliary: FC<any>
114
+ Auxiliary: FC<NativeContextMenuAuxiliaryProps>
115
+ }
116
+ }
117
+
118
+ // shared helpers (stateless, no need to recreate per call)
119
+
120
+ function getComponentType(displayName: string): MappedComponentType | null {
121
+ for (const type of MAPPED_TYPES) {
122
+ if (displayName === type || displayName.includes(`(${type})`)) {
123
+ return type
124
+ }
125
+ }
126
+ return null
127
+ }
128
+
129
+ function isItemLike(props: Record<string, unknown>, displayName: string): boolean {
130
+ if (getComponentType(displayName)) return false
131
+ return 'onSelect' in props || 'textValue' in props
132
+ }
133
+
134
+ function isPortalLike(displayName: string): boolean {
135
+ return displayName === 'Portal' || displayName.includes('Portal')
136
+ }
137
+
138
+ function isTriggerLike(displayName: string): boolean {
139
+ return displayName === 'Trigger' || displayName.includes('(Trigger)')
140
+ }
141
+
142
+ function composeHandlers<T extends (...args: any[]) => void>(first?: T, second?: T) {
143
+ return (...args: Parameters<T>) => {
144
+ first?.(...args)
145
+ second?.(...args)
146
+ }
147
+ }
148
+
149
+ function getTriggerDebugName(
150
+ menuType: 'ContextMenu' | 'Menu',
151
+ props: Record<string, any>
152
+ ) {
153
+ const childProps =
154
+ React.isValidElement(props.children) && props.children.props
155
+ ? (props.children.props as Record<string, any>)
156
+ : null
157
+
158
+ const prefix = menuType === 'ContextMenu' ? 'ContextMenuTrigger' : 'MenuTrigger'
159
+ const detail =
160
+ childProps?.testID ??
161
+ childProps?.accessibilityLabel ??
162
+ (typeof props.textValue === 'string' ? props.textValue : null)
163
+
164
+ return [prefix, detail].filter(Boolean).join(':') || prefix
165
+ }
166
+
167
+ // stub used for web — never actually rendered, just needs to exist for withNativeMenu fallback
168
+ const emptyStub = (() => null) as FC<any>
169
+
170
+ function createWebStubs(): NativeMenuComponents {
171
+ return {
172
+ Menu: withStaticProperties(emptyStub as FC<NativeMenuProps>, {
173
+ Trigger: emptyStub as FC<MenuTriggerProps>,
174
+ Content: emptyStub as FC<NativeMenuContentProps>,
175
+ Item: emptyStub as FC<NativeMenuItemProps>,
176
+ ItemTitle: emptyStub as FC<NativeMenuItemTitleProps>,
177
+ ItemSubtitle: emptyStub as FC<NativeMenuItemSubtitleProps>,
178
+ SubTrigger: emptyStub as FC<NativeMenuSubTriggerProps>,
179
+ Group: emptyStub as FC<NativeMenuGroupProps>,
180
+ ItemIcon: emptyStub as FC<NativeMenuItemIconProps>,
181
+ Separator: emptyStub as FC<NativeMenuSeparatorProps>,
182
+ CheckboxItem: emptyStub as FC<NativeMenuCheckboxItemProps>,
183
+ ItemIndicator: emptyStub as FC<NativeMenuItemIndicatorProps>,
184
+ ItemImage: emptyStub as FC<NativeMenuItemImageProps>,
185
+ Label: emptyStub as FC<NativeMenuLabelProps>,
186
+ Arrow: emptyStub as FC<NativeMenuArrowProps>,
187
+ Sub: emptyStub as FC<NativeMenuSubProps>,
188
+ SubContent: emptyStub as FC<NativeMenuSubContentProps>,
189
+ Preview: emptyStub as FC<ContextMenuPreviewProps>,
190
+ Portal: emptyStub as FC<{ children: React.ReactNode }>,
191
+ RadioGroup: emptyStub as FC<{ children: React.ReactNode }>,
192
+ RadioItem: emptyStub as FC<{ children: React.ReactNode }>,
193
+ Auxiliary: emptyStub as FC<NativeContextMenuAuxiliaryProps>,
194
+ }),
56
195
  }
57
196
  }
58
197
 
59
198
  export const createNativeMenu = (
60
199
  MenuType: 'ContextMenu' | 'Menu'
61
200
  ): NativeMenuComponents => {
62
- // On web, return empty stubs - withNativeMenu will use the web components passed to it
63
201
  if (isWeb) {
64
- const Menu = {} as FC<MenuProps>
65
- const Trigger = {} as FC<MenuTriggerProps>
66
- const Content = {} as FC<MenuContentProps>
67
- const Preview = {} as FC<ContextMenuPreviewProps>
68
- const Item = {} as FC<MenuItemProps>
69
- const ItemIcon = {} as FC<MenuItemIconProps>
70
- const ItemImage = {} as FC<MenuItemImageProps>
71
- const SubTrigger = {} as FC<MenuSubTriggerProps>
72
- const ItemTitle = {} as FC<MenuItemTitleProps>
73
- const ItemSubtitle = {} as FC<MenuItemSubtitleProps>
74
- const Group = {} as FC<MenuGroupProps>
75
- const Separator = {} as FC<MenuSeparatorProps>
76
- const CheckboxItem = {} as FC<MenuCheckboxItemProps>
77
- const ItemIndicator = {} as FC<MenuItemIndicatorProps>
78
- const Label = {} as FC<MenuLabelProps>
79
- const Arrow = {} as FC<MenuArrowProps>
80
- const Sub = {} as FC<MenuSubProps>
81
- const SubContent = {} as FC<MenuSubContentProps>
82
- const Portal = {} as FC<{ children: React.ReactNode }>
83
- const RadioGroup = {} as FC<{ children: React.ReactNode }>
84
- const RadioItem = {} as FC<{ children: React.ReactNode }>
85
- const Auxiliary = {} as FC<any>
86
-
87
- return {
88
- Menu: withStaticProperties(Menu, {
89
- Trigger,
90
- Content,
91
- Item,
92
- ItemTitle,
93
- ItemSubtitle,
94
- SubTrigger,
95
- Group,
96
- ItemIcon,
97
- Separator,
98
- CheckboxItem,
99
- ItemIndicator,
100
- ItemImage,
101
- Label,
102
- Arrow,
103
- Sub,
104
- SubContent,
105
- Preview,
106
- Portal,
107
- RadioGroup,
108
- RadioItem,
109
- Auxiliary,
110
- }),
111
- }
202
+ return createWebStubs()
112
203
  }
113
204
 
114
205
  // ===========================================
115
- // Native implementation using Zeego
206
+ // native implementation lazily resolves zeego
116
207
  // ===========================================
117
208
 
118
- const zeego = getZeego()
119
- if (!zeego.isEnabled) {
120
- console.warn(
121
- `Warning: Must call import '@tamagui/native/setup-zeego' at your app entry point to use native menus`
122
- )
123
- return { Menu: {} as any }
124
- }
125
-
126
- const { DropdownMenu: ZeegoDropdownMenu, ContextMenu: ZeegoContextMenu } = zeego.state
127
-
128
209
  const isContextMenu = MenuType === 'ContextMenu'
129
- const ZeegoMenu = isContextMenu ? ZeegoContextMenu : ZeegoDropdownMenu
130
-
131
- // Map displayName patterns to Zeego components
132
- const COMPONENT_MAP: Record<string, any> = {
133
- SubContent: ZeegoMenu.SubContent,
134
- Content: ZeegoMenu.Content,
135
- Sub: ZeegoMenu.Sub,
136
- Group: ZeegoMenu.Group,
137
- SubTrigger: ZeegoMenu.SubTrigger,
138
- }
139
-
140
- // Components that need children transformation (containers)
141
- const CONTAINER_TYPES = ['SubContent', 'Content', 'Sub', 'Group']
142
-
143
- /**
144
- * Get component type from displayName (handles styled wrappers)
145
- */
146
- const getComponentType = (displayName: string): string | null => {
147
- // Check in specific order (SubContent before Content, SubTrigger before Trigger)
148
- for (const type of [
149
- 'SubContent',
150
- 'SubTrigger',
151
- 'Content',
152
- 'Sub',
153
- 'Group',
154
- 'CheckboxItem',
155
- ]) {
156
- if (displayName === type || displayName.includes(`(${type})`)) {
157
- return type
210
+ const isAndroid = !isIos && !isWeb
211
+
212
+ // cached after first successful resolve
213
+ let resolved: { menu: ZeegoMenuModule; componentMap: ComponentMap } | null = null
214
+ let warned = false
215
+
216
+ function resolve(): typeof resolved {
217
+ if (resolved) return resolved
218
+ const zeego = getZeego()
219
+ if (!zeego.isEnabled) {
220
+ if (!warned) {
221
+ warned = true
222
+ console.warn(
223
+ `Warning: Must call import '@tamagui/native/setup-zeego' at your app entry point to use native menus`
224
+ )
158
225
  }
226
+ return null
159
227
  }
160
- return null
161
- }
162
-
163
- /**
164
- * Check if component looks like a menu Item (has onSelect/textValue but isn't a special component)
165
- */
166
- const isItemLike = (props: Record<string, any>, displayName: string): boolean => {
167
- // If it matches a known component type, it's not a generic Item
168
- if (getComponentType(displayName)) {
169
- return false
228
+ const menu = (
229
+ isContextMenu ? zeego.state.ContextMenu : zeego.state.DropdownMenu
230
+ ) as ZeegoMenuModule
231
+ resolved = {
232
+ menu,
233
+ componentMap: {
234
+ SubContent: menu.SubContent,
235
+ Content: menu.Content,
236
+ Sub: menu.Sub,
237
+ Group: menu.Group,
238
+ SubTrigger: menu.SubTrigger,
239
+ },
170
240
  }
171
- return 'onSelect' in props || 'textValue' in props
241
+ return resolved
172
242
  }
173
243
 
174
- /**
175
- * Check if displayName matches Portal
176
- */
177
- const isPortal = (displayName: string): boolean => {
178
- return displayName === 'Portal' || displayName.includes('Portal')
244
+ type RadioContext = {
245
+ value?: string
246
+ onValueChange?: (value: string) => void
179
247
  }
180
248
 
181
- /**
182
- * Transform children tree for Zeego compatibility:
183
- * - Flatten Portal wrappers
184
- * - Recurse into containers (Content, Sub, Group, SubContent)
185
- * - Convert styled Items to Zeego Items
186
- * - Reverse children on iOS only for DropdownMenu at Content/SubContent level
187
- */
188
- const transformForZeego = (
249
+ // transform children tree for zeego compatibility
250
+ function transformChildren(
251
+ menu: ZeegoMenuModule,
252
+ map: ComponentMap,
189
253
  children: React.ReactNode,
190
- shouldReverseOnIos = false
191
- ): React.ReactNode => {
254
+ shouldReverseOnIos = false,
255
+ triggerBoundaryHandlers?: TriggerPressBoundaryHandlers,
256
+ radioContext?: RadioContext
257
+ ): React.ReactNode {
192
258
  const result: React.ReactNode[] = []
193
259
 
194
260
  React.Children.forEach(children, (child) => {
@@ -197,49 +263,149 @@ export const createNativeMenu = (
197
263
  return
198
264
  }
199
265
 
200
- const displayName = (child.type as any)?.displayName || ''
266
+ const displayName = (child.type as { displayName?: string })?.displayName || ''
201
267
  const props = child.props as Record<string, any>
202
268
 
203
- // Flatten Portal
204
- if (isPortal(displayName)) {
205
- const portalChildren = transformForZeego(props.children, false)
206
- React.Children.forEach(portalChildren, (c) => result.push(c))
269
+ // flatten portal wrappers
270
+ if (isPortalLike(displayName)) {
271
+ const inner = transformChildren(
272
+ menu,
273
+ map,
274
+ props.children as React.ReactNode,
275
+ false,
276
+ triggerBoundaryHandlers,
277
+ radioContext
278
+ )
279
+ React.Children.forEach(inner, (c) => result.push(c))
280
+ return
281
+ }
282
+
283
+ // flatten ScrollView (native passthrough — children need to be visible to zeego)
284
+ if (displayName.includes('ScrollView')) {
285
+ const inner = transformChildren(
286
+ menu,
287
+ map,
288
+ props.children as React.ReactNode,
289
+ false,
290
+ triggerBoundaryHandlers,
291
+ radioContext
292
+ )
293
+ React.Children.forEach(inner, (c) => result.push(c))
294
+ return
295
+ }
296
+
297
+ if (isTriggerLike(displayName)) {
298
+ const debugName = getTriggerDebugName(MenuType, props)
299
+ const claim = () => triggerBoundaryHandlers?.claim(debugName)
300
+ const release = () => triggerBoundaryHandlers?.release(debugName)
301
+
302
+ result.push(
303
+ React.cloneElement(child, {
304
+ onTouchStart: composeHandlers(claim, props.onTouchStart),
305
+ onTouchEnd: composeHandlers(props.onTouchEnd, release),
306
+ onTouchCancel: composeHandlers(props.onTouchCancel, release),
307
+ onResponderGrant: composeHandlers(claim, props.onResponderGrant),
308
+ onResponderRelease: composeHandlers(props.onResponderRelease, release),
309
+ onResponderTerminate: composeHandlers(props.onResponderTerminate, release),
310
+ onPressIn: composeHandlers(claim, props.onPressIn),
311
+ onPressOut: composeHandlers(props.onPressOut, release),
312
+ } as any)
313
+ )
314
+ return
315
+ }
316
+
317
+ // RadioGroup: render as a zeego Group and pipe value/onValueChange
318
+ // down to any RadioItem descendants via radioContext
319
+ if (displayName.includes('RadioGroup')) {
320
+ const {
321
+ value: rgValue,
322
+ onValueChange: rgOnValueChange,
323
+ children: rgChildren,
324
+ ...rest
325
+ } = props as Record<string, any>
326
+
327
+ result.push(
328
+ React.createElement(
329
+ menu.Group,
330
+ { ...rest, key: child.key } as any,
331
+ transformChildren(
332
+ menu,
333
+ map,
334
+ rgChildren as React.ReactNode,
335
+ false,
336
+ triggerBoundaryHandlers,
337
+ { value: rgValue, onValueChange: rgOnValueChange }
338
+ )
339
+ )
340
+ )
341
+ return
342
+ }
343
+
344
+ // RadioItem: zeego has no radio primitive, so emit a CheckboxItem whose
345
+ // 'on'/'off' state is derived from the enclosing RadioGroup's value.
346
+ if (displayName.includes('RadioItem') && radioContext) {
347
+ const {
348
+ value: itemValue,
349
+ children: rChildren,
350
+ ...rest
351
+ } = props as Record<string, any>
352
+
353
+ const cleanChildren = React.Children.map(rChildren, (c) => {
354
+ if (!React.isValidElement(c)) return c
355
+ const dn = (c.type as { displayName?: string })?.displayName || ''
356
+ if (dn.includes('ItemIndicator')) return null
357
+ return c
358
+ })
359
+
360
+ result.push(
361
+ React.createElement(
362
+ menu.CheckboxItem,
363
+ {
364
+ ...rest,
365
+ key: child.key,
366
+ value: itemValue === radioContext.value ? 'on' : 'off',
367
+ onValueChange: () => radioContext.onValueChange?.(itemValue),
368
+ } as any,
369
+ cleanChildren
370
+ )
371
+ )
207
372
  return
208
373
  }
209
374
 
210
- // Handle known component types (containers, SubTrigger, CheckboxItem)
211
375
  const componentType = getComponentType(displayName)
212
376
 
213
- // normalizing checked/value props
377
+ // normalize checkbox checked/value props
214
378
  if (componentType === 'CheckboxItem') {
215
- const { checked, onCheckedChange, value, onValueChange, children, ...rest } =
216
- props
379
+ const {
380
+ checked,
381
+ onCheckedChange,
382
+ value,
383
+ onValueChange,
384
+ children: cbChildren,
385
+ ...rest
386
+ } = props as Record<string, any>
217
387
 
218
388
  const finalValue = value ?? (checked ? 'on' : 'off')
219
389
  const finalOnValueChange =
220
390
  onValueChange ??
221
391
  (onCheckedChange && ((v: string) => onCheckedChange(v === 'on')))
222
392
 
223
- const cleanChildren = React.Children.map(children, (child) => {
224
- if (!React.isValidElement(child)) return child
225
-
226
- const childDisplayName = (child.type as any)?.displayName || ''
227
- // If it's an ItemIndicator, remove it (return null) so we don't double render the checkmark
228
- if (childDisplayName.includes('ItemIndicator')) {
229
- return null
230
- }
231
- return child
393
+ const cleanChildren = React.Children.map(cbChildren, (c) => {
394
+ if (!React.isValidElement(c)) return c
395
+ const dn = (c.type as { displayName?: string })?.displayName || ''
396
+ if (dn.includes('ItemIndicator')) return null
397
+ return c
232
398
  })
233
399
 
234
400
  result.push(
235
401
  React.createElement(
236
- ZeegoMenu.CheckboxItem,
402
+ menu.CheckboxItem,
237
403
  {
238
404
  ...rest,
239
405
  key: child.key,
240
406
  value: finalValue,
241
407
  onValueChange: finalOnValueChange,
242
- },
408
+ } as any,
243
409
  cleanChildren
244
410
  )
245
411
  )
@@ -248,41 +414,45 @@ export const createNativeMenu = (
248
414
 
249
415
  if (componentType) {
250
416
  const { children: childChildren, ...restProps } = props
251
- const isContainer = CONTAINER_TYPES.includes(componentType)
252
- // Only reverse children of Content and SubContent (not Group or Sub)
253
- const shouldReverseChildren =
417
+ const isContainer = (CONTAINER_TYPES as string[]).includes(componentType)
418
+ const shouldReverse =
254
419
  componentType === 'Content' || componentType === 'SubContent'
255
420
  result.push(
256
421
  React.createElement(
257
- COMPONENT_MAP[componentType],
258
- { ...restProps, key: child.key },
422
+ map[componentType as keyof ComponentMap],
423
+ { ...restProps, key: child.key } as any,
259
424
  isContainer
260
- ? transformForZeego(childChildren, shouldReverseChildren)
425
+ ? transformChildren(
426
+ menu,
427
+ map,
428
+ childChildren as React.ReactNode,
429
+ shouldReverse,
430
+ triggerBoundaryHandlers,
431
+ radioContext
432
+ )
261
433
  : childChildren
262
434
  )
263
435
  )
264
436
  return
265
437
  }
266
438
 
267
- // Convert Item-like components to Zeego Items
439
+ // convert Item-like components to zeego Items
268
440
  if (isItemLike(props, displayName)) {
269
441
  const { children: itemChildren, ...itemProps } = props
270
442
  result.push(
271
443
  React.createElement(
272
- ZeegoMenu.Item,
273
- { ...itemProps, key: child.key },
444
+ menu.Item,
445
+ { ...itemProps, key: child.key } as any,
274
446
  itemChildren
275
447
  )
276
448
  )
277
449
  return
278
450
  }
279
451
 
280
- // Pass through everything else
281
452
  result.push(child)
282
453
  })
283
454
 
284
- // iOS DropdownMenu (not ContextMenu) displays menu items in reverse order
285
- // Only reverse for Menu component, not ContextMenu
455
+ // iOS DropdownMenu displays items in reverse order
286
456
  if (isIos && shouldReverseOnIos && !isContextMenu) {
287
457
  result.reverse()
288
458
  }
@@ -290,31 +460,39 @@ export const createNativeMenu = (
290
460
  return result
291
461
  }
292
462
 
293
- // ===========================================
294
- // Component definitions (typed wrappers around Zeego)
295
- // ===========================================
463
+ // lazy wrapper — resolves the zeego component on first render
464
+ function lazyZeego<P extends Record<string, any>>(
465
+ name: keyof ZeegoMenuModule,
466
+ displayName?: string
467
+ ): FC<P> {
468
+ const Comp: FC<P> = (props) => {
469
+ const z = resolve()
470
+ if (!z) return null
471
+ return React.createElement(z.menu[name] as FC<any>, props)
472
+ }
473
+ Comp.displayName = displayName || name
474
+ return Comp
475
+ }
476
+
477
+ const Trigger = lazyZeego<MenuTriggerProps>('Trigger')
478
+ const Content = lazyZeego<NativeMenuContentProps>('Content')
479
+ const Item = lazyZeego<NativeMenuItemProps>('Item')
480
+ const ItemTitle = lazyZeego<NativeMenuItemTitleProps>('ItemTitle')
481
+ const ItemSubtitle = lazyZeego<NativeMenuItemSubtitleProps>('ItemSubtitle')
482
+ const ItemIcon = lazyZeego<NativeMenuItemIconProps>('ItemIcon')
483
+ const ItemImage = lazyZeego<NativeMenuItemImageProps>('ItemImage')
484
+ const ItemIndicator = lazyZeego<NativeMenuItemIndicatorProps>('ItemIndicator')
485
+ const Group = lazyZeego<NativeMenuGroupProps>('Group')
486
+ const Label = lazyZeego<NativeMenuLabelProps>('Label')
487
+ const Separator = lazyZeego<NativeMenuSeparatorProps>('Separator')
488
+ const Sub = lazyZeego<NativeMenuSubProps>('Sub')
489
+ const SubTrigger = lazyZeego<NativeMenuSubTriggerProps>('SubTrigger')
490
+ const SubContent = lazyZeego<NativeMenuSubContentProps>('SubContent')
296
491
 
297
- // Direct Zeego pass-throughs with proper types
298
- const Trigger: FC<MenuTriggerProps> = ZeegoMenu.Trigger
299
- const Content: FC<MenuContentProps> = ZeegoMenu.Content
300
- const Item: FC<MenuItemProps> = ZeegoMenu.Item
301
- const ItemTitle: FC<MenuItemTitleProps> = ZeegoMenu.ItemTitle
302
- const ItemSubtitle: FC<MenuItemSubtitleProps> = ZeegoMenu.ItemSubtitle
303
- const ItemIcon: FC<MenuItemIconProps> = ZeegoMenu.ItemIcon
304
- const ItemImage: FC<MenuItemImageProps> = ZeegoMenu.ItemImage
305
- const ItemIndicator: FC<MenuItemIndicatorProps> = ZeegoMenu.ItemIndicator
306
- const Group: FC<MenuGroupProps> = ZeegoMenu.Group
307
- const Label: FC<MenuLabelProps> = ZeegoMenu.Label
308
- const Separator: FC<MenuSeparatorProps> = ZeegoMenu.Separator
309
- const Sub: FC<MenuSubProps> = ZeegoMenu.Sub
310
- const SubTrigger: FC<MenuSubTriggerProps> = ZeegoMenu.SubTrigger
311
- const SubContent: FC<MenuSubContentProps> = ZeegoMenu.SubContent
312
-
313
- // Custom components
314
492
  const Portal: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
315
493
  Portal.displayName = 'Portal'
316
494
 
317
- const Arrow: FC<MenuArrowProps> = () => null
495
+ const Arrow: FC<NativeMenuArrowProps> = () => null
318
496
  Arrow.displayName = 'Arrow'
319
497
 
320
498
  const RadioGroup: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
@@ -322,36 +500,86 @@ export const createNativeMenu = (
322
500
 
323
501
  const RadioItem: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
324
502
  RadioItem.displayName = `${MenuType}RadioItem`
325
- // CheckboxItem wrapper to normalize checked/value props
326
- const CheckboxItem: FC<MenuCheckboxItemProps> = (props) => null
503
+
504
+ const CheckboxItem: FC<NativeMenuCheckboxItemProps> = () => null
327
505
  CheckboxItem.displayName = 'CheckboxItem'
328
506
 
329
- // Context menu specific
330
507
  const Preview: FC<ContextMenuPreviewProps> = isContextMenu
331
- ? ZeegoContextMenu.Preview
508
+ ? lazyZeego<ContextMenuPreviewProps>('Preview', `${MenuType}Preview`)
332
509
  : () => null
333
510
  Preview.displayName = `${MenuType}Preview`
334
511
 
335
- const Auxiliary: FC<ContextMenuPreviewProps> = isContextMenu
336
- ? ZeegoContextMenu.Auxiliary
512
+ const Auxiliary: FC<NativeContextMenuAuxiliaryProps> = isContextMenu
513
+ ? lazyZeego<NativeContextMenuAuxiliaryProps>('Auxiliary', `${MenuType}Auxiliary`)
337
514
  : () => null
338
515
  Auxiliary.displayName = `${MenuType}Auxiliary`
339
516
 
340
- // Main Menu component
341
- const Menu: FC<MenuProps> = ({ children, onOpenChange, onOpenWillChange }) => {
342
- const rootProps: Record<string, unknown> = { onOpenChange }
517
+ // on Android, provide NativeMenuContext so components use Gesture.Manual()
518
+ // instead of Gesture.Tap() (which sends ACTION_CANCEL to MenuView)
519
+ const Menu: FC<NativeMenuProps> = ({ children, onOpenChange, onOpenWillChange }) => {
520
+ const triggerOwnerRef = React.useRef<object | null>(null)
521
+ const claimTriggerBoundary = React.useCallback((debugName?: string | null) => {
522
+ if (triggerOwnerRef.current) {
523
+ unstable_releaseExternalPressOwnership(triggerOwnerRef.current, debugName)
524
+ }
525
+ triggerOwnerRef.current = unstable_claimExternalPressOwnership(debugName)
526
+ }, [])
527
+
528
+ const releaseTriggerBoundary = React.useCallback((debugName?: string | null) => {
529
+ if (!triggerOwnerRef.current) return
530
+ unstable_releaseExternalPressOwnership(triggerOwnerRef.current, debugName)
531
+ triggerOwnerRef.current = null
532
+ }, [])
533
+
534
+ React.useEffect(() => releaseTriggerBoundary, [releaseTriggerBoundary])
535
+
536
+ const z = resolve()
537
+ if (!z) return null
538
+
539
+ const handleOpenChange = React.useCallback(
540
+ (isOpen: boolean) => {
541
+ if (!isOpen) {
542
+ releaseTriggerBoundary()
543
+ }
544
+ onOpenChange?.(isOpen)
545
+ },
546
+ [onOpenChange, releaseTriggerBoundary]
547
+ )
548
+
549
+ const handleOpenWillChange = React.useCallback(
550
+ (willOpen: boolean) => {
551
+ if (!willOpen) {
552
+ releaseTriggerBoundary()
553
+ }
554
+ onOpenWillChange?.(willOpen)
555
+ },
556
+ [onOpenWillChange, releaseTriggerBoundary]
557
+ )
558
+
559
+ const rootProps: Record<string, unknown> = { onOpenChange: handleOpenChange }
343
560
  if (isContextMenu && onOpenWillChange) {
344
- rootProps.onOpenWillChange = onOpenWillChange
561
+ rootProps.onOpenWillChange = handleOpenWillChange
562
+ }
563
+
564
+ const content = (
565
+ <z.menu.Root {...rootProps}>
566
+ {transformChildren(z.menu, z.componentMap, children, false, {
567
+ claim: claimTriggerBoundary,
568
+ release: releaseTriggerBoundary,
569
+ })}
570
+ </z.menu.Root>
571
+ )
572
+
573
+ if (isAndroid) {
574
+ return (
575
+ <NativeMenuContext.Provider value={true}>{content}</NativeMenuContext.Provider>
576
+ )
345
577
  }
346
578
 
347
- return <ZeegoMenu.Root {...rootProps}>{transformForZeego(children)}</ZeegoMenu.Root>
579
+ return content
348
580
  }
349
581
  Menu.displayName = MenuType
350
582
 
351
- // ===========================================
352
- // Export
353
- // ===========================================
354
-
355
583
  return {
356
584
  Menu: withStaticProperties(Menu, {
357
585
  Trigger,