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 +1 -1
- package/src/core/provider.tsx +2 -2
- package/src/hooks/useAnchoredMenuState.ts +27 -2
- package/src/hosts/ModalHost.tsx +13 -10
- package/src/hosts/ViewHost.tsx +18 -15
- package/src/utils/measure.ts +26 -14
package/package.json
CHANGED
package/src/core/provider.tsx
CHANGED
|
@@ -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,
|
|
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
|
|
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");
|
package/src/hosts/ModalHost.tsx
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/src/hosts/ViewHost.tsx
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 });
|
package/src/utils/measure.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { InteractionManager, UIManager, findNodeHandle } from "react-native";
|
|
2
|
-
import type { AnchorMeasurement,
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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) =>
|
|
48
|
+
await new Promise<void>((r) => {
|
|
49
|
+
InteractionManager.runAfterInteractions(() => {
|
|
50
|
+
r();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|