react-native-anchored-menu 0.0.14 → 0.0.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-anchored-menu",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Headless anchored context menu / popover for React Native (iOS/Android) with stable measurement (default view host).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,12 @@ function extractMarginsFromChild(children: React.ReactNode): AnchorMargins {
31
31
 
32
32
  export function MenuAnchor({ id, children }: MenuAnchorProps) {
33
33
  const actions = useContext(AnchoredMenuActionsContext);
34
- if (!actions) throw new Error("AnchoredMenuProvider is missing");
34
+ if (!actions) {
35
+ throw new Error(
36
+ "[react-native-anchored-menu] MenuAnchor must be used within an AnchoredMenuProvider. " +
37
+ "Make sure to wrap your component tree with <AnchoredMenuProvider> or <AnchoredMenuLayer>."
38
+ );
39
+ }
35
40
 
36
41
  const ref = useRef<View>(null) as AnchorRefObject;
37
42
 
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
- import { Platform } from "react-native";
2
+ import { AppState, Platform } from "react-native";
3
3
  import {
4
4
  AnchoredMenuActionsContext,
5
5
  AnchoredMenuStateContext,
@@ -25,6 +25,7 @@ import type {
25
25
  * Provider config
26
26
  * - defaultHost: which host to use when `open()` doesn't specify one (default: "view")
27
27
  * - autoHost: automatically mounts the host implementation (default: true)
28
+ * - autoCloseOnBackground: automatically close menus when app goes to background (default: true)
28
29
  */
29
30
  export function AnchoredMenuProvider({
30
31
  children,
@@ -32,6 +33,7 @@ export function AnchoredMenuProvider({
32
33
  host,
33
34
  defaultHost = (host ?? "view") as HostType,
34
35
  autoHost = true,
36
+ autoCloseOnBackground = true,
35
37
  }: AnchoredMenuProviderProps) {
36
38
  const anchorsRef = useRef(new Map<string, any>()); // id -> ref
37
39
  const pendingOpenRafRef = useRef<number | null>(null);
@@ -110,9 +112,33 @@ export function AnchoredMenuProvider({
110
112
  // Register this provider globally so parents can route `open({ id })` to the correct layer.
111
113
  useEffect(() => {
112
114
  const entry = { anchors: anchorsRef.current, setRequest };
113
- return registerProvider(entry);
115
+ const unregister = registerProvider(entry);
116
+ return () => {
117
+ // Close menu if open when provider unmounts
118
+ if (storeRef.current?.getSnapshot().isOpen) {
119
+ setRequest(null);
120
+ }
121
+ unregister();
122
+ };
114
123
  }, [setRequest]);
115
124
 
125
+ // Auto-close menu when app goes to background to avoid weird states
126
+ useEffect(() => {
127
+ if (!autoCloseOnBackground) return;
128
+
129
+ const subscription = AppState.addEventListener("change", (nextAppState: string) => {
130
+ if (nextAppState === "background" || nextAppState === "inactive") {
131
+ if (storeRef.current?.getSnapshot().isOpen) {
132
+ setRequest(null);
133
+ }
134
+ }
135
+ });
136
+
137
+ return () => {
138
+ subscription.remove();
139
+ };
140
+ }, [setRequest, autoCloseOnBackground]);
141
+
116
142
  const open = useCallback((payload: OpenMenuOptions) => {
117
143
  // Defer by default to avoid "open tap" being interpreted as an outside press
118
144
  // when a host mounts a Pressable backdrop during the active gesture.
@@ -138,8 +164,18 @@ export function AnchoredMenuProvider({
138
164
  );
139
165
  }
140
166
  const target = findProviderForAnchorId(anchorId);
141
- if (target && target.setRequest)
167
+ if (target && target.setRequest) {
142
168
  return target.setRequest(payload as MenuRequest);
169
+ }
170
+ // Anchor not found in any provider
171
+ if (__DEV__) {
172
+ // eslint-disable-next-line no-console
173
+ console.warn(
174
+ `[react-native-anchored-menu] Anchor with id="${anchorId}" not found in any provider. ` +
175
+ "Make sure the MenuAnchor is mounted and the id matches."
176
+ );
177
+ }
178
+ return; // Don't open menu if anchor doesn't exist
143
179
  }
144
180
 
145
181
  setRequest(payload as MenuRequest);
@@ -191,4 +227,3 @@ export function AnchoredMenuProvider({
191
227
  </AnchoredMenuActionsContext.Provider>
192
228
  );
193
229
  }
194
-
@@ -3,19 +3,27 @@ import {
3
3
  AnchoredMenuActionsContext,
4
4
  AnchoredMenuStateContext,
5
5
  } from "../core/context";
6
+ import { useAnchoredMenuState } from "./useAnchoredMenuState";
6
7
 
7
8
  export function useAnchoredMenu() {
8
9
  const actions = useContext(AnchoredMenuActionsContext);
9
10
  const state = useContext(AnchoredMenuStateContext);
10
- if (!actions || !state) throw new Error("AnchoredMenuProvider is missing");
11
+ if (!actions || !state) {
12
+ throw new Error(
13
+ "[react-native-anchored-menu] useAnchoredMenu must be used within an AnchoredMenuProvider. " +
14
+ "Make sure to wrap your component tree with <AnchoredMenuProvider> or <AnchoredMenuLayer>."
15
+ );
16
+ }
17
+
18
+ // Use useAnchoredMenuState to properly subscribe to isOpen changes
19
+ const isOpen = useAnchoredMenuState((s) => s.isOpen);
11
20
 
12
21
  return useMemo(
13
22
  () => ({
14
23
  open: actions.open,
15
24
  close: actions.close,
16
- isOpen: state.getSnapshot().isOpen,
25
+ isOpen,
17
26
  }),
18
- [actions.open, actions.close, state]
27
+ [actions.open, actions.close, isOpen]
19
28
  );
20
29
  }
21
-
@@ -11,7 +11,12 @@ export function useAnchoredMenuActions(): {
11
11
  close: () => void;
12
12
  } {
13
13
  const actions = useContext(AnchoredMenuActionsContext);
14
- if (!actions) throw new Error("AnchoredMenuProvider is missing");
14
+ if (!actions) {
15
+ throw new Error(
16
+ "[react-native-anchored-menu] useAnchoredMenuActions must be used within an AnchoredMenuProvider. " +
17
+ "Make sure to wrap your component tree with <AnchoredMenuProvider> or <AnchoredMenuLayer>."
18
+ );
19
+ }
15
20
 
16
21
  return useMemo(
17
22
  () => ({
@@ -1,7 +1,32 @@
1
- import { useContext, useSyncExternalStore } from "react";
1
+ import { useContext, useEffect, useState } from "react";
2
2
  import { AnchoredMenuStateContext } from "../core/context";
3
3
  import type { MenuState } from "../types";
4
4
 
5
+ // Polyfill for useSyncExternalStore for older React versions
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ let useSyncExternalStore: any;
8
+ try {
9
+ // Try to import from react (React 18+)
10
+ const react = require("react");
11
+ if (react.useSyncExternalStore) {
12
+ useSyncExternalStore = react.useSyncExternalStore;
13
+ } else {
14
+ throw new Error("Not available");
15
+ }
16
+ } catch {
17
+ // Fallback for older React versions - use useState + useEffect
18
+ useSyncExternalStore = (subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => any) => {
19
+ const [state, setState] = useState(() => getSnapshot());
20
+ useEffect(() => {
21
+ const unsubscribe = subscribe(() => {
22
+ setState(getSnapshot());
23
+ });
24
+ return unsubscribe;
25
+ }, [subscribe, getSnapshot]);
26
+ return state;
27
+ };
28
+ }
29
+
5
30
  /**
6
31
  * State selector hook.
7
32
  *
@@ -12,7 +37,12 @@ export function useAnchoredMenuState<T = MenuState>(
12
37
  selector: (state: MenuState) => T = ((s: MenuState) => s) as (state: MenuState) => T
13
38
  ): T {
14
39
  const store = useContext(AnchoredMenuStateContext);
15
- if (!store) throw new Error("AnchoredMenuProvider is missing");
40
+ if (!store) {
41
+ throw new Error(
42
+ "[react-native-anchored-menu] useAnchoredMenuState must be used within an AnchoredMenuProvider. " +
43
+ "Make sure to wrap your component tree with <AnchoredMenuProvider> or <AnchoredMenuLayer>."
44
+ );
45
+ }
16
46
 
17
47
  const getSelectedSnapshot = () => selector(store.getSnapshot());
18
48
  return useSyncExternalStore(
@@ -11,6 +11,7 @@ import {
11
11
  measureInWindowStable,
12
12
  } from "../utils/measure";
13
13
  import { computeMenuPosition } from "../utils/position";
14
+ import { isValidMeasurement, isValidMenuSize } from "../utils/validation";
14
15
  import { isFabricEnabled } from "../utils/runtime";
15
16
  import type {
16
17
  AnchorMeasurement,
@@ -27,7 +28,12 @@ interface MeasureCache {
27
28
  export function ModalHost() {
28
29
  const actions = useContext(AnchoredMenuActionsContext);
29
30
  const store = useContext(AnchoredMenuStateContext);
30
- if (!actions || !store) throw new Error("AnchoredMenuProvider is missing");
31
+ if (!actions || !store) {
32
+ throw new Error(
33
+ "[react-native-anchored-menu] ModalHost must be used within an AnchoredMenuProvider. " +
34
+ "This is usually handled automatically by AnchoredMenuProvider when autoHost=true."
35
+ );
36
+ }
31
37
 
32
38
  const activeHost = useAnchoredMenuState((s) => s.activeHost);
33
39
  if (activeHost !== "modal") return null;
@@ -71,7 +77,7 @@ export function ModalHost() {
71
77
  let cancelled = false;
72
78
 
73
79
  async function run() {
74
- if (!req) return;
80
+ if (!req || !actions) return;
75
81
  await new Promise((r) => requestAnimationFrame(r));
76
82
  await new Promise((r) => requestAnimationFrame(r));
77
83
 
@@ -86,11 +92,33 @@ export function ModalHost() {
86
92
 
87
93
  const [a, h] = await Promise.all([
88
94
  measure(refObj, strategy === "stable" ? { tries } : undefined),
89
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
90
97
  ]);
91
98
 
92
99
  if (cancelled) return;
100
+
101
+ // Validate measurements before using them
102
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
103
+ if (__DEV__) {
104
+ console.warn(
105
+ `[react-native-anchored-menu] Invalid measurement for anchor "${req.id}". ` +
106
+ "Menu will not be positioned correctly. This can happen during layout transitions."
107
+ );
108
+ }
109
+ return;
110
+ }
111
+
93
112
  const nextAnchorWin = applyAnchorMargins(a, refObj);
113
+ if (!isValidMeasurement(nextAnchorWin)) {
114
+ if (__DEV__) {
115
+ console.warn(
116
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins for "${req.id}".`
117
+ );
118
+ }
119
+ return;
120
+ }
121
+
94
122
  setAnchorWin(nextAnchorWin);
95
123
  setHostWin(h);
96
124
  measureCacheRef.current.set(req.id, {
@@ -117,7 +145,7 @@ export function ModalHost() {
117
145
  clearTimeout(remeasureTimeoutRef.current);
118
146
  }
119
147
  remeasureTimeoutRef.current = setTimeout(async () => {
120
- if (!req || !hostRef.current) return;
148
+ if (!req || !hostRef.current || !actions) return;
121
149
  const refObj = actions.anchors.get(req.id);
122
150
  if (!refObj) return;
123
151
 
@@ -129,10 +157,30 @@ export function ModalHost() {
129
157
 
130
158
  const [a, h] = await Promise.all([
131
159
  measure(refObj, strategy === "stable" ? { tries } : undefined),
132
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
133
162
  ]);
134
163
 
164
+ // Validate measurements before using them
165
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
166
+ if (__DEV__) {
167
+ console.warn(
168
+ `[react-native-anchored-menu] Invalid measurement during keyboard show for anchor "${req.id}".`
169
+ );
170
+ }
171
+ return;
172
+ }
173
+
135
174
  const nextAnchorWin = applyAnchorMargins(a, refObj);
175
+ if (!isValidMeasurement(nextAnchorWin)) {
176
+ if (__DEV__) {
177
+ console.warn(
178
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins during keyboard show for "${req.id}".`
179
+ );
180
+ }
181
+ return;
182
+ }
183
+
136
184
  setAnchorWin(nextAnchorWin);
137
185
  setHostWin(h);
138
186
  measureCacheRef.current.set(req.id, {
@@ -150,7 +198,7 @@ export function ModalHost() {
150
198
  clearTimeout(remeasureTimeoutRef.current);
151
199
  }
152
200
  remeasureTimeoutRef.current = setTimeout(async () => {
153
- if (!req || !hostRef.current) return;
201
+ if (!req || !hostRef.current || !actions) return;
154
202
  const refObj = actions.anchors.get(req.id);
155
203
  if (!refObj) return;
156
204
 
@@ -162,10 +210,30 @@ export function ModalHost() {
162
210
 
163
211
  const [a, h] = await Promise.all([
164
212
  measure(refObj, strategy === "stable" ? { tries } : undefined),
165
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
166
215
  ]);
167
216
 
217
+ // Validate measurements before using them
218
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
219
+ if (__DEV__) {
220
+ console.warn(
221
+ `[react-native-anchored-menu] Invalid measurement during keyboard hide for anchor "${req.id}".`
222
+ );
223
+ }
224
+ return;
225
+ }
226
+
168
227
  const nextAnchorWin = applyAnchorMargins(a, refObj);
228
+ if (!isValidMeasurement(nextAnchorWin)) {
229
+ if (__DEV__) {
230
+ console.warn(
231
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins during keyboard hide for "${req.id}".`
232
+ );
233
+ }
234
+ return;
235
+ }
236
+
169
237
  setAnchorWin(nextAnchorWin);
170
238
  setHostWin(h);
171
239
  measureCacheRef.current.set(req.id, {
@@ -197,6 +265,17 @@ export function ModalHost() {
197
265
 
198
266
  const position = useMemo(() => {
199
267
  if (!req || !anchorInHost) return null;
268
+
269
+ // Validate anchor measurement before computing position
270
+ if (!isValidMeasurement(anchorInHost)) {
271
+ if (__DEV__) {
272
+ console.warn(
273
+ `[react-native-anchored-menu] Cannot compute position for anchor "${req.id}": invalid measurement.`
274
+ );
275
+ }
276
+ return null;
277
+ }
278
+
200
279
  const viewport: Viewport | undefined =
201
280
  hostSize.width && hostSize.height
202
281
  ? {
@@ -206,7 +285,7 @@ export function ModalHost() {
206
285
  : undefined;
207
286
  return computeMenuPosition({
208
287
  anchor: anchorInHost,
209
- menuSize,
288
+ menuSize: isValidMenuSize(menuSize) ? menuSize : null,
210
289
  viewport,
211
290
  placement: req.placement ?? "auto",
212
291
  offset: req.offset ?? 8,
@@ -216,7 +295,7 @@ export function ModalHost() {
216
295
  });
217
296
  }, [req, anchorInHost, menuSize, hostSize, keyboardHeight]);
218
297
 
219
- const needsInitialMeasure = menuSize.width === 0 || menuSize.height === 0;
298
+ const needsInitialMeasure = !isValidMenuSize(menuSize);
220
299
 
221
300
  const statusBarTranslucent =
222
301
  req?.statusBarTranslucent ?? (Platform.OS === "android" ? false : true);
@@ -11,6 +11,7 @@ import {
11
11
  measureInWindowStable,
12
12
  } from "../utils/measure";
13
13
  import { computeMenuPosition } from "../utils/position";
14
+ import { isValidMeasurement, isValidMenuSize } from "../utils/validation";
14
15
  import type {
15
16
  AnchorMeasurement,
16
17
  MenuSize,
@@ -35,7 +36,12 @@ interface MeasureCache {
35
36
  export function ViewHost() {
36
37
  const actions = useContext(AnchoredMenuActionsContext);
37
38
  const store = useContext(AnchoredMenuStateContext);
38
- if (!actions || !store) throw new Error("AnchoredMenuProvider is missing");
39
+ if (!actions || !store) {
40
+ throw new Error(
41
+ "[react-native-anchored-menu] ViewHost must be used within an AnchoredMenuProvider. " +
42
+ "This is usually handled automatically by AnchoredMenuProvider when autoHost=true."
43
+ );
44
+ }
39
45
 
40
46
  const activeHost = useAnchoredMenuState((s) => s.activeHost);
41
47
  if (activeHost !== "view") return null;
@@ -77,7 +83,7 @@ export function ViewHost() {
77
83
  let cancelled = false;
78
84
 
79
85
  async function run() {
80
- if (!req || !hostRef.current) return;
86
+ if (!req || !hostRef.current || !actions) return;
81
87
  await new Promise((r) => requestAnimationFrame(r));
82
88
 
83
89
  const refObj = actions.anchors.get(req.id); // ref object
@@ -91,11 +97,33 @@ export function ViewHost() {
91
97
 
92
98
  const [a, h] = await Promise.all([
93
99
  measure(refObj, strategy === "stable" ? { tries } : undefined),
94
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
95
102
  ]);
96
103
 
97
104
  if (cancelled) return;
105
+
106
+ // Validate measurements before using them
107
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
108
+ if (__DEV__) {
109
+ console.warn(
110
+ `[react-native-anchored-menu] Invalid measurement for anchor "${req.id}". ` +
111
+ "Menu will not be positioned correctly. This can happen during layout transitions."
112
+ );
113
+ }
114
+ return;
115
+ }
116
+
98
117
  const nextAnchorWin = applyAnchorMargins(a, refObj);
118
+ if (!isValidMeasurement(nextAnchorWin)) {
119
+ if (__DEV__) {
120
+ console.warn(
121
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins for "${req.id}".`
122
+ );
123
+ }
124
+ return;
125
+ }
126
+
99
127
  setAnchorWin(nextAnchorWin);
100
128
  setHostWin(h);
101
129
  measureCacheRef.current.set(req.id, {
@@ -122,7 +150,7 @@ export function ViewHost() {
122
150
  clearTimeout(remeasureTimeoutRef.current);
123
151
  }
124
152
  remeasureTimeoutRef.current = setTimeout(async () => {
125
- if (!req || !hostRef.current) return;
153
+ if (!req || !hostRef.current || !actions) return;
126
154
  const refObj = actions.anchors.get(req.id);
127
155
  if (!refObj) return;
128
156
 
@@ -134,10 +162,30 @@ export function ViewHost() {
134
162
 
135
163
  const [a, h] = await Promise.all([
136
164
  measure(refObj, strategy === "stable" ? { tries } : undefined),
137
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
138
167
  ]);
139
168
 
169
+ // Validate measurements before using them
170
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
171
+ if (__DEV__) {
172
+ console.warn(
173
+ `[react-native-anchored-menu] Invalid measurement during keyboard show for anchor "${req.id}".`
174
+ );
175
+ }
176
+ return;
177
+ }
178
+
140
179
  const nextAnchorWin = applyAnchorMargins(a, refObj);
180
+ if (!isValidMeasurement(nextAnchorWin)) {
181
+ if (__DEV__) {
182
+ console.warn(
183
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins during keyboard show for "${req.id}".`
184
+ );
185
+ }
186
+ return;
187
+ }
188
+
141
189
  setAnchorWin(nextAnchorWin);
142
190
  setHostWin(h);
143
191
  measureCacheRef.current.set(req.id, {
@@ -155,7 +203,7 @@ export function ViewHost() {
155
203
  clearTimeout(remeasureTimeoutRef.current);
156
204
  }
157
205
  remeasureTimeoutRef.current = setTimeout(async () => {
158
- if (!req || !hostRef.current) return;
206
+ if (!req || !hostRef.current || !actions) return;
159
207
  const refObj = actions.anchors.get(req.id);
160
208
  if (!refObj) return;
161
209
 
@@ -167,10 +215,30 @@ export function ViewHost() {
167
215
 
168
216
  const [a, h] = await Promise.all([
169
217
  measure(refObj, strategy === "stable" ? { tries } : undefined),
170
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
218
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
171
220
  ]);
172
221
 
222
+ // Validate measurements before using them
223
+ if (!isValidMeasurement(a) || !isValidMeasurement(h)) {
224
+ if (__DEV__) {
225
+ console.warn(
226
+ `[react-native-anchored-menu] Invalid measurement during keyboard hide for anchor "${req.id}".`
227
+ );
228
+ }
229
+ return;
230
+ }
231
+
173
232
  const nextAnchorWin = applyAnchorMargins(a, refObj);
233
+ if (!isValidMeasurement(nextAnchorWin)) {
234
+ if (__DEV__) {
235
+ console.warn(
236
+ `[react-native-anchored-menu] Invalid anchor measurement after applying margins during keyboard hide for "${req.id}".`
237
+ );
238
+ }
239
+ return;
240
+ }
241
+
174
242
  setAnchorWin(nextAnchorWin);
175
243
  setHostWin(h);
176
244
  measureCacheRef.current.set(req.id, {
@@ -201,6 +269,17 @@ export function ViewHost() {
201
269
 
202
270
  const position = useMemo(() => {
203
271
  if (!req || !anchorInHost) return null;
272
+
273
+ // Validate anchor measurement before computing position
274
+ if (!isValidMeasurement(anchorInHost)) {
275
+ if (__DEV__) {
276
+ console.warn(
277
+ `[react-native-anchored-menu] Cannot compute position for anchor "${req.id}": invalid measurement.`
278
+ );
279
+ }
280
+ return null;
281
+ }
282
+
204
283
  const viewport: Viewport | undefined =
205
284
  hostSize.width && hostSize.height
206
285
  ? {
@@ -211,7 +290,7 @@ export function ViewHost() {
211
290
 
212
291
  return computeMenuPosition({
213
292
  anchor: anchorInHost,
214
- menuSize,
293
+ menuSize: isValidMenuSize(menuSize) ? menuSize : null,
215
294
  viewport,
216
295
  placement: req.placement ?? "auto",
217
296
  offset: req.offset ?? 8,
@@ -221,7 +300,7 @@ export function ViewHost() {
221
300
  });
222
301
  }, [req, anchorInHost, menuSize, hostSize, keyboardHeight]);
223
302
 
224
- const needsInitialMeasure = menuSize.width === 0 || menuSize.height === 0;
303
+ const needsInitialMeasure = !isValidMenuSize(menuSize);
225
304
  if (!visible) return null;
226
305
 
227
306
  return (
package/src/types.ts CHANGED
@@ -152,6 +152,8 @@ export interface AnchoredMenuProviderProps {
152
152
  children: ReactNode;
153
153
  defaultHost?: HostType;
154
154
  autoHost?: boolean;
155
+ /** Whether to automatically close menus when app goes to background/inactive (default: true) */
156
+ autoCloseOnBackground?: boolean;
155
157
  /** @deprecated Use defaultHost instead */
156
158
  host?: HostType;
157
159
  }
@@ -4,11 +4,14 @@ import type { AnchorMeasurement, AnchorRefObject } from "../types";
4
4
  const raf = (): Promise<number> => new Promise((r) => requestAnimationFrame(r));
5
5
 
6
6
  async function measureInWindowOnce(
7
- target: { current: number | null } | number | null
7
+ target: { current: number | null } | number | null | any
8
8
  ): Promise<AnchorMeasurement | null> {
9
- const node = findNodeHandle(
10
- (target as { current?: number | null })?.current ?? target
11
- );
9
+ // Handle both ref objects and direct node handles
10
+ const nodeHandle = typeof target === "number"
11
+ ? target
12
+ : target?.current ?? target;
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const node = findNodeHandle(nodeHandle as any);
12
15
  if (!node) return null;
13
16
  return await new Promise<AnchorMeasurement>((resolve) => {
14
17
  // UIManager.measureInWindow callback signature varies by RN version
@@ -24,7 +27,7 @@ async function measureInWindowOnce(
24
27
  * Useful for very simple layouts where Android/FlatList flakiness isn't a concern.
25
28
  */
26
29
  export async function measureInWindowFast(
27
- target: { current: number | null } | number | null
30
+ target: { current: number | null } | number | null | any
28
31
  ): Promise<AnchorMeasurement | null> {
29
32
  await raf();
30
33
  return await measureInWindowOnce(target);
@@ -39,14 +42,21 @@ export interface MeasureOptions {
39
42
  * and retries until values stabilize.
40
43
  */
41
44
  export async function measureInWindowStable(
42
- target: { current: number | null } | number | null,
45
+ target: { current: number | null } | number | null | any,
43
46
  { tries = 8 }: MeasureOptions = {}
44
47
  ): Promise<AnchorMeasurement | null> {
45
- await new Promise((r) => InteractionManager.runAfterInteractions(r));
48
+ await new Promise<void>((r) => {
49
+ InteractionManager.runAfterInteractions(() => {
50
+ r();
51
+ });
52
+ });
46
53
 
47
- const node = findNodeHandle(
48
- (target as { current?: number | null })?.current ?? target
49
- );
54
+ // Handle both ref objects and direct node handles
55
+ const nodeHandle = typeof target === "number"
56
+ ? target
57
+ : target?.current ?? target;
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const node = findNodeHandle(nodeHandle as any);
50
60
  if (!node) return null;
51
61
 
52
62
  let last: AnchorMeasurement | null = null;
@@ -77,7 +87,7 @@ export async function measureInWindowStable(
77
87
  * Uses measureInWindow so coords match overlay/Modal window coordinates.
78
88
  */
79
89
  export async function measureAnchorInWindow(
80
- ref: { current: number | null } | number | null
90
+ ref: { current: number | null } | number | null | any
81
91
  ): Promise<AnchorMeasurement | null> {
82
92
  return await measureInWindowStable(ref, { tries: 8 });
83
93
  }