react-native-anchored-menu 0.0.13 → 0.0.15

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.13",
3
+ "version": "0.0.15",
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",
@@ -49,11 +49,11 @@ export function AnchoredMenuProvider({
49
49
  };
50
50
  storeRef.current = {
51
51
  getSnapshot: () => snapshot,
52
- subscribe: (listener) => {
52
+ subscribe: (listener: () => void) => {
53
53
  listeners.add(listener);
54
54
  return () => listeners.delete(listener);
55
55
  },
56
- _setSnapshot: (next) => {
56
+ _setSnapshot: (next: MenuState) => {
57
57
  snapshot = next;
58
58
  listeners.forEach((l) => l());
59
59
  },
@@ -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
  *
@@ -9,7 +34,7 @@ import type { MenuState } from "../types";
9
34
  * const isOpen = useAnchoredMenuState(s => s.isOpen)
10
35
  */
11
36
  export function useAnchoredMenuState<T = MenuState>(
12
- selector: (state: MenuState) => T = ((s) => s) as any
37
+ selector: (state: MenuState) => T = ((s: MenuState) => s) as (state: MenuState) => T
13
38
  ): T {
14
39
  const store = useContext(AnchoredMenuStateContext);
15
40
  if (!store) throw new Error("AnchoredMenuProvider is missing");
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
2
- import { Keyboard, Modal, Platform, Pressable, View } from "react-native";
2
+ import { Keyboard, Modal, Platform, Pressable, View, LayoutChangeEvent } from "react-native";
3
3
  import {
4
4
  AnchoredMenuActionsContext,
5
5
  AnchoredMenuStateContext,
@@ -44,7 +44,7 @@ export function ModalHost() {
44
44
  const [menuSize, setMenuSize] = useState<MenuSize>({ width: 0, height: 0 });
45
45
  const [keyboardHeight, setKeyboardHeight] = useState(0);
46
46
  const measureCacheRef = useRef(new Map<string, MeasureCache>());
47
- const remeasureTimeoutRef = useRef<NodeJS.Timeout | null>(null);
47
+ const remeasureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
48
48
 
49
49
  useEffect(() => {
50
50
  if (!req) {
@@ -71,7 +71,7 @@ export function ModalHost() {
71
71
  let cancelled = false;
72
72
 
73
73
  async function run() {
74
- if (!req) return;
74
+ if (!req || !actions) return;
75
75
  await new Promise((r) => requestAnimationFrame(r));
76
76
  await new Promise((r) => requestAnimationFrame(r));
77
77
 
@@ -86,7 +86,8 @@ export function ModalHost() {
86
86
 
87
87
  const [a, h] = await Promise.all([
88
88
  measure(refObj, strategy === "stable" ? { tries } : undefined),
89
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
90
91
  ]);
91
92
 
92
93
  if (cancelled) return;
@@ -117,7 +118,7 @@ export function ModalHost() {
117
118
  clearTimeout(remeasureTimeoutRef.current);
118
119
  }
119
120
  remeasureTimeoutRef.current = setTimeout(async () => {
120
- if (!req || !hostRef.current) return;
121
+ if (!req || !hostRef.current || !actions) return;
121
122
  const refObj = actions.anchors.get(req.id);
122
123
  if (!refObj) return;
123
124
 
@@ -129,7 +130,8 @@ export function ModalHost() {
129
130
 
130
131
  const [a, h] = await Promise.all([
131
132
  measure(refObj, strategy === "stable" ? { tries } : undefined),
132
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
133
135
  ]);
134
136
 
135
137
  const nextAnchorWin = applyAnchorMargins(a, refObj);
@@ -150,7 +152,7 @@ export function ModalHost() {
150
152
  clearTimeout(remeasureTimeoutRef.current);
151
153
  }
152
154
  remeasureTimeoutRef.current = setTimeout(async () => {
153
- if (!req || !hostRef.current) return;
155
+ if (!req || !hostRef.current || !actions) return;
154
156
  const refObj = actions.anchors.get(req.id);
155
157
  if (!refObj) return;
156
158
 
@@ -162,7 +164,8 @@ export function ModalHost() {
162
164
 
163
165
  const [a, h] = await Promise.all([
164
166
  measure(refObj, strategy === "stable" ? { tries } : undefined),
165
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
166
169
  ]);
167
170
 
168
171
  const nextAnchorWin = applyAnchorMargins(a, refObj);
@@ -235,7 +238,7 @@ export function ModalHost() {
235
238
  ref={hostRef}
236
239
  collapsable={false}
237
240
  style={{ flex: 1 }}
238
- onLayout={(e) => {
241
+ onLayout={(e: LayoutChangeEvent) => {
239
242
  const { width, height } = e.nativeEvent.layout;
240
243
  if (width !== hostSize.width || height !== hostSize.height) {
241
244
  setHostSize({ width, height });
@@ -255,7 +258,7 @@ export function ModalHost() {
255
258
  }}
256
259
  pointerEvents={needsInitialMeasure ? "none" : "auto"}
257
260
  onStartShouldSetResponder={() => true}
258
- onLayout={(e) => {
261
+ onLayout={(e: LayoutChangeEvent) => {
259
262
  const { width, height } = e.nativeEvent.layout;
260
263
  if (width !== menuSize.width || height !== menuSize.height) {
261
264
  setMenuSize({ width, height });
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
2
- import { Keyboard, Pressable, View } from "react-native";
2
+ import { Keyboard, Pressable, View, LayoutChangeEvent } from "react-native";
3
3
  import {
4
4
  AnchoredMenuActionsContext,
5
5
  AnchoredMenuStateContext,
@@ -51,7 +51,7 @@ export function ViewHost() {
51
51
  const [menuSize, setMenuSize] = useState<MenuSize>({ width: 0, height: 0 });
52
52
  const [keyboardHeight, setKeyboardHeight] = useState(0);
53
53
  const measureCacheRef = useRef(new Map<string, MeasureCache>());
54
- const remeasureTimeoutRef = useRef<NodeJS.Timeout | null>(null);
54
+ const remeasureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
55
55
 
56
56
  useEffect(() => {
57
57
  if (!req) {
@@ -77,7 +77,7 @@ export function ViewHost() {
77
77
  let cancelled = false;
78
78
 
79
79
  async function run() {
80
- if (!req || !hostRef.current) return;
80
+ if (!req || !hostRef.current || !actions) return;
81
81
  await new Promise((r) => requestAnimationFrame(r));
82
82
 
83
83
  const refObj = actions.anchors.get(req.id); // ref object
@@ -91,7 +91,8 @@ export function ViewHost() {
91
91
 
92
92
  const [a, h] = await Promise.all([
93
93
  measure(refObj, strategy === "stable" ? { tries } : undefined),
94
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
95
96
  ]);
96
97
 
97
98
  if (cancelled) return;
@@ -122,7 +123,7 @@ export function ViewHost() {
122
123
  clearTimeout(remeasureTimeoutRef.current);
123
124
  }
124
125
  remeasureTimeoutRef.current = setTimeout(async () => {
125
- if (!req || !hostRef.current) return;
126
+ if (!req || !hostRef.current || !actions) return;
126
127
  const refObj = actions.anchors.get(req.id);
127
128
  if (!refObj) return;
128
129
 
@@ -134,7 +135,8 @@ export function ViewHost() {
134
135
 
135
136
  const [a, h] = await Promise.all([
136
137
  measure(refObj, strategy === "stable" ? { tries } : undefined),
137
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
138
140
  ]);
139
141
 
140
142
  const nextAnchorWin = applyAnchorMargins(a, refObj);
@@ -155,7 +157,7 @@ export function ViewHost() {
155
157
  clearTimeout(remeasureTimeoutRef.current);
156
158
  }
157
159
  remeasureTimeoutRef.current = setTimeout(async () => {
158
- if (!req || !hostRef.current) return;
160
+ if (!req || !hostRef.current || !actions) return;
159
161
  const refObj = actions.anchors.get(req.id);
160
162
  if (!refObj) return;
161
163
 
@@ -167,7 +169,8 @@ export function ViewHost() {
167
169
 
168
170
  const [a, h] = await Promise.all([
169
171
  measure(refObj, strategy === "stable" ? { tries } : undefined),
170
- measure(hostRef, strategy === "stable" ? { tries } : undefined),
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
+ measure(hostRef as any, strategy === "stable" ? { tries } : undefined),
171
174
  ]);
172
175
 
173
176
  const nextAnchorWin = applyAnchorMargins(a, refObj);
@@ -238,12 +241,12 @@ export function ViewHost() {
238
241
  elevation: 9999,
239
242
  }}
240
243
  pointerEvents="box-none"
241
- onLayout={(e) => {
242
- const { width, height } = e.nativeEvent.layout;
243
- if (width !== hostSize.width || height !== hostSize.height) {
244
- setHostSize({ width, height });
245
- }
246
- }}
244
+ onLayout={(e: LayoutChangeEvent) => {
245
+ const { width, height } = e.nativeEvent.layout;
246
+ if (width !== hostSize.width || height !== hostSize.height) {
247
+ setHostSize({ width, height });
248
+ }
249
+ }}
247
250
  >
248
251
  {/* Tap outside to dismiss */}
249
252
  <Pressable style={{ flex: 1 }} onPress={actions.close}>
@@ -260,7 +263,7 @@ export function ViewHost() {
260
263
  }}
261
264
  pointerEvents={needsInitialMeasure ? "none" : "auto"}
262
265
  onStartShouldSetResponder={() => true}
263
- onLayout={(e) => {
266
+ onLayout={(e: LayoutChangeEvent) => {
264
267
  const { width, height } = e.nativeEvent.layout;
265
268
  if (width !== menuSize.width || height !== menuSize.height) {
266
269
  setMenuSize({ width, height });
@@ -1,17 +1,22 @@
1
1
  import { InteractionManager, UIManager, findNodeHandle } from "react-native";
2
- import type { AnchorMeasurement, AnchorMargins, AnchorRefObject } from "../types";
2
+ import type { AnchorMeasurement, AnchorRefObject } from "../types";
3
3
 
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
- return await new Promise((resolve) => {
14
- UIManager.measureInWindow(node, (x, y, width, height) => {
16
+ return await new Promise<AnchorMeasurement>((resolve) => {
17
+ // UIManager.measureInWindow callback signature varies by RN version
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ (UIManager.measureInWindow as any)(node, (x: number, y: number, width: number, height: number) => {
15
20
  resolve({ pageX: x, pageY: y, width, height });
16
21
  });
17
22
  });
@@ -22,7 +27,7 @@ async function measureInWindowOnce(
22
27
  * Useful for very simple layouts where Android/FlatList flakiness isn't a concern.
23
28
  */
24
29
  export async function measureInWindowFast(
25
- target: { current: number | null } | number | null
30
+ target: { current: number | null } | number | null | any
26
31
  ): Promise<AnchorMeasurement | null> {
27
32
  await raf();
28
33
  return await measureInWindowOnce(target);
@@ -37,14 +42,21 @@ export interface MeasureOptions {
37
42
  * and retries until values stabilize.
38
43
  */
39
44
  export async function measureInWindowStable(
40
- target: { current: number | null } | number | null,
45
+ target: { current: number | null } | number | null | any,
41
46
  { tries = 8 }: MeasureOptions = {}
42
47
  ): Promise<AnchorMeasurement | null> {
43
- await new Promise((r) => InteractionManager.runAfterInteractions(r));
48
+ await new Promise<void>((r) => {
49
+ InteractionManager.runAfterInteractions(() => {
50
+ r();
51
+ });
52
+ });
44
53
 
45
- const node = findNodeHandle(
46
- (target as { current?: number | null })?.current ?? target
47
- );
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);
48
60
  if (!node) return null;
49
61
 
50
62
  let last: AnchorMeasurement | null = null;
@@ -75,7 +87,7 @@ export async function measureInWindowStable(
75
87
  * Uses measureInWindow so coords match overlay/Modal window coordinates.
76
88
  */
77
89
  export async function measureAnchorInWindow(
78
- ref: { current: number | null } | number | null
90
+ ref: { current: number | null } | number | null | any
79
91
  ): Promise<AnchorMeasurement | null> {
80
92
  return await measureInWindowStable(ref, { tries: 8 });
81
93
  }