@tamagui/create-menu 2.7.7 → 3.0.0-beta.643.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/cjs/createBaseMenu.cjs +1033 -1247
  2. package/dist/cjs/createBaseMenu.native.cjs +1217 -0
  3. package/dist/cjs/createBaseMenu.native.js +1155 -1387
  4. package/dist/cjs/createBaseMenu.native.js.map +1 -1
  5. package/dist/cjs/createNativeMenu/createNativeMenu.cjs +346 -377
  6. package/dist/cjs/createNativeMenu/createNativeMenu.native.cjs +439 -0
  7. package/dist/cjs/createNativeMenu/createNativeMenu.native.js +405 -439
  8. package/dist/cjs/createNativeMenu/createNativeMenu.native.js.map +1 -1
  9. package/dist/cjs/createNativeMenu/createNativeMenuTypes.cjs +10 -11
  10. package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.cjs +19 -0
  11. package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js +10 -10
  12. package/dist/cjs/createNativeMenu/createNativeMenuTypes.native.js.map +1 -1
  13. package/dist/cjs/createNativeMenu/utils.cjs +76 -86
  14. package/dist/cjs/createNativeMenu/utils.native.cjs +107 -0
  15. package/dist/cjs/createNativeMenu/utils.native.js +82 -92
  16. package/dist/cjs/createNativeMenu/utils.native.js.map +1 -1
  17. package/dist/cjs/createNativeMenu/withNativeMenu.cjs +27 -35
  18. package/dist/cjs/createNativeMenu/withNativeMenu.native.cjs +42 -0
  19. package/dist/cjs/createNativeMenu/withNativeMenu.native.js +27 -34
  20. package/dist/cjs/createNativeMenu/withNativeMenu.native.js.map +1 -1
  21. package/dist/cjs/index.cjs +9 -20
  22. package/dist/cjs/index.native.cjs +23 -0
  23. package/dist/cjs/index.native.js +10 -20
  24. package/dist/cjs/index.native.js.map +1 -1
  25. package/dist/esm/createBaseMenu.mjs +1021 -1225
  26. package/dist/esm/createBaseMenu.mjs.map +1 -1
  27. package/dist/esm/createBaseMenu.native.js +1144 -1367
  28. package/dist/esm/createBaseMenu.native.js.map +1 -1
  29. package/dist/esm/createNativeMenu/createNativeMenu.mjs +332 -353
  30. package/dist/esm/createNativeMenu/createNativeMenu.mjs.map +1 -1
  31. package/dist/esm/createNativeMenu/createNativeMenu.native.js +392 -417
  32. package/dist/esm/createNativeMenu/createNativeMenu.native.js.map +1 -1
  33. package/dist/esm/createNativeMenu/createNativeMenuTypes.mjs +0 -2
  34. package/dist/esm/createNativeMenu/createNativeMenuTypes.native.js +0 -2
  35. package/dist/esm/createNativeMenu/utils.mjs +55 -57
  36. package/dist/esm/createNativeMenu/utils.mjs.map +1 -1
  37. package/dist/esm/createNativeMenu/utils.native.js +62 -65
  38. package/dist/esm/createNativeMenu/utils.native.js.map +1 -1
  39. package/dist/esm/createNativeMenu/withNativeMenu.mjs +14 -17
  40. package/dist/esm/createNativeMenu/withNativeMenu.mjs.map +1 -1
  41. package/dist/esm/createNativeMenu/withNativeMenu.native.js +15 -18
  42. package/dist/esm/createNativeMenu/withNativeMenu.native.js.map +1 -1
  43. package/dist/esm/index.js +5 -6
  44. package/dist/esm/index.mjs +5 -6
  45. package/dist/esm/index.native.js +5 -6
  46. package/dist/jsx/createBaseMenu.mjs +1021 -1225
  47. package/dist/jsx/createBaseMenu.mjs.map +1 -1
  48. package/dist/jsx/createBaseMenu.native.cjs +1217 -0
  49. package/dist/jsx/createBaseMenu.native.js +1155 -1387
  50. package/dist/jsx/createBaseMenu.native.js.map +1 -1
  51. package/dist/jsx/createNativeMenu/createNativeMenu.mjs +332 -353
  52. package/dist/jsx/createNativeMenu/createNativeMenu.mjs.map +1 -1
  53. package/dist/jsx/createNativeMenu/createNativeMenu.native.cjs +439 -0
  54. package/dist/jsx/createNativeMenu/createNativeMenu.native.js +405 -439
  55. package/dist/jsx/createNativeMenu/createNativeMenu.native.js.map +1 -1
  56. package/dist/jsx/createNativeMenu/createNativeMenuTypes.mjs +0 -2
  57. package/dist/jsx/createNativeMenu/createNativeMenuTypes.native.cjs +19 -0
  58. package/dist/jsx/createNativeMenu/createNativeMenuTypes.native.js +10 -10
  59. package/dist/jsx/createNativeMenu/createNativeMenuTypes.native.js.map +1 -1
  60. package/dist/jsx/createNativeMenu/utils.mjs +55 -57
  61. package/dist/jsx/createNativeMenu/utils.mjs.map +1 -1
  62. package/dist/jsx/createNativeMenu/utils.native.cjs +107 -0
  63. package/dist/jsx/createNativeMenu/utils.native.js +82 -92
  64. package/dist/jsx/createNativeMenu/utils.native.js.map +1 -1
  65. package/dist/jsx/createNativeMenu/withNativeMenu.mjs +14 -17
  66. package/dist/jsx/createNativeMenu/withNativeMenu.mjs.map +1 -1
  67. package/dist/jsx/createNativeMenu/withNativeMenu.native.cjs +42 -0
  68. package/dist/jsx/createNativeMenu/withNativeMenu.native.js +27 -34
  69. package/dist/jsx/createNativeMenu/withNativeMenu.native.js.map +1 -1
  70. package/dist/jsx/index.js +5 -6
  71. package/dist/jsx/index.mjs +5 -6
  72. package/dist/jsx/index.native.cjs +23 -0
  73. package/dist/jsx/index.native.js +10 -20
  74. package/dist/jsx/index.native.js.map +1 -1
  75. package/package.json +23 -22
  76. package/src/createBaseMenu.tsx +211 -231
  77. package/src/createNativeMenu/createNativeMenuTypes.ts +0 -6
  78. package/src/createNativeMenu/utils.tsx +6 -6
  79. package/src/createNativeMenu/withNativeMenu.tsx +1 -2
  80. package/src/index.tsx +0 -1
  81. package/types/createBaseMenu.d.ts +38 -126
  82. package/types/createBaseMenu.d.ts.map +1 -1
  83. package/types/createNativeMenu/createNativeMenu.d.ts +1 -1
  84. package/types/createNativeMenu/createNativeMenu.d.ts.map +1 -1
  85. package/types/createNativeMenu/createNativeMenuTypes.d.ts +0 -6
  86. package/types/createNativeMenu/createNativeMenuTypes.d.ts.map +1 -1
  87. package/types/createNativeMenu/utils.d.ts +5 -5
  88. package/types/createNativeMenu/utils.d.ts.map +1 -1
  89. package/types/createNativeMenu/withNativeMenu.d.ts.map +1 -1
  90. package/types/index.d.ts +0 -1
  91. package/types/index.d.ts.map +1 -1
  92. package/dist/cjs/MenuPredefined.cjs +0 -191
  93. package/dist/cjs/MenuPredefined.native.js +0 -194
  94. package/dist/cjs/MenuPredefined.native.js.map +0 -1
  95. package/dist/esm/MenuPredefined.mjs +0 -166
  96. package/dist/esm/MenuPredefined.mjs.map +0 -1
  97. package/dist/esm/MenuPredefined.native.js +0 -166
  98. package/dist/esm/MenuPredefined.native.js.map +0 -1
  99. package/dist/esm/createNativeMenu/createNativeMenuTypes.mjs.map +0 -1
  100. package/dist/esm/createNativeMenu/createNativeMenuTypes.native.js.map +0 -1
  101. package/dist/esm/index.js.map +0 -1
  102. package/dist/esm/index.mjs.map +0 -1
  103. package/dist/esm/index.native.js.map +0 -1
  104. package/dist/jsx/MenuPredefined.mjs +0 -166
  105. package/dist/jsx/MenuPredefined.mjs.map +0 -1
  106. package/dist/jsx/MenuPredefined.native.js +0 -194
  107. package/dist/jsx/MenuPredefined.native.js.map +0 -1
  108. package/dist/jsx/createNativeMenu/createNativeMenuTypes.mjs.map +0 -1
  109. package/dist/jsx/index.js.map +0 -1
  110. package/dist/jsx/index.mjs.map +0 -1
  111. package/src/MenuPredefined.tsx +0 -204
  112. package/types/MenuPredefined.d.ts +0 -31
  113. package/types/MenuPredefined.d.ts.map +0 -1
@@ -1,1267 +1,1063 @@
1
+ import { createRefComponent, createStyledHOC } from "@tamagui/core";
1
2
  import { Animate } from "@tamagui/animate";
2
- import { AnimatePresence as Presence } from "@tamagui/animate-presence";
3
+ import { AnimatePresence } from "@tamagui/animate-presence";
3
4
  import { createCollection } from "@tamagui/collection";
4
- import { Dismissable as DismissableLayer, dispatchDiscreteCustomEvent } from "@tamagui/dismissable";
5
+ import { Dismissable, dispatchDiscreteCustomEvent } from "@tamagui/dismissable";
5
6
  import { useFocusGuards } from "@tamagui/focus-guard";
6
7
  import { FocusScope } from "@tamagui/focus-scope";
8
+ import { Image } from "@tamagui/image";
7
9
  import * as PopperPrimitive from "@tamagui/popper";
8
- import { needsPortalRepropagation, Portal as PortalPrimitive } from "@tamagui/portal";
10
+ import { Portal, needsPortalRepropagation } from "@tamagui/portal";
9
11
  import { RemoveScroll } from "@tamagui/remove-scroll";
10
12
  import { RovingFocusGroup } from "@tamagui/roving-focus";
11
13
  import { useCallbackRef } from "@tamagui/use-callback-ref";
12
14
  import { useDirection } from "@tamagui/use-direction";
13
- import { composeEventHandlers, composeRefs, createStyledContext, isWeb, styled, Text, Theme, useComposedRefs, useIsTouchDevice, useThemeName, View, withStaticProperties } from "@tamagui/web";
15
+ import { Text, Theme, View, composeEventHandlers, composeRefs, createStyledContext, isWeb, styled, useComposedRefs, useIsTouchDevice, useThemeName, withStaticProperties } from "@tamagui/web";
14
16
  import * as React from "react";
15
17
  import { useId } from "react";
16
- import { MenuPredefined } from "./MenuPredefined.mjs";
17
18
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+
18
20
  function whenMouse(handler) {
19
- return event => event.pointerType === "mouse" ? handler(event) : void 0;
21
+ return (event) => event.pointerType === "mouse" ? handler(event) : void 0;
20
22
  }
21
23
  const SELECTION_KEYS = ["Enter", " "];
22
- const FIRST_KEYS = ["ArrowDown", "PageUp", "Home"];
23
- const LAST_KEYS = ["ArrowUp", "PageDown", "End"];
24
+ const FIRST_KEYS = [
25
+ "ArrowDown",
26
+ "PageUp",
27
+ "Home"
28
+ ];
29
+ const LAST_KEYS = [
30
+ "ArrowUp",
31
+ "PageDown",
32
+ "End"
33
+ ];
24
34
  const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
25
35
  const SUB_OPEN_KEYS = {
26
- ltr: [...SELECTION_KEYS, "ArrowRight"],
27
- rtl: [...SELECTION_KEYS, "ArrowLeft"]
36
+ ltr: [...SELECTION_KEYS, "ArrowRight"],
37
+ rtl: [...SELECTION_KEYS, "ArrowLeft"]
28
38
  };
29
39
  const SUB_CLOSE_KEYS = {
30
- ltr: ["ArrowLeft"],
31
- rtl: ["ArrowRight"]
40
+ ltr: ["ArrowLeft"],
41
+ rtl: ["ArrowRight"]
32
42
  };
33
43
  const MENU_NAME = "Menu";
34
44
  const [Collection, useCollection] = createCollection(MENU_NAME);
35
- const {
36
- Provider: MenuProvider,
37
- useStyledContext: useMenuContext
38
- } = createStyledContext();
39
- const {
40
- Provider: MenuRootProvider,
41
- useStyledContext: useMenuRootContext
42
- } = createStyledContext();
45
+ const { Provider: MenuProvider, useStyledContext: useMenuContext } = createStyledContext();
46
+ const { Provider: MenuRootProvider, useStyledContext: useMenuRootContext } = createStyledContext();
43
47
  const MENU_CONTEXT = "MenuContext";
44
- function createBaseMenu({
45
- Item: _Item = MenuPredefined.MenuItem,
46
- Title: _Title = MenuPredefined.Title,
47
- SubTitle: _SubTitle = MenuPredefined.SubTitle,
48
- Image: _Image = MenuPredefined.MenuImage,
49
- Icon: _Icon = MenuPredefined.MenuIcon,
50
- Indicator: _Indicator = MenuPredefined.MenuIndicator,
51
- Separator: _Separator = MenuPredefined.MenuSeparator,
52
- MenuGroup: _MenuGroup = MenuPredefined.MenuGroup,
53
- Label: _Label = MenuPredefined.MenuLabel
54
- }) {
55
- const MenuComp = props => {
56
- const direction = useDirection(props.dir);
57
- const defaultPlacement = direction === "rtl" ? "bottom-end" : "bottom-start";
58
- const {
59
- scope = MENU_CONTEXT,
60
- open = false,
61
- children,
62
- dir,
63
- onOpenChange,
64
- modal = true,
65
- allowFlip = {
66
- padding: 10
67
- },
68
- stayInFrame = {
69
- padding: 10
70
- },
71
- placement = defaultPlacement,
72
- resize = true,
73
- offset = 10,
74
- ...rest
75
- } = props;
76
- const [content, setContent] = React.useState(null);
77
- const isUsingKeyboardRef = React.useRef(false);
78
- const handleOpenChange = useCallbackRef(onOpenChange);
79
- if (isWeb) {
80
- React.useEffect(() => {
81
- const handleKeyDown = () => {
82
- isUsingKeyboardRef.current = true;
83
- document.addEventListener("pointerdown", handlePointer, {
84
- capture: true,
85
- once: true
86
- });
87
- document.addEventListener("pointermove", handlePointer, {
88
- capture: true,
89
- once: true
90
- });
91
- };
92
- const handlePointer = () => isUsingKeyboardRef.current = false;
93
- document.addEventListener("keydown", handleKeyDown, {
94
- capture: true
95
- });
96
- return () => {
97
- document.removeEventListener("keydown", handleKeyDown, {
98
- capture: true
99
- });
100
- document.removeEventListener("pointerdown", handlePointer, {
101
- capture: true
102
- });
103
- document.removeEventListener("pointermove", handlePointer, {
104
- capture: true
105
- });
106
- };
107
- }, []);
108
- }
109
- return /* @__PURE__ */jsx(PopperPrimitive.Popper, {
110
- scope,
111
- open,
112
- placement,
113
- allowFlip,
114
- stayInFrame,
115
- resize,
116
- offset,
117
- ...rest,
118
- children: /* @__PURE__ */jsx(MenuProvider, {
119
- scope,
120
- open,
121
- onOpenChange: handleOpenChange,
122
- content,
123
- onContentChange: setContent,
124
- children: /* @__PURE__ */jsx(MenuRootProvider, {
125
- scope,
126
- open,
127
- onClose: React.useCallback(() => handleOpenChange(false), [handleOpenChange]),
128
- isUsingKeyboardRef,
129
- dir: direction,
130
- modal,
131
- children: /* @__PURE__ */jsx(MenuSubProvider, {
132
- scope,
133
- children
134
- })
135
- })
136
- })
137
- });
138
- };
139
- const RepropagateMenuAndMenuRootProvider = props => {
140
- const {
141
- scope = MENU_CONTEXT,
142
- menuContext,
143
- rootContext,
144
- popperContext,
145
- menuSubContext,
146
- children
147
- } = props;
148
- return /* @__PURE__ */jsx(PopperPrimitive.PopperProvider, {
149
- ...popperContext,
150
- scope,
151
- children: /* @__PURE__ */jsx(MenuProvider, {
152
- scope,
153
- ...menuContext,
154
- children: /* @__PURE__ */jsx(MenuRootProvider, {
155
- scope,
156
- ...rootContext,
157
- children: menuSubContext ? /* @__PURE__ */jsx(MenuSubProvider, {
158
- scope,
159
- ...menuSubContext,
160
- children
161
- }) : children
162
- })
163
- })
164
- });
165
- };
166
- MenuComp.displayName = MENU_NAME;
167
- const ANCHOR_NAME = "MenuAnchor";
168
- const MenuAnchor = props => {
169
- return /* @__PURE__ */jsx(PopperPrimitive.PopperAnchor, {
170
- scope: MENU_CONTEXT,
171
- ...props
172
- });
173
- };
174
- MenuAnchor.displayName = ANCHOR_NAME;
175
- const PORTAL_NAME = "MenuPortal";
176
- const {
177
- Provider: PortalProvider,
178
- useStyledContext: usePortalContext
179
- } = createStyledContext(void 0, "Portal");
180
- const MenuPortal = props => {
181
- const {
182
- scope = MENU_CONTEXT,
183
- forceMount,
184
- zIndex,
185
- children
186
- } = props;
187
- const menuContext = useMenuContext(scope);
188
- const rootContext = useMenuRootContext(scope);
189
- const popperContext = PopperPrimitive.usePopperContext(scope);
190
- const menuSubContext = useMenuSubContext(scope);
191
- const themeName = useThemeName();
192
- const themedChildren = /* @__PURE__ */jsx(Theme, {
193
- forceClassName: true,
194
- name: themeName,
195
- children
196
- });
197
- const content = needsPortalRepropagation() ? /* @__PURE__ */jsx(RepropagateMenuAndMenuRootProvider, {
198
- menuContext,
199
- rootContext,
200
- popperContext,
201
- menuSubContext,
202
- scope,
203
- children: themedChildren
204
- }) : themedChildren;
205
- const isPresent = forceMount || rootContext.open && menuContext.open;
206
- return /* @__PURE__ */jsx(Animate, {
207
- type: "presence",
208
- present: isPresent,
209
- children: /* @__PURE__ */jsx(PortalPrimitive, {
210
- stackZIndex: true,
211
- children: /* @__PURE__ */jsx(Fragment, {
212
- children: /* @__PURE__ */jsx(PortalProvider, {
213
- scope,
214
- forceMount,
215
- children: /* @__PURE__ */jsxs(View, {
216
- zIndex: zIndex || 100,
217
- inset: 0,
218
- position: "absolute",
219
- children: [!!menuContext.open && !isWeb && /* @__PURE__ */jsx(View, {
220
- inset: 0,
221
- position: "absolute",
222
- onPress: () => menuContext.onOpenChange(!menuContext.open)
223
- }), content]
224
- })
225
- })
226
- })
227
- })
228
- });
229
- };
230
- MenuPortal.displayName = PORTAL_NAME;
231
- const CONTENT_NAME = "MenuContent";
232
- const {
233
- Provider: MenuContentProvider,
234
- useStyledContext: useMenuContentContext
235
- } = createStyledContext();
236
- const MenuContentFrame = styled(PopperPrimitive.PopperContentFrame, {
237
- name: CONTENT_NAME
238
- });
239
- const MenuContent = MenuContentFrame.styleable((props, forwardedRef) => {
240
- const scope = props.scope || MENU_CONTEXT;
241
- const portalContext = usePortalContext(scope);
242
- const {
243
- forceMount = portalContext.forceMount,
244
- ...contentProps
245
- } = props;
246
- const rootContext = useMenuRootContext(scope);
247
- return /* @__PURE__ */jsx(Collection.Provider, {
248
- scope,
249
- children: /* @__PURE__ */jsx(Collection.Slot, {
250
- scope,
251
- children: rootContext.modal ? /* @__PURE__ */jsx(MenuRootContentModal, {
252
- ...contentProps,
253
- ref: forwardedRef
254
- }) : /* @__PURE__ */jsx(MenuRootContentNonModal, {
255
- ...contentProps,
256
- ref: forwardedRef
257
- })
258
- })
259
- });
260
- });
261
- const MenuRootContentModal = React.forwardRef((props, forwardedRef) => {
262
- const scope = props.scope || MENU_CONTEXT;
263
- const context = useMenuContext(scope);
264
- const ref = React.useRef(null);
265
- const composedRefs = useComposedRefs(forwardedRef, ref);
266
- return /* @__PURE__ */jsx(MenuContentImpl, {
267
- ...props,
268
- scope,
269
- ref: composedRefs,
270
- trapFocus: context.open,
271
- disableOutsidePointerEvents: context.open,
272
- disableOutsideScroll: false,
273
- onFocusOutside: composeEventHandlers(props.onFocusOutside, event => event.preventDefault(), {
274
- checkDefaultPrevented: false
275
- }),
276
- onDismiss: () => context.onOpenChange(false)
277
- });
278
- });
279
- const MenuRootContentNonModal = React.forwardRef((props, forwardedRef) => {
280
- const scope = props.scope || MENU_CONTEXT;
281
- const context = useMenuContext(scope);
282
- return /* @__PURE__ */jsx(MenuContentImpl, {
283
- ...props,
284
- scope,
285
- ref: forwardedRef,
286
- trapFocus: false,
287
- disableOutsidePointerEvents: false,
288
- disableOutsideScroll: false,
289
- onDismiss: () => context.onOpenChange(false)
290
- });
291
- });
292
- const MenuContentImpl = React.forwardRef((props, forwardedRef) => {
293
- const {
294
- scope = MENU_CONTEXT,
295
- loop = false,
296
- trapFocus,
297
- onOpenAutoFocus,
298
- onCloseAutoFocus,
299
- disableOutsidePointerEvents,
300
- onEntryFocus,
301
- onEscapeKeyDown,
302
- onPointerDownOutside,
303
- onFocusOutside,
304
- onInteractOutside,
305
- onDismiss,
306
- disableOutsideScroll,
307
- disableDismissOnScroll = false,
308
- unstyled = process.env.TAMAGUI_HEADLESS === "1",
309
- ...contentProps
310
- } = props;
311
- const context = useMenuContext(scope);
312
- const rootContext = useMenuRootContext(scope);
313
- const popperContext = PopperPrimitive.usePopperContext(scope);
314
- const getItems = useCollection(scope);
315
- const [currentItemId, setCurrentItemId] = React.useState(null);
316
- const contentRef = React.useRef(null);
317
- const focusableContentRef = React.useRef(null);
318
- const composedRefs = useComposedRefs(forwardedRef, contentRef, context.onContentChange);
319
- const timerRef = React.useRef(0);
320
- const searchRef = React.useRef("");
321
- const pointerGraceTimerRef = React.useRef(0);
322
- const pointerGraceIntentRef = React.useRef(null);
323
- const pointerDirRef = React.useRef("right");
324
- const lastPointerXRef = React.useRef(0);
325
- const handleTypeaheadSearch = key => {
326
- const search = searchRef.current + key;
327
- const items = getItems().filter(item => !item.disabled);
328
- const currentItem = document.activeElement;
329
- const currentMatch = items.find(item => item.ref.current === currentItem)?.textValue;
330
- const values = items.map(item => item.textValue);
331
- const nextMatch = getNextMatch(values, search, currentMatch);
332
- const newItem = items.find(item => item.textValue === nextMatch)?.ref.current;
333
- (function updateSearch(value) {
334
- searchRef.current = value;
335
- clearTimeout(timerRef.current);
336
- if (value !== "") timerRef.current = setTimeout(() => updateSearch(""), 1e3);
337
- })(search);
338
- if (newItem) {
339
- setTimeout(() => newItem.focus());
340
- }
341
- };
342
- React.useEffect(() => {
343
- return () => clearTimeout(timerRef.current);
344
- }, []);
345
- React.useEffect(() => {
346
- if (!isWeb || !context.open) return;
347
- const frame = requestAnimationFrame(() => {
348
- const container = contentRef.current;
349
- const el = container?.querySelector("[data-tamagui-menu-content]");
350
- if (el) focusableContentRef.current = el;
351
- });
352
- return () => cancelAnimationFrame(frame);
353
- }, [context.open]);
354
- React.useEffect(() => {
355
- if (!isWeb || disableDismissOnScroll || !context.open) return;
356
- const handleScroll = event => {
357
- const scrolled = event.target;
358
- if (!scrolled) return;
359
- const content2 = contentRef.current;
360
- if (content2?.contains(scrolled)) return;
361
- const reference = popperContext.refs?.reference?.current;
362
- const anchor = reference instanceof Element ? reference : reference?.contextElement ?? null;
363
- if (anchor && !scrolled.contains(anchor)) return;
364
- onDismiss?.();
365
- };
366
- window.addEventListener("scroll", handleScroll, {
367
- capture: true,
368
- passive: true
369
- });
370
- return () => {
371
- window.removeEventListener("scroll", handleScroll, {
372
- capture: true
373
- });
374
- };
375
- }, [disableDismissOnScroll, context.open, onDismiss, popperContext.refs]);
376
- if (isWeb) {
377
- useFocusGuards();
378
- }
379
- const isPointerMovingToSubmenu = React.useCallback(event => {
380
- const isMovingTowards = pointerDirRef.current === pointerGraceIntentRef.current?.side;
381
- const inArea = isPointerInGraceArea(event, pointerGraceIntentRef.current?.area);
382
- return isMovingTowards && inArea;
383
- }, []);
384
- const content = /* @__PURE__ */jsx(PopperPrimitive.PopperContent, {
385
- role: "menu",
386
- tabIndex: -1,
387
- unstyled,
388
- ...(!unstyled && {
389
- backgroundColor: "$background",
390
- borderWidth: 1,
391
- borderColor: "$borderColor",
392
- outlineWidth: 0,
393
- minWidth: 180
394
- }),
395
- "aria-orientation": "vertical",
396
- "data-state": getOpenState(context.open),
397
- "data-tamagui-menu-content": "",
398
- dir: rootContext.dir,
399
- scope: scope || MENU_CONTEXT,
400
- ...contentProps,
401
- ref: composedRefs,
402
- className: contentProps.transition ? void 0 : contentProps.className,
403
- ...(isWeb ? {
404
- onKeyDown: composeEventHandlers(contentProps.onKeyDown, event => {
405
- const target = event.target;
406
- const isKeyDownInside = target.closest("[data-tamagui-menu-content]") === event.currentTarget;
407
- const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;
408
- const isCharacterKey = event.key.length === 1;
409
- if (isKeyDownInside) {
410
- if (event.key === "Tab") event.preventDefault();
411
- if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key);
412
- }
413
- const isOnContentFrame = event.target.hasAttribute("data-tamagui-menu-content");
414
- if (!isKeyDownInside || !isOnContentFrame) return;
415
- if (!FIRST_LAST_KEYS.includes(event.key)) return;
416
- event.preventDefault();
417
- const items = getItems().filter(item => !item.disabled);
418
- const candidateNodes = items.map(item => item.ref.current);
419
- if (LAST_KEYS.includes(event.key)) candidateNodes.reverse();
420
- focusFirst(candidateNodes, {
421
- focusVisible: true
422
- });
423
- }),
424
- // TODO
425
- // @ts-ignore
426
- onBlur: composeEventHandlers(props.onBlur, event => {
427
- if (!event.currentTarget?.contains(event.target)) {
428
- clearTimeout(timerRef.current);
429
- searchRef.current = "";
430
- }
431
- }),
432
- // TODO
433
- onPointerMove: composeEventHandlers(props.onPointerMove, event => {
434
- if (event.pointerType !== "mouse") return;
435
- const target = event.target;
436
- const pointerXHasChanged = lastPointerXRef.current !== event.clientX;
437
- if (event.currentTarget?.contains(target) && pointerXHasChanged) {
438
- const newDir = event.clientX > lastPointerXRef.current ? "right" : "left";
439
- pointerDirRef.current = newDir;
440
- lastPointerXRef.current = event.clientX;
441
- }
442
- })
443
- } : {})
444
- });
445
- return /* @__PURE__ */jsx(MenuContentProvider, {
446
- scope,
447
- searchRef,
448
- onItemEnter: React.useCallback(event => {
449
- if (isPointerMovingToSubmenu(event)) event.preventDefault();
450
- }, [isPointerMovingToSubmenu]),
451
- onItemLeave: React.useCallback(event => {
452
- if (isPointerMovingToSubmenu(event)) return;
453
- focusableContentRef.current?.focus();
454
- setCurrentItemId(null);
455
- }, [isPointerMovingToSubmenu]),
456
- onTriggerLeave: React.useCallback(event => {
457
- if (isPointerMovingToSubmenu(event)) event.preventDefault();
458
- }, [isPointerMovingToSubmenu]),
459
- pointerGraceTimerRef,
460
- onPointerGraceIntentChange: React.useCallback(intent => {
461
- pointerGraceIntentRef.current = intent;
462
- }, []),
463
- children: /* @__PURE__ */jsx(RemoveScroll, {
464
- enabled: disableOutsideScroll,
465
- children: /* @__PURE__ */jsx(FocusScope, {
466
- asChild: false,
467
- trapped: trapFocus,
468
- onMountAutoFocus: composeEventHandlers(onOpenAutoFocus, event => {
469
- event.preventDefault();
470
- const content2 = document.querySelector("[data-tamagui-menu-content]");
471
- content2?.focus({
472
- preventScroll: true
473
- });
474
- }),
475
- onUnmountAutoFocus: onCloseAutoFocus,
476
- children: /* @__PURE__ */jsx(DismissableLayer, {
477
- disableOutsidePointerEvents,
478
- onEscapeKeyDown,
479
- onPointerDownOutside,
480
- onFocusOutside,
481
- onInteractOutside,
482
- onDismiss,
483
- asChild: true,
484
- children: /* @__PURE__ */jsx(RovingFocusGroup, {
485
- asChild: true,
486
- __scopeRovingFocusGroup: scope || MENU_CONTEXT,
487
- dir: rootContext.dir,
488
- orientation: "vertical",
489
- loop,
490
- currentTabStopId: currentItemId,
491
- onCurrentTabStopIdChange: setCurrentItemId,
492
- onEntryFocus: composeEventHandlers(onEntryFocus, event => {
493
- if (!rootContext.isUsingKeyboardRef.current) {
494
- event.preventDefault();
495
- }
496
- }),
497
- children: content
498
- })
499
- })
500
- })
501
- })
502
- });
503
- });
504
- MenuContent.displayName = CONTENT_NAME;
505
- const ITEM_NAME = "MenuItem";
506
- const ITEM_SELECT = "menu.itemSelect";
507
- const MenuItem = _Item.styleable((props, forwardedRef) => {
508
- const {
509
- disabled = false,
510
- onSelect,
511
- preventCloseOnSelect,
512
- children,
513
- scope = MENU_CONTEXT,
514
- // filter out native-only props that shouldn't reach the DOM
515
- // @ts-ignore
516
- destructive,
517
- // @ts-ignore
518
- hidden,
519
- // @ts-ignore
520
- androidIconName,
521
- // @ts-ignore
522
- iosIconName,
523
- ...itemProps
524
- } = props;
525
- const ref = React.useRef(null);
526
- const rootContext = useMenuRootContext(scope);
527
- const contentContext = useMenuContentContext(scope);
528
- const composedRefs = useComposedRefs(forwardedRef, ref);
529
- const isPointerDownRef = React.useRef(false);
530
- const handleSelect = () => {
531
- const menuItem = ref.current;
532
- if (!disabled && menuItem) {
533
- if (isWeb) {
534
- const menuItemEl = menuItem;
535
- const itemSelectEvent = new CustomEvent(ITEM_SELECT, {
536
- bubbles: true,
537
- cancelable: true
538
- });
539
- menuItemEl.addEventListener(ITEM_SELECT, event => onSelect?.(event), {
540
- once: true
541
- });
542
- dispatchDiscreteCustomEvent(menuItemEl, itemSelectEvent);
543
- if (itemSelectEvent.defaultPrevented || preventCloseOnSelect) {
544
- isPointerDownRef.current = false;
545
- } else {
546
- rootContext.onClose();
547
- }
548
- } else {
549
- onSelect?.({
550
- target: menuItem
551
- });
552
- isPointerDownRef.current = false;
553
- if (!preventCloseOnSelect) {
554
- rootContext.onClose();
555
- }
556
- }
557
- }
558
- };
559
- const content = typeof children === "string" ? /* @__PURE__ */jsx(Text, {
560
- children
561
- }) : children;
562
- return /* @__PURE__ */jsx(MenuItemImpl, {
563
- outlineStyle: "none",
564
- ...itemProps,
565
- scope,
566
- ref: composedRefs,
567
- disabled,
568
- onPress: composeEventHandlers(props.onPress, handleSelect),
569
- onPointerDown: event => {
570
- props.onPointerDown?.(event);
571
- isPointerDownRef.current = true;
572
- },
573
- onPointerUp: composeEventHandlers(props.onPointerUp, event => {
574
- if (isWeb) {
575
- if (!isPointerDownRef.current) event.currentTarget?.click();
576
- }
577
- }),
578
- ...(isWeb ? {
579
- onKeyDown: composeEventHandlers(props.onKeyDown, event => {
580
- const isTypingAhead = contentContext.searchRef.current !== "";
581
- if (disabled || isTypingAhead && event.key === " ") return;
582
- if (SELECTION_KEYS.includes(event.key)) {
583
- event.currentTarget?.click();
584
- event.preventDefault();
585
- }
586
- })
587
- } : {}),
588
- children: content
589
- });
590
- });
591
- const MenuItemImpl = React.forwardRef((props, forwardedRef) => {
592
- const {
593
- scope = MENU_CONTEXT,
594
- disabled = false,
595
- textValue,
596
- unstyled = process.env.TAMAGUI_HEADLESS === "1",
597
- ...itemProps
598
- } = props;
599
- const contentContext = useMenuContentContext(scope);
600
- const ref = React.useRef(null);
601
- const composedRefs = useComposedRefs(forwardedRef, ref);
602
- const [isFocused, setIsFocused] = React.useState(false);
603
- const [textContent, setTextContent] = React.useState("");
604
- if (isWeb) {
605
- React.useEffect(() => {
606
- const menuItem = ref.current;
607
- if (menuItem) {
608
- setTextContent((menuItem.textContent ?? "").trim());
609
- }
610
- }, [itemProps.children]);
611
- }
612
- return /* @__PURE__ */jsx(Collection.ItemSlot, {
613
- scope,
614
- disabled,
615
- textValue: textValue ?? textContent,
616
- children: /* @__PURE__ */jsx(RovingFocusGroup.Item, {
617
- asChild: true,
618
- __scopeRovingFocusGroup: scope,
619
- focusable: !disabled,
620
- children: /* @__PURE__ */jsx(_Item, {
621
- unstyled,
622
- componentName: ITEM_NAME,
623
- role: "menuitem",
624
- "data-highlighted": isFocused ? "" : void 0,
625
- "aria-disabled": disabled || void 0,
626
- "data-disabled": disabled ? "" : void 0,
627
- ...itemProps,
628
- ref: composedRefs,
629
- onPointerMove: composeEventHandlers(props.onPointerMove, event => {
630
- if (event.pointerType !== "mouse") return;
631
- if (disabled) {
632
- contentContext.onItemLeave(event);
633
- } else {
634
- contentContext.onItemEnter(event);
635
- if (!event.defaultPrevented) {
636
- const item = event.currentTarget;
637
- item.focus({
638
- preventScroll: true,
639
- focusVisible: false
640
- });
641
- }
642
- }
643
- }),
644
- onPointerLeave: composeEventHandlers(props.onPointerLeave, event => {
645
- contentContext.onItemLeave(event);
646
- }),
647
- onFocus: composeEventHandlers(props.onFocus, () => setIsFocused(true)),
648
- onBlur: composeEventHandlers(props.onBlur, () => setIsFocused(false))
649
- })
650
- })
651
- });
652
- });
653
- MenuItem.displayName = ITEM_NAME;
654
- const ITEM_TITLE_NAME = "MenuItemTitle";
655
- const MenuItemTitle = _Title.styleable((props, forwardedRef) => {
656
- return /* @__PURE__ */jsx(_Title, {
657
- ...props,
658
- ref: forwardedRef
659
- });
660
- });
661
- MenuItemTitle.displayName = ITEM_TITLE_NAME;
662
- const ITEM_SUB_TITLE_NAME = "MenuItemSubTitle";
663
- const MenuItemSubTitle = _SubTitle.styleable((props, forwardedRef) => {
664
- return /* @__PURE__ */jsx(_SubTitle, {
665
- ...props,
666
- ref: forwardedRef
667
- });
668
- });
669
- MenuItemSubTitle.displayName = ITEM_SUB_TITLE_NAME;
670
- const ITEM_IMAGE = "MenuItemImage";
671
- const MenuItemImage = React.forwardRef((props, forwardedRef) => {
672
- const {
673
- // @ts-ignore - native menu ios config
674
- ios,
675
- // @ts-ignore
676
- androidIconName,
677
- // @ts-ignore
678
- iosIconName,
679
- ...rest
680
- } = props;
681
- return /* @__PURE__ */jsx(_Image, {
682
- ...rest,
683
- ref: forwardedRef
684
- });
685
- });
686
- MenuItemImage.displayName = ITEM_IMAGE;
687
- const ITEM_ICON = "MenuItemIcon";
688
- const MenuItemIcon = _Icon.styleable((props, forwardedRef) => {
689
- const {
690
- // @ts-ignore
691
- ios,
692
- // @ts-ignore
693
- android,
694
- // @ts-ignore
695
- androidIconName,
696
- // @ts-ignore
697
- iosIconName,
698
- ...rest
699
- } = props;
700
- return /* @__PURE__ */jsx(_Icon, {
701
- ...rest,
702
- ref: forwardedRef
703
- });
704
- });
705
- MenuItemIcon.displayName = ITEM_ICON;
706
- const CHECKBOX_ITEM_NAME = "MenuCheckboxItem";
707
- const MenuCheckboxItem = _Item.styleable((props, forwardedRef) => {
708
- const {
709
- checked = false,
710
- onCheckedChange,
711
- scope = MENU_CONTEXT,
712
- // filter out native-only props
713
- // @ts-ignore - native menu value state
714
- value,
715
- // @ts-ignore - native menu value change handler
716
- onValueChange,
717
- ...checkboxItemProps
718
- } = props;
719
- return /* @__PURE__ */jsx(ItemIndicatorProvider, {
720
- scope,
721
- checked,
722
- children: /* @__PURE__ */jsx(MenuItem, {
723
- componentName: CHECKBOX_ITEM_NAME,
724
- role: isWeb ? "menuitemcheckbox" : "menuitem",
725
- "aria-checked": isIndeterminate(checked) ? "mixed" : checked,
726
- ...checkboxItemProps,
727
- scope,
728
- ref: forwardedRef,
729
- "data-state": getCheckedState(checked),
730
- onSelect: composeEventHandlers(checkboxItemProps.onSelect, () => onCheckedChange?.(isIndeterminate(checked) ? true : !checked), {
731
- checkDefaultPrevented: false
732
- })
733
- })
734
- });
735
- });
736
- MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;
737
- const RADIO_GROUP_NAME = "MenuRadioGroup";
738
- const {
739
- Provider: RadioGroupProvider,
740
- useStyledContext: useRadioGroupContext
741
- } = createStyledContext();
742
- const MenuRadioGroup = _MenuGroup.styleable((props, forwardedRef) => {
743
- const {
744
- value,
745
- onValueChange,
746
- scope = MENU_CONTEXT,
747
- ...groupProps
748
- } = props;
749
- const handleValueChange = useCallbackRef(onValueChange);
750
- return /* @__PURE__ */jsx(RadioGroupProvider, {
751
- scope,
752
- value,
753
- onValueChange: handleValueChange,
754
- children: /* @__PURE__ */jsx(_MenuGroup, {
755
- componentName: RADIO_GROUP_NAME,
756
- ...groupProps,
757
- ref: forwardedRef
758
- })
759
- });
760
- });
761
- MenuRadioGroup.displayName = RADIO_GROUP_NAME;
762
- const RADIO_ITEM_NAME = "MenuRadioItem";
763
- const MenuRadioItem = _Item.styleable((props, forwardedRef) => {
764
- const {
765
- value,
766
- scope = MENU_CONTEXT,
767
- ...radioItemProps
768
- } = props;
769
- const context = useRadioGroupContext(scope);
770
- const checked = value === context.value;
771
- return /* @__PURE__ */jsx(ItemIndicatorProvider, {
772
- scope,
773
- checked,
774
- children: /* @__PURE__ */jsx(MenuItem, {
775
- componentName: RADIO_ITEM_NAME,
776
- ...radioItemProps,
777
- scope,
778
- "aria-checked": checked,
779
- ref: forwardedRef,
780
- role: isWeb ? "menuitemradio" : "menuitem",
781
- "data-state": getCheckedState(checked),
782
- onSelect: composeEventHandlers(radioItemProps.onSelect, () => context.onValueChange?.(value), {
783
- checkDefaultPrevented: false
784
- })
785
- })
786
- });
787
- });
788
- MenuRadioItem.displayName = RADIO_ITEM_NAME;
789
- const ITEM_INDICATOR_NAME = "MenuItemIndicator";
790
- const {
791
- Provider: ItemIndicatorProvider,
792
- useStyledContext: useItemIndicatorContext
793
- } = createStyledContext();
794
- const MenuItemIndicator = _Indicator.styleable((props, forwardedRef) => {
795
- const {
796
- scope = MENU_CONTEXT,
797
- forceMount,
798
- ...itemIndicatorProps
799
- } = props;
800
- const indicatorContext = useItemIndicatorContext(scope);
801
- return /* @__PURE__ */jsx(Presence, {
802
- children: forceMount || isIndeterminate(indicatorContext.checked) || indicatorContext.checked === true ? /* @__PURE__ */jsx(_Indicator, {
803
- componentName: ITEM_INDICATOR_NAME,
804
- render: "span",
805
- ...itemIndicatorProps,
806
- ref: forwardedRef,
807
- "data-state": getCheckedState(indicatorContext.checked)
808
- }) : null
809
- });
810
- });
811
- MenuItemIndicator.displayName = ITEM_INDICATOR_NAME;
812
- const MenuArrow = React.forwardRef(function MenuArrow2(props, forwardedRef) {
813
- const {
814
- scope = MENU_CONTEXT,
815
- unstyled = process.env.TAMAGUI_HEADLESS === "1",
816
- ...rest
817
- } = props;
818
- return /* @__PURE__ */jsx(PopperPrimitive.PopperArrow, {
819
- scope,
820
- componentName: "PopperArrow",
821
- unstyled,
822
- ...(!unstyled && {
823
- backgroundColor: "$background"
824
- }),
825
- ...rest,
826
- ref: forwardedRef
827
- });
828
- });
829
- const SUB_NAME = "MenuSub";
830
- const {
831
- Provider: MenuSubProvider,
832
- useStyledContext: useMenuSubContext
833
- } = createStyledContext();
834
- const MenuSub = props => {
835
- const isTouchDevice = useIsTouchDevice();
836
- const {
837
- scope = MENU_CONTEXT
838
- } = props;
839
- const rootContext = useMenuRootContext(scope);
840
- const parentPopperContext = PopperPrimitive.usePopperContext(scope);
841
- const parentSide = parentPopperContext.placement?.split("-")[0];
842
- const isNestedSubmenu = parentSide === "left" || parentSide === "right";
843
- const defaultPlacement = isTouchDevice ? "bottom" : isNestedSubmenu ? `${parentSide}-start` : rootContext.dir === "rtl" ? "left-start" : "right-start";
844
- const {
845
- children,
846
- open = false,
847
- onOpenChange,
848
- allowFlip: allowFlipProp = {
849
- padding: 10
850
- },
851
- stayInFrame = {
852
- padding: 10
853
- },
854
- placement = defaultPlacement,
855
- ...rest
856
- } = props;
857
- const allowFlip = React.useMemo(() => {
858
- if (!isNestedSubmenu || typeof allowFlipProp === "boolean") return allowFlipProp;
859
- if (allowFlipProp.fallbackPlacements) return allowFlipProp;
860
- const side = placement.split("-")[0];
861
- const align = placement.split("-")[1] || "start";
862
- const otherAlign = align === "start" ? "end" : "start";
863
- if (side === "left" || side === "right") {
864
- const oppositeSide = side === "right" ? "left" : "right";
865
- return {
866
- ...(typeof allowFlipProp === "object" ? allowFlipProp : {}),
867
- fallbackPlacements: [`${side}-${otherAlign}`, `${oppositeSide}-${align}`, `${oppositeSide}-${otherAlign}`]
868
- };
869
- }
870
- return allowFlipProp;
871
- }, [isNestedSubmenu, allowFlipProp, placement]);
872
- const parentMenuContext = useMenuContext(scope);
873
- const [trigger, setTrigger] = React.useState(null);
874
- const [content, setContent] = React.useState(null);
875
- const handleOpenChange = useCallbackRef(onOpenChange);
876
- React.useEffect(() => {
877
- if (parentMenuContext.open === false) handleOpenChange(false);
878
- return () => handleOpenChange(false);
879
- }, [parentMenuContext.open, handleOpenChange]);
880
- return /* @__PURE__ */jsx(PopperPrimitive.Popper, {
881
- open,
882
- placement,
883
- allowFlip,
884
- stayInFrame,
885
- ...rest,
886
- scope,
887
- children: /* @__PURE__ */jsx(MenuProvider, {
888
- scope,
889
- open,
890
- onOpenChange: handleOpenChange,
891
- content,
892
- onContentChange: setContent,
893
- children: /* @__PURE__ */jsx(MenuSubProvider, {
894
- scope,
895
- contentId: useId(),
896
- triggerId: useId(),
897
- trigger,
898
- onTriggerChange: setTrigger,
899
- children
900
- })
901
- })
902
- });
903
- };
904
- MenuSub.displayName = SUB_NAME;
905
- const SUB_TRIGGER_NAME = "MenuSubTrigger";
906
- const MenuSubTrigger = React.forwardRef((props, forwardedRef) => {
907
- const scope = props.scope || MENU_CONTEXT;
908
- const context = useMenuContext(scope);
909
- const rootContext = useMenuRootContext(scope);
910
- const subContext = useMenuSubContext(scope);
911
- const contentContext = useMenuContentContext(scope);
912
- const popperContext = PopperPrimitive.usePopperContext(scope);
913
- const openTimerRef = React.useRef(null);
914
- const {
915
- pointerGraceTimerRef,
916
- onPointerGraceIntentChange
917
- } = contentContext;
918
- const effectiveDir = rootContext.dir;
919
- const clearOpenTimer = React.useCallback(() => {
920
- if (openTimerRef.current) window.clearTimeout(openTimerRef.current);
921
- openTimerRef.current = null;
922
- }, []);
923
- React.useEffect(() => clearOpenTimer, [clearOpenTimer]);
924
- React.useEffect(() => {
925
- const pointerGraceTimer = pointerGraceTimerRef.current;
926
- return () => {
927
- window.clearTimeout(pointerGraceTimer);
928
- onPointerGraceIntentChange(null);
929
- };
930
- }, [pointerGraceTimerRef, onPointerGraceIntentChange]);
931
- return /* @__PURE__ */jsx(MenuAnchor, {
932
- componentName: SUB_TRIGGER_NAME,
933
- asChild: "except-style",
934
- scope,
935
- children: /* @__PURE__ */jsx(MenuItemImpl, {
936
- id: subContext.triggerId,
937
- "aria-haspopup": "menu",
938
- "aria-expanded": context.open,
939
- "aria-controls": subContext.contentId,
940
- "data-state": getOpenState(context.open),
941
- outlineStyle: "none",
942
- ...props,
943
- ref: composeRefs(forwardedRef, subContext.onTriggerChange),
944
- onPress: event => {
945
- props.onPress?.(event);
946
- if (props.disabled || event.defaultPrevented) return;
947
- if (isWeb) {
948
- event.currentTarget.focus();
949
- }
950
- if (!context.open) context.onOpenChange(true);
951
- },
952
- onPointerMove: composeEventHandlers(props.onPointerMove,
953
- // @ts-ignore
954
- whenMouse(event => {
955
- contentContext.onItemEnter(event);
956
- if (event.defaultPrevented) return;
957
- if (!props.disabled && !context.open && !openTimerRef.current) {
958
- contentContext.onPointerGraceIntentChange(null);
959
- openTimerRef.current = window.setTimeout(() => {
960
- context.onOpenChange(true);
961
- clearOpenTimer();
962
- }, 100);
963
- }
964
- })),
965
- onPointerLeave: composeEventHandlers(props.onPointerLeave, eventIn => {
966
- const event = eventIn;
967
- clearOpenTimer();
968
- const contentRect = context.content?.getBoundingClientRect();
969
- if (contentRect) {
970
- const contentEl = context.content;
971
- const sideEl = contentEl?.dataset?.side ? contentEl : contentEl?.querySelector("[data-side]");
972
- const side = sideEl?.dataset?.side || "right";
973
- const rightSide = side === "right";
974
- const bleed = rightSide ? -5 : 5;
975
- const contentNearEdge = contentRect[rightSide ? "left" : "right"];
976
- const contentFarEdge = contentRect[rightSide ? "right" : "left"];
977
- const polygon = {
978
- area: [
979
- // Apply a bleed on clientX to ensure that our exit point is
980
- // consistently within polygon bounds
981
- {
982
- x: event.clientX + bleed,
983
- y: event.clientY
984
- }, {
985
- x: contentNearEdge,
986
- y: contentRect.top
987
- }, {
988
- x: contentFarEdge,
989
- y: contentRect.top
990
- }, {
991
- x: contentFarEdge,
992
- y: contentRect.bottom
993
- }, {
994
- x: contentNearEdge,
995
- y: contentRect.bottom
996
- }],
997
- side
998
- };
999
- contentContext.onPointerGraceIntentChange(polygon);
1000
- window.clearTimeout(pointerGraceTimerRef.current);
1001
- pointerGraceTimerRef.current = window.setTimeout(() => contentContext.onPointerGraceIntentChange(null), 300);
1002
- } else if (isWeb && subContext.trigger) {
1003
- const triggerEl = subContext.trigger;
1004
- const triggerRect = triggerEl?.getBoundingClientRect();
1005
- if (triggerRect) {
1006
- const placementSide = popperContext.placement?.split("-")[0];
1007
- const side = placementSide === "left" || placementSide === "right" ? placementSide : rootContext.dir === "rtl" ? "left" : "right";
1008
- const rightSide = side === "right";
1009
- const bleed = rightSide ? -5 : 5;
1010
- const nearEdge = rightSide ? triggerRect.right + 4 : triggerRect.left - 4;
1011
- const farEdge = rightSide ? nearEdge + 200 : nearEdge - 200;
1012
- const polygon = {
1013
- area: [{
1014
- x: event.clientX + bleed,
1015
- y: event.clientY
1016
- }, {
1017
- x: nearEdge,
1018
- y: triggerRect.top - 50
1019
- }, {
1020
- x: farEdge,
1021
- y: triggerRect.top - 50
1022
- }, {
1023
- x: farEdge,
1024
- y: triggerRect.bottom + 50
1025
- }, {
1026
- x: nearEdge,
1027
- y: triggerRect.bottom + 50
1028
- }],
1029
- side
1030
- };
1031
- contentContext.onPointerGraceIntentChange(polygon);
1032
- window.clearTimeout(pointerGraceTimerRef.current);
1033
- pointerGraceTimerRef.current = window.setTimeout(() => contentContext.onPointerGraceIntentChange(null), 300);
1034
- }
1035
- } else {
1036
- contentContext.onTriggerLeave(event);
1037
- if (event.defaultPrevented) return;
1038
- contentContext.onPointerGraceIntentChange(null);
1039
- }
1040
- }),
1041
- ...(isWeb ? {
1042
- onKeyDown: composeEventHandlers(props.onKeyDown, event => {
1043
- const isTypingAhead = contentContext.searchRef.current !== "";
1044
- if (props.disabled || isTypingAhead && event.key === " ") return;
1045
- const willOpen = SUB_OPEN_KEYS[effectiveDir].includes(event.key);
1046
- if (willOpen) {
1047
- if (context.open && context.content) {
1048
- const contentEl = context.content;
1049
- const firstItem = contentEl.querySelector?.('[role="menuitem"]:not([data-disabled])');
1050
- if (firstItem) {
1051
- firstItem.focus({
1052
- focusVisible: true
1053
- });
1054
- event.preventDefault();
1055
- return;
1056
- }
1057
- }
1058
- const triggerEl = event.currentTarget;
1059
- popperContext.refs?.setReference(triggerEl);
1060
- context.onOpenChange(true);
1061
- requestAnimationFrame(() => {
1062
- popperContext.update?.();
1063
- });
1064
- context.content?.focus({
1065
- focusVisible: true
1066
- });
1067
- event.preventDefault();
1068
- }
1069
- })
1070
- } : null)
1071
- })
1072
- });
1073
- });
1074
- MenuSubTrigger.displayName = SUB_TRIGGER_NAME;
1075
- const SUB_CONTENT_NAME = "MenuSubContent";
1076
- const MenuSubContentFrame = styled(PopperPrimitive.PopperContentFrame, {
1077
- name: SUB_CONTENT_NAME
1078
- });
1079
- const MenuSubContent = MenuSubContentFrame.styleable((props, forwardedRef) => {
1080
- const scope = props.scope || MENU_CONTEXT;
1081
- const portalContext = usePortalContext(scope);
1082
- const {
1083
- forceMount = portalContext.forceMount,
1084
- ...subContentProps
1085
- } = props;
1086
- const context = useMenuContext(scope);
1087
- const rootContext = useMenuRootContext(scope);
1088
- const subContext = useMenuSubContext(scope);
1089
- const popperContext = PopperPrimitive.usePopperContext(scope);
1090
- const ref = React.useRef(null);
1091
- const composedRefs = useComposedRefs(forwardedRef, ref);
1092
- const placementSide = popperContext.placement?.split("-")[0];
1093
- const dataSide = placementSide === "left" || placementSide === "right" ? placementSide : rootContext.dir === "rtl" ? "left" : "right";
1094
- const effectiveDir = rootContext.dir;
1095
- return /* @__PURE__ */jsx(Collection.Provider, {
1096
- scope,
1097
- children: /* @__PURE__ */jsx(Collection.Slot, {
1098
- scope,
1099
- children: /* @__PURE__ */jsx(MenuContentImpl, {
1100
- id: subContext.contentId,
1101
- "aria-labelledby": subContext.triggerId,
1102
- ...subContentProps,
1103
- ref: composedRefs,
1104
- "data-side": dataSide,
1105
- disableOutsidePointerEvents: false,
1106
- disableOutsideScroll: false,
1107
- trapFocus: false,
1108
- onOpenAutoFocus: event => {
1109
- if (rootContext.isUsingKeyboardRef.current) {
1110
- const root = ref.current;
1111
- const content = root?.querySelector?.("[data-tamagui-menu-content]");
1112
- (content || root)?.focus({
1113
- preventScroll: true
1114
- });
1115
- }
1116
- event.preventDefault();
1117
- },
1118
- onCloseAutoFocus: event => event.preventDefault(),
1119
- onFocusOutside: composeEventHandlers(props.onFocusOutside, event => {
1120
- if (event.target !== subContext.trigger) context.onOpenChange(false);
1121
- }),
1122
- onEscapeKeyDown: composeEventHandlers(props.onEscapeKeyDown, event => {
1123
- context.onOpenChange(false);
1124
- subContext.trigger?.focus({
1125
- focusVisible: true
1126
- });
1127
- event.preventDefault();
1128
- }),
1129
- ...(isWeb ? {
1130
- onKeyDown: composeEventHandlers(props.onKeyDown, event => {
1131
- const isKeyDownInside = event.currentTarget.contains(event.target);
1132
- const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key);
1133
- if (isKeyDownInside && isCloseKey) {
1134
- context.onOpenChange(false);
1135
- subContext.trigger?.focus({
1136
- focusVisible: true
1137
- });
1138
- event.preventDefault();
1139
- }
1140
- })
1141
- } : null)
1142
- })
1143
- })
1144
- });
1145
- });
1146
- MenuSubContent.displayName = SUB_CONTENT_NAME;
1147
- const Anchor = MenuAnchor;
1148
- const Portal = MenuPortal;
1149
- const Content = MenuContent;
1150
- const Group = _MenuGroup.styleable((props, ref) => {
1151
- return /* @__PURE__ */jsx(_MenuGroup, {
1152
- ...props,
1153
- ref
1154
- });
1155
- });
1156
- Group.displayName = "MenuGroup";
1157
- const Label = _Label.styleable((props, ref) => {
1158
- return /* @__PURE__ */jsx(_Label, {
1159
- ...props,
1160
- ref
1161
- });
1162
- });
1163
- Label.displayName = "MenuLabel";
1164
- const Item = MenuItem;
1165
- const CheckboxItem = MenuCheckboxItem;
1166
- const RadioGroup = MenuRadioGroup;
1167
- const RadioItem = MenuRadioItem;
1168
- const ItemIndicator = MenuItemIndicator;
1169
- const Separator = _Separator.styleable((props, ref) => {
1170
- return /* @__PURE__ */jsx(_Separator, {
1171
- ...props,
1172
- ref
1173
- });
1174
- });
1175
- Separator.displayName = "MenuSeparator";
1176
- const Arrow = MenuArrow;
1177
- const Sub = MenuSub;
1178
- const SubTrigger = MenuSubTrigger;
1179
- const SubContent = MenuSubContent;
1180
- const ItemTitle = MenuItemTitle;
1181
- const ItemSubtitle = MenuItemSubTitle;
1182
- const ItemImage = MenuItemImage;
1183
- const ItemIcon = MenuItemIcon;
1184
- const Menu = withStaticProperties(MenuComp, {
1185
- Anchor,
1186
- Portal,
1187
- Content,
1188
- Group,
1189
- Label,
1190
- Item,
1191
- CheckboxItem,
1192
- RadioGroup,
1193
- RadioItem,
1194
- ItemIndicator,
1195
- Separator,
1196
- Arrow,
1197
- Sub,
1198
- SubTrigger,
1199
- SubContent,
1200
- ItemTitle,
1201
- ItemSubtitle,
1202
- ItemImage,
1203
- ItemIcon
1204
- });
1205
- return {
1206
- Menu
1207
- };
48
+ const MenuImageFrame = styled(Image, { displayName: "MenuItemImage" });
49
+ function createBaseMenu() {
50
+ const MenuComp = (props) => {
51
+ const direction = useDirection(props.dir);
52
+ const defaultPlacement = direction === "rtl" ? "bottom-end" : "bottom-start";
53
+ const { scope = MENU_CONTEXT, open = false, children, dir, onOpenChange, modal = true, allowFlip = { padding: 10 }, stayInFrame = { padding: 10 }, placement = defaultPlacement, resize = true, offset = 10, ...rest } = props;
54
+ const [content, setContent] = React.useState(null);
55
+ const isUsingKeyboardRef = React.useRef(false);
56
+ const handleOpenChange = useCallbackRef(onOpenChange);
57
+ if (isWeb) {
58
+ React.useEffect(() => {
59
+ const handleKeyDown = () => {
60
+ isUsingKeyboardRef.current = true;
61
+ document.addEventListener("pointerdown", handlePointer, {
62
+ capture: true,
63
+ once: true
64
+ });
65
+ document.addEventListener("pointermove", handlePointer, {
66
+ capture: true,
67
+ once: true
68
+ });
69
+ };
70
+ const handlePointer = () => isUsingKeyboardRef.current = false;
71
+ document.addEventListener("keydown", handleKeyDown, { capture: true });
72
+ return () => {
73
+ document.removeEventListener("keydown", handleKeyDown, { capture: true });
74
+ document.removeEventListener("pointerdown", handlePointer, { capture: true });
75
+ document.removeEventListener("pointermove", handlePointer, { capture: true });
76
+ };
77
+ }, []);
78
+ }
79
+ return /* @__PURE__ */ jsx(PopperPrimitive.Popper, {
80
+ scope,
81
+ open,
82
+ placement,
83
+ allowFlip,
84
+ stayInFrame,
85
+ resize,
86
+ offset,
87
+ ...rest,
88
+ children: /* @__PURE__ */ jsx(MenuProvider, {
89
+ scope,
90
+ open,
91
+ onOpenChange: handleOpenChange,
92
+ content,
93
+ onContentChange: setContent,
94
+ children: /* @__PURE__ */ jsx(MenuRootProvider, {
95
+ scope,
96
+ open,
97
+ onClose: React.useCallback(() => handleOpenChange(false), [handleOpenChange]),
98
+ isUsingKeyboardRef,
99
+ dir: direction,
100
+ modal,
101
+ children: /* @__PURE__ */ jsx(MenuSubProvider, {
102
+ scope,
103
+ children
104
+ })
105
+ })
106
+ })
107
+ });
108
+ };
109
+ const RepropagateMenuAndMenuRootProvider = (props) => {
110
+ const { scope = MENU_CONTEXT, menuContext, rootContext, popperContext, menuSubContext, children } = props;
111
+ return /* @__PURE__ */ jsx(PopperPrimitive.PopperProvider, {
112
+ ...popperContext,
113
+ scope,
114
+ children: /* @__PURE__ */ jsx(MenuProvider, {
115
+ scope,
116
+ ...menuContext,
117
+ children: /* @__PURE__ */ jsx(MenuRootProvider, {
118
+ scope,
119
+ ...rootContext,
120
+ children: menuSubContext ? /* @__PURE__ */ jsx(MenuSubProvider, {
121
+ scope,
122
+ ...menuSubContext,
123
+ children
124
+ }) : children
125
+ })
126
+ })
127
+ });
128
+ };
129
+ MenuComp.displayName = MENU_NAME;
130
+ const ANCHOR_NAME = "MenuAnchor";
131
+ const MenuAnchor = (props) => {
132
+ return /* @__PURE__ */ jsx(PopperPrimitive.PopperAnchor, {
133
+ scope: MENU_CONTEXT,
134
+ ...props
135
+ });
136
+ };
137
+ MenuAnchor.displayName = ANCHOR_NAME;
138
+ const PORTAL_NAME = "MenuPortal";
139
+ const { Provider: PortalProvider, useStyledContext: usePortalContext } = createStyledContext(void 0, "Portal");
140
+ const MenuPortal = (props) => {
141
+ const { scope = MENU_CONTEXT, forceMount, zIndex, children } = props;
142
+ const menuContext = useMenuContext(scope);
143
+ const rootContext = useMenuRootContext(scope);
144
+ const popperContext = PopperPrimitive.usePopperContext(scope);
145
+ const menuSubContext = useMenuSubContext(scope);
146
+ const themeName = useThemeName();
147
+ const themedChildren = /* @__PURE__ */ jsx(Theme, {
148
+ forceClassName: true,
149
+ name: themeName,
150
+ children
151
+ });
152
+ const content = needsPortalRepropagation() ? /* @__PURE__ */ jsx(RepropagateMenuAndMenuRootProvider, {
153
+ menuContext,
154
+ rootContext,
155
+ popperContext,
156
+ menuSubContext,
157
+ scope,
158
+ children: themedChildren
159
+ }) : themedChildren;
160
+ const isPresent = forceMount || rootContext.open && menuContext.open;
161
+ return /* @__PURE__ */ jsx(Animate, {
162
+ type: "presence",
163
+ present: isPresent,
164
+ children: /* @__PURE__ */ jsx(Portal, {
165
+ stackZIndex: true,
166
+ children: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(PortalProvider, {
167
+ scope,
168
+ forceMount,
169
+ children: /* @__PURE__ */ jsxs(View, {
170
+ zIndex: zIndex || 100,
171
+ inset: 0,
172
+ position: "absolute",
173
+ children: [!!menuContext.open && !isWeb && /* @__PURE__ */ jsx(View, {
174
+ inset: 0,
175
+ position: "absolute",
176
+ onPress: () => menuContext.onOpenChange(!menuContext.open)
177
+ }), content]
178
+ })
179
+ }) })
180
+ })
181
+ });
182
+ };
183
+ MenuPortal.displayName = PORTAL_NAME;
184
+ const CONTENT_NAME = "MenuContent";
185
+ const { Provider: MenuContentProvider, useStyledContext: useMenuContentContext } = createStyledContext();
186
+ const MenuContentFrame = styled(PopperPrimitive.PopperContentFrame, { displayName: CONTENT_NAME });
187
+ const MenuContent = createStyledHOC(MenuContentFrame, (props, forwardedRef) => {
188
+ const scope = props.scope || MENU_CONTEXT;
189
+ const portalContext = usePortalContext(scope);
190
+ const { forceMount = portalContext.forceMount, ...contentProps } = props;
191
+ const rootContext = useMenuRootContext(scope);
192
+ return /* @__PURE__ */ jsx(Collection.Provider, {
193
+ scope,
194
+ children: /* @__PURE__ */ jsx(Collection.Slot, {
195
+ scope,
196
+ children: rootContext.modal ? /* @__PURE__ */ jsx(MenuRootContentModal, {
197
+ ...contentProps,
198
+ ref: forwardedRef
199
+ }) : /* @__PURE__ */ jsx(MenuRootContentNonModal, {
200
+ ...contentProps,
201
+ ref: forwardedRef
202
+ })
203
+ })
204
+ });
205
+ });
206
+ const MenuRootContentModal = createRefComponent((props, forwardedRef) => {
207
+ const scope = props.scope || MENU_CONTEXT;
208
+ const context = useMenuContext(scope);
209
+ const ref = React.useRef(null);
210
+ const composedRefs = useComposedRefs(forwardedRef, ref);
211
+ return /* @__PURE__ */ jsx(MenuContentImpl, {
212
+ ...props,
213
+ scope,
214
+ ref: composedRefs,
215
+ trapFocus: context.open,
216
+ disableOutsidePointerEvents: context.open,
217
+ disableOutsideScroll: false,
218
+ onFocusOutside: composeEventHandlers(props.onFocusOutside, (event) => event.cancel(), { checkDefaultPrevented: false }),
219
+ onDismiss: () => context.onOpenChange(false)
220
+ });
221
+ });
222
+ const MenuRootContentNonModal = createRefComponent((props, forwardedRef) => {
223
+ const scope = props.scope || MENU_CONTEXT;
224
+ const context = useMenuContext(scope);
225
+ return /* @__PURE__ */ jsx(MenuContentImpl, {
226
+ ...props,
227
+ scope,
228
+ ref: forwardedRef,
229
+ trapFocus: false,
230
+ disableOutsidePointerEvents: false,
231
+ disableOutsideScroll: false,
232
+ onDismiss: () => context.onOpenChange(false)
233
+ });
234
+ });
235
+ const MenuContentImpl = createRefComponent((props, forwardedRef) => {
236
+ const { scope = MENU_CONTEXT, loop = false, trapFocus, onOpenAutoFocus, onCloseAutoFocus, disableOutsidePointerEvents, onEntryFocus, onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, onDismiss, disableOutsideScroll, disableDismissOnScroll = false, ...contentProps } = props;
237
+ const context = useMenuContext(scope);
238
+ const rootContext = useMenuRootContext(scope);
239
+ const popperContext = PopperPrimitive.usePopperContext(scope);
240
+ const getItems = useCollection(scope);
241
+ const [currentItemId, setCurrentItemId] = React.useState(null);
242
+ const contentRef = React.useRef(null);
243
+ const focusableContentRef = React.useRef(null);
244
+ const composedRefs = useComposedRefs(forwardedRef, contentRef, context.onContentChange);
245
+ const timerRef = React.useRef(0);
246
+ const searchRef = React.useRef("");
247
+ const pointerGraceTimerRef = React.useRef(0);
248
+ const pointerGraceIntentRef = React.useRef(null);
249
+ const pointerDirRef = React.useRef("right");
250
+ const lastPointerXRef = React.useRef(0);
251
+ const handleTypeaheadSearch = (key) => {
252
+ const search = searchRef.current + key;
253
+ const items = getItems().filter((item) => !item.disabled);
254
+ const currentItem = document.activeElement;
255
+ const currentMatch = items.find((item) => item.ref.current === currentItem)?.textValue;
256
+ const values = items.map((item) => item.textValue);
257
+ const nextMatch = getNextMatch(values, search, currentMatch);
258
+ const newItem = items.find((item) => item.textValue === nextMatch)?.ref.current;
259
+ (function updateSearch(value) {
260
+ searchRef.current = value;
261
+ clearTimeout(timerRef.current);
262
+ if (value !== "") timerRef.current = setTimeout(() => updateSearch(""), 1e3);
263
+ })(search);
264
+ if (newItem) {
265
+ setTimeout(() => newItem.focus());
266
+ }
267
+ };
268
+ React.useEffect(() => {
269
+ return () => clearTimeout(timerRef.current);
270
+ }, []);
271
+ React.useEffect(() => {
272
+ if (!isWeb || !context.open) return;
273
+ const frame = requestAnimationFrame(() => {
274
+ const container = contentRef.current;
275
+ const el = container?.querySelector("[data-tamagui-menu-content]");
276
+ if (el) focusableContentRef.current = el;
277
+ });
278
+ return () => cancelAnimationFrame(frame);
279
+ }, [context.open]);
280
+ React.useEffect(() => {
281
+ if (!isWeb || disableDismissOnScroll || !context.open) return;
282
+ const handleScroll = (event) => {
283
+ const scrolled = event.target;
284
+ if (!scrolled) return;
285
+ const content2 = contentRef.current;
286
+ if (content2?.contains(scrolled)) return;
287
+ const reference = popperContext.refs?.reference?.current;
288
+ const anchor = reference instanceof Element ? reference : reference?.contextElement ?? null;
289
+ if (anchor && !scrolled.contains(anchor)) return;
290
+ context.onOpenChange(false);
291
+ };
292
+ window.addEventListener("scroll", handleScroll, {
293
+ capture: true,
294
+ passive: true
295
+ });
296
+ return () => {
297
+ window.removeEventListener("scroll", handleScroll, { capture: true });
298
+ };
299
+ }, [
300
+ disableDismissOnScroll,
301
+ context.open,
302
+ context.onOpenChange,
303
+ popperContext.refs
304
+ ]);
305
+ if (isWeb) {
306
+ useFocusGuards();
307
+ }
308
+ const isPointerMovingToSubmenu = React.useCallback((event) => {
309
+ const isMovingTowards = pointerDirRef.current === pointerGraceIntentRef.current?.side;
310
+ const inArea = isPointerInGraceArea(event, pointerGraceIntentRef.current?.area);
311
+ return isMovingTowards && inArea;
312
+ }, []);
313
+ const content = /* @__PURE__ */ jsx(PopperPrimitive.PopperContent, {
314
+ role: "menu",
315
+ tabIndex: -1,
316
+ "aria-orientation": "vertical",
317
+ "data-state": getOpenState(context.open),
318
+ "data-tamagui-menu-content": "",
319
+ dir: rootContext.dir,
320
+ scope: scope || MENU_CONTEXT,
321
+ ...contentProps,
322
+ ref: composedRefs,
323
+ className: contentProps.transition ? void 0 : contentProps.className,
324
+ ...isWeb ? {
325
+ onKeyDown: composeEventHandlers(contentProps.onKeyDown, (event) => {
326
+ const target = event.target;
327
+ const isKeyDownInside = target.closest("[data-tamagui-menu-content]") === event.currentTarget;
328
+ const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;
329
+ const isCharacterKey = event.key.length === 1;
330
+ if (isKeyDownInside) {
331
+ if (event.key === "Tab") event.preventDefault();
332
+ if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key);
333
+ }
334
+ const isOnContentFrame = event.target.hasAttribute("data-tamagui-menu-content");
335
+ if (!isKeyDownInside || !isOnContentFrame) return;
336
+ if (!FIRST_LAST_KEYS.includes(event.key)) return;
337
+ event.preventDefault();
338
+ const items = getItems().filter((item) => !item.disabled);
339
+ const candidateNodes = items.map((item) => item.ref.current);
340
+ if (LAST_KEYS.includes(event.key)) candidateNodes.reverse();
341
+ focusFirst(candidateNodes, { focusVisible: true });
342
+ }),
343
+ onBlur: composeEventHandlers(props.onBlur, (event) => {
344
+ if (!event.currentTarget?.contains(event.target)) {
345
+ clearTimeout(timerRef.current);
346
+ searchRef.current = "";
347
+ }
348
+ }),
349
+ onPointerMove: composeEventHandlers(props.onPointerMove, (event) => {
350
+ if (event.pointerType !== "mouse") return;
351
+ const target = event.target;
352
+ const pointerXHasChanged = lastPointerXRef.current !== event.clientX;
353
+ if (event.currentTarget?.contains(target) && pointerXHasChanged) {
354
+ const newDir = event.clientX > lastPointerXRef.current ? "right" : "left";
355
+ pointerDirRef.current = newDir;
356
+ lastPointerXRef.current = event.clientX;
357
+ }
358
+ })
359
+ } : {}
360
+ });
361
+ return /* @__PURE__ */ jsx(MenuContentProvider, {
362
+ scope,
363
+ searchRef,
364
+ onItemEnter: React.useCallback((event) => {
365
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
366
+ }, [isPointerMovingToSubmenu]),
367
+ onItemLeave: React.useCallback((event) => {
368
+ if (isPointerMovingToSubmenu(event)) return;
369
+ focusableContentRef.current?.focus();
370
+ setCurrentItemId(null);
371
+ }, [isPointerMovingToSubmenu]),
372
+ onTriggerLeave: React.useCallback((event) => {
373
+ if (isPointerMovingToSubmenu(event)) event.preventDefault();
374
+ }, [isPointerMovingToSubmenu]),
375
+ pointerGraceTimerRef,
376
+ onPointerGraceIntentChange: React.useCallback((intent) => {
377
+ pointerGraceIntentRef.current = intent;
378
+ }, []),
379
+ children: /* @__PURE__ */ jsx(RemoveScroll, {
380
+ enabled: disableOutsideScroll,
381
+ children: /* @__PURE__ */ jsx(FocusScope, {
382
+ asChild: false,
383
+ trapped: trapFocus,
384
+ onMountAutoFocus: composeEventHandlers(onOpenAutoFocus, (event) => {
385
+ event.cancel();
386
+ const content2 = document.querySelector("[data-tamagui-menu-content]");
387
+ content2?.focus({ preventScroll: true });
388
+ }),
389
+ onUnmountAutoFocus: onCloseAutoFocus,
390
+ children: /* @__PURE__ */ jsx(Dismissable, {
391
+ disableOutsidePointerEvents,
392
+ onEscapeKeyDown,
393
+ onPointerDownOutside,
394
+ onFocusOutside,
395
+ onInteractOutside,
396
+ onDismiss,
397
+ asChild: true,
398
+ children: /* @__PURE__ */ jsx(RovingFocusGroup, {
399
+ asChild: true,
400
+ __scopeRovingFocusGroup: scope || MENU_CONTEXT,
401
+ dir: rootContext.dir,
402
+ orientation: "vertical",
403
+ loop,
404
+ currentTabStopId: currentItemId,
405
+ onCurrentTabStopIdChange: setCurrentItemId,
406
+ onEntryFocus: composeEventHandlers(onEntryFocus, (event) => {
407
+ if (!rootContext.isUsingKeyboardRef.current) {
408
+ event.cancel();
409
+ }
410
+ }),
411
+ children: content
412
+ })
413
+ })
414
+ })
415
+ })
416
+ });
417
+ });
418
+ MenuContent.displayName = CONTENT_NAME;
419
+ const ITEM_NAME = "MenuItem";
420
+ const ITEM_SELECT = "menu.itemSelect";
421
+ const MenuItem = createStyledHOC(View, (props, forwardedRef) => {
422
+ const { disabled = false, onSelect, preventCloseOnSelect, children, scope = MENU_CONTEXT, destructive, hidden, androidIconName, ...itemProps } = props;
423
+ const ref = React.useRef(null);
424
+ const rootContext = useMenuRootContext(scope);
425
+ const contentContext = useMenuContentContext(scope);
426
+ const composedRefs = useComposedRefs(forwardedRef, ref);
427
+ const isPointerDownRef = React.useRef(false);
428
+ const handleSelect = () => {
429
+ const menuItem = ref.current;
430
+ if (!disabled && menuItem) {
431
+ if (isWeb) {
432
+ const menuItemEl = menuItem;
433
+ const itemSelectEvent = new CustomEvent(ITEM_SELECT, {
434
+ bubbles: true,
435
+ cancelable: true
436
+ });
437
+ menuItemEl.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), { once: true });
438
+ dispatchDiscreteCustomEvent(menuItemEl, itemSelectEvent);
439
+ if (itemSelectEvent.defaultPrevented || preventCloseOnSelect) {
440
+ isPointerDownRef.current = false;
441
+ } else {
442
+ rootContext.onClose();
443
+ }
444
+ } else {
445
+ onSelect?.({ target: menuItem });
446
+ isPointerDownRef.current = false;
447
+ if (!preventCloseOnSelect) {
448
+ rootContext.onClose();
449
+ }
450
+ }
451
+ }
452
+ };
453
+ const content = typeof children === "string" ? /* @__PURE__ */ jsx(Text, { children }) : children;
454
+ return /* @__PURE__ */ jsx(MenuItemImpl, {
455
+ ...itemProps,
456
+ scope,
457
+ ref: composedRefs,
458
+ disabled,
459
+ onPress: composeEventHandlers(props.onPress, handleSelect),
460
+ onPointerDown: (event) => {
461
+ props.onPointerDown?.(event);
462
+ isPointerDownRef.current = true;
463
+ },
464
+ onPointerUp: composeEventHandlers(props.onPointerUp, (event) => {
465
+ if (isWeb) {
466
+ if (!isPointerDownRef.current) event.currentTarget?.click();
467
+ }
468
+ }),
469
+ ...isWeb ? { onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
470
+ const isTypingAhead = contentContext.searchRef.current !== "";
471
+ if (disabled || isTypingAhead && event.key === " ") return;
472
+ if (SELECTION_KEYS.includes(event.key)) {
473
+ event.currentTarget?.click();
474
+ event.preventDefault();
475
+ }
476
+ }) } : {},
477
+ children: content
478
+ });
479
+ });
480
+ const MenuItemImpl = createRefComponent((props, forwardedRef) => {
481
+ const { scope = MENU_CONTEXT, className, disabled = false, textValue, ...itemProps } = props;
482
+ const contentContext = useMenuContentContext(scope);
483
+ const ref = React.useRef(null);
484
+ const composedRefs = useComposedRefs(forwardedRef, ref);
485
+ const [isFocused, setIsFocused] = React.useState(false);
486
+ const [textContent, setTextContent] = React.useState("");
487
+ if (isWeb) {
488
+ React.useEffect(() => {
489
+ const menuItem = ref.current;
490
+ if (menuItem) {
491
+ setTextContent((menuItem.textContent ?? "").trim());
492
+ }
493
+ }, [itemProps.children]);
494
+ }
495
+ return /* @__PURE__ */ jsx(Collection.ItemSlot, {
496
+ scope,
497
+ disabled,
498
+ textValue: textValue ?? textContent,
499
+ children: /* @__PURE__ */ jsx(RovingFocusGroup.Item, {
500
+ asChild: true,
501
+ __scopeRovingFocusGroup: scope,
502
+ tabIndex: disabled ? -1 : 0,
503
+ children: /* @__PURE__ */ jsx(View, {
504
+ className: `is_${ITEM_NAME} ${className || ""}`.trim(),
505
+ role: "menuitem",
506
+ "data-highlighted": isFocused ? "" : void 0,
507
+ "aria-disabled": disabled || void 0,
508
+ "data-disabled": disabled ? "" : void 0,
509
+ ...itemProps,
510
+ ref: composedRefs,
511
+ onPointerMove: composeEventHandlers(props.onPointerMove, (event) => {
512
+ if (event.pointerType !== "mouse") return;
513
+ if (disabled) {
514
+ contentContext.onItemLeave(event);
515
+ } else {
516
+ contentContext.onItemEnter(event);
517
+ if (!event.defaultPrevented) {
518
+ const item = event.currentTarget;
519
+ item.focus({
520
+ preventScroll: true,
521
+ focusVisible: false
522
+ });
523
+ }
524
+ }
525
+ }),
526
+ onPointerLeave: composeEventHandlers(props.onPointerLeave, (event) => {
527
+ contentContext.onItemLeave(event);
528
+ }),
529
+ onFocus: composeEventHandlers(props.onFocus, () => setIsFocused(true)),
530
+ onBlur: composeEventHandlers(props.onBlur, () => setIsFocused(false))
531
+ })
532
+ })
533
+ });
534
+ });
535
+ MenuItem.displayName = ITEM_NAME;
536
+ const ITEM_TITLE_NAME = "MenuItemTitle";
537
+ const MenuItemTitle = createStyledHOC(Text, (props, forwardedRef) => {
538
+ return /* @__PURE__ */ jsx(Text, {
539
+ ...props,
540
+ ref: forwardedRef
541
+ });
542
+ });
543
+ MenuItemTitle.displayName = ITEM_TITLE_NAME;
544
+ const ITEM_SUB_TITLE_NAME = "MenuItemSubTitle";
545
+ const MenuItemSubTitle = createStyledHOC(Text, (props, forwardedRef) => {
546
+ return /* @__PURE__ */ jsx(Text, {
547
+ ...props,
548
+ ref: forwardedRef
549
+ });
550
+ });
551
+ MenuItemSubTitle.displayName = ITEM_SUB_TITLE_NAME;
552
+ const ITEM_IMAGE = "MenuItemImage";
553
+ const MenuItemImage = createRefComponent((props, forwardedRef) => {
554
+ const { ios, androidIconName, ...rest } = props;
555
+ return /* @__PURE__ */ jsx(MenuImageFrame, {
556
+ ...rest,
557
+ ref: forwardedRef
558
+ });
559
+ });
560
+ MenuItemImage.displayName = ITEM_IMAGE;
561
+ const ITEM_ICON = "MenuItemIcon";
562
+ const MenuItemIcon = createStyledHOC(View, (props, forwardedRef) => {
563
+ const { ios, android, androidIconName, ...rest } = props;
564
+ return /* @__PURE__ */ jsx(View, {
565
+ ...rest,
566
+ ref: forwardedRef
567
+ });
568
+ });
569
+ MenuItemIcon.displayName = ITEM_ICON;
570
+ const CHECKBOX_ITEM_NAME = "MenuCheckboxItem";
571
+ const MenuCheckboxItem = createStyledHOC(View, (props, forwardedRef) => {
572
+ const { checked = false, onCheckedChange, scope = MENU_CONTEXT, value, onValueChange, className, ...checkboxItemProps } = props;
573
+ return /* @__PURE__ */ jsx(ItemIndicatorProvider, {
574
+ scope,
575
+ checked,
576
+ children: /* @__PURE__ */ jsx(MenuItem, {
577
+ className: `is_${CHECKBOX_ITEM_NAME} ${className || ""}`.trim(),
578
+ role: isWeb ? "menuitemcheckbox" : "menuitem",
579
+ "aria-checked": isIndeterminate(checked) ? "mixed" : checked,
580
+ ...checkboxItemProps,
581
+ scope,
582
+ ref: forwardedRef,
583
+ "data-state": getCheckedState(checked),
584
+ onSelect: composeEventHandlers(checkboxItemProps.onSelect, () => onCheckedChange?.(isIndeterminate(checked) ? true : !checked), { checkDefaultPrevented: false })
585
+ })
586
+ });
587
+ });
588
+ MenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;
589
+ const RADIO_GROUP_NAME = "MenuRadioGroup";
590
+ const { Provider: RadioGroupProvider, useStyledContext: useRadioGroupContext } = createStyledContext();
591
+ const MenuRadioGroup = createStyledHOC(View, (props, forwardedRef) => {
592
+ const { value, onValueChange, scope = MENU_CONTEXT, className, ...groupProps } = props;
593
+ const handleValueChange = useCallbackRef(onValueChange);
594
+ return /* @__PURE__ */ jsx(RadioGroupProvider, {
595
+ scope,
596
+ value,
597
+ onValueChange: handleValueChange,
598
+ children: /* @__PURE__ */ jsx(View, {
599
+ className: `is_${RADIO_GROUP_NAME} ${className || ""}`.trim(),
600
+ ...groupProps,
601
+ ref: forwardedRef
602
+ })
603
+ });
604
+ });
605
+ MenuRadioGroup.displayName = RADIO_GROUP_NAME;
606
+ const RADIO_ITEM_NAME = "MenuRadioItem";
607
+ const MenuRadioItem = createStyledHOC(View, (props, forwardedRef) => {
608
+ const { value, scope = MENU_CONTEXT, className, ...radioItemProps } = props;
609
+ const context = useRadioGroupContext(scope);
610
+ const checked = value === context.value;
611
+ return /* @__PURE__ */ jsx(ItemIndicatorProvider, {
612
+ scope,
613
+ checked,
614
+ children: /* @__PURE__ */ jsx(MenuItem, {
615
+ className: `is_${RADIO_ITEM_NAME} ${className || ""}`.trim(),
616
+ ...radioItemProps,
617
+ scope,
618
+ "aria-checked": checked,
619
+ ref: forwardedRef,
620
+ role: isWeb ? "menuitemradio" : "menuitem",
621
+ "data-state": getCheckedState(checked),
622
+ onSelect: composeEventHandlers(radioItemProps.onSelect, () => context.onValueChange?.(value), { checkDefaultPrevented: false })
623
+ })
624
+ });
625
+ });
626
+ MenuRadioItem.displayName = RADIO_ITEM_NAME;
627
+ const ITEM_INDICATOR_NAME = "MenuItemIndicator";
628
+ const { Provider: ItemIndicatorProvider, useStyledContext: useItemIndicatorContext } = createStyledContext();
629
+ const MenuItemIndicator = createStyledHOC(View, (props, forwardedRef) => {
630
+ const { scope = MENU_CONTEXT, className, forceMount, ...itemIndicatorProps } = props;
631
+ const indicatorContext = useItemIndicatorContext(scope);
632
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: forceMount || isIndeterminate(indicatorContext.checked) || indicatorContext.checked === true ? /* @__PURE__ */ jsx(View, {
633
+ className: `is_${ITEM_INDICATOR_NAME} ${className || ""}`.trim(),
634
+ render: "span",
635
+ ...itemIndicatorProps,
636
+ ref: forwardedRef,
637
+ "data-state": getCheckedState(indicatorContext.checked)
638
+ }) : null });
639
+ });
640
+ MenuItemIndicator.displayName = ITEM_INDICATOR_NAME;
641
+ const MenuArrow = createRefComponent(function MenuArrow2(props, forwardedRef) {
642
+ const { scope = MENU_CONTEXT, className, ...rest } = props;
643
+ return /* @__PURE__ */ jsx(PopperPrimitive.PopperArrow, {
644
+ scope,
645
+ className: `is_PopperArrow ${className || ""}`.trim(),
646
+ ...rest,
647
+ ref: forwardedRef
648
+ });
649
+ });
650
+ const SUB_NAME = "MenuSub";
651
+ const { Provider: MenuSubProvider, useStyledContext: useMenuSubContext } = createStyledContext();
652
+ const MenuSub = (props) => {
653
+ const isTouchDevice = useIsTouchDevice();
654
+ const { scope = MENU_CONTEXT } = props;
655
+ const rootContext = useMenuRootContext(scope);
656
+ const parentPopperContext = PopperPrimitive.usePopperContext(scope);
657
+ const parentSide = parentPopperContext.placement?.split("-")[0];
658
+ const isNestedSubmenu = parentSide === "left" || parentSide === "right";
659
+ const defaultPlacement = isTouchDevice ? "bottom" : isNestedSubmenu ? `${parentSide}-start` : rootContext.dir === "rtl" ? "left-start" : "right-start";
660
+ const { children, open = false, onOpenChange, allowFlip: allowFlipProp = { padding: 10 }, stayInFrame = { padding: 10 }, placement = defaultPlacement, ...rest } = props;
661
+ const allowFlip = React.useMemo(() => {
662
+ if (!isNestedSubmenu || typeof allowFlipProp === "boolean") return allowFlipProp;
663
+ if (allowFlipProp.fallbackPlacements) return allowFlipProp;
664
+ const side = placement.split("-")[0];
665
+ const align = placement.split("-")[1] || "start";
666
+ const otherAlign = align === "start" ? "end" : "start";
667
+ if (side === "left" || side === "right") {
668
+ const oppositeSide = side === "right" ? "left" : "right";
669
+ return {
670
+ ...typeof allowFlipProp === "object" ? allowFlipProp : {},
671
+ fallbackPlacements: [
672
+ `${side}-${otherAlign}`,
673
+ `${oppositeSide}-${align}`,
674
+ `${oppositeSide}-${otherAlign}`
675
+ ]
676
+ };
677
+ }
678
+ return allowFlipProp;
679
+ }, [
680
+ isNestedSubmenu,
681
+ allowFlipProp,
682
+ placement
683
+ ]);
684
+ const parentMenuContext = useMenuContext(scope);
685
+ const [trigger, setTrigger] = React.useState(null);
686
+ const [content, setContent] = React.useState(null);
687
+ const handleOpenChange = useCallbackRef(onOpenChange);
688
+ React.useEffect(() => {
689
+ if (parentMenuContext.open === false) handleOpenChange(false);
690
+ return () => handleOpenChange(false);
691
+ }, [parentMenuContext.open, handleOpenChange]);
692
+ return /* @__PURE__ */ jsx(PopperPrimitive.Popper, {
693
+ open,
694
+ placement,
695
+ allowFlip,
696
+ stayInFrame,
697
+ ...rest,
698
+ scope,
699
+ children: /* @__PURE__ */ jsx(MenuProvider, {
700
+ scope,
701
+ open,
702
+ onOpenChange: handleOpenChange,
703
+ content,
704
+ onContentChange: setContent,
705
+ children: /* @__PURE__ */ jsx(MenuSubProvider, {
706
+ scope,
707
+ contentId: useId(),
708
+ triggerId: useId(),
709
+ trigger,
710
+ onTriggerChange: setTrigger,
711
+ children
712
+ })
713
+ })
714
+ });
715
+ };
716
+ MenuSub.displayName = SUB_NAME;
717
+ const SUB_TRIGGER_NAME = "MenuSubTrigger";
718
+ const MenuSubTrigger = createRefComponent((props, forwardedRef) => {
719
+ const scope = props.scope || MENU_CONTEXT;
720
+ const context = useMenuContext(scope);
721
+ const rootContext = useMenuRootContext(scope);
722
+ const subContext = useMenuSubContext(scope);
723
+ const contentContext = useMenuContentContext(scope);
724
+ const popperContext = PopperPrimitive.usePopperContext(scope);
725
+ const openTimerRef = React.useRef(null);
726
+ const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;
727
+ const effectiveDir = rootContext.dir;
728
+ const clearOpenTimer = React.useCallback(() => {
729
+ if (openTimerRef.current) window.clearTimeout(openTimerRef.current);
730
+ openTimerRef.current = null;
731
+ }, []);
732
+ React.useEffect(() => clearOpenTimer, [clearOpenTimer]);
733
+ React.useEffect(() => {
734
+ const pointerGraceTimer = pointerGraceTimerRef.current;
735
+ return () => {
736
+ window.clearTimeout(pointerGraceTimer);
737
+ onPointerGraceIntentChange(null);
738
+ };
739
+ }, [pointerGraceTimerRef, onPointerGraceIntentChange]);
740
+ return /* @__PURE__ */ jsx(MenuAnchor, {
741
+ className: `is_${SUB_TRIGGER_NAME}`,
742
+ asChild: "except-style",
743
+ scope,
744
+ children: /* @__PURE__ */ jsx(MenuItemImpl, {
745
+ id: subContext.triggerId,
746
+ "aria-haspopup": "menu",
747
+ "aria-expanded": context.open,
748
+ "aria-controls": subContext.contentId,
749
+ "data-state": getOpenState(context.open),
750
+ ...props,
751
+ ref: composeRefs(forwardedRef, subContext.onTriggerChange),
752
+ onPress: (event) => {
753
+ props.onPress?.(event);
754
+ if (props.disabled || event.defaultPrevented) return;
755
+ if (isWeb) {
756
+ event.currentTarget.focus();
757
+ }
758
+ if (!context.open) context.onOpenChange(true);
759
+ },
760
+ onPointerMove: composeEventHandlers(props.onPointerMove, whenMouse((event) => {
761
+ contentContext.onItemEnter(event);
762
+ if (event.defaultPrevented) return;
763
+ if (!props.disabled && !context.open && !openTimerRef.current) {
764
+ contentContext.onPointerGraceIntentChange(null);
765
+ openTimerRef.current = window.setTimeout(() => {
766
+ context.onOpenChange(true);
767
+ clearOpenTimer();
768
+ }, 100);
769
+ }
770
+ })),
771
+ onPointerLeave: composeEventHandlers(props.onPointerLeave, (eventIn) => {
772
+ const event = eventIn;
773
+ clearOpenTimer();
774
+ const contentRect = context.content?.getBoundingClientRect();
775
+ if (contentRect) {
776
+ const contentEl = context.content;
777
+ const sideEl = contentEl?.dataset?.side ? contentEl : contentEl?.querySelector("[data-side]");
778
+ const side = sideEl?.dataset?.side || "right";
779
+ const rightSide = side === "right";
780
+ const bleed = rightSide ? -5 : 5;
781
+ const contentNearEdge = contentRect[rightSide ? "left" : "right"];
782
+ const contentFarEdge = contentRect[rightSide ? "right" : "left"];
783
+ const polygon = {
784
+ area: [
785
+ {
786
+ x: event.clientX + bleed,
787
+ y: event.clientY
788
+ },
789
+ {
790
+ x: contentNearEdge,
791
+ y: contentRect.top
792
+ },
793
+ {
794
+ x: contentFarEdge,
795
+ y: contentRect.top
796
+ },
797
+ {
798
+ x: contentFarEdge,
799
+ y: contentRect.bottom
800
+ },
801
+ {
802
+ x: contentNearEdge,
803
+ y: contentRect.bottom
804
+ }
805
+ ],
806
+ side
807
+ };
808
+ contentContext.onPointerGraceIntentChange(polygon);
809
+ window.clearTimeout(pointerGraceTimerRef.current);
810
+ pointerGraceTimerRef.current = window.setTimeout(() => contentContext.onPointerGraceIntentChange(null), 300);
811
+ } else if (isWeb && subContext.trigger) {
812
+ const triggerEl = subContext.trigger;
813
+ const triggerRect = triggerEl?.getBoundingClientRect();
814
+ if (triggerRect) {
815
+ const placementSide = popperContext.placement?.split("-")[0];
816
+ const side = placementSide === "left" || placementSide === "right" ? placementSide : rootContext.dir === "rtl" ? "left" : "right";
817
+ const rightSide = side === "right";
818
+ const bleed = rightSide ? -5 : 5;
819
+ const nearEdge = rightSide ? triggerRect.right + 4 : triggerRect.left - 4;
820
+ const farEdge = rightSide ? nearEdge + 200 : nearEdge - 200;
821
+ const polygon = {
822
+ area: [
823
+ {
824
+ x: event.clientX + bleed,
825
+ y: event.clientY
826
+ },
827
+ {
828
+ x: nearEdge,
829
+ y: triggerRect.top - 50
830
+ },
831
+ {
832
+ x: farEdge,
833
+ y: triggerRect.top - 50
834
+ },
835
+ {
836
+ x: farEdge,
837
+ y: triggerRect.bottom + 50
838
+ },
839
+ {
840
+ x: nearEdge,
841
+ y: triggerRect.bottom + 50
842
+ }
843
+ ],
844
+ side
845
+ };
846
+ contentContext.onPointerGraceIntentChange(polygon);
847
+ window.clearTimeout(pointerGraceTimerRef.current);
848
+ pointerGraceTimerRef.current = window.setTimeout(() => contentContext.onPointerGraceIntentChange(null), 300);
849
+ }
850
+ } else {
851
+ contentContext.onTriggerLeave(event);
852
+ if (event.defaultPrevented) return;
853
+ contentContext.onPointerGraceIntentChange(null);
854
+ }
855
+ }),
856
+ ...isWeb ? { onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
857
+ const isTypingAhead = contentContext.searchRef.current !== "";
858
+ if (props.disabled || isTypingAhead && event.key === " ") return;
859
+ const willOpen = SUB_OPEN_KEYS[effectiveDir].includes(event.key);
860
+ if (willOpen) {
861
+ if (context.open && context.content) {
862
+ const contentEl = context.content;
863
+ const firstItem = contentEl.querySelector?.("[role=\"menuitem\"]:not([data-disabled])");
864
+ if (firstItem) {
865
+ firstItem.focus({ focusVisible: true });
866
+ event.preventDefault();
867
+ return;
868
+ }
869
+ }
870
+ const triggerEl = event.currentTarget;
871
+ popperContext.refs?.setReference(triggerEl);
872
+ context.onOpenChange(true);
873
+ requestAnimationFrame(() => {
874
+ popperContext.update?.();
875
+ });
876
+ context.content?.focus({ focusVisible: true });
877
+ event.preventDefault();
878
+ }
879
+ }) } : null
880
+ })
881
+ });
882
+ });
883
+ MenuSubTrigger.displayName = SUB_TRIGGER_NAME;
884
+ const SUB_CONTENT_NAME = "MenuSubContent";
885
+ const MenuSubContentFrame = styled(PopperPrimitive.PopperContentFrame, { displayName: SUB_CONTENT_NAME });
886
+ const MenuSubContent = createStyledHOC(MenuSubContentFrame, (props, forwardedRef) => {
887
+ const scope = props.scope || MENU_CONTEXT;
888
+ const portalContext = usePortalContext(scope);
889
+ const { forceMount = portalContext.forceMount, ...subContentProps } = props;
890
+ const context = useMenuContext(scope);
891
+ const rootContext = useMenuRootContext(scope);
892
+ const subContext = useMenuSubContext(scope);
893
+ const popperContext = PopperPrimitive.usePopperContext(scope);
894
+ const ref = React.useRef(null);
895
+ const composedRefs = useComposedRefs(forwardedRef, ref);
896
+ const placementSide = popperContext.placement?.split("-")[0];
897
+ const dataSide = placementSide === "left" || placementSide === "right" ? placementSide : rootContext.dir === "rtl" ? "left" : "right";
898
+ const effectiveDir = rootContext.dir;
899
+ return /* @__PURE__ */ jsx(Collection.Provider, {
900
+ scope,
901
+ children: /* @__PURE__ */ jsx(Collection.Slot, {
902
+ scope,
903
+ children: /* @__PURE__ */ jsx(MenuContentImpl, {
904
+ id: subContext.contentId,
905
+ "aria-labelledby": subContext.triggerId,
906
+ ...subContentProps,
907
+ ref: composedRefs,
908
+ "data-side": dataSide,
909
+ disableOutsidePointerEvents: false,
910
+ disableOutsideScroll: false,
911
+ trapFocus: false,
912
+ onOpenAutoFocus: (event) => {
913
+ if (rootContext.isUsingKeyboardRef.current) {
914
+ const root = ref.current;
915
+ const content = root?.querySelector?.("[data-tamagui-menu-content]");
916
+ (content || root)?.focus({ preventScroll: true });
917
+ }
918
+ event.cancel();
919
+ },
920
+ onCloseAutoFocus: (event) => event.cancel(),
921
+ onFocusOutside: composeEventHandlers(props.onFocusOutside, (event) => {
922
+ if (event.event?.target !== subContext.trigger) context.onOpenChange(false);
923
+ }),
924
+ onEscapeKeyDown: composeEventHandlers(props.onEscapeKeyDown, (event) => {
925
+ context.onOpenChange(false);
926
+ subContext.trigger?.focus({ focusVisible: true });
927
+ event.cancel();
928
+ event.event?.preventDefault?.();
929
+ }),
930
+ ...isWeb ? { onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
931
+ const isKeyDownInside = event.currentTarget.contains(event.target);
932
+ const isCloseKey = SUB_CLOSE_KEYS[effectiveDir].includes(event.key);
933
+ if (isKeyDownInside && isCloseKey) {
934
+ context.onOpenChange(false);
935
+ subContext.trigger?.focus({ focusVisible: true });
936
+ event.preventDefault();
937
+ }
938
+ }) } : null
939
+ })
940
+ })
941
+ });
942
+ });
943
+ MenuSubContent.displayName = SUB_CONTENT_NAME;
944
+ const Anchor = MenuAnchor;
945
+ const Portal$1 = MenuPortal;
946
+ const Content = MenuContent;
947
+ const Group = createStyledHOC(View, (props, ref) => {
948
+ return /* @__PURE__ */ jsx(View, {
949
+ role: "group",
950
+ ...props,
951
+ ref
952
+ });
953
+ });
954
+ Group.displayName = "MenuGroup";
955
+ const Label = createStyledHOC(Text, (props, ref) => {
956
+ return /* @__PURE__ */ jsx(Text, {
957
+ ...props,
958
+ ref
959
+ });
960
+ });
961
+ Label.displayName = "MenuLabel";
962
+ const Item = MenuItem;
963
+ const CheckboxItem = MenuCheckboxItem;
964
+ const RadioGroup = MenuRadioGroup;
965
+ const RadioItem = MenuRadioItem;
966
+ const ItemIndicator = MenuItemIndicator;
967
+ const Separator = createStyledHOC(View, (props, ref) => {
968
+ return /* @__PURE__ */ jsx(View, {
969
+ role: "separator",
970
+ "aria-orientation": "horizontal",
971
+ ...props,
972
+ ref
973
+ });
974
+ });
975
+ Separator.displayName = "MenuSeparator";
976
+ const Arrow = MenuArrow;
977
+ const Sub = MenuSub;
978
+ const SubTrigger = MenuSubTrigger;
979
+ const SubContent = MenuSubContent;
980
+ const ItemTitle = MenuItemTitle;
981
+ const ItemSubtitle = MenuItemSubTitle;
982
+ const ItemImage = MenuItemImage;
983
+ const ItemIcon = MenuItemIcon;
984
+ const Menu = withStaticProperties(MenuComp, {
985
+ Anchor,
986
+ Portal: Portal$1,
987
+ Content,
988
+ Group,
989
+ Label,
990
+ Item,
991
+ CheckboxItem,
992
+ RadioGroup,
993
+ RadioItem,
994
+ ItemIndicator,
995
+ Separator,
996
+ Arrow,
997
+ Sub,
998
+ SubTrigger,
999
+ SubContent,
1000
+ ItemTitle,
1001
+ ItemSubtitle,
1002
+ ItemImage,
1003
+ ItemIcon
1004
+ });
1005
+ return { Menu };
1208
1006
  }
1209
1007
  function getOpenState(open) {
1210
- return open ? "open" : "closed";
1008
+ return open ? "open" : "closed";
1211
1009
  }
1212
1010
  function isIndeterminate(checked) {
1213
- return checked === "indeterminate";
1011
+ return checked === "indeterminate";
1214
1012
  }
1215
1013
  function getCheckedState(checked) {
1216
- return isIndeterminate(checked) ? "indeterminate" : checked ? "checked" : "unchecked";
1014
+ return isIndeterminate(checked) ? "indeterminate" : checked ? "checked" : "unchecked";
1217
1015
  }
1218
1016
  function focusFirst(candidates, options) {
1219
- const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
1220
- for (const candidate of candidates) {
1221
- if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;
1222
- candidate.focus({
1223
- preventScroll: true,
1224
- focusVisible: options?.focusVisible
1225
- });
1226
- if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;
1227
- }
1017
+ const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
1018
+ for (const candidate of candidates) {
1019
+ if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;
1020
+ candidate.focus({
1021
+ preventScroll: true,
1022
+ focusVisible: options?.focusVisible
1023
+ });
1024
+ if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;
1025
+ }
1228
1026
  }
1229
1027
  function wrapArray(array, startIndex) {
1230
- return array.map((_, index) => array[(startIndex + index) % array.length]);
1028
+ return array.map((_, index) => array[(startIndex + index) % array.length]);
1231
1029
  }
1232
1030
  function getNextMatch(values, search, currentMatch) {
1233
- const isRepeated = search.length > 1 && Array.from(search).every(char => char === search[0]);
1234
- const normalizedSearch = isRepeated ? search[0] : search;
1235
- const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
1236
- let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));
1237
- const excludeCurrentMatch = normalizedSearch.length === 1;
1238
- if (excludeCurrentMatch) wrappedValues = wrappedValues.filter(v => v !== currentMatch);
1239
- const nextMatch = wrappedValues.find(value => value.toLowerCase().startsWith(normalizedSearch.toLowerCase()));
1240
- return nextMatch !== currentMatch ? nextMatch : void 0;
1031
+ const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);
1032
+ const normalizedSearch = isRepeated ? search[0] : search;
1033
+ const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
1034
+ let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));
1035
+ const excludeCurrentMatch = normalizedSearch.length === 1;
1036
+ if (excludeCurrentMatch) wrappedValues = wrappedValues.filter((v) => v !== currentMatch);
1037
+ const nextMatch = wrappedValues.find((value) => value.toLowerCase().startsWith(normalizedSearch.toLowerCase()));
1038
+ return nextMatch !== currentMatch ? nextMatch : void 0;
1241
1039
  }
1242
1040
  function isPointInPolygon(point, polygon) {
1243
- const {
1244
- x,
1245
- y
1246
- } = point;
1247
- let inside = false;
1248
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
1249
- const xi = polygon[i].x;
1250
- const yi = polygon[i].y;
1251
- const xj = polygon[j].x;
1252
- const yj = polygon[j].y;
1253
- const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
1254
- if (intersect) inside = !inside;
1255
- }
1256
- return inside;
1041
+ const { x, y } = point;
1042
+ let inside = false;
1043
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
1044
+ const xi = polygon[i].x;
1045
+ const yi = polygon[i].y;
1046
+ const xj = polygon[j].x;
1047
+ const yj = polygon[j].y;
1048
+ const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
1049
+ if (intersect) inside = !inside;
1050
+ }
1051
+ return inside;
1257
1052
  }
1258
1053
  function isPointerInGraceArea(event, area) {
1259
- if (!area) return false;
1260
- const cursorPos = {
1261
- x: event.clientX,
1262
- y: event.clientY
1263
- };
1264
- return isPointInPolygon(cursorPos, area);
1054
+ if (!area) return false;
1055
+ const cursorPos = {
1056
+ x: event.clientX,
1057
+ y: event.clientY
1058
+ };
1059
+ return isPointInPolygon(cursorPos, area);
1265
1060
  }
1061
+
1266
1062
  export { createBaseMenu };
1267
1063
  //# sourceMappingURL=createBaseMenu.mjs.map