@tamagui/native 2.6.3 → 2.7.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 (69) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cjs/expoUIMenuAdapter.cjs +265 -0
  3. package/dist/cjs/expoUIMenuAdapter.native.js +321 -0
  4. package/dist/cjs/expoUIMenuAdapter.native.js.map +1 -0
  5. package/dist/cjs/index.cjs +5 -2
  6. package/dist/cjs/index.native.js +5 -2
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/{zeegoState.cjs → nativeMenuState.cjs} +19 -19
  9. package/dist/cjs/{zeegoState.native.js → nativeMenuState.native.js} +20 -20
  10. package/dist/cjs/nativeMenuState.native.js.map +1 -0
  11. package/dist/cjs/setup-expo-ui-menu.cjs +17 -0
  12. package/dist/cjs/setup-expo-ui-menu.native.js +20 -0
  13. package/dist/cjs/setup-expo-ui-menu.native.js.map +1 -0
  14. package/dist/cjs/setup-zeego.cjs +11 -13
  15. package/dist/cjs/setup-zeego.native.js +11 -13
  16. package/dist/cjs/setup-zeego.native.js.map +1 -1
  17. package/dist/esm/expoUIMenuAdapter.mjs +229 -0
  18. package/dist/esm/expoUIMenuAdapter.mjs.map +1 -0
  19. package/dist/esm/expoUIMenuAdapter.native.js +282 -0
  20. package/dist/esm/expoUIMenuAdapter.native.js.map +1 -0
  21. package/dist/esm/index.js +3 -2
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/index.mjs +3 -2
  24. package/dist/esm/index.mjs.map +1 -1
  25. package/dist/esm/index.native.js +3 -2
  26. package/dist/esm/index.native.js.map +1 -1
  27. package/dist/esm/nativeMenuState.mjs +20 -0
  28. package/dist/esm/nativeMenuState.mjs.map +1 -0
  29. package/dist/esm/nativeMenuState.native.js +20 -0
  30. package/dist/esm/nativeMenuState.native.js.map +1 -0
  31. package/dist/esm/setup-expo-ui-menu.mjs +18 -0
  32. package/dist/esm/setup-expo-ui-menu.mjs.map +1 -0
  33. package/dist/esm/setup-expo-ui-menu.native.js +18 -0
  34. package/dist/esm/setup-expo-ui-menu.native.js.map +1 -0
  35. package/dist/esm/setup-zeego.mjs +11 -13
  36. package/dist/esm/setup-zeego.mjs.map +1 -1
  37. package/dist/esm/setup-zeego.native.js +11 -13
  38. package/dist/esm/setup-zeego.native.js.map +1 -1
  39. package/package.json +14 -2
  40. package/setup-expo-ui-menu/index.cjs +1 -0
  41. package/setup-expo-ui-menu/index.js +1 -0
  42. package/setup-expo-ui-menu/index.native.cjs +1 -0
  43. package/setup-expo-ui-menu/index.native.js +1 -0
  44. package/setup-expo-ui-menu.cjs +1 -0
  45. package/src/expoUIMenuAdapter.tsx +320 -0
  46. package/src/index.ts +5 -4
  47. package/src/nativeMenuState.ts +63 -0
  48. package/src/setup-expo-ui-menu.ts +25 -0
  49. package/src/setup-zeego.ts +12 -16
  50. package/src/types.ts +0 -6
  51. package/types/expoUIMenuAdapter.d.ts +8 -0
  52. package/types/expoUIMenuAdapter.d.ts.map +11 -0
  53. package/types/index.d.ts +5 -3
  54. package/types/index.d.ts.map +2 -2
  55. package/types/nativeMenuState.d.ts +38 -0
  56. package/types/nativeMenuState.d.ts.map +11 -0
  57. package/types/setup-expo-ui-menu.d.ts +3 -0
  58. package/types/setup-expo-ui-menu.d.ts.map +11 -0
  59. package/types/setup-zeego.d.ts.map +1 -1
  60. package/types/types.d.ts +0 -5
  61. package/types/types.d.ts.map +2 -2
  62. package/dist/cjs/zeegoState.native.js.map +0 -1
  63. package/dist/esm/zeegoState.mjs +0 -21
  64. package/dist/esm/zeegoState.mjs.map +0 -1
  65. package/dist/esm/zeegoState.native.js +0 -21
  66. package/dist/esm/zeegoState.native.js.map +0 -1
  67. package/src/zeegoState.ts +0 -28
  68. package/types/zeegoState.d.ts +0 -9
  69. package/types/zeegoState.d.ts.map +0 -11
@@ -0,0 +1,320 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from 'react'
2
+ import React from 'react'
3
+
4
+ import type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'
5
+
6
+ type ExpoMenuAction = {
7
+ id: string
8
+ title: string
9
+ image?: unknown
10
+ state?: 'on' | 'off'
11
+ attributes?: {
12
+ destructive?: boolean
13
+ disabled?: boolean
14
+ hidden?: boolean
15
+ }
16
+ subactions?: ExpoMenuAction[]
17
+ displayInline?: boolean
18
+ }
19
+
20
+ type ExpoMenuViewProps = {
21
+ actions: ExpoMenuAction[]
22
+ title?: string
23
+ shouldOpenOnLongPress?: boolean
24
+ onPressAction?: (event: { nativeEvent: { event: string } }) => void
25
+ onOpenMenu?: () => void
26
+ onCloseMenu?: () => void
27
+ children?: ReactNode
28
+ }
29
+
30
+ export type ExpoUIMenuAdapterOptions = {
31
+ MenuView: ComponentType<any>
32
+ }
33
+
34
+ type MarkerProps = {
35
+ children?: ReactNode
36
+ [key: string]: unknown
37
+ }
38
+
39
+ function marker(name: string): ComponentType<MarkerProps> {
40
+ const Marker = () => null
41
+ Marker.displayName = `ExpoUI${name}`
42
+ return Marker
43
+ }
44
+
45
+ function elementOfType(
46
+ children: ReactNode,
47
+ type: ComponentType<MarkerProps>
48
+ ): ReactElement<MarkerProps> | null {
49
+ for (const child of React.Children.toArray(children)) {
50
+ if (React.isValidElement<MarkerProps>(child) && child.type === type) {
51
+ return child
52
+ }
53
+ }
54
+ return null
55
+ }
56
+
57
+ function textFromNode(node: ReactNode): string {
58
+ if (typeof node === 'string' || typeof node === 'number') return String(node)
59
+ if (!React.isValidElement<MarkerProps>(node)) return ''
60
+ return React.Children.toArray(node.props.children).map(textFromNode).join('')
61
+ }
62
+
63
+ export function createExpoUIMenuAdapter({
64
+ MenuView,
65
+ }: ExpoUIMenuAdapterOptions): NativeMenuAdapter {
66
+ const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView
67
+
68
+ function createModule(isContextMenu: boolean): NativeMenuModule {
69
+ const Trigger = marker('Trigger')
70
+ const Content = marker('Content')
71
+ const Item = marker('Item')
72
+ const ItemTitle = marker('ItemTitle')
73
+ const ItemSubtitle = marker('ItemSubtitle')
74
+ const ItemIcon = marker('ItemIcon')
75
+ const ItemImage = marker('ItemImage')
76
+ const ItemIndicator = marker('ItemIndicator')
77
+ const Group = marker('Group')
78
+ const Label = marker('Label')
79
+ const Separator = marker('Separator')
80
+ const Sub = marker('Sub')
81
+ const SubTrigger = marker('SubTrigger')
82
+ const SubContent = marker('SubContent')
83
+ const CheckboxItem = marker('CheckboxItem')
84
+ const Preview = marker('Preview')
85
+ const Auxiliary = marker('Auxiliary')
86
+
87
+ function Root({
88
+ children,
89
+ onOpenChange,
90
+ onOpenWillChange,
91
+ }: {
92
+ children?: ReactNode
93
+ onOpenChange?: (open: boolean) => void
94
+ onOpenWillChange?: (open: boolean) => void
95
+ }) {
96
+ const trigger = elementOfType(children, Trigger)
97
+ const content = elementOfType(children, Content)
98
+ const preview = elementOfType(children, Preview)
99
+ const auxiliary = elementOfType(children, Auxiliary)
100
+
101
+ if (preview || auxiliary) {
102
+ throw new Error(
103
+ '@expo/ui/community/menu does not support Tamagui context-menu previews or auxiliary views'
104
+ )
105
+ }
106
+ if (!trigger || !content) {
107
+ throw new Error('Tamagui native menus require one Trigger and one Content')
108
+ }
109
+
110
+ let nextId = 0
111
+ const handlers = new Map<string, () => void>()
112
+
113
+ function actionId(element: ReactElement<MarkerProps>): string {
114
+ return element.key == null
115
+ ? `tamagui-menu-${nextId++}`
116
+ : String(element.key).replace(/^\.\$/, '')
117
+ }
118
+
119
+ function itemTitle(element: ReactElement<MarkerProps>): string {
120
+ if (typeof element.props.textValue === 'string') return element.props.textValue
121
+ const title = elementOfType(element.props.children, ItemTitle)
122
+ return title ? textFromNode(title.props.children) : ''
123
+ }
124
+
125
+ function actionImage(element: ReactElement<MarkerProps>): unknown {
126
+ const image = elementOfType(element.props.children, ItemImage)
127
+ if (image?.props.source) return image.props.source
128
+ const icon = elementOfType(element.props.children, ItemIcon)
129
+ if (!icon) return undefined
130
+ const ios = icon.props.ios
131
+ if (
132
+ typeof ios === 'object' &&
133
+ ios &&
134
+ 'name' in ios &&
135
+ typeof ios.name === 'string'
136
+ ) {
137
+ return ios.name
138
+ }
139
+ return typeof icon.props.iosIconName === 'string'
140
+ ? icon.props.iosIconName
141
+ : undefined
142
+ }
143
+
144
+ function attributes(
145
+ element: ReactElement<MarkerProps>
146
+ ): ExpoMenuAction['attributes'] {
147
+ const destructive = element.props.destructive === true
148
+ const disabled = element.props.disabled === true
149
+ const hidden = element.props.hidden === true
150
+ return destructive || disabled || hidden
151
+ ? { destructive, disabled, hidden }
152
+ : undefined
153
+ }
154
+
155
+ function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {
156
+ const actions: ExpoMenuAction[] = []
157
+ let section: ExpoMenuAction[] | null = null
158
+
159
+ function append(action: ExpoMenuAction) {
160
+ if (section) {
161
+ section.push(action)
162
+ } else {
163
+ actions.push(action)
164
+ }
165
+ }
166
+
167
+ function flushSection() {
168
+ if (!section?.length) {
169
+ section = []
170
+ return
171
+ }
172
+ actions.push({
173
+ id: `tamagui-menu-section-${nextId++}`,
174
+ title: '',
175
+ subactions: section,
176
+ displayInline: true,
177
+ })
178
+ section = []
179
+ }
180
+
181
+ for (const child of React.Children.toArray(childrenToParse)) {
182
+ if (!React.isValidElement<MarkerProps>(child)) continue
183
+
184
+ if (child.type === Separator) {
185
+ if (!section) section = actions.splice(0)
186
+ flushSection()
187
+ continue
188
+ }
189
+
190
+ if (child.type === Group) {
191
+ const label = elementOfType(child.props.children, Label)
192
+ append({
193
+ id: actionId(child),
194
+ title: label ? textFromNode(label.props.children) : '',
195
+ subactions: actionsFrom(child.props.children),
196
+ displayInline: true,
197
+ })
198
+ continue
199
+ }
200
+
201
+ if (child.type === Sub) {
202
+ const subTrigger = elementOfType(child.props.children, SubTrigger)
203
+ const subContent = elementOfType(child.props.children, SubContent)
204
+ if (!subTrigger || !subContent) {
205
+ throw new Error(
206
+ 'Tamagui native submenus require one SubTrigger and SubContent'
207
+ )
208
+ }
209
+ append({
210
+ id: actionId(child),
211
+ title: itemTitle(subTrigger),
212
+ image: actionImage(subTrigger),
213
+ attributes: attributes(subTrigger),
214
+ subactions: actionsFrom(subContent.props.children),
215
+ })
216
+ continue
217
+ }
218
+
219
+ if (child.type !== Item && child.type !== CheckboxItem) continue
220
+
221
+ if (elementOfType(child.props.children, ItemSubtitle)) {
222
+ throw new Error(
223
+ '@expo/ui/community/menu does not support menu item subtitles'
224
+ )
225
+ }
226
+
227
+ const id = actionId(child)
228
+ if (child.type === CheckboxItem) {
229
+ const current =
230
+ child.props.value === true ||
231
+ child.props.value === 'on' ||
232
+ child.props.checked === true
233
+ const onValueChange = child.props.onValueChange
234
+ const onCheckedChange = child.props.onCheckedChange
235
+ handlers.set(id, () => {
236
+ if (typeof onValueChange === 'function') {
237
+ onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')
238
+ } else if (typeof onCheckedChange === 'function') {
239
+ onCheckedChange(!current)
240
+ }
241
+ })
242
+ append({
243
+ id,
244
+ title: itemTitle(child),
245
+ image: actionImage(child),
246
+ state: current ? 'on' : 'off',
247
+ attributes: attributes(child),
248
+ })
249
+ continue
250
+ }
251
+
252
+ const onSelect = child.props.onSelect
253
+ if (typeof onSelect === 'function') {
254
+ handlers.set(id, () => onSelect())
255
+ }
256
+ append({
257
+ id,
258
+ title: itemTitle(child),
259
+ image: actionImage(child),
260
+ attributes: attributes(child),
261
+ })
262
+ }
263
+
264
+ if (section) flushSection()
265
+ return actions
266
+ }
267
+
268
+ const label = elementOfType(content.props.children, Label)
269
+ const triggerAction = trigger.props.action
270
+
271
+ return (
272
+ <NativeMenuView
273
+ actions={actionsFrom(content.props.children)}
274
+ title={label ? textFromNode(label.props.children) : undefined}
275
+ shouldOpenOnLongPress={
276
+ triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)
277
+ }
278
+ onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}
279
+ onOpenMenu={() => {
280
+ onOpenWillChange?.(true)
281
+ onOpenChange?.(true)
282
+ }}
283
+ onCloseMenu={() => {
284
+ onOpenWillChange?.(false)
285
+ onOpenChange?.(false)
286
+ }}
287
+ >
288
+ {trigger.props.children}
289
+ </NativeMenuView>
290
+ )
291
+ }
292
+
293
+ return {
294
+ Root,
295
+ Trigger,
296
+ Content,
297
+ Item,
298
+ ItemTitle,
299
+ ItemSubtitle,
300
+ ItemIcon,
301
+ ItemImage,
302
+ ItemIndicator,
303
+ Group,
304
+ Label,
305
+ Separator,
306
+ Sub,
307
+ SubTrigger,
308
+ SubContent,
309
+ CheckboxItem,
310
+ Preview,
311
+ Auxiliary,
312
+ }
313
+ }
314
+
315
+ return {
316
+ name: 'expo-ui',
317
+ Menu: createModule(false),
318
+ ContextMenu: createModule(true),
319
+ }
320
+ }
package/src/index.ts CHANGED
@@ -29,12 +29,12 @@ export type {
29
29
  SafeAreaFrame,
30
30
  SafeAreaMetrics,
31
31
  LinearGradientState,
32
- ZeegoState,
33
32
  BurntState,
34
33
  NativePortalProps,
35
34
  NativePortalHostProps,
36
35
  NativePortalProviderProps,
37
36
  } from './types'
37
+ export type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'
38
38
 
39
39
  // portal
40
40
  export { getPortal } from './portalState'
@@ -76,9 +76,10 @@ export {
76
76
  } from './keyboardControllerState'
77
77
  export type { KeyboardControllerState } from './keyboardControllerState'
78
78
 
79
- // zeego (native menus)
80
- export { getZeego } from './zeegoState'
81
- export type { ZeegoAccessor } from './zeegoState'
79
+ // native menus
80
+ export { getNativeMenuAdapter, registerNativeMenuAdapter } from './nativeMenuState'
81
+ export { createExpoUIMenuAdapter } from './expoUIMenuAdapter'
82
+ export type { ExpoUIMenuAdapterOptions } from './expoUIMenuAdapter'
82
83
  export { NativeMenuContext } from './nativeMenuContext'
83
84
 
84
85
  // burnt (native toasts)
@@ -0,0 +1,63 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ import { createGlobalState } from './globalState'
4
+
5
+ export type NativeMenuModule = {
6
+ Root: ComponentType<any>
7
+ Trigger: ComponentType<any>
8
+ Content: ComponentType<any>
9
+ Item: ComponentType<any>
10
+ ItemTitle: ComponentType<any>
11
+ ItemSubtitle: ComponentType<any>
12
+ ItemIcon: ComponentType<any>
13
+ ItemImage: ComponentType<any>
14
+ ItemIndicator: ComponentType<any>
15
+ Group: ComponentType<any>
16
+ Label: ComponentType<any>
17
+ Separator: ComponentType<any>
18
+ Sub: ComponentType<any>
19
+ SubTrigger: ComponentType<any>
20
+ SubContent: ComponentType<any>
21
+ CheckboxItem: ComponentType<any>
22
+ Preview?: ComponentType<any>
23
+ Auxiliary?: ComponentType<any>
24
+ }
25
+
26
+ /**
27
+ * A native implementation for Tamagui Menu and ContextMenu compound components.
28
+ * Register one adapter before the first native menu renders.
29
+ */
30
+ export type NativeMenuAdapter = {
31
+ name: string
32
+ Menu: NativeMenuModule
33
+ ContextMenu: NativeMenuModule
34
+ }
35
+
36
+ type NativeMenuState = {
37
+ enabled: boolean
38
+ adapter: NativeMenuAdapter | null
39
+ }
40
+
41
+ const state = createGlobalState<NativeMenuState>('native-menu-adapter', {
42
+ enabled: false,
43
+ adapter: null,
44
+ })
45
+
46
+ /**
47
+ * Registers the process-wide native menu implementation. Re-registering the same
48
+ * named adapter supports fast refresh; registering a different adapter throws.
49
+ */
50
+ export function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void {
51
+ const current = state.get().adapter
52
+ if (current && current.name !== adapter.name) {
53
+ throw new Error(
54
+ `A native menu adapter named "${current.name}" is already registered. ` +
55
+ `Register exactly one adapter before rendering Tamagui menus.`
56
+ )
57
+ }
58
+ state.set({ enabled: true, adapter })
59
+ }
60
+
61
+ export function getNativeMenuAdapter(): NativeMenuAdapter | null {
62
+ return state.get().adapter
63
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Setup Expo UI for Tamagui native Menu and ContextMenu components.
3
+ *
4
+ * Import this module once at the top of the app entry point after installing
5
+ * `@expo/ui`.
6
+ */
7
+
8
+ import { createExpoUIMenuAdapter } from './expoUIMenuAdapter'
9
+ import { registerNativeMenuAdapter } from './nativeMenuState'
10
+
11
+ function setup(): void {
12
+ let MenuView
13
+ try {
14
+ MenuView = require('@expo/ui/community/menu').MenuView
15
+ } catch (error) {
16
+ if (process.env.NODE_ENV === 'development') {
17
+ console.warn('Error setting up Expo UI native menus', error)
18
+ }
19
+ return
20
+ }
21
+
22
+ registerNativeMenuAdapter(createExpoUIMenuAdapter({ MenuView }))
23
+ }
24
+
25
+ setup()
@@ -12,30 +12,26 @@
12
12
  * Menu and ContextMenu native mode.
13
13
  */
14
14
 
15
- import { getZeego } from './zeegoState'
15
+ import { registerNativeMenuAdapter } from './nativeMenuState'
16
16
 
17
17
  function setup(): void {
18
- const g = globalThis as any
19
- if (g.__tamagui_native_zeego_setup) return
20
- g.__tamagui_native_zeego_setup = true
21
-
18
+ let Menu
19
+ let ContextMenu
22
20
  try {
23
- const DropdownMenu = require('zeego/dropdown-menu')
24
- const ContextMenu = require('zeego/context-menu')
25
-
26
- if (DropdownMenu && ContextMenu) {
27
- getZeego().set({
28
- enabled: true,
29
- DropdownMenu,
30
- ContextMenu,
31
- })
32
- }
21
+ Menu = require('zeego/dropdown-menu')
22
+ ContextMenu = require('zeego/context-menu')
33
23
  } catch (err) {
34
24
  if (process.env.NODE_ENV === 'development') {
35
25
  console.warn(`Error setting up Zeego`, err)
36
26
  }
37
- // zeego not installed
27
+ return
38
28
  }
29
+
30
+ registerNativeMenuAdapter({
31
+ name: 'zeego',
32
+ Menu,
33
+ ContextMenu,
34
+ })
39
35
  }
40
36
 
41
37
  // run setup immediately on import
package/src/types.ts CHANGED
@@ -68,12 +68,6 @@ export interface LinearGradientState {
68
68
  Component: any
69
69
  }
70
70
 
71
- export interface ZeegoState {
72
- enabled: boolean
73
- DropdownMenu: any
74
- ContextMenu: any
75
- }
76
-
77
71
  export interface BurntState {
78
72
  enabled: boolean
79
73
  toast: ((options: any) => void) | null
@@ -0,0 +1,8 @@
1
+ import type { ComponentType } from "react";
2
+ import type { NativeMenuAdapter } from "./nativeMenuState";
3
+ export type ExpoUIMenuAdapterOptions = {
4
+ MenuView: ComponentType<any>;
5
+ };
6
+ export declare function createExpoUIMenuAdapter({ MenuView }: ExpoUIMenuAdapterOptions): NativeMenuAdapter;
7
+
8
+ //# sourceMappingURL=expoUIMenuAdapter.d.ts.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "mappings": "AAAA,cAAc,qBAA8C;AAG5D,cAAc,yBAA2C;AA0BzD,YAAY,2BAA2B;CACrC,UAAU;;AAgCZ,OAAO,iBAAS,wBAAwB,EACtC,YACC,2BAA2B",
3
+ "names": [],
4
+ "sources": [
5
+ "src/expoUIMenuAdapter.tsx"
6
+ ],
7
+ "version": 3,
8
+ "sourcesContent": [
9
+ "import type { ComponentType, ReactElement, ReactNode } from 'react'\nimport React from 'react'\n\nimport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\ntype ExpoMenuAction = {\n id: string\n title: string\n image?: unknown\n state?: 'on' | 'off'\n attributes?: {\n destructive?: boolean\n disabled?: boolean\n hidden?: boolean\n }\n subactions?: ExpoMenuAction[]\n displayInline?: boolean\n}\n\ntype ExpoMenuViewProps = {\n actions: ExpoMenuAction[]\n title?: string\n shouldOpenOnLongPress?: boolean\n onPressAction?: (event: { nativeEvent: { event: string } }) => void\n onOpenMenu?: () => void\n onCloseMenu?: () => void\n children?: ReactNode\n}\n\nexport type ExpoUIMenuAdapterOptions = {\n MenuView: ComponentType<any>\n}\n\ntype MarkerProps = {\n children?: ReactNode\n [key: string]: unknown\n}\n\nfunction marker(name: string): ComponentType<MarkerProps> {\n const Marker = () => null\n Marker.displayName = `ExpoUI${name}`\n return Marker\n}\n\nfunction elementOfType(\n children: ReactNode,\n type: ComponentType<MarkerProps>\n): ReactElement<MarkerProps> | null {\n for (const child of React.Children.toArray(children)) {\n if (React.isValidElement<MarkerProps>(child) && child.type === type) {\n return child\n }\n }\n return null\n}\n\nfunction textFromNode(node: ReactNode): string {\n if (typeof node === 'string' || typeof node === 'number') return String(node)\n if (!React.isValidElement<MarkerProps>(node)) return ''\n return React.Children.toArray(node.props.children).map(textFromNode).join('')\n}\n\nexport function createExpoUIMenuAdapter({\n MenuView,\n}: ExpoUIMenuAdapterOptions): NativeMenuAdapter {\n const NativeMenuView: ComponentType<ExpoMenuViewProps> = MenuView\n\n function createModule(isContextMenu: boolean): NativeMenuModule {\n const Trigger = marker('Trigger')\n const Content = marker('Content')\n const Item = marker('Item')\n const ItemTitle = marker('ItemTitle')\n const ItemSubtitle = marker('ItemSubtitle')\n const ItemIcon = marker('ItemIcon')\n const ItemImage = marker('ItemImage')\n const ItemIndicator = marker('ItemIndicator')\n const Group = marker('Group')\n const Label = marker('Label')\n const Separator = marker('Separator')\n const Sub = marker('Sub')\n const SubTrigger = marker('SubTrigger')\n const SubContent = marker('SubContent')\n const CheckboxItem = marker('CheckboxItem')\n const Preview = marker('Preview')\n const Auxiliary = marker('Auxiliary')\n\n function Root({\n children,\n onOpenChange,\n onOpenWillChange,\n }: {\n children?: ReactNode\n onOpenChange?: (open: boolean) => void\n onOpenWillChange?: (open: boolean) => void\n }) {\n const trigger = elementOfType(children, Trigger)\n const content = elementOfType(children, Content)\n const preview = elementOfType(children, Preview)\n const auxiliary = elementOfType(children, Auxiliary)\n\n if (preview || auxiliary) {\n throw new Error(\n '@expo/ui/community/menu does not support Tamagui context-menu previews or auxiliary views'\n )\n }\n if (!trigger || !content) {\n throw new Error('Tamagui native menus require one Trigger and one Content')\n }\n\n let nextId = 0\n const handlers = new Map<string, () => void>()\n\n function actionId(element: ReactElement<MarkerProps>): string {\n return element.key == null\n ? `tamagui-menu-${nextId++}`\n : String(element.key).replace(/^\\.\\$/, '')\n }\n\n function itemTitle(element: ReactElement<MarkerProps>): string {\n if (typeof element.props.textValue === 'string') return element.props.textValue\n const title = elementOfType(element.props.children, ItemTitle)\n return title ? textFromNode(title.props.children) : ''\n }\n\n function actionImage(element: ReactElement<MarkerProps>): unknown {\n const image = elementOfType(element.props.children, ItemImage)\n if (image?.props.source) return image.props.source\n const icon = elementOfType(element.props.children, ItemIcon)\n if (!icon) return undefined\n const ios = icon.props.ios\n if (\n typeof ios === 'object' &&\n ios &&\n 'name' in ios &&\n typeof ios.name === 'string'\n ) {\n return ios.name\n }\n return typeof icon.props.iosIconName === 'string'\n ? icon.props.iosIconName\n : undefined\n }\n\n function attributes(\n element: ReactElement<MarkerProps>\n ): ExpoMenuAction['attributes'] {\n const destructive = element.props.destructive === true\n const disabled = element.props.disabled === true\n const hidden = element.props.hidden === true\n return destructive || disabled || hidden\n ? { destructive, disabled, hidden }\n : undefined\n }\n\n function actionsFrom(childrenToParse: ReactNode): ExpoMenuAction[] {\n const actions: ExpoMenuAction[] = []\n let section: ExpoMenuAction[] | null = null\n\n function append(action: ExpoMenuAction) {\n if (section) {\n section.push(action)\n } else {\n actions.push(action)\n }\n }\n\n function flushSection() {\n if (!section?.length) {\n section = []\n return\n }\n actions.push({\n id: `tamagui-menu-section-${nextId++}`,\n title: '',\n subactions: section,\n displayInline: true,\n })\n section = []\n }\n\n for (const child of React.Children.toArray(childrenToParse)) {\n if (!React.isValidElement<MarkerProps>(child)) continue\n\n if (child.type === Separator) {\n if (!section) section = actions.splice(0)\n flushSection()\n continue\n }\n\n if (child.type === Group) {\n const label = elementOfType(child.props.children, Label)\n append({\n id: actionId(child),\n title: label ? textFromNode(label.props.children) : '',\n subactions: actionsFrom(child.props.children),\n displayInline: true,\n })\n continue\n }\n\n if (child.type === Sub) {\n const subTrigger = elementOfType(child.props.children, SubTrigger)\n const subContent = elementOfType(child.props.children, SubContent)\n if (!subTrigger || !subContent) {\n throw new Error(\n 'Tamagui native submenus require one SubTrigger and SubContent'\n )\n }\n append({\n id: actionId(child),\n title: itemTitle(subTrigger),\n image: actionImage(subTrigger),\n attributes: attributes(subTrigger),\n subactions: actionsFrom(subContent.props.children),\n })\n continue\n }\n\n if (child.type !== Item && child.type !== CheckboxItem) continue\n\n if (elementOfType(child.props.children, ItemSubtitle)) {\n throw new Error(\n '@expo/ui/community/menu does not support menu item subtitles'\n )\n }\n\n const id = actionId(child)\n if (child.type === CheckboxItem) {\n const current =\n child.props.value === true ||\n child.props.value === 'on' ||\n child.props.checked === true\n const onValueChange = child.props.onValueChange\n const onCheckedChange = child.props.onCheckedChange\n handlers.set(id, () => {\n if (typeof onValueChange === 'function') {\n onValueChange(current ? 'off' : 'on', current ? 'on' : 'off')\n } else if (typeof onCheckedChange === 'function') {\n onCheckedChange(!current)\n }\n })\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n state: current ? 'on' : 'off',\n attributes: attributes(child),\n })\n continue\n }\n\n const onSelect = child.props.onSelect\n if (typeof onSelect === 'function') {\n handlers.set(id, () => onSelect())\n }\n append({\n id,\n title: itemTitle(child),\n image: actionImage(child),\n attributes: attributes(child),\n })\n }\n\n if (section) flushSection()\n return actions\n }\n\n const label = elementOfType(content.props.children, Label)\n const triggerAction = trigger.props.action\n\n return (\n <NativeMenuView\n actions={actionsFrom(content.props.children)}\n title={label ? textFromNode(label.props.children) : undefined}\n shouldOpenOnLongPress={\n triggerAction === 'longPress' || (triggerAction !== 'press' && isContextMenu)\n }\n onPressAction={(event) => handlers.get(event.nativeEvent.event)?.()}\n onOpenMenu={() => {\n onOpenWillChange?.(true)\n onOpenChange?.(true)\n }}\n onCloseMenu={() => {\n onOpenWillChange?.(false)\n onOpenChange?.(false)\n }}\n >\n {trigger.props.children}\n </NativeMenuView>\n )\n }\n\n return {\n Root,\n Trigger,\n Content,\n Item,\n ItemTitle,\n ItemSubtitle,\n ItemIcon,\n ItemImage,\n ItemIndicator,\n Group,\n Label,\n Separator,\n Sub,\n SubTrigger,\n SubContent,\n CheckboxItem,\n Preview,\n Auxiliary,\n }\n }\n\n return {\n name: 'expo-ui',\n Menu: createModule(false),\n ContextMenu: createModule(true),\n }\n}\n"
10
+ ]
11
+ }
package/types/index.d.ts CHANGED
@@ -18,7 +18,8 @@
18
18
  * // LinearGradient will use expo-linear-gradient when installed
19
19
  * ```
20
20
  */
21
- export type { NativePortalState, GestureState, WorkletsState, SafeAreaState, SafeAreaInsets, SafeAreaFrame, SafeAreaMetrics, LinearGradientState, ZeegoState, BurntState, NativePortalProps, NativePortalHostProps, NativePortalProviderProps } from "./types";
21
+ export type { NativePortalState, GestureState, WorkletsState, SafeAreaState, SafeAreaInsets, SafeAreaFrame, SafeAreaMetrics, LinearGradientState, BurntState, NativePortalProps, NativePortalHostProps, NativePortalProviderProps } from "./types";
22
+ export type { NativeMenuAdapter, NativeMenuModule } from "./nativeMenuState";
22
23
  export { getPortal } from "./portalState";
23
24
  export type { PortalAccessor } from "./portalState";
24
25
  export { getGestureHandler } from "./gestureState";
@@ -33,8 +34,9 @@ export { getLinearGradient } from "./linearGradientState";
33
34
  export type { LinearGradientAccessor } from "./linearGradientState";
34
35
  export { isKeyboardControllerEnabled, getKeyboardControllerState, setKeyboardControllerState } from "./keyboardControllerState";
35
36
  export type { KeyboardControllerState } from "./keyboardControllerState";
36
- export { getZeego } from "./zeegoState";
37
- export type { ZeegoAccessor } from "./zeegoState";
37
+ export { getNativeMenuAdapter, registerNativeMenuAdapter } from "./nativeMenuState";
38
+ export { createExpoUIMenuAdapter } from "./expoUIMenuAdapter";
39
+ export type { ExpoUIMenuAdapterOptions } from "./expoUIMenuAdapter";
38
40
  export { NativeMenuContext } from "./nativeMenuContext";
39
41
  export { getBurnt } from "./burntState";
40
42
  export type { BurntAccessor } from "./burntState";
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAsBA,cACE,mBACA,cACA,eACA,eACA,gBACA,eACA,iBACA,qBACA,YACA,YACA,mBACA,uBACA,iCACK;AAGP,SAAS,iBAAiB;AAC1B,cAAc,sBAAsB;AAGpC,SAAS,yBAAyB;AAClC,SACE,+BAA+B,sCAC/B,6BAA6B,oCAC7B,iCAAiC,8CAC5B;AACP,cACE,6BACA,wBACA,0BACK;AAGP,cAAc,4BAA4B;AAG1C,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,yBAAyB;AAClC,cAAc,8BAA8B;AAG5C,SACE,6BACA,4BACA,kCACK;AACP,cAAc,+BAA+B;AAG7C,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AACnC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AAGnC,SAAS,cAAc,kBAAkB,4BAA4B",
2
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAsBA,cACE,mBACA,cACA,eACA,eACA,gBACA,eACA,iBACA,qBACA,YACA,mBACA,uBACA,iCACK;AACP,cAAc,mBAAmB,wBAAwB;AAGzD,SAAS,iBAAiB;AAC1B,cAAc,sBAAsB;AAGpC,SAAS,yBAAyB;AAClC,SACE,+BAA+B,sCAC/B,6BAA6B,oCAC7B,iCAAiC,8CAC5B;AACP,cACE,6BACA,wBACA,0BACK;AAGP,cAAc,4BAA4B;AAG1C,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,mBAAmB;AAC5B,cAAc,wBAAwB;AAGtC,SAAS,yBAAyB;AAClC,cAAc,8BAA8B;AAG5C,SACE,6BACA,4BACA,kCACK;AACP,cAAc,+BAA+B;AAG7C,SAAS,sBAAsB,iCAAiC;AAChE,SAAS,+BAA+B;AACxC,cAAc,gCAAgC;AAC9C,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB;AACzB,cAAc,qBAAqB;AAGnC,SAAS,cAAc,kBAAkB,4BAA4B",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/index.ts"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "/**\n * @tamagui/native\n *\n * Native setup modules for Tamagui. Import these at the top of your app entry point.\n *\n * @example\n * ```tsx\n * // In your app entry (index.js or App.tsx)\n * import '@tamagui/native/setup-teleport'\n * import '@tamagui/native/setup-gesture-handler'\n * import '@tamagui/native/setup-worklets'\n * import '@tamagui/native/setup-safe-area'\n * import '@tamagui/native/expo-linear-gradient'\n * import '@tamagui/native/setup-keyboard-controller'\n *\n * // Then use Tamagui components normally\n * // Sheet will automatically use native gestures when available\n * // LinearGradient will use expo-linear-gradient when installed\n * ```\n */\n\n// types\nexport type {\n NativePortalState,\n GestureState,\n WorkletsState,\n SafeAreaState,\n SafeAreaInsets,\n SafeAreaFrame,\n SafeAreaMetrics,\n LinearGradientState,\n ZeegoState,\n BurntState,\n NativePortalProps,\n NativePortalHostProps,\n NativePortalProviderProps,\n} from './types'\n\n// portal\nexport { getPortal } from './portalState'\nexport type { PortalAccessor } from './portalState'\n\n// gesture handler\nexport { getGestureHandler } from './gestureState'\nexport {\n claimExternalPressOwnership as unstable_claimExternalPressOwnership,\n hasExternalPressOwnership as unstable_hasExternalPressOwnership,\n releaseExternalPressOwnership as unstable_releaseExternalPressOwnership,\n} from './gestureState'\nexport type {\n ExternalPressOwnershipToken,\n GestureHandlerAccessor,\n PressGestureConfig,\n} from './gestureState'\n// NOTE: setupGestureHandler is exported from setup-gesture-handler.ts entry point,\n// not here, to avoid bundler pulling in RNGH require during tree-shaking\nexport type { GestureHandlerConfig } from './setup-gesture-handler'\n\n// worklets\nexport { getWorklets } from './workletsState'\nexport type { WorkletsAccessor } from './workletsState'\n\n// safe area\nexport { getSafeArea } from './safeAreaState'\nexport type { SafeAreaAccessor } from './safeAreaState'\n\n// linear gradient\nexport { getLinearGradient } from './linearGradientState'\nexport type { LinearGradientAccessor } from './linearGradientState'\n\n// keyboard controller state exports (safe - no side effects)\nexport {\n isKeyboardControllerEnabled,\n getKeyboardControllerState,\n setKeyboardControllerState,\n} from './keyboardControllerState'\nexport type { KeyboardControllerState } from './keyboardControllerState'\n\n// zeego (native menus)\nexport { getZeego } from './zeegoState'\nexport type { ZeegoAccessor } from './zeegoState'\nexport { NativeMenuContext } from './nativeMenuContext'\n\n// burnt (native toasts)\nexport { getBurnt } from './burntState'\nexport type { BurntAccessor } from './burntState'\n\n// components\nexport { NativePortal, NativePortalHost, NativePortalProvider } from './components'\n"
9
+ "/**\n * @tamagui/native\n *\n * Native setup modules for Tamagui. Import these at the top of your app entry point.\n *\n * @example\n * ```tsx\n * // In your app entry (index.js or App.tsx)\n * import '@tamagui/native/setup-teleport'\n * import '@tamagui/native/setup-gesture-handler'\n * import '@tamagui/native/setup-worklets'\n * import '@tamagui/native/setup-safe-area'\n * import '@tamagui/native/expo-linear-gradient'\n * import '@tamagui/native/setup-keyboard-controller'\n *\n * // Then use Tamagui components normally\n * // Sheet will automatically use native gestures when available\n * // LinearGradient will use expo-linear-gradient when installed\n * ```\n */\n\n// types\nexport type {\n NativePortalState,\n GestureState,\n WorkletsState,\n SafeAreaState,\n SafeAreaInsets,\n SafeAreaFrame,\n SafeAreaMetrics,\n LinearGradientState,\n BurntState,\n NativePortalProps,\n NativePortalHostProps,\n NativePortalProviderProps,\n} from './types'\nexport type { NativeMenuAdapter, NativeMenuModule } from './nativeMenuState'\n\n// portal\nexport { getPortal } from './portalState'\nexport type { PortalAccessor } from './portalState'\n\n// gesture handler\nexport { getGestureHandler } from './gestureState'\nexport {\n claimExternalPressOwnership as unstable_claimExternalPressOwnership,\n hasExternalPressOwnership as unstable_hasExternalPressOwnership,\n releaseExternalPressOwnership as unstable_releaseExternalPressOwnership,\n} from './gestureState'\nexport type {\n ExternalPressOwnershipToken,\n GestureHandlerAccessor,\n PressGestureConfig,\n} from './gestureState'\n// NOTE: setupGestureHandler is exported from setup-gesture-handler.ts entry point,\n// not here, to avoid bundler pulling in RNGH require during tree-shaking\nexport type { GestureHandlerConfig } from './setup-gesture-handler'\n\n// worklets\nexport { getWorklets } from './workletsState'\nexport type { WorkletsAccessor } from './workletsState'\n\n// safe area\nexport { getSafeArea } from './safeAreaState'\nexport type { SafeAreaAccessor } from './safeAreaState'\n\n// linear gradient\nexport { getLinearGradient } from './linearGradientState'\nexport type { LinearGradientAccessor } from './linearGradientState'\n\n// keyboard controller state exports (safe - no side effects)\nexport {\n isKeyboardControllerEnabled,\n getKeyboardControllerState,\n setKeyboardControllerState,\n} from './keyboardControllerState'\nexport type { KeyboardControllerState } from './keyboardControllerState'\n\n// native menus\nexport { getNativeMenuAdapter, registerNativeMenuAdapter } from './nativeMenuState'\nexport { createExpoUIMenuAdapter } from './expoUIMenuAdapter'\nexport type { ExpoUIMenuAdapterOptions } from './expoUIMenuAdapter'\nexport { NativeMenuContext } from './nativeMenuContext'\n\n// burnt (native toasts)\nexport { getBurnt } from './burntState'\nexport type { BurntAccessor } from './burntState'\n\n// components\nexport { NativePortal, NativePortalHost, NativePortalProvider } from './components'\n"
10
10
  ]
11
11
  }
@@ -0,0 +1,38 @@
1
+ import type { ComponentType } from "react";
2
+ export type NativeMenuModule = {
3
+ Root: ComponentType<any>;
4
+ Trigger: ComponentType<any>;
5
+ Content: ComponentType<any>;
6
+ Item: ComponentType<any>;
7
+ ItemTitle: ComponentType<any>;
8
+ ItemSubtitle: ComponentType<any>;
9
+ ItemIcon: ComponentType<any>;
10
+ ItemImage: ComponentType<any>;
11
+ ItemIndicator: ComponentType<any>;
12
+ Group: ComponentType<any>;
13
+ Label: ComponentType<any>;
14
+ Separator: ComponentType<any>;
15
+ Sub: ComponentType<any>;
16
+ SubTrigger: ComponentType<any>;
17
+ SubContent: ComponentType<any>;
18
+ CheckboxItem: ComponentType<any>;
19
+ Preview?: ComponentType<any>;
20
+ Auxiliary?: ComponentType<any>;
21
+ };
22
+ /**
23
+ * A native implementation for Tamagui Menu and ContextMenu compound components.
24
+ * Register one adapter before the first native menu renders.
25
+ */
26
+ export type NativeMenuAdapter = {
27
+ name: string;
28
+ Menu: NativeMenuModule;
29
+ ContextMenu: NativeMenuModule;
30
+ };
31
+ /**
32
+ * Registers the process-wide native menu implementation. Re-registering the same
33
+ * named adapter supports fast refresh; registering a different adapter throws.
34
+ */
35
+ export declare function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void;
36
+ export declare function getNativeMenuAdapter(): NativeMenuAdapter | null;
37
+
38
+ //# sourceMappingURL=nativeMenuState.d.ts.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "mappings": "AAAA,cAAc,qBAAqB;AAInC,YAAY,mBAAmB;CAC7B,MAAM;CACN,SAAS;CACT,SAAS;CACT,MAAM;CACN,WAAW;CACX,cAAc;CACd,UAAU;CACV,WAAW;CACX,eAAe;CACf,OAAO;CACP,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY;CACZ,YAAY;CACZ,cAAc;CACd,UAAU;CACV,YAAY;;;;;;AAOd,YAAY,oBAAoB;CAC9B;CACA,MAAM;CACN,aAAa;;;;;;AAiBf,OAAO,iBAAS,0BAA0B,SAAS;AAWnD,OAAO,iBAAS,wBAAwB",
3
+ "names": [],
4
+ "sources": [
5
+ "src/nativeMenuState.ts"
6
+ ],
7
+ "version": 3,
8
+ "sourcesContent": [
9
+ "import type { ComponentType } from 'react'\n\nimport { createGlobalState } from './globalState'\n\nexport type NativeMenuModule = {\n Root: ComponentType<any>\n Trigger: ComponentType<any>\n Content: ComponentType<any>\n Item: ComponentType<any>\n ItemTitle: ComponentType<any>\n ItemSubtitle: ComponentType<any>\n ItemIcon: ComponentType<any>\n ItemImage: ComponentType<any>\n ItemIndicator: ComponentType<any>\n Group: ComponentType<any>\n Label: ComponentType<any>\n Separator: ComponentType<any>\n Sub: ComponentType<any>\n SubTrigger: ComponentType<any>\n SubContent: ComponentType<any>\n CheckboxItem: ComponentType<any>\n Preview?: ComponentType<any>\n Auxiliary?: ComponentType<any>\n}\n\n/**\n * A native implementation for Tamagui Menu and ContextMenu compound components.\n * Register one adapter before the first native menu renders.\n */\nexport type NativeMenuAdapter = {\n name: string\n Menu: NativeMenuModule\n ContextMenu: NativeMenuModule\n}\n\ntype NativeMenuState = {\n enabled: boolean\n adapter: NativeMenuAdapter | null\n}\n\nconst state = createGlobalState<NativeMenuState>('native-menu-adapter', {\n enabled: false,\n adapter: null,\n})\n\n/**\n * Registers the process-wide native menu implementation. Re-registering the same\n * named adapter supports fast refresh; registering a different adapter throws.\n */\nexport function registerNativeMenuAdapter(adapter: NativeMenuAdapter): void {\n const current = state.get().adapter\n if (current && current.name !== adapter.name) {\n throw new Error(\n `A native menu adapter named \"${current.name}\" is already registered. ` +\n `Register exactly one adapter before rendering Tamagui menus.`\n )\n }\n state.set({ enabled: true, adapter })\n}\n\nexport function getNativeMenuAdapter(): NativeMenuAdapter | null {\n return state.get().adapter\n}\n"
10
+ ]
11
+ }
@@ -0,0 +1,3 @@
1
+ export {};
2
+
3
+ //# sourceMappingURL=setup-expo-ui-menu.d.ts.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "mappings": "",
3
+ "names": [],
4
+ "sources": [
5
+ "src/setup-expo-ui-menu.ts"
6
+ ],
7
+ "version": 3,
8
+ "sourcesContent": [
9
+ "/**\n * Setup Expo UI for Tamagui native Menu and ContextMenu components.\n *\n * Import this module once at the top of the app entry point after installing\n * `@expo/ui`.\n */\n\nimport { createExpoUIMenuAdapter } from './expoUIMenuAdapter'\nimport { registerNativeMenuAdapter } from './nativeMenuState'\n\nfunction setup(): void {\n let MenuView\n try {\n MenuView = require('@expo/ui/community/menu').MenuView\n } catch (error) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('Error setting up Expo UI native menus', error)\n }\n return\n }\n\n registerNativeMenuAdapter(createExpoUIMenuAdapter({ MenuView }))\n}\n\nsetup()\n"
10
+ ]
11
+ }