@seakoi/native-ui 1.2.0 → 1.3.0

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 (178) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commonjs/components/base/index.js +33 -11
  3. package/dist/commonjs/components/base/input/base-input.js +4 -2
  4. package/dist/commonjs/components/base/overflow/all-mode-overflow.js +49 -0
  5. package/dist/commonjs/components/base/overflow/fixed-count-overflow.js +70 -0
  6. package/dist/commonjs/components/base/overflow/index.js +16 -0
  7. package/dist/commonjs/components/base/overflow/overflow.js +72 -0
  8. package/dist/commonjs/components/base/overflow/responsive-overflow.js +280 -0
  9. package/dist/commonjs/components/base/overflow/style/index.js +39 -0
  10. package/dist/commonjs/components/base/overflow/types.js +5 -0
  11. package/dist/commonjs/components/base/picker/picker-content.js +16 -12
  12. package/dist/commonjs/components/base/picker/picker-field.js +9 -7
  13. package/dist/commonjs/components/base/picker/picker.js +6 -1
  14. package/dist/commonjs/components/base/picker/style/index.js +7 -1
  15. package/dist/commonjs/components/base/portal/portal-host.js +5 -3
  16. package/dist/commonjs/components/base/select/hooks/use-select-actions.js +155 -0
  17. package/dist/commonjs/components/base/select/hooks/use-select-options.js +169 -0
  18. package/dist/commonjs/components/base/select/hooks/use-selector.js +104 -0
  19. package/dist/commonjs/components/base/select/index.js +16 -0
  20. package/dist/commonjs/components/base/select/select-multiple-content.js +182 -0
  21. package/dist/commonjs/components/base/select/select-popup.js +233 -0
  22. package/dist/commonjs/components/base/select/select-single-content.js +100 -0
  23. package/dist/commonjs/components/base/select/select-suffix.js +67 -0
  24. package/dist/commonjs/components/base/select/select.js +285 -0
  25. package/dist/commonjs/components/base/select/style/index.js +40 -0
  26. package/dist/commonjs/components/base/select/style/select-multiple-content-styles.js +46 -0
  27. package/dist/commonjs/components/base/select/style/select-popup-styles.js +67 -0
  28. package/dist/commonjs/components/base/select/style/select-single-content-styles.js +28 -0
  29. package/dist/commonjs/components/base/select/style/select-styles.js +46 -0
  30. package/dist/commonjs/components/base/select/style/select-suffix-styles.js +21 -0
  31. package/dist/commonjs/components/base/select/types.js +5 -0
  32. package/dist/commonjs/components/base/tabs/style/index.js +37 -0
  33. package/dist/commonjs/components/base/tabs/tabs.js +90 -45
  34. package/dist/commonjs/native-provider/native-provider.js +5 -5
  35. package/dist/commonjs/shared/utils/index.js +11 -0
  36. package/dist/commonjs/shared/utils/object.js +39 -0
  37. package/dist/module/components/base/index.js +2 -0
  38. package/dist/module/components/base/input/base-input.js +4 -2
  39. package/dist/module/components/base/overflow/all-mode-overflow.js +43 -0
  40. package/dist/module/components/base/overflow/fixed-count-overflow.js +64 -0
  41. package/dist/module/components/base/overflow/index.js +3 -0
  42. package/dist/module/components/base/overflow/overflow.js +66 -0
  43. package/dist/module/components/base/overflow/responsive-overflow.js +274 -0
  44. package/dist/module/components/base/overflow/style/index.js +36 -0
  45. package/dist/module/components/base/overflow/types.js +3 -0
  46. package/dist/module/components/base/picker/picker-content.js +16 -12
  47. package/dist/module/components/base/picker/picker-field.js +9 -7
  48. package/dist/module/components/base/picker/picker.js +6 -1
  49. package/dist/module/components/base/picker/style/index.js +6 -0
  50. package/dist/module/components/base/portal/portal-host.js +4 -3
  51. package/dist/module/components/base/select/hooks/use-select-actions.js +151 -0
  52. package/dist/module/components/base/select/hooks/use-select-options.js +162 -0
  53. package/dist/module/components/base/select/hooks/use-selector.js +100 -0
  54. package/dist/module/components/base/select/index.js +3 -0
  55. package/dist/module/components/base/select/select-multiple-content.js +176 -0
  56. package/dist/module/components/base/select/select-popup.js +227 -0
  57. package/dist/module/components/base/select/select-single-content.js +94 -0
  58. package/dist/module/components/base/select/select-suffix.js +61 -0
  59. package/dist/module/components/base/select/select.js +279 -0
  60. package/dist/module/components/base/select/style/index.js +7 -0
  61. package/dist/module/components/base/select/style/select-multiple-content-styles.js +43 -0
  62. package/dist/module/components/base/select/style/select-popup-styles.js +64 -0
  63. package/dist/module/components/base/select/style/select-single-content-styles.js +25 -0
  64. package/dist/module/components/base/select/style/select-styles.js +43 -0
  65. package/dist/module/components/base/select/style/select-suffix-styles.js +18 -0
  66. package/dist/module/components/base/select/types.js +3 -0
  67. package/dist/module/components/base/tabs/style/index.js +33 -0
  68. package/dist/module/components/base/tabs/tabs.js +92 -47
  69. package/dist/module/native-provider/native-provider.js +5 -5
  70. package/dist/module/shared/utils/index.js +2 -1
  71. package/dist/module/shared/utils/object.js +35 -0
  72. package/dist/typescript/components/base/index.d.ts +2 -0
  73. package/dist/typescript/components/base/index.d.ts.map +1 -1
  74. package/dist/typescript/components/base/input/base-input.d.ts.map +1 -1
  75. package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts +11 -0
  76. package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts.map +1 -0
  77. package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts +11 -0
  78. package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts.map +1 -0
  79. package/dist/typescript/components/base/overflow/index.d.ts +3 -0
  80. package/dist/typescript/components/base/overflow/index.d.ts.map +1 -0
  81. package/dist/typescript/components/base/overflow/overflow.d.ts +13 -0
  82. package/dist/typescript/components/base/overflow/overflow.d.ts.map +1 -0
  83. package/dist/typescript/components/base/overflow/responsive-overflow.d.ts +12 -0
  84. package/dist/typescript/components/base/overflow/responsive-overflow.d.ts.map +1 -0
  85. package/dist/typescript/components/base/overflow/style/index.d.ts +31 -0
  86. package/dist/typescript/components/base/overflow/style/index.d.ts.map +1 -0
  87. package/dist/typescript/components/base/overflow/types.d.ts +77 -0
  88. package/dist/typescript/components/base/overflow/types.d.ts.map +1 -0
  89. package/dist/typescript/components/base/picker/picker-content.d.ts +12 -0
  90. package/dist/typescript/components/base/picker/picker-content.d.ts.map +1 -1
  91. package/dist/typescript/components/base/picker/picker-field.d.ts.map +1 -1
  92. package/dist/typescript/components/base/picker/picker.d.ts +9 -0
  93. package/dist/typescript/components/base/picker/picker.d.ts.map +1 -1
  94. package/dist/typescript/components/base/picker/style/index.d.ts +6 -0
  95. package/dist/typescript/components/base/picker/style/index.d.ts.map +1 -1
  96. package/dist/typescript/components/base/portal/portal-host.d.ts +1 -0
  97. package/dist/typescript/components/base/portal/portal-host.d.ts.map +1 -1
  98. package/dist/typescript/components/base/portal/types.d.ts +2 -0
  99. package/dist/typescript/components/base/portal/types.d.ts.map +1 -1
  100. package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts +144 -0
  101. package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts.map +1 -0
  102. package/dist/typescript/components/base/select/hooks/use-select-options.d.ts +91 -0
  103. package/dist/typescript/components/base/select/hooks/use-select-options.d.ts.map +1 -0
  104. package/dist/typescript/components/base/select/hooks/use-selector.d.ts +90 -0
  105. package/dist/typescript/components/base/select/hooks/use-selector.d.ts.map +1 -0
  106. package/dist/typescript/components/base/select/index.d.ts +3 -0
  107. package/dist/typescript/components/base/select/index.d.ts.map +1 -0
  108. package/dist/typescript/components/base/select/select-multiple-content.d.ts +51 -0
  109. package/dist/typescript/components/base/select/select-multiple-content.d.ts.map +1 -0
  110. package/dist/typescript/components/base/select/select-popup.d.ts +107 -0
  111. package/dist/typescript/components/base/select/select-popup.d.ts.map +1 -0
  112. package/dist/typescript/components/base/select/select-single-content.d.ts +48 -0
  113. package/dist/typescript/components/base/select/select-single-content.d.ts.map +1 -0
  114. package/dist/typescript/components/base/select/select-suffix.d.ts +43 -0
  115. package/dist/typescript/components/base/select/select-suffix.d.ts.map +1 -0
  116. package/dist/typescript/components/base/select/select.d.ts +40 -0
  117. package/dist/typescript/components/base/select/select.d.ts.map +1 -0
  118. package/dist/typescript/components/base/select/style/index.d.ts +6 -0
  119. package/dist/typescript/components/base/select/style/index.d.ts.map +1 -0
  120. package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts +40 -0
  121. package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts.map +1 -0
  122. package/dist/typescript/components/base/select/style/select-popup-styles.d.ts +61 -0
  123. package/dist/typescript/components/base/select/style/select-popup-styles.d.ts.map +1 -0
  124. package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts +22 -0
  125. package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts.map +1 -0
  126. package/dist/typescript/components/base/select/style/select-styles.d.ts +40 -0
  127. package/dist/typescript/components/base/select/style/select-styles.d.ts.map +1 -0
  128. package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts +15 -0
  129. package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts.map +1 -0
  130. package/dist/typescript/components/base/select/types.d.ts +206 -0
  131. package/dist/typescript/components/base/select/types.d.ts.map +1 -0
  132. package/dist/typescript/components/base/tabs/style/index.d.ts +29 -0
  133. package/dist/typescript/components/base/tabs/style/index.d.ts.map +1 -0
  134. package/dist/typescript/components/base/tabs/tabs.d.ts +26 -5
  135. package/dist/typescript/components/base/tabs/tabs.d.ts.map +1 -1
  136. package/dist/typescript/native-provider/native-provider.d.ts +2 -0
  137. package/dist/typescript/native-provider/native-provider.d.ts.map +1 -1
  138. package/dist/typescript/shared/utils/index.d.ts +1 -0
  139. package/dist/typescript/shared/utils/index.d.ts.map +1 -1
  140. package/dist/typescript/shared/utils/object.d.ts +21 -0
  141. package/dist/typescript/shared/utils/object.d.ts.map +1 -0
  142. package/package.json +1 -1
  143. package/src/components/base/index.ts +2 -0
  144. package/src/components/base/input/base-input.tsx +4 -2
  145. package/src/components/base/overflow/all-mode-overflow.tsx +49 -0
  146. package/src/components/base/overflow/fixed-count-overflow.tsx +71 -0
  147. package/src/components/base/overflow/index.ts +2 -0
  148. package/src/components/base/overflow/overflow.tsx +60 -0
  149. package/src/components/base/overflow/responsive-overflow.tsx +349 -0
  150. package/src/components/base/overflow/style/index.ts +32 -0
  151. package/src/components/base/overflow/types.ts +75 -0
  152. package/src/components/base/picker/picker-content.tsx +24 -9
  153. package/src/components/base/picker/picker-field.tsx +19 -13
  154. package/src/components/base/picker/picker.tsx +10 -1
  155. package/src/components/base/picker/style/index.ts +4 -0
  156. package/src/components/base/portal/portal-host.tsx +13 -3
  157. package/src/components/base/portal/types.ts +2 -0
  158. package/src/components/base/select/hooks/use-select-actions.ts +263 -0
  159. package/src/components/base/select/hooks/use-select-options.ts +250 -0
  160. package/src/components/base/select/hooks/use-selector.ts +155 -0
  161. package/src/components/base/select/index.ts +2 -0
  162. package/src/components/base/select/select-multiple-content.tsx +292 -0
  163. package/src/components/base/select/select-popup.tsx +384 -0
  164. package/src/components/base/select/select-single-content.tsx +127 -0
  165. package/src/components/base/select/select-suffix.tsx +100 -0
  166. package/src/components/base/select/select.tsx +302 -0
  167. package/src/components/base/select/style/index.ts +5 -0
  168. package/src/components/base/select/style/select-multiple-content-styles.ts +41 -0
  169. package/src/components/base/select/style/select-popup-styles.ts +62 -0
  170. package/src/components/base/select/style/select-single-content-styles.ts +23 -0
  171. package/src/components/base/select/style/select-styles.ts +41 -0
  172. package/src/components/base/select/style/select-suffix-styles.ts +16 -0
  173. package/src/components/base/select/types.ts +261 -0
  174. package/src/components/base/tabs/style/index.ts +32 -0
  175. package/src/components/base/tabs/tabs.tsx +146 -55
  176. package/src/native-provider/native-provider.tsx +4 -4
  177. package/src/shared/utils/index.ts +1 -0
  178. package/src/shared/utils/object.ts +37 -0
@@ -0,0 +1,384 @@
1
+ import { CheckOutlined } from '@seakoi/svg-icons';
2
+ import { useMemoizedFn } from 'ahooks';
3
+ import React, { useMemo } from 'react';
4
+ import {
5
+ ActivityIndicator,
6
+ Dimensions,
7
+ FlatList,
8
+ type GestureResponderEvent,
9
+ type LayoutRectangle,
10
+ type NativeScrollEvent,
11
+ type NativeSyntheticEvent,
12
+ Pressable,
13
+ type StyleProp,
14
+ type TextStyle,
15
+ View,
16
+ type ViewStyle,
17
+ } from 'react-native';
18
+
19
+ import { Portal, Text } from '#components/base';
20
+ import {
21
+ isSelectFlattenedGroupOption,
22
+ type SelectFlattenedGroupOption,
23
+ type UseSelectOptionsResult,
24
+ } from '#components/base/select/hooks/use-select-options';
25
+ import { useTheme } from '#native-provider';
26
+
27
+ import { useSelectPopupStyles } from './style';
28
+ import type { SelectOption } from './types';
29
+
30
+ /** 下拉菜单默认最大高度 */
31
+ export const DEFAULT_POPUP_MAX_HEIGHT = 256 as const;
32
+
33
+ /**
34
+ * SelectPopup 组件的属性
35
+ *
36
+ * @template ValueType - 选项值的类型
37
+ */
38
+ export interface SelectPopupProps<
39
+ ValueType = unknown,
40
+ OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
41
+ > {
42
+ /**
43
+ * 是否展开
44
+ */
45
+ open: boolean;
46
+ /**
47
+ * 选择器布局信息
48
+ */
49
+ selectorLayout: LayoutRectangle;
50
+ /**
51
+ * 下拉菜单和选择器同宽
52
+ */
53
+ matchWidth?: boolean | number;
54
+ /**
55
+ * 下拉菜单位置,不指定时自动计算
56
+ */
57
+ placement?: 'top' | 'bottom';
58
+ /**
59
+ * 加载中状态
60
+ */
61
+ loading?: boolean;
62
+ /**
63
+ * 下拉列表为空时显示的内容
64
+ */
65
+ notFoundContent?: React.ReactNode;
66
+ /**
67
+ * 自定义选中项图标
68
+ */
69
+ selectedIcon?: React.ReactNode;
70
+ /**
71
+ * 自定义渲染下拉选项
72
+ */
73
+ optionRender?: (
74
+ option: OptionType,
75
+ info: { index: number; selected: boolean },
76
+ ) => React.ReactNode;
77
+ /**
78
+ * 下拉菜单最大高度
79
+ * @default {@link DEFAULT_POPUP_MAX_HEIGHT}
80
+ */
81
+ popupMaxHeight?: number;
82
+ /**
83
+ * 过滤后的选项列表
84
+ */
85
+ filteredOptions: UseSelectOptionsResult<ValueType, OptionType>['filteredOptions'];
86
+ /**
87
+ * 搜索值
88
+ */
89
+ searchValue: string;
90
+ /**
91
+ * 选择模式
92
+ */
93
+ mode?: 'multiple' | 'tags';
94
+ /**
95
+ * 自定义样式
96
+ */
97
+ popupRootStyle?: StyleProp<ViewStyle>;
98
+ /**
99
+ * 弹出菜单列表样式
100
+ */
101
+ popupListStyle?: StyleProp<ViewStyle>;
102
+ /**
103
+ * 弹出菜单条目样式
104
+ */
105
+ popupListItemStyle?: StyleProp<ViewStyle>;
106
+ /**
107
+ * 分组标签样式
108
+ */
109
+ popupGroupLabelStyle?: StyleProp<TextStyle>;
110
+ /**
111
+ * 自定义下拉框内容
112
+ */
113
+ popupRender?: (originNode: React.ReactNode) => React.ReactNode;
114
+ /**
115
+ * 判断选项是否被选中
116
+ */
117
+ isOptionSelected: (option: OptionType) => boolean;
118
+ /**
119
+ * 关闭回调
120
+ */
121
+ onClose: () => void;
122
+ /**
123
+ * 选项点击回调
124
+ */
125
+ onOptionPress: (option: OptionType) => void;
126
+ /**
127
+ * 创建选项回调
128
+ */
129
+ onCreateOption: () => void;
130
+ /**
131
+ * 下拉列表滚动时回调
132
+ */
133
+ onPopupScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
134
+ }
135
+
136
+ export const SelectPopup = <
137
+ ValueType = unknown,
138
+ OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
139
+ >({
140
+ open,
141
+ selectorLayout,
142
+ matchWidth,
143
+ placement: forcedPlacement,
144
+ popupMaxHeight = DEFAULT_POPUP_MAX_HEIGHT,
145
+ loading,
146
+ filteredOptions,
147
+ notFoundContent,
148
+ searchValue,
149
+ mode,
150
+ popupRootStyle,
151
+ popupListStyle,
152
+ popupListItemStyle,
153
+ popupGroupLabelStyle,
154
+ selectedIcon,
155
+ optionRender,
156
+ popupRender,
157
+ isOptionSelected,
158
+ onClose,
159
+ onOptionPress,
160
+ onCreateOption,
161
+ onPopupScroll,
162
+ }: SelectPopupProps<ValueType, OptionType>) => {
163
+ const theme = useTheme();
164
+ const styles = useSelectPopupStyles();
165
+
166
+ /**
167
+ * 计算下拉框位置信息
168
+ *
169
+ * @returns 包含位置信息和计算状态的对象
170
+ */
171
+ const positionInfo = useMemo(() => {
172
+ // 如果未打开或没有选择器布局信息,返回未计算状态
173
+ if (!open || !selectorLayout) {
174
+ return {
175
+ placement: 'bottom' as const,
176
+ isPositionCalculated: false,
177
+ };
178
+ }
179
+
180
+ const screenHeight = Dimensions.get('window').height;
181
+ const spaceBelow = screenHeight - (selectorLayout.y + selectorLayout.height);
182
+ const spaceAbove = selectorLayout.y;
183
+ const spacing = theme.spacing.xxs;
184
+
185
+ // 1. 如果指定了 placement,强制使用指定的位置
186
+ if (forcedPlacement) {
187
+ return {
188
+ placement: forcedPlacement,
189
+ isPositionCalculated: true,
190
+ };
191
+ }
192
+
193
+ // 2. 没有指定 placement,尝试在下方
194
+ if (spaceBelow >= popupMaxHeight + spacing) {
195
+ return {
196
+ placement: 'bottom' as const,
197
+ isPositionCalculated: true,
198
+ };
199
+ }
200
+
201
+ // 3. 下方空间不够,尝试上方
202
+ if (spaceAbove >= popupMaxHeight + spacing) {
203
+ return {
204
+ placement: 'top' as const,
205
+ isPositionCalculated: true,
206
+ };
207
+ }
208
+
209
+ // 4. 上方也不够,直接显示在下方
210
+ return {
211
+ placement: 'bottom' as const,
212
+ isPositionCalculated: true,
213
+ };
214
+ }, [open, selectorLayout, theme.spacing.xxs, forcedPlacement, popupMaxHeight]);
215
+
216
+ const { placement, isPositionCalculated } = positionInfo;
217
+
218
+ /**
219
+ * 下拉框容器样式
220
+ */
221
+ const popupStyle = useMemo<ViewStyle>(() => {
222
+ const style: ViewStyle = {
223
+ left: selectorLayout.x,
224
+ width:
225
+ matchWidth === true
226
+ ? selectorLayout.width
227
+ : typeof matchWidth === 'number'
228
+ ? matchWidth
229
+ : undefined,
230
+ maxHeight: popupMaxHeight,
231
+ };
232
+
233
+ if (placement === 'bottom') {
234
+ style.top = selectorLayout.y + selectorLayout.height + theme.spacing.xxs;
235
+ } else {
236
+ style.bottom =
237
+ Dimensions.get('window').height - selectorLayout.y + theme.spacing.xxs;
238
+ }
239
+
240
+ return style;
241
+ }, [selectorLayout, matchWidth, placement, theme.spacing.xxs, popupMaxHeight]);
242
+
243
+ /**
244
+ * 列表样式
245
+ */
246
+ const listStyle = useMemo<ViewStyle>(
247
+ () => ({
248
+ maxHeight: popupMaxHeight,
249
+ }),
250
+ [popupMaxHeight],
251
+ );
252
+
253
+ // 渲染下拉选项
254
+ const renderOption = ({
255
+ item,
256
+ index,
257
+ }: {
258
+ item: OptionType | SelectFlattenedGroupOption;
259
+ index: number;
260
+ }) => {
261
+ if (isSelectFlattenedGroupOption(item)) {
262
+ return (
263
+ <Text
264
+ color={theme.palette.fontGray3}
265
+ size={12}
266
+ font="semiBold"
267
+ style={[styles.popupGroupLabel, popupGroupLabelStyle]}
268
+ >
269
+ {item.label}
270
+ </Text>
271
+ );
272
+ }
273
+
274
+ const option = item;
275
+ const isSelected = isOptionSelected(option);
276
+
277
+ return (
278
+ <Pressable
279
+ style={[
280
+ styles.popupListItem,
281
+ isSelected && styles.popupListItemSelected,
282
+ option.disabled && styles.popupListItemDisabled,
283
+ popupListItemStyle,
284
+ option.style,
285
+ ]}
286
+ onPress={() => onOptionPress(option)}
287
+ disabled={option.disabled}
288
+ >
289
+ {optionRender ? (
290
+ optionRender(option, { index, selected: isSelected })
291
+ ) : (
292
+ <>
293
+ <Text
294
+ color={theme.palette.fontGray1}
295
+ size={14}
296
+ style={styles.popupListItemText}
297
+ >
298
+ {option.label}
299
+ </Text>
300
+ {isSelected &&
301
+ (selectedIcon || (
302
+ <CheckOutlined color={theme.palette.brand7} size={18} />
303
+ ))}
304
+ </>
305
+ )}
306
+ </Pressable>
307
+ );
308
+ };
309
+
310
+ // 渲染下拉框内容
311
+ const renderPopupContent = () => {
312
+ if (loading) {
313
+ return (
314
+ <View style={styles.loading}>
315
+ <ActivityIndicator color={theme.palette.brand7} />
316
+ </View>
317
+ );
318
+ }
319
+
320
+ if (filteredOptions.length === 0) {
321
+ if (mode === 'tags' && searchValue.trim()) {
322
+ return (
323
+ <Pressable style={styles.popupCreateOption} onPress={onCreateOption}>
324
+ <Text color={theme.palette.brand7} size={14}>
325
+ + 创建 "{searchValue.trim()}"
326
+ </Text>
327
+ </Pressable>
328
+ );
329
+ }
330
+
331
+ return (
332
+ <View style={styles.empty}>
333
+ <Text color={theme.palette.fontGray3} size={14}>
334
+ {notFoundContent}
335
+ </Text>
336
+ </View>
337
+ );
338
+ }
339
+
340
+ return (
341
+ <FlatList
342
+ data={filteredOptions}
343
+ renderItem={renderOption}
344
+ keyExtractor={(item, index) =>
345
+ isSelectFlattenedGroupOption(item)
346
+ ? `group-${item.key}`
347
+ : `option-${item.value}-${index}`
348
+ }
349
+ style={[styles.popupList, listStyle, popupListStyle]}
350
+ onScroll={onPopupScroll}
351
+ showsVerticalScrollIndicator
352
+ removeClippedSubviews
353
+ maxToRenderPerBatch={10}
354
+ windowSize={5}
355
+ initialNumToRender={10}
356
+ />
357
+ );
358
+ };
359
+
360
+ // 使用 useMemoizedFn 包装回调函数
361
+ const handleBackdropPress = useMemoizedFn((e: GestureResponderEvent) => {
362
+ if (e.target === e.currentTarget) {
363
+ onClose();
364
+ return;
365
+ }
366
+ e.stopPropagation();
367
+ });
368
+
369
+ if (!open || !isPositionCalculated) return null;
370
+
371
+ const popupContent = (
372
+ <View style={[styles.popupRoot, popupStyle, popupRootStyle]}>
373
+ {renderPopupContent()}
374
+ </View>
375
+ );
376
+
377
+ return (
378
+ <Portal>
379
+ <Pressable style={styles.backdrop} onPress={handleBackdropPress}>
380
+ {popupRender ? popupRender(popupContent) : popupContent}
381
+ </Pressable>
382
+ </Portal>
383
+ );
384
+ };
@@ -0,0 +1,127 @@
1
+ import React, { useCallback } from 'react';
2
+ import type { StyleProp, TextStyle } from 'react-native';
3
+
4
+ import { Input, type SelectProps, Text } from '#components/base';
5
+ import { useTheme } from '#native-provider';
6
+
7
+ import { useSelectSingleContentStyles } from './style';
8
+ import type { SelectMode, SelectOption } from './types';
9
+
10
+ /**
11
+ * Select 单选内容组件的 Props
12
+ *
13
+ * @template ValueType - 选项值的类型
14
+ */
15
+ export interface SelectSingleContentProps<ValueType> {
16
+ /** 选中的选项 */
17
+ selectedOption?: SelectOption<ValueType>;
18
+ /** 自定义标签渲染函数 */
19
+ labelRender?: SelectProps<ValueType>['labelRender'];
20
+ /** 是否显示搜索输入框 */
21
+ showSearchInput?: boolean;
22
+ /** 当前搜索值 */
23
+ searchValue?: string;
24
+ /** 搜索值变化回调函数 */
25
+ onSearchChange?: (value: string) => void;
26
+ /** 占位符文本 */
27
+ placeholder?: string;
28
+ /** 选择模式 */
29
+ mode?: SelectMode;
30
+ /** 创建新选项的回调函数 */
31
+ onCreateOption?: (value: string) => void;
32
+ /** 占位符样式 */
33
+ placeholderStyle?: StyleProp<TextStyle>;
34
+ /** 输入框样式 */
35
+ inputStyle?: StyleProp<TextStyle>;
36
+ }
37
+
38
+ /**
39
+ * Select 单选内容组件
40
+ *
41
+ * 负责渲染单选模式下的选中值显示,支持占位符、自定义渲染和搜索输入。
42
+ *
43
+ * @template ValueType - 选项值的类型
44
+ * @param props - 组件属性
45
+ *
46
+ * @example
47
+ * <SelectSingleContent
48
+ * selectedOption={{ label: '选项1', value: 1 }}
49
+ * placeholder="请选择"
50
+ * styles={styles}
51
+ * />
52
+ */
53
+ export const SelectSingleContent = <ValueType = unknown,>({
54
+ selectedOption,
55
+ labelRender,
56
+ showSearchInput = false,
57
+ searchValue = '',
58
+ onSearchChange,
59
+ placeholder = '',
60
+ mode,
61
+ onCreateOption,
62
+ placeholderStyle,
63
+ inputStyle,
64
+ }: SelectSingleContentProps<ValueType>) => {
65
+ const theme = useTheme();
66
+ const styles = useSelectSingleContentStyles();
67
+
68
+ /**
69
+ * 渲染占位符文本
70
+ *
71
+ * @param text - 占位符文本内容
72
+ * @returns 占位符文本组件
73
+ */
74
+ const renderPlaceholder = useCallback(
75
+ (text: string) => (
76
+ <Text size={14} color={theme.palette.fontGray3} style={placeholderStyle}>
77
+ {text}
78
+ </Text>
79
+ ),
80
+ [theme.palette.fontGray3, placeholderStyle],
81
+ );
82
+
83
+ /**
84
+ * 渲染选中内容
85
+ *
86
+ * @param content - 选中的内容
87
+ * @returns 选中内容组件
88
+ */
89
+ const renderSelectedContent = useCallback(
90
+ (content: React.ReactNode) => (
91
+ <Text size={14} color={theme.palette.fontGray1}>
92
+ {content}
93
+ </Text>
94
+ ),
95
+ [theme.palette.fontGray1],
96
+ );
97
+
98
+ if (showSearchInput) {
99
+ return (
100
+ <Input
101
+ variant="borderless"
102
+ style={[styles.input, inputStyle]}
103
+ inputStyle={styles.inputInner}
104
+ value={searchValue}
105
+ onChangeText={onSearchChange}
106
+ placeholder={!selectedOption ? placeholder : ''}
107
+ placeholderTextColor={theme.palette.fontGray3}
108
+ onSubmitEditing={() => {
109
+ if (mode === 'tags' && searchValue.trim() && onCreateOption) {
110
+ onCreateOption(searchValue);
111
+ }
112
+ }}
113
+ autoFocus
114
+ />
115
+ );
116
+ }
117
+
118
+ if (!selectedOption) {
119
+ return renderPlaceholder(placeholder);
120
+ }
121
+
122
+ if (labelRender) {
123
+ return renderSelectedContent(labelRender(selectedOption, { index: 0 }));
124
+ }
125
+
126
+ return renderSelectedContent(selectedOption.label);
127
+ };
@@ -0,0 +1,100 @@
1
+ import { CloseOutlined, DownOutlined, UpOutlined } from '@seakoi/svg-icons';
2
+ import React from 'react';
3
+ import {
4
+ type GestureResponderEvent,
5
+ Pressable,
6
+ type StyleProp,
7
+ View,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import { useTheme } from '#native-provider';
12
+
13
+ import { useSelectSuffixStyles } from './style';
14
+ import type { SelectProps } from './types';
15
+
16
+ /**
17
+ * SelectSuffix 组件的属性
18
+ */
19
+ export interface SelectSuffixProps {
20
+ /**
21
+ * 是否显示清除按钮
22
+ */
23
+ showClearButton: boolean;
24
+ /**
25
+ * 是否允许清除
26
+ */
27
+ allowClear?: SelectProps['allowClear'];
28
+ /**
29
+ * 是否打开下拉框
30
+ */
31
+ open: boolean;
32
+ /**
33
+ * 自定义后缀图标
34
+ */
35
+ suffixIcon?: SelectProps['suffixIcon'];
36
+ /**
37
+ * 后缀元素样式
38
+ */
39
+ suffixStyle?: StyleProp<ViewStyle>;
40
+ /**
41
+ * 清除按钮样式
42
+ */
43
+ clearStyle?: StyleProp<ViewStyle>;
44
+ /**
45
+ * 清除按钮点击回调
46
+ */
47
+ onClear: (e: GestureResponderEvent) => void;
48
+ }
49
+
50
+ /**
51
+ * Select 后缀区域组件
52
+ *
53
+ * 包含清除按钮和下拉箭头图标
54
+ */
55
+ export const SelectSuffix: React.FC<SelectSuffixProps> = ({
56
+ showClearButton,
57
+ allowClear,
58
+ open,
59
+ suffixIcon,
60
+ suffixStyle,
61
+ clearStyle,
62
+ onClear,
63
+ }) => {
64
+ const theme = useTheme();
65
+ const styles = useSelectSuffixStyles();
66
+
67
+ const renderClearIcon = () => {
68
+ if (typeof allowClear === 'object' && allowClear.clearIcon) {
69
+ return allowClear.clearIcon;
70
+ }
71
+ return <CloseOutlined size={12} color={theme.palette.fontGray3} />;
72
+ };
73
+
74
+ const renderSuffixIcon = () => {
75
+ if (typeof suffixIcon === 'function') {
76
+ return suffixIcon(open);
77
+ }
78
+
79
+ if (suffixIcon) {
80
+ return suffixIcon;
81
+ }
82
+
83
+ return open ? (
84
+ <UpOutlined size={12} color={theme.palette.fontGray3} />
85
+ ) : (
86
+ <DownOutlined size={12} color={theme.palette.fontGray3} />
87
+ );
88
+ };
89
+
90
+ return (
91
+ <View style={[styles.suffix, suffixStyle]}>
92
+ {showClearButton && (
93
+ <Pressable style={[styles.clearButton, clearStyle]} onPress={onClear}>
94
+ {renderClearIcon()}
95
+ </Pressable>
96
+ )}
97
+ {renderSuffixIcon()}
98
+ </View>
99
+ );
100
+ };