@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,71 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import { Text, View } from 'react-native';
3
+
4
+ import { type useOverflowStyles } from './style';
5
+ import type { OverflowProps } from './types';
6
+
7
+ /**
8
+ * 固定数量模式组件
9
+ */
10
+ export const FixedCountOverflow = <ItemType = unknown,>({
11
+ data,
12
+ renderItem,
13
+ renderRest,
14
+ maxCount,
15
+ prefix,
16
+ suffix,
17
+ style,
18
+ prefixStyle,
19
+ itemStyle,
20
+ suffixStyle,
21
+ restStyle,
22
+ onVisibleCountChange,
23
+ styles,
24
+ getKey,
25
+ }: OverflowProps<ItemType> & {
26
+ styles: ReturnType<typeof useOverflowStyles>;
27
+ getKey: (item: ItemType, index: number) => React.Key;
28
+ }) => {
29
+ const visibleCount = Math.min(data.length, maxCount as number);
30
+ const showRest = data.length > (maxCount as number);
31
+ const omittedData = data.slice(maxCount as number);
32
+
33
+ useEffect(() => {
34
+ onVisibleCountChange?.(visibleCount);
35
+ }, [visibleCount, onVisibleCountChange]);
36
+
37
+ const renderRestContent = useCallback(() => {
38
+ if (typeof renderRest === 'function') {
39
+ return renderRest(omittedData);
40
+ }
41
+ if (renderRest) {
42
+ return renderRest;
43
+ }
44
+ return <Text style={[styles.rest]}>+{omittedData.length}...</Text>;
45
+ }, [renderRest, omittedData, styles.rest]);
46
+
47
+ return (
48
+ <View style={[styles.container, style]}>
49
+ {/* 前缀 */}
50
+ {prefix && <View style={[styles.item, prefixStyle]}>{prefix}</View>}
51
+
52
+ {/* 显示固定数量的元素 */}
53
+ {data.slice(0, visibleCount).map((item, index) => {
54
+ const key = getKey(item, index);
55
+ return (
56
+ <View key={key} style={[styles.item, itemStyle]}>
57
+ {renderItem(item, { index })}
58
+ </View>
59
+ );
60
+ })}
61
+
62
+ {/* 显示剩余数量提示 */}
63
+ {showRest && (
64
+ <View style={[styles.item, restStyle]}>{renderRestContent()}</View>
65
+ )}
66
+
67
+ {/* 后缀 */}
68
+ {suffix && <View style={[styles.item, suffixStyle]}>{suffix}</View>}
69
+ </View>
70
+ );
71
+ };
@@ -0,0 +1,2 @@
1
+ export * from './overflow';
2
+ export type * from './types';
@@ -0,0 +1,60 @@
1
+ import { isNumber } from 'lodash-es';
2
+ import React, { useCallback } from 'react';
3
+
4
+ import { AllModeOverflow } from './all-mode-overflow';
5
+ import { FixedCountOverflow } from './fixed-count-overflow';
6
+ import { ResponsiveOverflow } from './responsive-overflow';
7
+ import { useOverflowStyles } from './style';
8
+ import type { OverflowProps } from './types';
9
+
10
+ /**
11
+ * Overflow 组件
12
+ *
13
+ * 利用 onLayout 在渲染前触发的特性,实现单次渲染的溢出处理组件
14
+ * 固定数量模式直接渲染,响应式模式通过预先计算确定最终显示状态
15
+ *
16
+ * @param props - 组件属性
17
+ * @returns React 元素
18
+ */
19
+ export const Overflow = <ItemType = unknown,>(props: OverflowProps<ItemType>) => {
20
+ const { maxCount = 'responsive', itemKey } = props;
21
+
22
+ const styles = useOverflowStyles({ maxCount });
23
+
24
+ /**
25
+ * 获取元素唯一标识
26
+ */
27
+ const getKey = useCallback(
28
+ (item: ItemType, index: number): React.Key => {
29
+ if (typeof itemKey === 'function') {
30
+ return itemKey(item, index);
31
+ }
32
+ if (
33
+ itemKey &&
34
+ typeof item === 'object' &&
35
+ item !== null &&
36
+ typeof itemKey === 'string'
37
+ ) {
38
+ const value = (item as Record<string, unknown>)[itemKey as string];
39
+ return typeof value === 'string' || typeof value === 'number'
40
+ ? value
41
+ : index;
42
+ }
43
+ return index;
44
+ },
45
+ [itemKey],
46
+ );
47
+
48
+ // 固定数量模式
49
+ if (isNumber(maxCount)) {
50
+ return <FixedCountOverflow {...props} styles={styles} getKey={getKey} />;
51
+ }
52
+
53
+ // 全部显示模式
54
+ if (maxCount === 'all') {
55
+ return <AllModeOverflow {...props} styles={styles} getKey={getKey} />;
56
+ }
57
+
58
+ // 响应式模式
59
+ return <ResponsiveOverflow {...props} styles={styles} getKey={getKey} />;
60
+ };
@@ -0,0 +1,349 @@
1
+ import { useMemoizedFn } from 'ahooks';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { type LayoutChangeEvent, Text, View } from 'react-native';
4
+
5
+ import { type useOverflowStyles } from './style';
6
+ import type { OverflowProps } from './types';
7
+
8
+ /**
9
+ * 测量数据 - 存储所有测量的宽度信息
10
+ */
11
+ interface MeasurementData {
12
+ container: number;
13
+ items: Map<React.Key, number>;
14
+ prefix: number;
15
+ suffix: number;
16
+ rest: number;
17
+ }
18
+
19
+ /**
20
+ * 计算结果 - 存储基于测量数据计算出的显示状态
21
+ */
22
+ interface CalculationResult {
23
+ visibleCount: number;
24
+ showRest: boolean;
25
+ }
26
+
27
+ /**
28
+ * 响应式 Overflow 组件
29
+ * 只有在 props 变化时才重新测量,onLayout 不直接触发测量
30
+ */
31
+ export const ResponsiveOverflow = <ItemType = unknown,>({
32
+ data,
33
+ renderItem,
34
+ renderRest,
35
+ prefix,
36
+ suffix,
37
+ style,
38
+ prefixStyle,
39
+ itemStyle,
40
+ suffixStyle,
41
+ restStyle,
42
+ onVisibleCountChange,
43
+ itemsWidthTolerance = 0,
44
+ styles,
45
+ getKey,
46
+ }: OverflowProps<ItemType> & {
47
+ styles: ReturnType<typeof useOverflowStyles>;
48
+ getKey: (item: ItemType, index: number) => React.Key;
49
+ }) => {
50
+ // 测量数据
51
+ const measurementDataRef = useRef<MeasurementData>({
52
+ container: 0,
53
+ items: new Map(),
54
+ prefix: 0,
55
+ suffix: 0,
56
+ rest: 0,
57
+ });
58
+
59
+ // 计算结果
60
+ const calculationResultRef = useRef<CalculationResult>({
61
+ visibleCount: 0,
62
+ showRest: false,
63
+ });
64
+
65
+ // 上一次的结果(用于测量期间的显示优化)
66
+ const previousResultRef = useRef<CalculationResult>({
67
+ visibleCount: 0,
68
+ showRest: false,
69
+ });
70
+
71
+ // 测量完成标记
72
+ const measurementFlags = useRef({
73
+ container: false,
74
+ prefix: false,
75
+ suffix: false,
76
+ rest: false,
77
+ items: new Set<React.Key>(),
78
+ });
79
+
80
+ // 状态管理
81
+ const [, forceUpdate] = useState({});
82
+ const [remeasureKey, setRemeasureKey] = useState(0);
83
+ const isComplete = useRef(false);
84
+
85
+ const triggerRerender = useMemoizedFn(() => forceUpdate({}));
86
+ /**
87
+ * 重置所有测量状态
88
+ */
89
+ const resetMeasurement = useMemoizedFn(() => {
90
+ if (!isComplete.current) {
91
+ return;
92
+ }
93
+ // 保存当前结果作为上一次的结果
94
+ previousResultRef.current = { ...calculationResultRef.current };
95
+
96
+ measurementDataRef.current = {
97
+ container: 0,
98
+ items: new Map(),
99
+ prefix: 0,
100
+ suffix: 0,
101
+ rest: 0,
102
+ };
103
+
104
+ calculationResultRef.current = {
105
+ visibleCount: 0,
106
+ showRest: false,
107
+ };
108
+
109
+ isComplete.current = false;
110
+
111
+ measurementFlags.current = {
112
+ container: false,
113
+ prefix: false,
114
+ suffix: false,
115
+ rest: false,
116
+ items: new Set<React.Key>(),
117
+ };
118
+
119
+ setRemeasureKey(prev => prev + 1);
120
+ });
121
+
122
+ /**
123
+ * 检查是否所有必要的测量都已完成
124
+ */
125
+ const isAllMeasurementsComplete = (): boolean => {
126
+ const flags = measurementFlags.current;
127
+
128
+ if (!flags.container || !flags.rest) return false;
129
+ if (prefix && !flags.prefix) return false;
130
+ if (suffix && !flags.suffix) return false;
131
+
132
+ // 检查所有元素是否都已测量
133
+ for (let i = 0; i < data.length; i++) {
134
+ const key = getKey(data[i], i);
135
+ if (!flags.items.has(key)) return false;
136
+ }
137
+
138
+ return true;
139
+ };
140
+
141
+ /**
142
+ * 计算最终显示状态
143
+ */
144
+ const calculateDisplayState = (
145
+ measurementData: MeasurementData,
146
+ ): CalculationResult => {
147
+ const { container, items, prefix, suffix, rest } = measurementData;
148
+
149
+ if (!container || data.length === 0) {
150
+ return { visibleCount: 0, showRest: false };
151
+ }
152
+
153
+ const availableWidth = container - prefix - suffix;
154
+ let currentWidth = 0;
155
+ let visibleCount = 0;
156
+
157
+ // 计算所有元素的总宽度
158
+ let totalItemsWidth = 0;
159
+ for (let i = 0; i < data.length; i++) {
160
+ const key = getKey(data[i], i);
161
+ const itemWidth = items.get(key) || 0;
162
+ totalItemsWidth += itemWidth;
163
+ }
164
+
165
+ // 如果所有元素都能放下(考虑容差)
166
+ if (totalItemsWidth <= availableWidth + itemsWidthTolerance) {
167
+ return {
168
+ visibleCount: data.length,
169
+ showRest: false,
170
+ };
171
+ }
172
+
173
+ // 需要截断,从前往后累积宽度
174
+ for (let i = 0; i < data.length; i++) {
175
+ const key = getKey(data[i], i);
176
+ const itemWidth = items.get(key) || 0;
177
+
178
+ // 检查加上当前元素和rest节点是否会超出
179
+ if (currentWidth + itemWidth + rest > availableWidth) {
180
+ // 如果是第一个元素就超出,至少显示一个
181
+ visibleCount = i === 0 ? 1 : i;
182
+ break;
183
+ }
184
+
185
+ currentWidth += itemWidth;
186
+ visibleCount = i + 1;
187
+ }
188
+
189
+ return {
190
+ visibleCount,
191
+ showRest: visibleCount < data.length,
192
+ };
193
+ };
194
+
195
+ /**
196
+ * 完成测量并更新结果
197
+ */
198
+ const completeMeasurement = useMemoizedFn(() => {
199
+ if (!isAllMeasurementsComplete()) return;
200
+
201
+ const result = calculateDisplayState(measurementDataRef.current);
202
+
203
+ calculationResultRef.current = result;
204
+ isComplete.current = true;
205
+ previousResultRef.current = { ...result };
206
+
207
+ onVisibleCountChange?.(result.visibleCount);
208
+ triggerRerender();
209
+ });
210
+
211
+ // 影响布局的 props 变化时标记需要重新计算
212
+ useEffect(() => {
213
+ resetMeasurement();
214
+ }, [
215
+ data,
216
+ renderItem,
217
+ renderRest,
218
+ prefix,
219
+ suffix,
220
+ style,
221
+ prefixStyle,
222
+ itemStyle,
223
+ suffixStyle,
224
+ restStyle,
225
+ onVisibleCountChange,
226
+ itemsWidthTolerance,
227
+ styles,
228
+ getKey,
229
+ ]);
230
+
231
+ /**
232
+ * 布局测量处理函数
233
+ */
234
+ const createLayoutHandler = useMemoizedFn(
235
+ (elementType: keyof MeasurementData, itemKey?: React.Key) =>
236
+ (event: LayoutChangeEvent) => {
237
+ // 测量完成后不再更新数据
238
+ if (isComplete.current) {
239
+ return;
240
+ }
241
+ const { width } = event.nativeEvent.layout;
242
+
243
+ // 更新测量数据和标记
244
+ if (elementType === 'items') {
245
+ measurementFlags.current.items.add(itemKey!);
246
+ measurementDataRef.current.items.set(itemKey!, width);
247
+ } else {
248
+ measurementFlags.current[elementType] = true;
249
+ measurementDataRef.current[elementType] = width;
250
+ }
251
+
252
+ // 检查是否可以完成测量
253
+ completeMeasurement();
254
+ },
255
+ );
256
+
257
+ /**
258
+ * 渲染 Rest 内容
259
+ */
260
+ const renderRestContent = () => {
261
+ const omittedData = data.slice(calculationResultRef.current.visibleCount);
262
+
263
+ if (typeof renderRest === 'function') {
264
+ return renderRest(omittedData);
265
+ }
266
+ if (renderRest) {
267
+ return renderRest;
268
+ }
269
+ return <Text style={[styles.rest, restStyle]}>+{omittedData.length}...</Text>;
270
+ };
271
+
272
+ return (
273
+ <View
274
+ key={remeasureKey}
275
+ onLayout={createLayoutHandler('container')}
276
+ style={[styles.container, style]}
277
+ >
278
+ {/* 前缀 */}
279
+ {prefix && (
280
+ <View
281
+ onLayout={createLayoutHandler('prefix')}
282
+ style={[styles.item, prefixStyle]}
283
+ >
284
+ {prefix}
285
+ </View>
286
+ )}
287
+
288
+ {/* 元素列表 */}
289
+ {data.map((item, index) => {
290
+ const key = getKey(item, index);
291
+
292
+ // 测量完成后,不渲染超出可见范围的元素
293
+ if (
294
+ isComplete.current &&
295
+ index >= calculationResultRef.current.visibleCount
296
+ ) {
297
+ return null;
298
+ }
299
+
300
+ // 测量期间,如果有上一次的结果且当前元素超出上一次的可见范围,需要隐藏
301
+ const shouldHideInMeasurement =
302
+ !isComplete.current &&
303
+ previousResultRef.current.visibleCount > 0 &&
304
+ index >= previousResultRef.current.visibleCount;
305
+
306
+ return (
307
+ <View
308
+ key={key}
309
+ onLayout={createLayoutHandler('items', key)}
310
+ style={[
311
+ styles.item,
312
+ itemStyle,
313
+ shouldHideInMeasurement && styles.hidden,
314
+ ]}
315
+ >
316
+ {renderItem(item, { index })}
317
+ </View>
318
+ );
319
+ })}
320
+
321
+ {/* Rest节点 - 测量期间始终渲染以获取宽度,测量完成后根据需要渲染 */}
322
+ {!isComplete.current || calculationResultRef.current.showRest ? (
323
+ <View
324
+ onLayout={createLayoutHandler('rest')}
325
+ style={[
326
+ styles.item,
327
+ restStyle,
328
+ // 测量期间隐藏,测量完成后根据 showRest 决定是否隐藏
329
+ !isComplete.current &&
330
+ !previousResultRef.current.showRest &&
331
+ styles.hidden,
332
+ ]}
333
+ >
334
+ {renderRestContent()}
335
+ </View>
336
+ ) : null}
337
+
338
+ {/* 后缀 */}
339
+ {suffix && (
340
+ <View
341
+ onLayout={createLayoutHandler('suffix')}
342
+ style={[styles.item, suffixStyle]}
343
+ >
344
+ {suffix}
345
+ </View>
346
+ )}
347
+ </View>
348
+ );
349
+ };
@@ -0,0 +1,32 @@
1
+ import { createThemedStyles } from '#native-provider';
2
+
3
+ /**
4
+ * 创建样式
5
+ */
6
+ export const useOverflowStyles = createThemedStyles((theme, { maxCount }) => ({
7
+ container: {
8
+ flexDirection: 'row',
9
+ flexWrap: maxCount === 'responsive' ? 'nowrap' : 'wrap',
10
+ overflow: 'hidden',
11
+ width: '100%',
12
+ },
13
+ item: {
14
+ flexShrink: 0,
15
+ paddingHorizontal: theme.spacing.xxs / 2,
16
+ paddingVertical: theme.spacing.xxs / 4,
17
+ },
18
+ rest: {
19
+ backgroundColor: theme.palette.gray1,
20
+ borderRadius: theme.roundness.sm,
21
+ paddingHorizontal: theme.spacing.xs,
22
+ height: '100%',
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ color: theme.palette.fontGray3,
26
+ },
27
+ hidden: {
28
+ opacity: 0,
29
+ pointerEvents: 'none',
30
+ position: 'absolute',
31
+ },
32
+ }));
@@ -0,0 +1,75 @@
1
+ import type React from 'react';
2
+ import { type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ /**
5
+ * Overflow 组件属性
6
+ */
7
+ export interface OverflowProps<ItemType = unknown> {
8
+ /**
9
+ * 数据源
10
+ */
11
+ data: ItemType[];
12
+ /**
13
+ * 获取元素唯一标识
14
+ * @default (item, index) => index
15
+ */
16
+ itemKey?: keyof ItemType | ((item: ItemType, index: number) => React.Key);
17
+ /**
18
+ * 渲染单个元素
19
+ * @param item 数据项
20
+ * @param info 渲染信息(索引)
21
+ */
22
+ renderItem: (item: ItemType, info: { index: number }) => React.ReactNode;
23
+ /**
24
+ * 渲染"更多"节点
25
+ * @param omittedItems 被折叠的元素数组
26
+ * @default (items) => `+${items.length}...`
27
+ */
28
+ renderRest?: React.ReactNode | ((omittedItems: ItemType[]) => React.ReactNode);
29
+ /**
30
+ * 最大显示数量
31
+ * - 'responsive': 响应式模式,根据容器宽度自动计算
32
+ * - 'all': 显示所有内容,不进行任何截断
33
+ * - number: 固定显示数量
34
+ * @default 'responsive'
35
+ */
36
+ maxCount?: number | 'responsive' | 'all';
37
+ /**
38
+ * 前缀元素
39
+ */
40
+ prefix?: React.ReactNode;
41
+ /**
42
+ * 后缀元素
43
+ */
44
+ suffix?: React.ReactNode;
45
+ /**
46
+ * 容器样式
47
+ */
48
+ style?: StyleProp<ViewStyle>;
49
+ /**
50
+ * 前缀元素样式
51
+ */
52
+ prefixStyle?: StyleProp<ViewStyle>;
53
+ /**
54
+ * 数据项元素样式
55
+ */
56
+ itemStyle?: StyleProp<ViewStyle>;
57
+ /**
58
+ * 后缀元素样式
59
+ */
60
+ suffixStyle?: StyleProp<ViewStyle>;
61
+ /**
62
+ * "更多"节点样式
63
+ */
64
+ restStyle?: StyleProp<ViewStyle>;
65
+ /**
66
+ * 可见元素数量变化回调
67
+ * @param visibleCount 当前可见的元素数量
68
+ */
69
+ onVisibleCountChange?: (visibleCount: number) => void;
70
+ /**
71
+ * 在响应式模式下,当元素总宽度超出容器宽度但在容差范围内时,仍然显示全部元素而不截断
72
+ * @default 0
73
+ */
74
+ itemsWidthTolerance?: number;
75
+ }
@@ -11,21 +11,35 @@ import {
11
11
  import type { Any } from '#shared/utils';
12
12
 
13
13
  import { PickerContext } from './picker-context';
14
+ import { usePickerContentStyles } from './style';
14
15
 
16
+ /**
17
+ * Picker 内容组件的属性接口
18
+ * 继承自 Modal 组件,排除 visible 和 children 属性
19
+ */
15
20
  export interface PickerContentProps
16
21
  extends Omit<ModalProps, 'visible' | 'children'> {
22
+ /** 是否隐藏头部关闭图标 */
17
23
  hiddenHeaderCloseIcon?: boolean;
24
+ /** 头部标题内容 */
18
25
  headerTitle?: React.ReactNode;
26
+ /** 头部样式 */
19
27
  headerStyle?: StyleProp<ViewStyle>;
28
+ /** 子元素,可以是 React 元素或渲染函数 */
20
29
  children?:
21
30
  | React.ReactElement
22
31
  | ((props: {
32
+ /** 当前选中的值 */
23
33
  value: Any;
34
+ /** 值变化回调函数 */
24
35
  onChange?: (value: Any) => void;
25
36
  }) => React.ReactElement);
37
+ /** 内容区域样式 */
38
+ style?: StyleProp<ViewStyle>;
26
39
  }
27
40
 
28
41
  export const PickerContent: React.FC<PickerContentProps> = ({
42
+ style,
29
43
  children,
30
44
  headerTitle,
31
45
  headerStyle,
@@ -33,22 +47,23 @@ export const PickerContent: React.FC<PickerContentProps> = ({
33
47
  ...modalProps
34
48
  }) => {
35
49
  const context = useContext(PickerContext);
50
+ const styles = usePickerContentStyles();
36
51
 
37
52
  let childNode: React.ReactElement<Any> | null = null;
38
53
 
54
+ const injectToChildrenProps = {
55
+ value: context?.value,
56
+ onChange: context?.onChange,
57
+ };
58
+
39
59
  if (React.isValidElement(children)) {
40
- childNode = React.cloneElement<Any>(children, {
41
- value: context?.value,
42
- onChange: context?.onChange,
43
- });
60
+ childNode = React.cloneElement<Any>(children, injectToChildrenProps);
44
61
  } else if (typeof children === 'function') {
45
- childNode = children({
46
- value: context?.value,
47
- onChange: context?.onChange,
48
- });
62
+ childNode = children(injectToChildrenProps);
49
63
  }
50
64
  useEffect(() => {
51
65
  context?.setDisabled?.(!!childNode?.props?.disabled);
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
67
  }, [childNode?.props?.disabled, context?.setDisabled]);
53
68
 
54
69
  return (
@@ -60,7 +75,7 @@ export const PickerContent: React.FC<PickerContentProps> = ({
60
75
  {...modalProps}
61
76
  visible={!!context?.visible}
62
77
  >
63
- <Flex vertical align="stretch" gap={16} style={{ flex: 1, padding: 16 }}>
78
+ <Flex vertical align="stretch" gap={16} style={[styles.container, style]}>
64
79
  <ModalHeader
65
80
  title={headerTitle}
66
81
  style={headerStyle}