@tamagui/create-menu 2.0.0-rc.4 → 2.0.0-rc.40

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 (149) 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 +1144 -933
  5. package/dist/cjs/createBaseMenu.native.js +1266 -1100
  6. package/dist/cjs/createBaseMenu.native.js.map +1 -1
  7. package/dist/cjs/createNativeMenu/createNativeMenu.cjs +282 -159
  8. package/dist/cjs/createNativeMenu/createNativeMenu.native.js +390 -268
  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 +1104 -895
  27. package/dist/esm/createBaseMenu.mjs.map +1 -1
  28. package/dist/esm/createBaseMenu.native.js +1226 -1062
  29. package/dist/esm/createBaseMenu.native.js.map +1 -1
  30. package/dist/esm/createNativeMenu/createNativeMenu.mjs +255 -134
  31. package/dist/esm/createNativeMenu/createNativeMenu.mjs.map +1 -1
  32. package/dist/esm/createNativeMenu/createNativeMenu.native.js +336 -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 +1104 -895
  53. package/dist/jsx/createBaseMenu.mjs.map +1 -1
  54. package/dist/jsx/createBaseMenu.native.js +1266 -1100
  55. package/dist/jsx/createBaseMenu.native.js.map +1 -1
  56. package/dist/jsx/createNativeMenu/createNativeMenu.mjs +255 -134
  57. package/dist/jsx/createNativeMenu/createNativeMenu.mjs.map +1 -1
  58. package/dist/jsx/createNativeMenu/createNativeMenu.native.js +390 -268
  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 +25 -27
  76. package/src/MenuPredefined.tsx +1 -1
  77. package/src/createBaseMenu.tsx +359 -271
  78. package/src/createNativeMenu/createNativeMenu.tsx +383 -222
  79. package/src/createNativeMenu/createNativeMenuTypes.ts +20 -20
  80. package/src/createNativeMenu/withNativeMenu.tsx +5 -3
  81. package/src/index.tsx +3 -5
  82. package/types/createBaseMenu.d.ts +121 -35
  83. package/types/createBaseMenu.d.ts.map +1 -1
  84. package/types/createNativeMenu/createNativeMenu.d.ts +21 -21
  85. package/types/createNativeMenu/createNativeMenu.d.ts.map +1 -1
  86. package/types/createNativeMenu/createNativeMenuTypes.d.ts +20 -20
  87. package/types/createNativeMenu/createNativeMenuTypes.d.ts.map +1 -1
  88. package/types/createNativeMenu/withNativeMenu.d.ts +3 -3
  89. package/types/createNativeMenu/withNativeMenu.d.ts.map +1 -1
  90. package/types/index.d.ts +3 -2
  91. package/types/index.d.ts.map +1 -1
  92. package/dist/cjs/MenuPredefined.js +0 -168
  93. package/dist/cjs/MenuPredefined.js.map +0 -6
  94. package/dist/cjs/createBaseMenu.js +0 -832
  95. package/dist/cjs/createBaseMenu.js.map +0 -6
  96. package/dist/cjs/createNativeMenu/createNativeMenu.js +0 -177
  97. package/dist/cjs/createNativeMenu/createNativeMenu.js.map +0 -6
  98. package/dist/cjs/createNativeMenu/createNativeMenuTypes.js +0 -14
  99. package/dist/cjs/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  100. package/dist/cjs/createNativeMenu/index.cjs +0 -19
  101. package/dist/cjs/createNativeMenu/index.js +0 -16
  102. package/dist/cjs/createNativeMenu/index.js.map +0 -6
  103. package/dist/cjs/createNativeMenu/index.native.js +0 -22
  104. package/dist/cjs/createNativeMenu/index.native.js.map +0 -1
  105. package/dist/cjs/createNativeMenu/utils.js +0 -66
  106. package/dist/cjs/createNativeMenu/utils.js.map +0 -6
  107. package/dist/cjs/createNativeMenu/withNativeMenu.js +0 -30
  108. package/dist/cjs/createNativeMenu/withNativeMenu.js.map +0 -6
  109. package/dist/cjs/index.js +0 -23
  110. package/dist/cjs/index.js.map +0 -6
  111. package/dist/esm/MenuPredefined.js +0 -154
  112. package/dist/esm/MenuPredefined.js.map +0 -6
  113. package/dist/esm/createBaseMenu.js +0 -838
  114. package/dist/esm/createBaseMenu.js.map +0 -6
  115. package/dist/esm/createNativeMenu/createNativeMenu.js +0 -156
  116. package/dist/esm/createNativeMenu/createNativeMenu.js.map +0 -6
  117. package/dist/esm/createNativeMenu/createNativeMenuTypes.js +0 -1
  118. package/dist/esm/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  119. package/dist/esm/createNativeMenu/index.js +0 -3
  120. package/dist/esm/createNativeMenu/index.js.map +0 -6
  121. package/dist/esm/createNativeMenu/index.mjs +0 -3
  122. package/dist/esm/createNativeMenu/index.mjs.map +0 -1
  123. package/dist/esm/createNativeMenu/index.native.js +0 -3
  124. package/dist/esm/createNativeMenu/index.native.js.map +0 -1
  125. package/dist/esm/createNativeMenu/utils.js +0 -47
  126. package/dist/esm/createNativeMenu/utils.js.map +0 -6
  127. package/dist/esm/createNativeMenu/withNativeMenu.js +0 -15
  128. package/dist/esm/createNativeMenu/withNativeMenu.js.map +0 -6
  129. package/dist/jsx/MenuPredefined.js +0 -154
  130. package/dist/jsx/MenuPredefined.js.map +0 -6
  131. package/dist/jsx/createBaseMenu.js +0 -838
  132. package/dist/jsx/createBaseMenu.js.map +0 -6
  133. package/dist/jsx/createNativeMenu/createNativeMenu.js +0 -156
  134. package/dist/jsx/createNativeMenu/createNativeMenu.js.map +0 -6
  135. package/dist/jsx/createNativeMenu/createNativeMenuTypes.js +0 -1
  136. package/dist/jsx/createNativeMenu/createNativeMenuTypes.js.map +0 -6
  137. package/dist/jsx/createNativeMenu/index.js +0 -3
  138. package/dist/jsx/createNativeMenu/index.js.map +0 -6
  139. package/dist/jsx/createNativeMenu/index.mjs +0 -3
  140. package/dist/jsx/createNativeMenu/index.mjs.map +0 -1
  141. package/dist/jsx/createNativeMenu/index.native.js +0 -22
  142. package/dist/jsx/createNativeMenu/index.native.js.map +0 -1
  143. package/dist/jsx/createNativeMenu/utils.js +0 -47
  144. package/dist/jsx/createNativeMenu/utils.js.map +0 -6
  145. package/dist/jsx/createNativeMenu/withNativeMenu.js +0 -15
  146. package/dist/jsx/createNativeMenu/withNativeMenu.js.map +0 -6
  147. package/src/createNativeMenu/index.tsx +0 -7
  148. package/types/createNativeMenu/index.d.ts +0 -4
  149. package/types/createNativeMenu/index.d.ts.map +0 -1
@@ -1,194 +1,254 @@
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')
179
- }
180
-
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 = (
244
+ // transform children tree for zeego compatibility
245
+ function transformChildren(
246
+ menu: ZeegoMenuModule,
247
+ map: ComponentMap,
189
248
  children: React.ReactNode,
190
- shouldReverseOnIos = false
191
- ): React.ReactNode => {
249
+ shouldReverseOnIos = false,
250
+ triggerBoundaryHandlers?: TriggerPressBoundaryHandlers
251
+ ): React.ReactNode {
192
252
  const result: React.ReactNode[] = []
193
253
 
194
254
  React.Children.forEach(children, (child) => {
@@ -197,49 +257,89 @@ export const createNativeMenu = (
197
257
  return
198
258
  }
199
259
 
200
- const displayName = (child.type as any)?.displayName || ''
260
+ const displayName = (child.type as { displayName?: string })?.displayName || ''
201
261
  const props = child.props as Record<string, any>
202
262
 
203
- // Flatten Portal
204
- if (isPortal(displayName)) {
205
- const portalChildren = transformForZeego(props.children, false)
206
- React.Children.forEach(portalChildren, (c) => result.push(c))
263
+ // flatten portal wrappers
264
+ if (isPortalLike(displayName)) {
265
+ const inner = transformChildren(
266
+ menu,
267
+ map,
268
+ props.children as React.ReactNode,
269
+ false,
270
+ triggerBoundaryHandlers
271
+ )
272
+ React.Children.forEach(inner, (c) => result.push(c))
273
+ return
274
+ }
275
+
276
+ // flatten ScrollView (native passthrough — children need to be visible to zeego)
277
+ if (displayName.includes('ScrollView')) {
278
+ const inner = transformChildren(
279
+ menu,
280
+ map,
281
+ props.children as React.ReactNode,
282
+ false,
283
+ triggerBoundaryHandlers
284
+ )
285
+ React.Children.forEach(inner, (c) => result.push(c))
286
+ return
287
+ }
288
+
289
+ if (isTriggerLike(displayName)) {
290
+ const debugName = getTriggerDebugName(MenuType, props)
291
+ const claim = () => triggerBoundaryHandlers?.claim(debugName)
292
+ const release = () => triggerBoundaryHandlers?.release(debugName)
293
+
294
+ result.push(
295
+ React.cloneElement(child, {
296
+ onTouchStart: composeHandlers(claim, props.onTouchStart),
297
+ onTouchEnd: composeHandlers(props.onTouchEnd, release),
298
+ onTouchCancel: composeHandlers(props.onTouchCancel, release),
299
+ onResponderGrant: composeHandlers(claim, props.onResponderGrant),
300
+ onResponderRelease: composeHandlers(props.onResponderRelease, release),
301
+ onResponderTerminate: composeHandlers(props.onResponderTerminate, release),
302
+ onPressIn: composeHandlers(claim, props.onPressIn),
303
+ onPressOut: composeHandlers(props.onPressOut, release),
304
+ } as any)
305
+ )
207
306
  return
208
307
  }
209
308
 
210
- // Handle known component types (containers, SubTrigger, CheckboxItem)
211
309
  const componentType = getComponentType(displayName)
212
310
 
213
- // normalizing checked/value props
311
+ // normalize checkbox checked/value props
214
312
  if (componentType === 'CheckboxItem') {
215
- const { checked, onCheckedChange, value, onValueChange, children, ...rest } =
216
- props
313
+ const {
314
+ checked,
315
+ onCheckedChange,
316
+ value,
317
+ onValueChange,
318
+ children: cbChildren,
319
+ ...rest
320
+ } = props as Record<string, any>
217
321
 
218
322
  const finalValue = value ?? (checked ? 'on' : 'off')
219
323
  const finalOnValueChange =
220
324
  onValueChange ??
221
325
  (onCheckedChange && ((v: string) => onCheckedChange(v === 'on')))
222
326
 
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
327
+ const cleanChildren = React.Children.map(cbChildren, (c) => {
328
+ if (!React.isValidElement(c)) return c
329
+ const dn = (c.type as { displayName?: string })?.displayName || ''
330
+ if (dn.includes('ItemIndicator')) return null
331
+ return c
232
332
  })
233
333
 
234
334
  result.push(
235
335
  React.createElement(
236
- ZeegoMenu.CheckboxItem,
336
+ menu.CheckboxItem,
237
337
  {
238
338
  ...rest,
239
339
  key: child.key,
240
340
  value: finalValue,
241
341
  onValueChange: finalOnValueChange,
242
- },
342
+ } as any,
243
343
  cleanChildren
244
344
  )
245
345
  )
@@ -248,41 +348,44 @@ export const createNativeMenu = (
248
348
 
249
349
  if (componentType) {
250
350
  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 =
351
+ const isContainer = (CONTAINER_TYPES as string[]).includes(componentType)
352
+ const shouldReverse =
254
353
  componentType === 'Content' || componentType === 'SubContent'
255
354
  result.push(
256
355
  React.createElement(
257
- COMPONENT_MAP[componentType],
258
- { ...restProps, key: child.key },
356
+ map[componentType as keyof ComponentMap],
357
+ { ...restProps, key: child.key } as any,
259
358
  isContainer
260
- ? transformForZeego(childChildren, shouldReverseChildren)
359
+ ? transformChildren(
360
+ menu,
361
+ map,
362
+ childChildren as React.ReactNode,
363
+ shouldReverse,
364
+ triggerBoundaryHandlers
365
+ )
261
366
  : childChildren
262
367
  )
263
368
  )
264
369
  return
265
370
  }
266
371
 
267
- // Convert Item-like components to Zeego Items
372
+ // convert Item-like components to zeego Items
268
373
  if (isItemLike(props, displayName)) {
269
374
  const { children: itemChildren, ...itemProps } = props
270
375
  result.push(
271
376
  React.createElement(
272
- ZeegoMenu.Item,
273
- { ...itemProps, key: child.key },
377
+ menu.Item,
378
+ { ...itemProps, key: child.key } as any,
274
379
  itemChildren
275
380
  )
276
381
  )
277
382
  return
278
383
  }
279
384
 
280
- // Pass through everything else
281
385
  result.push(child)
282
386
  })
283
387
 
284
- // iOS DropdownMenu (not ContextMenu) displays menu items in reverse order
285
- // Only reverse for Menu component, not ContextMenu
388
+ // iOS DropdownMenu displays items in reverse order
286
389
  if (isIos && shouldReverseOnIos && !isContextMenu) {
287
390
  result.reverse()
288
391
  }
@@ -290,31 +393,39 @@ export const createNativeMenu = (
290
393
  return result
291
394
  }
292
395
 
293
- // ===========================================
294
- // Component definitions (typed wrappers around Zeego)
295
- // ===========================================
396
+ // lazy wrapper — resolves the zeego component on first render
397
+ function lazyZeego<P extends Record<string, any>>(
398
+ name: keyof ZeegoMenuModule,
399
+ displayName?: string
400
+ ): FC<P> {
401
+ const Comp: FC<P> = (props) => {
402
+ const z = resolve()
403
+ if (!z) return null
404
+ return React.createElement(z.menu[name] as FC<any>, props)
405
+ }
406
+ Comp.displayName = displayName || name
407
+ return Comp
408
+ }
409
+
410
+ const Trigger = lazyZeego<MenuTriggerProps>('Trigger')
411
+ const Content = lazyZeego<NativeMenuContentProps>('Content')
412
+ const Item = lazyZeego<NativeMenuItemProps>('Item')
413
+ const ItemTitle = lazyZeego<NativeMenuItemTitleProps>('ItemTitle')
414
+ const ItemSubtitle = lazyZeego<NativeMenuItemSubtitleProps>('ItemSubtitle')
415
+ const ItemIcon = lazyZeego<NativeMenuItemIconProps>('ItemIcon')
416
+ const ItemImage = lazyZeego<NativeMenuItemImageProps>('ItemImage')
417
+ const ItemIndicator = lazyZeego<NativeMenuItemIndicatorProps>('ItemIndicator')
418
+ const Group = lazyZeego<NativeMenuGroupProps>('Group')
419
+ const Label = lazyZeego<NativeMenuLabelProps>('Label')
420
+ const Separator = lazyZeego<NativeMenuSeparatorProps>('Separator')
421
+ const Sub = lazyZeego<NativeMenuSubProps>('Sub')
422
+ const SubTrigger = lazyZeego<NativeMenuSubTriggerProps>('SubTrigger')
423
+ const SubContent = lazyZeego<NativeMenuSubContentProps>('SubContent')
296
424
 
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
425
  const Portal: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
315
426
  Portal.displayName = 'Portal'
316
427
 
317
- const Arrow: FC<MenuArrowProps> = () => null
428
+ const Arrow: FC<NativeMenuArrowProps> = () => null
318
429
  Arrow.displayName = 'Arrow'
319
430
 
320
431
  const RadioGroup: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
@@ -322,36 +433,86 @@ export const createNativeMenu = (
322
433
 
323
434
  const RadioItem: FC<{ children: React.ReactNode }> = ({ children }) => <>{children}</>
324
435
  RadioItem.displayName = `${MenuType}RadioItem`
325
- // CheckboxItem wrapper to normalize checked/value props
326
- const CheckboxItem: FC<MenuCheckboxItemProps> = (props) => null
436
+
437
+ const CheckboxItem: FC<NativeMenuCheckboxItemProps> = () => null
327
438
  CheckboxItem.displayName = 'CheckboxItem'
328
439
 
329
- // Context menu specific
330
440
  const Preview: FC<ContextMenuPreviewProps> = isContextMenu
331
- ? ZeegoContextMenu.Preview
441
+ ? lazyZeego<ContextMenuPreviewProps>('Preview', `${MenuType}Preview`)
332
442
  : () => null
333
443
  Preview.displayName = `${MenuType}Preview`
334
444
 
335
- const Auxiliary: FC<ContextMenuPreviewProps> = isContextMenu
336
- ? ZeegoContextMenu.Auxiliary
445
+ const Auxiliary: FC<NativeContextMenuAuxiliaryProps> = isContextMenu
446
+ ? lazyZeego<NativeContextMenuAuxiliaryProps>('Auxiliary', `${MenuType}Auxiliary`)
337
447
  : () => null
338
448
  Auxiliary.displayName = `${MenuType}Auxiliary`
339
449
 
340
- // Main Menu component
341
- const Menu: FC<MenuProps> = ({ children, onOpenChange, onOpenWillChange }) => {
342
- const rootProps: Record<string, unknown> = { onOpenChange }
450
+ // on Android, provide NativeMenuContext so components use Gesture.Manual()
451
+ // instead of Gesture.Tap() (which sends ACTION_CANCEL to MenuView)
452
+ const Menu: FC<NativeMenuProps> = ({ children, onOpenChange, onOpenWillChange }) => {
453
+ const triggerOwnerRef = React.useRef<object | null>(null)
454
+ const claimTriggerBoundary = React.useCallback((debugName?: string | null) => {
455
+ if (triggerOwnerRef.current) {
456
+ unstable_releaseExternalPressOwnership(triggerOwnerRef.current, debugName)
457
+ }
458
+ triggerOwnerRef.current = unstable_claimExternalPressOwnership(debugName)
459
+ }, [])
460
+
461
+ const releaseTriggerBoundary = React.useCallback((debugName?: string | null) => {
462
+ if (!triggerOwnerRef.current) return
463
+ unstable_releaseExternalPressOwnership(triggerOwnerRef.current, debugName)
464
+ triggerOwnerRef.current = null
465
+ }, [])
466
+
467
+ React.useEffect(() => releaseTriggerBoundary, [releaseTriggerBoundary])
468
+
469
+ const z = resolve()
470
+ if (!z) return null
471
+
472
+ const handleOpenChange = React.useCallback(
473
+ (isOpen: boolean) => {
474
+ if (!isOpen) {
475
+ releaseTriggerBoundary()
476
+ }
477
+ onOpenChange?.(isOpen)
478
+ },
479
+ [onOpenChange, releaseTriggerBoundary]
480
+ )
481
+
482
+ const handleOpenWillChange = React.useCallback(
483
+ (willOpen: boolean) => {
484
+ if (!willOpen) {
485
+ releaseTriggerBoundary()
486
+ }
487
+ onOpenWillChange?.(willOpen)
488
+ },
489
+ [onOpenWillChange, releaseTriggerBoundary]
490
+ )
491
+
492
+ const rootProps: Record<string, unknown> = { onOpenChange: handleOpenChange }
343
493
  if (isContextMenu && onOpenWillChange) {
344
- rootProps.onOpenWillChange = onOpenWillChange
494
+ rootProps.onOpenWillChange = handleOpenWillChange
495
+ }
496
+
497
+ const content = (
498
+ <z.menu.Root {...rootProps}>
499
+ {transformChildren(z.menu, z.componentMap, children, false, {
500
+ claim: claimTriggerBoundary,
501
+ release: releaseTriggerBoundary,
502
+ })}
503
+ </z.menu.Root>
504
+ )
505
+
506
+ if (isAndroid) {
507
+ return (
508
+ <NativeMenuContext.Provider value={true}>{content}</NativeMenuContext.Provider>
509
+ )
345
510
  }
346
511
 
347
- return <ZeegoMenu.Root {...rootProps}>{transformForZeego(children)}</ZeegoMenu.Root>
512
+ return content
348
513
  }
349
514
  Menu.displayName = MenuType
350
515
 
351
- // ===========================================
352
- // Export
353
- // ===========================================
354
-
355
516
  return {
356
517
  Menu: withStaticProperties(Menu, {
357
518
  Trigger,