Skip to content

Commit 1383be0

Browse files
committed
feat: uikit: add context menu
1 parent b24c95d commit 1383be0

File tree

13 files changed

+465
-112
lines changed

13 files changed

+465
-112
lines changed

examples/uikit-example/src/__dev__/AppDev.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { View } from 'react-native';
33
import { usePermissions } from 'react-native-chat-uikit';
44

55
// import { GestureHandlerRootView } from 'react-native-gesture-handler';
6-
import { default as Test } from './test_context_menu2';
6+
import { default as Test } from './test_context_menu3';
77

88
// if (
99
// Platform.OS === 'android' &&

examples/uikit-example/src/__dev__/test_context_menu2.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ const ContextMenu = (props: {
3232
);
3333
console.log('test:ContextMenu:', screenWidth, screenHeight);
3434

35-
// 计算组件在屏幕中的位置,返回组件屏幕上下左右的位置
36-
// 如果组件超出屏幕范围,则调整组件位置
35+
// 计算组件的位置。
36+
// 通过按下屏幕坐标位置,根据组件宽高值计算组件在屏幕上下左右的位置
37+
// 由于组件可能由于宽或者高的原因超出屏幕范围,所以需要调整组件的位置
3738
// 如果组件左边距离右边屏幕小于组件宽度,则将左边值设置为undefined,右边值为鼠标点击位置
3839
// 如果组件右边距离左边屏幕小于组件宽度,则将右边值设置为undefined, 左边值为鼠标点击位置
3940
// 如果组件上边距离下边屏幕小于组件高度,则将上边值设置为undefined,下边值为鼠标点击位置
@@ -147,7 +148,7 @@ const ContextMenu = (props: {
147148
);
148149
};
149150

150-
export default function YourComponent() {
151+
export function TestContextMenu1() {
151152
const modalRef = React.useRef<SlideModalRef>({} as any);
152153
const touchRef = React.useRef<TouchableOpacity>(null);
153154
const [touchPosition, setTouchPosition] = useState({ x: 0, y: 0 });
@@ -159,8 +160,9 @@ export default function YourComponent() {
159160
{ label: 'Option 2', onPress: () => console.log('Option 2 selected') },
160161
];
161162

162-
// 通过计算选择组件的上下左右的坐标点
163-
// 组件内部分为4个象限,分别为左上,右上,左下,右下,如果按下坐标在其中一个象限,则选择该象限的坐标
163+
// 计算组件的4个定点的屏幕坐标。
164+
// 通过按下位置的屏幕坐标,获取当前该组件的屏幕坐标(4个定点中的1个)
165+
// 组件内部分为4个象限,分别为左上,右上,左下,右下,如果按下坐标在其中一个象限,则返回该象限的组件的顶点的坐标值
164166
const calculate = (params: {
165167
pressedX: number;
166168
pressedY: number;
@@ -247,6 +249,7 @@ export default function YourComponent() {
247249
console.log('pressedY:', pressedY);
248250
console.log('locationX:', locationX);
249251
console.log('locationY:', locationY);
252+
// 设置当前按下的坐标点
250253
// setTouchPosition({ x: pressedX, y: pressedY }); // 设置当前长按位置坐标
251254

252255
// 获取当前组件的屏幕坐标
@@ -260,7 +263,7 @@ export default function YourComponent() {
260263
console.log('ScreenWidth:', screenWidth);
261264
console.log('ScreenHeight:', screenHeight);
262265

263-
// setTouchPosition({ x: pageX, y: pageY }); // 设置当前组件位置坐标
266+
// 设置当前组件的坐标点
264267
setTouchPosition(
265268
calculate({
266269
pressedX: pressedX,
@@ -317,3 +320,7 @@ const styles = StyleSheet.create({
317320
fontSize: 16,
318321
},
319322
});
323+
324+
export default function TestMain() {
325+
return <TestContextMenu1 />;
326+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Dimensions,
4+
GestureResponderEvent,
5+
StyleSheet,
6+
Text,
7+
TouchableOpacity,
8+
View,
9+
} from 'react-native';
10+
import {
11+
ContextMenu,
12+
SlideModalRef,
13+
useContextMenu,
14+
} from 'react-native-chat-uikit';
15+
16+
export function TestContextMenu1() {
17+
const modalRef = React.useRef<SlideModalRef>({} as any);
18+
const touchRef = React.useRef<TouchableOpacity>(null);
19+
const [touchPosition, setTouchPosition] = useState({ x: 0, y: 0 });
20+
const screenWidth = React.useRef(Dimensions.get('window').width).current;
21+
const screenHeight = React.useRef(Dimensions.get('window').height).current;
22+
const { getComponentVerticesCoordinate } = useContextMenu({
23+
propsRef: modalRef,
24+
position: touchPosition,
25+
});
26+
27+
const options = React.useMemo(
28+
() => [
29+
{ label: 'Option 1', onPress: () => console.log('Option 1 selected') },
30+
{ label: 'Option 2', onPress: () => console.log('Option 2 selected') },
31+
],
32+
[]
33+
);
34+
35+
const handleLongPress = (event: GestureResponderEvent) => {
36+
const {
37+
pageX: pressedX,
38+
pageY: pressedY,
39+
locationX,
40+
locationY,
41+
} = event.nativeEvent;
42+
43+
console.log('pressedX:', pressedX);
44+
console.log('pressedY:', pressedY);
45+
console.log('locationX:', locationX);
46+
console.log('locationY:', locationY);
47+
// 设置当前按下的坐标点
48+
// setTouchPosition({ x: pressedX, y: pressedY }); // 设置当前长按位置坐标
49+
50+
// 获取当前组件的屏幕坐标
51+
touchRef.current?.measure((x, y, width, height, pageX, pageY) => {
52+
console.log('X:', x);
53+
console.log('Y:', y);
54+
console.log('Width:', width);
55+
console.log('Height:', height);
56+
console.log('PageX:', pageX);
57+
console.log('PageY:', pageY);
58+
console.log('ScreenWidth:', screenWidth);
59+
console.log('ScreenHeight:', screenHeight);
60+
61+
// 设置当前组件的坐标点
62+
setTouchPosition(
63+
getComponentVerticesCoordinate({
64+
pressedX: pressedX,
65+
pressedY: pressedY,
66+
componentX: pageX,
67+
componentY: pageY,
68+
componentWidth: width,
69+
componentHeight: height,
70+
})
71+
); // 设置当前组件位置坐标
72+
});
73+
74+
modalRef.current.startShow();
75+
};
76+
77+
return (
78+
<View style={{ top: 100, justifyContent: 'center', alignItems: 'center' }}>
79+
<TouchableOpacity
80+
ref={touchRef}
81+
onLongPress={handleLongPress}
82+
style={{
83+
width: screenWidth - 40,
84+
height: screenHeight - 140,
85+
backgroundColor: 'green',
86+
}}
87+
>
88+
<Text>Long press me</Text>
89+
</TouchableOpacity>
90+
<ContextMenu
91+
propsRef={modalRef}
92+
position={touchPosition}
93+
// containerStyle={styles.menuContainer}
94+
>
95+
<View style={styles.menuContainer}>
96+
{options.map((option, index) => (
97+
<TouchableOpacity
98+
key={index}
99+
style={styles.menuItem}
100+
onPress={option.onPress}
101+
>
102+
<Text style={styles.menuItemText}>{option.label}</Text>
103+
</TouchableOpacity>
104+
))}
105+
</View>
106+
</ContextMenu>
107+
</View>
108+
);
109+
}
110+
111+
const styles = StyleSheet.create({
112+
modalOverlay: {
113+
flex: 1,
114+
justifyContent: 'center',
115+
alignItems: 'center',
116+
backgroundColor: 'rgba(0,0,0,0.5)',
117+
},
118+
menuContainer: {
119+
backgroundColor: 'white',
120+
borderRadius: 5,
121+
padding: 10,
122+
},
123+
menuItem: {
124+
padding: 10,
125+
},
126+
menuItemText: {
127+
fontSize: 16,
128+
},
129+
});
130+
131+
export default function TestMain() {
132+
return <TestContextMenu1 />;
133+
}

packages/react-native-chat-uikit/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export * from './theme';
4343
export * from './types';
4444
export * from './ui/Alert';
4545
export * from './ui/Button';
46+
export * from './ui/ContextMenu';
4647
export * from './ui/FlatList';
4748
export * from './ui/Image';
4849
export * from './ui/ImagePreview';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as React from 'react';
2+
import { Dimensions, LayoutChangeEvent, View } from 'react-native';
3+
4+
import { SlideModal } from '../Modal';
5+
import { ContextMenuProps } from './types';
6+
import { useContextMenu } from './useContextMenu';
7+
8+
export function ContextMenu(props: ContextMenuProps) {
9+
const { position, children, propsRef, containerStyle } = props;
10+
const { calculateComponentPosition } = useContextMenu(props);
11+
const screenWidth = React.useRef(Dimensions.get('window').width).current;
12+
const screenHeight = React.useRef(Dimensions.get('window').height).current;
13+
const [componentHeight, setComponentHeight] = React.useState<
14+
number | undefined
15+
>(undefined);
16+
const [componentWidth, setComponentWidth] = React.useState<
17+
number | undefined
18+
>(undefined);
19+
20+
const calculateResult = React.useMemo(
21+
() =>
22+
calculateComponentPosition({
23+
pressedX: position.x,
24+
pressedY: position.y,
25+
screenWidth: screenWidth,
26+
screenHeight: screenHeight,
27+
componentWidth: componentWidth ?? 0,
28+
componentHeight: componentHeight ?? 0,
29+
}),
30+
[
31+
calculateComponentPosition,
32+
componentHeight,
33+
componentWidth,
34+
position.x,
35+
position.y,
36+
screenHeight,
37+
screenWidth,
38+
]
39+
);
40+
41+
const _onLayout = React.useCallback((event: LayoutChangeEvent) => {
42+
setComponentHeight(event.nativeEvent.layout.height);
43+
setComponentWidth(event.nativeEvent.layout.width);
44+
}, []);
45+
46+
return (
47+
<SlideModal
48+
propsRef={propsRef}
49+
modalAnimationType="fade"
50+
backgroundColor={'rgba(1,1,1, 0.2)'}
51+
backgroundTransparent={false}
52+
onRequestModalClose={() => {
53+
propsRef.current.startHide();
54+
}}
55+
enableSlideComponent={false}
56+
>
57+
<View
58+
style={[
59+
containerStyle,
60+
{
61+
position: 'absolute',
62+
top: position.y,
63+
left: position.x,
64+
bottom: undefined,
65+
right: undefined,
66+
height: componentHeight,
67+
width: componentWidth,
68+
...calculateResult,
69+
},
70+
]}
71+
onLayout={_onLayout}
72+
>
73+
{children}
74+
</View>
75+
</SlideModal>
76+
);
77+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './ContextMenu';
2+
export * from './types';
3+
export * from './useContextMenu';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { PropsWithChildren } from 'react';
2+
import type { StyleProp, ViewStyle } from 'react-native';
3+
4+
import type { SlideModalRef } from '../Modal';
5+
6+
export type ContextMenuProps = PropsWithChildren<{
7+
propsRef: React.RefObject<SlideModalRef>;
8+
position: {
9+
x: number;
10+
y: number;
11+
};
12+
containerStyle?: StyleProp<ViewStyle> | undefined;
13+
}>;

0 commit comments

Comments
 (0)