@tarojs/components-react 4.1.12-beta.4 → 4.1.12-beta.41
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/dist/components/image/index.js +5 -3
- package/dist/components/image/index.js.map +1 -1
- package/dist/components/map/MapContext.js +628 -0
- package/dist/components/map/MapContext.js.map +1 -0
- package/dist/components/map/MapCustomCallout.js +91 -0
- package/dist/components/map/MapCustomCallout.js.map +1 -0
- package/dist/components/map/common.js +4 -0
- package/dist/components/map/common.js.map +1 -0
- package/dist/components/map/createMapContext.js +34 -0
- package/dist/components/map/createMapContext.js.map +1 -0
- package/dist/components/map/handler.js +50 -0
- package/dist/components/map/handler.js.map +1 -0
- package/dist/components/map/index.js +327 -0
- package/dist/components/map/index.js.map +1 -0
- package/dist/components/picker/index.js +71 -34
- package/dist/components/picker/index.js.map +1 -1
- package/dist/components/picker/picker-group.js +455 -111
- package/dist/components/picker/picker-group.js.map +1 -1
- package/dist/components/scroll-view/index.js +80 -36
- package/dist/components/scroll-view/index.js.map +1 -1
- package/dist/components/switch/index.js +125 -0
- package/dist/components/switch/index.js.map +1 -0
- package/dist/components/switch/style/index.scss.js +4 -0
- package/dist/components/switch/style/index.scss.js.map +1 -0
- package/dist/contexts/ScrollElementContext.js +6 -0
- package/dist/contexts/ScrollElementContext.js.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/original/components/image/index.js +5 -3
- package/dist/original/components/image/index.js.map +1 -1
- package/dist/original/components/image/style/index.scss +5 -1
- package/dist/original/components/map/MapContext.js +628 -0
- package/dist/original/components/map/MapContext.js.map +1 -0
- package/dist/original/components/map/MapCustomCallout.js +91 -0
- package/dist/original/components/map/MapCustomCallout.js.map +1 -0
- package/dist/original/components/map/common.js +4 -0
- package/dist/original/components/map/common.js.map +1 -0
- package/dist/original/components/map/createMapContext.js +34 -0
- package/dist/original/components/map/createMapContext.js.map +1 -0
- package/dist/original/components/map/handler.js +50 -0
- package/dist/original/components/map/handler.js.map +1 -0
- package/dist/original/components/map/index.js +327 -0
- package/dist/original/components/map/index.js.map +1 -0
- package/dist/original/components/picker/index.js +71 -34
- package/dist/original/components/picker/index.js.map +1 -1
- package/dist/original/components/picker/picker-group.js +455 -111
- package/dist/original/components/picker/picker-group.js.map +1 -1
- package/dist/original/components/scroll-view/index.js +80 -36
- package/dist/original/components/scroll-view/index.js.map +1 -1
- package/dist/original/components/switch/index.js +125 -0
- package/dist/original/components/switch/index.js.map +1 -0
- package/dist/original/components/switch/style/index.scss +35 -0
- package/dist/original/contexts/ScrollElementContext.js +6 -0
- package/dist/original/contexts/ScrollElementContext.js.map +1 -0
- package/dist/original/index.js +5 -1
- package/dist/original/index.js.map +1 -1
- package/dist/solid/components/image/index.js +5 -3
- package/dist/solid/components/image/index.js.map +1 -1
- package/dist/solid/components/picker/index.js +75 -38
- package/dist/solid/components/picker/index.js.map +1 -1
- package/dist/solid/components/picker/picker-group.js +478 -135
- package/dist/solid/components/picker/picker-group.js.map +1 -1
- package/dist/solid/components/scroll-view/index.js +84 -40
- package/dist/solid/components/scroll-view/index.js.map +1 -1
- package/dist/solid/contexts/ScrollElementContext.js +6 -0
- package/dist/solid/contexts/ScrollElementContext.js.map +1 -0
- package/dist/solid/index.css +1 -1
- package/package.json +8 -6
|
@@ -3,6 +3,31 @@ import { View, ScrollView } from '@tarojs/components';
|
|
|
3
3
|
import Taro from '@tarojs/taro';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
|
+
function requestAccessibilityFocusOnView(node) {
|
|
7
|
+
if (node == null) return;
|
|
8
|
+
const fn = Taro.setAccessibilityFocus;
|
|
9
|
+
if (typeof fn !== 'function') return;
|
|
10
|
+
fn({
|
|
11
|
+
viewRef: {
|
|
12
|
+
current: node
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** 部分端同 id 连续 scrollIntoView 不生效:先清空再在下一宏任务设回目标 id */
|
|
17
|
+
function usePickerItemScrollIntoView() {
|
|
18
|
+
const [scrollIntoView, setScrollIntoView] = React.useState('');
|
|
19
|
+
const pulseRef = React.useRef(0);
|
|
20
|
+
const scrollToItemId = React.useCallback(itemId => {
|
|
21
|
+
pulseRef.current += 1;
|
|
22
|
+
const token = pulseRef.current;
|
|
23
|
+
setScrollIntoView('');
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (pulseRef.current !== token) return;
|
|
26
|
+
setScrollIntoView(itemId);
|
|
27
|
+
}, 0);
|
|
28
|
+
}, []);
|
|
29
|
+
return [scrollIntoView, scrollToItemId];
|
|
30
|
+
}
|
|
6
31
|
// 定义常量
|
|
7
32
|
const PICKER_LINE_HEIGHT = 34; // px
|
|
8
33
|
const PICKER_VISIBLE_ITEMS = 7; // 可见行数
|
|
@@ -13,6 +38,80 @@ const getIndicatorStyle = lineColor => {
|
|
|
13
38
|
borderBottomColor: lineColor
|
|
14
39
|
};
|
|
15
40
|
};
|
|
41
|
+
// 大屏方案版本要求
|
|
42
|
+
const MIN_DESIGN_APP_VERSION = 16;
|
|
43
|
+
const MIN_APP_VERSION = '15.7.0';
|
|
44
|
+
// semver 版本比较
|
|
45
|
+
const isAppVersionAtLeast = (version, min) => {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
if (!version || typeof version !== 'string') return false;
|
|
48
|
+
const parts = v => {
|
|
49
|
+
const m = String(v).trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
50
|
+
if (!m) return [];
|
|
51
|
+
return [parseInt(m[1], 10) || 0, parseInt(m[2] || '0', 10) || 0, parseInt(m[3] || '0', 10) || 0];
|
|
52
|
+
};
|
|
53
|
+
const a = parts(version);
|
|
54
|
+
const b = parts(min);
|
|
55
|
+
if (a.length === 0) return false;
|
|
56
|
+
if (b.length === 0) return true;
|
|
57
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
58
|
+
const da = (_a = a[i]) !== null && _a !== void 0 ? _a : 0;
|
|
59
|
+
const db = (_b = b[i]) !== null && _b !== void 0 ? _b : 0;
|
|
60
|
+
if (da > db) return true;
|
|
61
|
+
if (da < db) return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
// 读取 JDMobileConfig,异常时返回 undefined
|
|
66
|
+
const tryGetMobileConfigSync = opt => {
|
|
67
|
+
var _a;
|
|
68
|
+
try {
|
|
69
|
+
const fn = (_a = Taro.JDMobileConfig) === null || _a === void 0 ? void 0 : _a.getMobileConfigSync;
|
|
70
|
+
if (typeof fn !== 'function') return undefined;
|
|
71
|
+
return fn(opt);
|
|
72
|
+
} catch (_b) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// 判断是否启用测量值缩放适配(true=启用, false=使用系统侧缩放)
|
|
77
|
+
const resolveUseMeasuredScale = res => {
|
|
78
|
+
// H5/weapp 不参与大屏系数
|
|
79
|
+
if (process.env.TARO_ENV === 'h5' || process.env.TARO_ENV === 'weapp') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// 条件1: designAppVersion < 16,不满足则使用系统侧缩放
|
|
83
|
+
const designAppVersionRaw = res.designAppVersion;
|
|
84
|
+
const designAppVersionMajor = designAppVersionRaw != null ? parseInt(String(designAppVersionRaw).trim(), 10) : Number.NaN;
|
|
85
|
+
if (!Number.isFinite(designAppVersionMajor) || designAppVersionMajor < MIN_DESIGN_APP_VERSION) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// 条件2: appVersion < 15.7.0,不满足则使用系统侧缩放
|
|
89
|
+
if (!isAppVersionAtLeast(res.version, MIN_APP_VERSION)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// 条件3: 平台判断
|
|
93
|
+
const platform = String(res.platform || '').toLowerCase();
|
|
94
|
+
if (platform === 'harmony') {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (platform === 'android') {
|
|
98
|
+
const raw = tryGetMobileConfigSync({
|
|
99
|
+
space: 'taro',
|
|
100
|
+
configName: 'config',
|
|
101
|
+
key: 'disableFixBoundingScaleRatio'
|
|
102
|
+
});
|
|
103
|
+
return raw !== 1 && raw !== '1';
|
|
104
|
+
}
|
|
105
|
+
if (platform === 'ios') {
|
|
106
|
+
const raw = tryGetMobileConfigSync({
|
|
107
|
+
space: 'Taro',
|
|
108
|
+
configName: 'excutor',
|
|
109
|
+
key: 'disableBoundingScaleRatio'
|
|
110
|
+
});
|
|
111
|
+
return raw !== 1 && raw !== '1';
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
};
|
|
16
115
|
// 辅助函数:计算 lengthScaleRatio
|
|
17
116
|
const calculateLengthScaleRatio = res => {
|
|
18
117
|
let lengthScaleRatio = res === null || res === void 0 ? void 0 : res.lengthScaleRatio;
|
|
@@ -51,6 +150,29 @@ const setTargetScrollTopWithScale = function (setTargetScrollTop, baseValue, ran
|
|
|
51
150
|
setTargetScrollTop(finalValue);
|
|
52
151
|
}
|
|
53
152
|
};
|
|
153
|
+
// 根据 scrollTop 计算选中索引
|
|
154
|
+
// 系统侧缩放模式:scrollHeight 已被系统缩放,直接相除即可
|
|
155
|
+
// 自行缩放模式:需要除以 ratio 换算(harmony 特殊处理,ratio 已内嵌在 itemHeight 中)
|
|
156
|
+
const getSelectedIndex = function (scrollTop, itemHeight) {
|
|
157
|
+
let lengthScaleRatio = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
|
|
158
|
+
let useMeasuredScale = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
|
|
159
|
+
if (!useMeasuredScale || process.env.TARO_PLATFORM === 'harmony') {
|
|
160
|
+
return Math.round(scrollTop / itemHeight);
|
|
161
|
+
}
|
|
162
|
+
return Math.round(scrollTop / lengthScaleRatio / itemHeight);
|
|
163
|
+
};
|
|
164
|
+
// 计算单项高度(返回设计稿值)
|
|
165
|
+
const calculateItemHeight = function (scrollView, lengthScaleRatio) {
|
|
166
|
+
let useMeasuredScale = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
167
|
+
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
168
|
+
return useMeasuredScale ? PICKER_LINE_HEIGHT * lengthScaleRatio : PICKER_LINE_HEIGHT;
|
|
169
|
+
}
|
|
170
|
+
if (scrollView && (scrollView === null || scrollView === void 0 ? void 0 : scrollView.scrollHeight)) {
|
|
171
|
+
return useMeasuredScale ? scrollView.scrollHeight / lengthScaleRatio / scrollView.childNodes.length : scrollView.scrollHeight / scrollView.childNodes.length;
|
|
172
|
+
}
|
|
173
|
+
console.warn('Height measurement anomaly');
|
|
174
|
+
return PICKER_LINE_HEIGHT;
|
|
175
|
+
};
|
|
54
176
|
function PickerGroupBasic(props) {
|
|
55
177
|
const {
|
|
56
178
|
range = [],
|
|
@@ -60,69 +182,72 @@ function PickerGroupBasic(props) {
|
|
|
60
182
|
onColumnChange,
|
|
61
183
|
selectedIndex = 0,
|
|
62
184
|
// 使用selectedIndex参数,默认为0
|
|
63
|
-
colors = {}
|
|
185
|
+
colors = {},
|
|
186
|
+
enableClickItemScroll = true
|
|
64
187
|
} = props;
|
|
65
188
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
66
189
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
190
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
67
191
|
const scrollViewRef = React.useRef(null);
|
|
68
192
|
const itemRefs = React.useRef([]);
|
|
193
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
194
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
195
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
196
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
69
197
|
// 使用selectedIndex初始化当前索引
|
|
70
198
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
71
199
|
// 触摸状态用于优化用户体验
|
|
72
|
-
const
|
|
200
|
+
const isTouchingRef = React.useRef(false);
|
|
73
201
|
const lengthScaleRatioRef = React.useRef(1);
|
|
202
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
74
203
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
75
|
-
// 初始化时计算 lengthScaleRatio
|
|
204
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
76
205
|
React.useEffect(() => {
|
|
77
206
|
Taro.getSystemInfo({
|
|
78
207
|
success: res => {
|
|
79
208
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
209
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
210
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
80
211
|
},
|
|
81
212
|
fail: () => {
|
|
82
|
-
// 失败时使用默认值 1
|
|
83
213
|
lengthScaleRatioRef.current = 1;
|
|
214
|
+
useMeasuredScaleRef.current = false;
|
|
84
215
|
}
|
|
85
216
|
});
|
|
86
217
|
}, []);
|
|
87
218
|
React.useEffect(() => {
|
|
88
|
-
|
|
89
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
90
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
91
|
-
} else {
|
|
92
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
93
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
94
|
-
} else {
|
|
95
|
-
console.warn('Height measurement anomaly');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
219
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
98
220
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
99
|
-
//
|
|
100
|
-
const getSelectedIndex = scrollTop => {
|
|
101
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
102
|
-
};
|
|
103
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
221
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
104
222
|
React.useEffect(() => {
|
|
105
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
223
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
224
|
+
syncScrollFromPropsRef.current = true;
|
|
106
225
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
107
226
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
108
227
|
setCurrentIndex(selectedIndex);
|
|
228
|
+
const tid = setTimeout(() => {
|
|
229
|
+
syncScrollFromPropsRef.current = false;
|
|
230
|
+
}, 400);
|
|
231
|
+
return () => clearTimeout(tid);
|
|
109
232
|
}
|
|
110
233
|
}, [selectedIndex, range]);
|
|
111
234
|
// 是否处于归中状态
|
|
112
235
|
const isCenterTimerId = React.useRef(null);
|
|
113
|
-
//
|
|
236
|
+
// 滚动静止后归中并同步选中
|
|
114
237
|
const handleScrollEnd = () => {
|
|
115
238
|
if (!scrollViewRef.current) return;
|
|
116
239
|
if (isCenterTimerId.current) {
|
|
117
240
|
clearTimeout(isCenterTimerId.current);
|
|
118
241
|
isCenterTimerId.current = null;
|
|
119
242
|
}
|
|
120
|
-
//
|
|
243
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
121
244
|
isCenterTimerId.current = setTimeout(() => {
|
|
122
245
|
if (!scrollViewRef.current) return;
|
|
123
246
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
124
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
125
|
-
|
|
247
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
248
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
249
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
250
|
+
isTouchingRef.current = false;
|
|
126
251
|
const baseValue = newIndex * itemHeightRef.current;
|
|
127
252
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
128
253
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -131,10 +256,15 @@ function PickerGroupBasic(props) {
|
|
|
131
256
|
columnId,
|
|
132
257
|
index: newIndex
|
|
133
258
|
});
|
|
259
|
+
pendingScrollSettleFocusRef.current = false;
|
|
260
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
261
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
262
|
+
}
|
|
263
|
+
syncScrollFromPropsRef.current = false;
|
|
134
264
|
isCenterTimerId.current = null;
|
|
135
265
|
}, 100);
|
|
136
266
|
};
|
|
137
|
-
//
|
|
267
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
138
268
|
const handleScroll = () => {
|
|
139
269
|
if (!scrollViewRef.current) return;
|
|
140
270
|
if (isCenterTimerId.current) {
|
|
@@ -142,7 +272,17 @@ function PickerGroupBasic(props) {
|
|
|
142
272
|
isCenterTimerId.current = null;
|
|
143
273
|
}
|
|
144
274
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
145
|
-
const
|
|
275
|
+
const ih = itemHeightRef.current;
|
|
276
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
277
|
+
const spi = selectedIndexPropRef.current;
|
|
278
|
+
if (newIndex !== spi) {
|
|
279
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
280
|
+
pendingScrollSettleFocusRef.current = true;
|
|
281
|
+
}
|
|
282
|
+
if (isTouchingRef.current) {
|
|
283
|
+
syncScrollFromPropsRef.current = false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
146
286
|
if (newIndex !== currentIndex) {
|
|
147
287
|
setCurrentIndex(newIndex);
|
|
148
288
|
}
|
|
@@ -150,11 +290,14 @@ function PickerGroupBasic(props) {
|
|
|
150
290
|
// 渲染选项
|
|
151
291
|
const pickerItem = range.map((item, index) => {
|
|
152
292
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
153
|
-
return createComponent(View, {
|
|
293
|
+
return createComponent(View, mergeProps({
|
|
154
294
|
id: `picker-item-${columnId}-${index}`,
|
|
155
295
|
key: index,
|
|
156
296
|
ref: el => itemRefs.current[index] = el,
|
|
157
|
-
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}
|
|
297
|
+
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
|
|
298
|
+
}, enableClickItemScroll ? {
|
|
299
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
300
|
+
} : {}, {
|
|
158
301
|
get style() {
|
|
159
302
|
return {
|
|
160
303
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -162,7 +305,7 @@ function PickerGroupBasic(props) {
|
|
|
162
305
|
};
|
|
163
306
|
},
|
|
164
307
|
children: content
|
|
165
|
-
});
|
|
308
|
+
}));
|
|
166
309
|
});
|
|
167
310
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
168
311
|
key: `blank-top-${idx}`,
|
|
@@ -177,6 +320,10 @@ function PickerGroupBasic(props) {
|
|
|
177
320
|
height: PICKER_LINE_HEIGHT
|
|
178
321
|
}
|
|
179
322
|
}))];
|
|
323
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
324
|
+
scrollIntoView,
|
|
325
|
+
scrollIntoViewAlignment: 'center'
|
|
326
|
+
} : {};
|
|
180
327
|
return createComponent(View, {
|
|
181
328
|
className: "taro-picker__group",
|
|
182
329
|
get children() {
|
|
@@ -186,7 +333,7 @@ function PickerGroupBasic(props) {
|
|
|
186
333
|
className: "taro-picker__indicator"
|
|
187
334
|
}, indicatorStyle ? {
|
|
188
335
|
style: indicatorStyle
|
|
189
|
-
} : {})), createComponent(ScrollView, {
|
|
336
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
190
337
|
ref: scrollViewRef,
|
|
191
338
|
scrollY: true,
|
|
192
339
|
showScrollbar: false,
|
|
@@ -194,13 +341,16 @@ function PickerGroupBasic(props) {
|
|
|
194
341
|
style: {
|
|
195
342
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
196
343
|
},
|
|
197
|
-
scrollTop: targetScrollTop
|
|
344
|
+
scrollTop: targetScrollTop
|
|
345
|
+
}, clickScrollViewProps, {
|
|
198
346
|
onScroll: handleScroll,
|
|
199
|
-
onTouchStart: () =>
|
|
347
|
+
onTouchStart: () => {
|
|
348
|
+
isTouchingRef.current = true;
|
|
349
|
+
},
|
|
200
350
|
onScrollEnd: handleScrollEnd,
|
|
201
351
|
scrollWithAnimation: true,
|
|
202
352
|
children: realPickerItems
|
|
203
|
-
})];
|
|
353
|
+
}))];
|
|
204
354
|
}
|
|
205
355
|
});
|
|
206
356
|
}
|
|
@@ -212,78 +362,189 @@ function PickerGroupTime(props) {
|
|
|
212
362
|
columnId,
|
|
213
363
|
updateIndex,
|
|
214
364
|
selectedIndex = 0,
|
|
215
|
-
colors = {}
|
|
365
|
+
colors = {},
|
|
366
|
+
timeA11yLimitFocus,
|
|
367
|
+
onTimeA11yLimitFocusConsumed,
|
|
368
|
+
timeA11yLimitEventNonce,
|
|
369
|
+
enableClickItemScroll = true
|
|
216
370
|
} = props;
|
|
217
371
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
218
372
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
373
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
219
374
|
const scrollViewRef = React.useRef(null);
|
|
220
375
|
const itemRefs = React.useRef([]);
|
|
376
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
377
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
378
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
379
|
+
const prevLimitEventNonceRef = React.useRef(undefined);
|
|
380
|
+
const suppressScrollSettleFocusNonceRef = React.useRef(null);
|
|
381
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
382
|
+
const pendingLimitFocusRef = React.useRef(null);
|
|
383
|
+
const limitFocusTimerRef = React.useRef(null);
|
|
221
384
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
222
|
-
const
|
|
385
|
+
const isTouchingRef = React.useRef(false);
|
|
223
386
|
const lengthScaleRatioRef = React.useRef(1);
|
|
387
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
224
388
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
225
|
-
|
|
389
|
+
const clearLimitFocusTimer = React.useCallback(() => {
|
|
390
|
+
if (limitFocusTimerRef.current) {
|
|
391
|
+
clearTimeout(limitFocusTimerRef.current);
|
|
392
|
+
limitFocusTimerRef.current = null;
|
|
393
|
+
}
|
|
394
|
+
}, []);
|
|
395
|
+
const tryFocusPendingLimit = React.useCallback(() => {
|
|
396
|
+
const pending = pendingLimitFocusRef.current;
|
|
397
|
+
const scrollView = scrollViewRef.current;
|
|
398
|
+
if (!pending || !scrollView) return;
|
|
399
|
+
const visualIndex = getSelectedIndex(scrollView.scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
400
|
+
const isStable = visualIndex === pending.index;
|
|
401
|
+
if (!isStable) {
|
|
402
|
+
if (pending.attempts >= 20) {
|
|
403
|
+
pendingLimitFocusRef.current = null;
|
|
404
|
+
if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
|
|
405
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
406
|
+
}
|
|
407
|
+
onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
pending.attempts += 1;
|
|
411
|
+
clearLimitFocusTimer();
|
|
412
|
+
limitFocusTimerRef.current = setTimeout(() => {
|
|
413
|
+
tryFocusPendingLimit();
|
|
414
|
+
}, 50);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const node = itemRefs.current[pending.index];
|
|
418
|
+
pendingLimitFocusRef.current = null;
|
|
419
|
+
if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
|
|
420
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
421
|
+
}
|
|
422
|
+
clearLimitFocusTimer();
|
|
423
|
+
requestAccessibilityFocusOnView(node);
|
|
424
|
+
onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
|
|
425
|
+
}, [clearLimitFocusTimer, onTimeA11yLimitFocusConsumed]);
|
|
426
|
+
const schedulePendingLimitFocus = React.useCallback(() => {
|
|
427
|
+
if (!pendingLimitFocusRef.current) return;
|
|
428
|
+
clearLimitFocusTimer();
|
|
429
|
+
limitFocusTimerRef.current = setTimeout(() => {
|
|
430
|
+
tryFocusPendingLimit();
|
|
431
|
+
}, 100);
|
|
432
|
+
}, [clearLimitFocusTimer, tryFocusPendingLimit]);
|
|
433
|
+
React.useEffect(() => clearLimitFocusTimer, [clearLimitFocusTimer]);
|
|
434
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
226
435
|
React.useEffect(() => {
|
|
227
436
|
Taro.getSystemInfo({
|
|
228
437
|
success: res => {
|
|
229
438
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
439
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
440
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
230
441
|
},
|
|
231
442
|
fail: () => {
|
|
232
|
-
// 失败时使用默认值 1
|
|
233
443
|
lengthScaleRatioRef.current = 1;
|
|
444
|
+
useMeasuredScaleRef.current = false;
|
|
234
445
|
}
|
|
235
446
|
});
|
|
236
447
|
}, []);
|
|
237
448
|
React.useEffect(() => {
|
|
238
|
-
|
|
239
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
240
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
241
|
-
} else {
|
|
242
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
243
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
244
|
-
} else {
|
|
245
|
-
console.warn('Height measurement anomaly');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
449
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
248
450
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
249
|
-
|
|
250
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
251
|
-
};
|
|
252
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
451
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
253
452
|
React.useEffect(() => {
|
|
254
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
453
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
454
|
+
syncScrollFromPropsRef.current = true;
|
|
255
455
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
256
456
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
257
457
|
setCurrentIndex(selectedIndex);
|
|
458
|
+
const tid = setTimeout(() => {
|
|
459
|
+
syncScrollFromPropsRef.current = false;
|
|
460
|
+
}, 400);
|
|
461
|
+
return () => clearTimeout(tid);
|
|
258
462
|
}
|
|
259
463
|
}, [selectedIndex, range]);
|
|
464
|
+
// time 限位 nonce 变更:强制对齐 scrollTop(避免 remount 打断读屏焦点)
|
|
465
|
+
React.useEffect(() => {
|
|
466
|
+
if (!timeA11yLimitEventNonce) return;
|
|
467
|
+
if (!scrollViewRef.current || range.length === 0 || isTouchingRef.current) return;
|
|
468
|
+
syncScrollFromPropsRef.current = true;
|
|
469
|
+
const baseValue = selectedIndex * itemHeightRef.current;
|
|
470
|
+
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, Math.random() * 0.001, lengthScaleRatioRef.current);
|
|
471
|
+
setCurrentIndex(selectedIndex);
|
|
472
|
+
const tid = setTimeout(() => {
|
|
473
|
+
syncScrollFromPropsRef.current = false;
|
|
474
|
+
}, 400);
|
|
475
|
+
return () => clearTimeout(tid);
|
|
476
|
+
}, [timeA11yLimitEventNonce]); // 仅依赖 nonce,其余用 ref
|
|
477
|
+
React.useLayoutEffect(() => {
|
|
478
|
+
if (timeA11yLimitEventNonce === undefined) return;
|
|
479
|
+
const prev = prevLimitEventNonceRef.current;
|
|
480
|
+
prevLimitEventNonceRef.current = timeA11yLimitEventNonce;
|
|
481
|
+
if (timeA11yLimitEventNonce > 0 && (prev === undefined || timeA11yLimitEventNonce > prev)) {
|
|
482
|
+
suppressScrollSettleFocusNonceRef.current = timeA11yLimitEventNonce;
|
|
483
|
+
}
|
|
484
|
+
}, [timeA11yLimitEventNonce]);
|
|
485
|
+
// 限位后登记本列待聚焦项,稳定后再 requestAccessibilityFocus
|
|
486
|
+
React.useEffect(() => {
|
|
487
|
+
const req = timeA11yLimitFocus;
|
|
488
|
+
if (!req || req.columnId !== columnId) return;
|
|
489
|
+
const idx = selectedIndex;
|
|
490
|
+
const nonce = req.nonce;
|
|
491
|
+
pendingScrollSettleFocusRef.current = false;
|
|
492
|
+
pendingLimitFocusRef.current = {
|
|
493
|
+
index: idx,
|
|
494
|
+
nonce,
|
|
495
|
+
attempts: 0
|
|
496
|
+
};
|
|
497
|
+
schedulePendingLimitFocus();
|
|
498
|
+
}, [timeA11yLimitFocus, columnId, selectedIndex, schedulePendingLimitFocus]);
|
|
260
499
|
// 是否处于归中状态
|
|
261
500
|
const isCenterTimerId = React.useRef(null);
|
|
262
|
-
//
|
|
501
|
+
// 滚动静止后归中并同步选中
|
|
263
502
|
const handleScrollEnd = () => {
|
|
264
503
|
if (!scrollViewRef.current) return;
|
|
265
504
|
if (isCenterTimerId.current) {
|
|
266
505
|
clearTimeout(isCenterTimerId.current);
|
|
267
506
|
isCenterTimerId.current = null;
|
|
268
507
|
}
|
|
269
|
-
//
|
|
508
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
270
509
|
isCenterTimerId.current = setTimeout(() => {
|
|
271
510
|
if (!scrollViewRef.current) return;
|
|
272
511
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
273
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
274
|
-
|
|
275
|
-
|
|
512
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
513
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
514
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
515
|
+
isTouchingRef.current = false;
|
|
276
516
|
const isLimited = Boolean(updateIndex(newIndex, columnId, true));
|
|
277
|
-
|
|
517
|
+
const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
|
|
518
|
+
if (isLimited) {
|
|
519
|
+
pendingScrollSettleFocusRef.current = false;
|
|
520
|
+
}
|
|
278
521
|
if (!isLimited) {
|
|
279
522
|
const baseValue = newIndex * itemHeightRef.current;
|
|
280
523
|
const randomOffset = Math.random() * 0.001;
|
|
281
524
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
282
525
|
}
|
|
526
|
+
if (!isLimited && hasPendingLimitFocus) {
|
|
527
|
+
pendingScrollSettleFocusRef.current = false;
|
|
528
|
+
schedulePendingLimitFocus();
|
|
529
|
+
syncScrollFromPropsRef.current = false;
|
|
530
|
+
isCenterTimerId.current = null;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!isLimited && pendingLimitFocusRef.current == null && suppressScrollSettleFocusNonceRef.current != null) {
|
|
534
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
535
|
+
}
|
|
536
|
+
const suppressByLimitNonce = suppressScrollSettleFocusNonceRef.current != null;
|
|
537
|
+
if (!isLimited && allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
538
|
+
pendingScrollSettleFocusRef.current = false;
|
|
539
|
+
if (!suppressByLimitNonce) {
|
|
540
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
syncScrollFromPropsRef.current = false;
|
|
283
544
|
isCenterTimerId.current = null;
|
|
284
545
|
}, 100);
|
|
285
546
|
};
|
|
286
|
-
//
|
|
547
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
287
548
|
const handleScroll = () => {
|
|
288
549
|
if (!scrollViewRef.current) return;
|
|
289
550
|
if (isCenterTimerId.current) {
|
|
@@ -291,19 +552,35 @@ function PickerGroupTime(props) {
|
|
|
291
552
|
isCenterTimerId.current = null;
|
|
292
553
|
}
|
|
293
554
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
294
|
-
const
|
|
555
|
+
const ih = itemHeightRef.current;
|
|
556
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
557
|
+
const spi = selectedIndexPropRef.current;
|
|
558
|
+
if (newIndex !== spi) {
|
|
559
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
560
|
+
pendingScrollSettleFocusRef.current = true;
|
|
561
|
+
}
|
|
562
|
+
if (isTouchingRef.current) {
|
|
563
|
+
syncScrollFromPropsRef.current = false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
295
566
|
if (newIndex !== currentIndex) {
|
|
296
567
|
setCurrentIndex(newIndex);
|
|
297
568
|
}
|
|
569
|
+
if (pendingLimitFocusRef.current) {
|
|
570
|
+
schedulePendingLimitFocus();
|
|
571
|
+
}
|
|
298
572
|
};
|
|
299
573
|
// 渲染选项
|
|
300
574
|
const pickerItem = range.map((item, index) => {
|
|
301
575
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
302
|
-
return createComponent(View, {
|
|
576
|
+
return createComponent(View, mergeProps({
|
|
303
577
|
id: `picker-item-${columnId}-${index}`,
|
|
304
578
|
key: index,
|
|
305
579
|
ref: el => itemRefs.current[index] = el,
|
|
306
|
-
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}
|
|
580
|
+
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
|
|
581
|
+
}, enableClickItemScroll ? {
|
|
582
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
583
|
+
} : {}, {
|
|
307
584
|
get style() {
|
|
308
585
|
return {
|
|
309
586
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -311,7 +588,7 @@ function PickerGroupTime(props) {
|
|
|
311
588
|
};
|
|
312
589
|
},
|
|
313
590
|
children: content
|
|
314
|
-
});
|
|
591
|
+
}));
|
|
315
592
|
});
|
|
316
593
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
317
594
|
key: `blank-top-${idx}`,
|
|
@@ -326,6 +603,10 @@ function PickerGroupTime(props) {
|
|
|
326
603
|
height: PICKER_LINE_HEIGHT
|
|
327
604
|
}
|
|
328
605
|
}))];
|
|
606
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
607
|
+
scrollIntoView,
|
|
608
|
+
scrollIntoViewAlignment: 'center'
|
|
609
|
+
} : {};
|
|
329
610
|
return createComponent(View, {
|
|
330
611
|
className: "taro-picker__group",
|
|
331
612
|
get children() {
|
|
@@ -335,7 +616,7 @@ function PickerGroupTime(props) {
|
|
|
335
616
|
className: "taro-picker__indicator"
|
|
336
617
|
}, indicatorStyle ? {
|
|
337
618
|
style: indicatorStyle
|
|
338
|
-
} : {})), createComponent(ScrollView, {
|
|
619
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
339
620
|
ref: scrollViewRef,
|
|
340
621
|
scrollY: true,
|
|
341
622
|
showScrollbar: false,
|
|
@@ -343,13 +624,16 @@ function PickerGroupTime(props) {
|
|
|
343
624
|
style: {
|
|
344
625
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
345
626
|
},
|
|
346
|
-
scrollTop: targetScrollTop
|
|
627
|
+
scrollTop: targetScrollTop
|
|
628
|
+
}, clickScrollViewProps, {
|
|
347
629
|
onScroll: handleScroll,
|
|
348
|
-
onTouchStart: () =>
|
|
630
|
+
onTouchStart: () => {
|
|
631
|
+
isTouchingRef.current = true;
|
|
632
|
+
},
|
|
349
633
|
onScrollEnd: handleScrollEnd,
|
|
350
634
|
scrollWithAnimation: true,
|
|
351
635
|
children: realPickerItems
|
|
352
|
-
})];
|
|
636
|
+
}))];
|
|
353
637
|
}
|
|
354
638
|
});
|
|
355
639
|
}
|
|
@@ -360,65 +644,70 @@ function PickerGroupDate(props) {
|
|
|
360
644
|
columnId,
|
|
361
645
|
updateDay,
|
|
362
646
|
selectedIndex = 0,
|
|
363
|
-
colors = {}
|
|
647
|
+
colors = {},
|
|
648
|
+
enableClickItemScroll = true
|
|
364
649
|
} = props;
|
|
365
650
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
366
651
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
652
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
367
653
|
const scrollViewRef = React.useRef(null);
|
|
654
|
+
const itemRefs = React.useRef([]);
|
|
655
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
656
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
657
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
658
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
368
659
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
369
|
-
const
|
|
660
|
+
const isTouchingRef = React.useRef(false);
|
|
370
661
|
const lengthScaleRatioRef = React.useRef(1);
|
|
662
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
371
663
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
372
|
-
// 初始化时计算 lengthScaleRatio
|
|
664
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
373
665
|
React.useEffect(() => {
|
|
374
666
|
Taro.getSystemInfo({
|
|
375
667
|
success: res => {
|
|
376
668
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
669
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
670
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
377
671
|
},
|
|
378
672
|
fail: () => {
|
|
379
|
-
// 失败时使用默认值 1
|
|
380
673
|
lengthScaleRatioRef.current = 1;
|
|
674
|
+
useMeasuredScaleRef.current = false;
|
|
381
675
|
}
|
|
382
676
|
});
|
|
383
677
|
}, []);
|
|
384
678
|
React.useEffect(() => {
|
|
385
|
-
|
|
386
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
387
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
388
|
-
} else {
|
|
389
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
390
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
391
|
-
} else {
|
|
392
|
-
console.warn('Height measurement anomaly');
|
|
393
|
-
}
|
|
394
|
-
}
|
|
679
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
395
680
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
396
|
-
|
|
397
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
398
|
-
};
|
|
399
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
681
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
400
682
|
React.useEffect(() => {
|
|
401
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
683
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
684
|
+
syncScrollFromPropsRef.current = true;
|
|
402
685
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
403
686
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
404
687
|
setCurrentIndex(selectedIndex);
|
|
688
|
+
const tid = setTimeout(() => {
|
|
689
|
+
syncScrollFromPropsRef.current = false;
|
|
690
|
+
}, 400);
|
|
691
|
+
return () => clearTimeout(tid);
|
|
405
692
|
}
|
|
406
693
|
}, [selectedIndex, range]);
|
|
407
694
|
// 是否处于归中状态
|
|
408
695
|
const isCenterTimerId = React.useRef(null);
|
|
409
|
-
//
|
|
696
|
+
// 滚动静止后归中并同步选中
|
|
410
697
|
const handleScrollEnd = () => {
|
|
411
698
|
if (!scrollViewRef.current) return;
|
|
412
699
|
if (isCenterTimerId.current) {
|
|
413
700
|
clearTimeout(isCenterTimerId.current);
|
|
414
701
|
isCenterTimerId.current = null;
|
|
415
702
|
}
|
|
416
|
-
//
|
|
703
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
417
704
|
isCenterTimerId.current = setTimeout(() => {
|
|
418
705
|
if (!scrollViewRef.current) return;
|
|
419
706
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
420
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
421
|
-
|
|
707
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
708
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
709
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
710
|
+
isTouchingRef.current = false;
|
|
422
711
|
const baseValue = newIndex * itemHeightRef.current;
|
|
423
712
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
424
713
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -429,10 +718,15 @@ function PickerGroupDate(props) {
|
|
|
429
718
|
const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
|
|
430
719
|
updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
|
|
431
720
|
}
|
|
721
|
+
pendingScrollSettleFocusRef.current = false;
|
|
722
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
723
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
724
|
+
}
|
|
725
|
+
syncScrollFromPropsRef.current = false;
|
|
432
726
|
isCenterTimerId.current = null;
|
|
433
727
|
}, 100);
|
|
434
728
|
};
|
|
435
|
-
//
|
|
729
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
436
730
|
const handleScroll = () => {
|
|
437
731
|
if (!scrollViewRef.current) return;
|
|
438
732
|
if (isCenterTimerId.current) {
|
|
@@ -440,17 +734,31 @@ function PickerGroupDate(props) {
|
|
|
440
734
|
isCenterTimerId.current = null;
|
|
441
735
|
}
|
|
442
736
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
443
|
-
const
|
|
737
|
+
const ih = itemHeightRef.current;
|
|
738
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
739
|
+
const spi = selectedIndexPropRef.current;
|
|
740
|
+
if (newIndex !== spi) {
|
|
741
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
742
|
+
pendingScrollSettleFocusRef.current = true;
|
|
743
|
+
}
|
|
744
|
+
if (isTouchingRef.current) {
|
|
745
|
+
syncScrollFromPropsRef.current = false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
444
748
|
if (newIndex !== currentIndex) {
|
|
445
749
|
setCurrentIndex(newIndex);
|
|
446
750
|
}
|
|
447
751
|
};
|
|
448
752
|
// 渲染选项
|
|
449
753
|
const pickerItem = range.map((item, index) => {
|
|
450
|
-
return createComponent(View, {
|
|
754
|
+
return createComponent(View, mergeProps({
|
|
451
755
|
id: `picker-item-${columnId}-${index}`,
|
|
452
756
|
key: index,
|
|
453
|
-
|
|
757
|
+
ref: el => itemRefs.current[index] = el,
|
|
758
|
+
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
|
|
759
|
+
}, enableClickItemScroll ? {
|
|
760
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
761
|
+
} : {}, {
|
|
454
762
|
get style() {
|
|
455
763
|
return {
|
|
456
764
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -458,7 +766,7 @@ function PickerGroupDate(props) {
|
|
|
458
766
|
};
|
|
459
767
|
},
|
|
460
768
|
children: item
|
|
461
|
-
});
|
|
769
|
+
}));
|
|
462
770
|
});
|
|
463
771
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
464
772
|
key: `blank-top-${idx}`,
|
|
@@ -473,6 +781,10 @@ function PickerGroupDate(props) {
|
|
|
473
781
|
height: PICKER_LINE_HEIGHT
|
|
474
782
|
}
|
|
475
783
|
}))];
|
|
784
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
785
|
+
scrollIntoView,
|
|
786
|
+
scrollIntoViewAlignment: 'center'
|
|
787
|
+
} : {};
|
|
476
788
|
return createComponent(View, {
|
|
477
789
|
className: "taro-picker__group",
|
|
478
790
|
get children() {
|
|
@@ -482,7 +794,7 @@ function PickerGroupDate(props) {
|
|
|
482
794
|
className: "taro-picker__indicator"
|
|
483
795
|
}, indicatorStyle ? {
|
|
484
796
|
style: indicatorStyle
|
|
485
|
-
} : {})), createComponent(ScrollView, {
|
|
797
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
486
798
|
ref: scrollViewRef,
|
|
487
799
|
scrollY: true,
|
|
488
800
|
showScrollbar: false,
|
|
@@ -490,13 +802,16 @@ function PickerGroupDate(props) {
|
|
|
490
802
|
style: {
|
|
491
803
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
492
804
|
},
|
|
493
|
-
scrollTop: targetScrollTop
|
|
805
|
+
scrollTop: targetScrollTop
|
|
806
|
+
}, clickScrollViewProps, {
|
|
494
807
|
onScroll: handleScroll,
|
|
495
|
-
onTouchStart: () =>
|
|
808
|
+
onTouchStart: () => {
|
|
809
|
+
isTouchingRef.current = true;
|
|
810
|
+
},
|
|
496
811
|
onScrollEnd: handleScrollEnd,
|
|
497
812
|
scrollWithAnimation: true,
|
|
498
813
|
children: realPickerItems
|
|
499
|
-
})];
|
|
814
|
+
}))];
|
|
500
815
|
}
|
|
501
816
|
});
|
|
502
817
|
}
|
|
@@ -509,52 +824,54 @@ function PickerGroupRegion(props) {
|
|
|
509
824
|
updateIndex,
|
|
510
825
|
selectedIndex = 0,
|
|
511
826
|
// 使用selectedIndex参数,默认为0
|
|
512
|
-
colors = {}
|
|
827
|
+
colors = {},
|
|
828
|
+
enableClickItemScroll = true
|
|
513
829
|
} = props;
|
|
514
830
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
515
831
|
const scrollViewRef = React.useRef(null);
|
|
516
832
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
833
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
517
834
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
518
|
-
const
|
|
835
|
+
const isTouchingRef = React.useRef(false);
|
|
519
836
|
const lengthScaleRatioRef = React.useRef(1);
|
|
837
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
520
838
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
521
|
-
const
|
|
522
|
-
|
|
839
|
+
const itemRefs = React.useRef([]);
|
|
840
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
841
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
842
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
843
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
844
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
523
845
|
React.useEffect(() => {
|
|
524
846
|
Taro.getSystemInfo({
|
|
525
847
|
success: res => {
|
|
526
848
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
849
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
850
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
527
851
|
},
|
|
528
852
|
fail: () => {
|
|
529
|
-
// 失败时使用默认值 1
|
|
530
853
|
lengthScaleRatioRef.current = 1;
|
|
854
|
+
useMeasuredScaleRef.current = false;
|
|
531
855
|
}
|
|
532
856
|
});
|
|
533
857
|
}, []);
|
|
534
858
|
React.useEffect(() => {
|
|
535
|
-
|
|
536
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
537
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
538
|
-
} else {
|
|
539
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
540
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
541
|
-
} else {
|
|
542
|
-
console.warn('Height measurement anomaly');
|
|
543
|
-
}
|
|
544
|
-
}
|
|
859
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
545
860
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
546
|
-
|
|
547
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
548
|
-
};
|
|
549
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
861
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
550
862
|
React.useEffect(() => {
|
|
551
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
863
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
864
|
+
syncScrollFromPropsRef.current = true;
|
|
552
865
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
553
|
-
setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
|
|
866
|
+
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
554
867
|
setCurrentIndex(selectedIndex);
|
|
868
|
+
const tid = setTimeout(() => {
|
|
869
|
+
syncScrollFromPropsRef.current = false;
|
|
870
|
+
}, 400);
|
|
871
|
+
return () => clearTimeout(tid);
|
|
555
872
|
}
|
|
556
873
|
}, [selectedIndex, range]);
|
|
557
|
-
//
|
|
874
|
+
// 滚动静止后归中(debounce)
|
|
558
875
|
const isCenterTimerId = React.useRef(null);
|
|
559
876
|
const handleScrollEnd = () => {
|
|
560
877
|
if (!scrollViewRef.current) return;
|
|
@@ -562,19 +879,27 @@ function PickerGroupRegion(props) {
|
|
|
562
879
|
clearTimeout(isCenterTimerId.current);
|
|
563
880
|
isCenterTimerId.current = null;
|
|
564
881
|
}
|
|
565
|
-
//
|
|
882
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
566
883
|
isCenterTimerId.current = setTimeout(() => {
|
|
567
884
|
if (!scrollViewRef.current) return;
|
|
568
885
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
569
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
570
|
-
|
|
886
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
887
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
888
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
889
|
+
isTouchingRef.current = false;
|
|
571
890
|
const baseValue = newIndex * itemHeightRef.current;
|
|
572
891
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
573
892
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
574
|
-
updateIndex(newIndex, columnId, false,
|
|
893
|
+
updateIndex(newIndex, columnId, false, allowA11yFocus);
|
|
894
|
+
pendingScrollSettleFocusRef.current = false;
|
|
895
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
896
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
897
|
+
}
|
|
898
|
+
syncScrollFromPropsRef.current = false;
|
|
899
|
+
isCenterTimerId.current = null;
|
|
575
900
|
}, 100);
|
|
576
901
|
};
|
|
577
|
-
//
|
|
902
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
578
903
|
const handleScroll = () => {
|
|
579
904
|
if (!scrollViewRef.current) return;
|
|
580
905
|
if (isCenterTimerId.current) {
|
|
@@ -582,7 +907,17 @@ function PickerGroupRegion(props) {
|
|
|
582
907
|
isCenterTimerId.current = null;
|
|
583
908
|
}
|
|
584
909
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
585
|
-
const
|
|
910
|
+
const ih = itemHeightRef.current;
|
|
911
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
912
|
+
const spi = selectedIndexPropRef.current;
|
|
913
|
+
if (newIndex !== spi) {
|
|
914
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
915
|
+
pendingScrollSettleFocusRef.current = true;
|
|
916
|
+
}
|
|
917
|
+
if (isTouchingRef.current) {
|
|
918
|
+
syncScrollFromPropsRef.current = false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
586
921
|
if (newIndex !== currentIndex) {
|
|
587
922
|
setCurrentIndex(newIndex);
|
|
588
923
|
}
|
|
@@ -590,10 +925,14 @@ function PickerGroupRegion(props) {
|
|
|
590
925
|
// 渲染选项
|
|
591
926
|
const pickerItem = range.map((item, index) => {
|
|
592
927
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
593
|
-
return createComponent(View, {
|
|
928
|
+
return createComponent(View, mergeProps({
|
|
594
929
|
id: `picker-item-${columnId}-${index}`,
|
|
595
930
|
key: index,
|
|
596
|
-
|
|
931
|
+
ref: el => itemRefs.current[index] = el,
|
|
932
|
+
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
|
|
933
|
+
}, enableClickItemScroll ? {
|
|
934
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
935
|
+
} : {}, {
|
|
597
936
|
get style() {
|
|
598
937
|
return {
|
|
599
938
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -601,7 +940,7 @@ function PickerGroupRegion(props) {
|
|
|
601
940
|
};
|
|
602
941
|
},
|
|
603
942
|
children: content
|
|
604
|
-
});
|
|
943
|
+
}));
|
|
605
944
|
});
|
|
606
945
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
607
946
|
key: `blank-top-${idx}`,
|
|
@@ -616,6 +955,10 @@ function PickerGroupRegion(props) {
|
|
|
616
955
|
height: PICKER_LINE_HEIGHT
|
|
617
956
|
}
|
|
618
957
|
}))];
|
|
958
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
959
|
+
scrollIntoView,
|
|
960
|
+
scrollIntoViewAlignment: 'center'
|
|
961
|
+
} : {};
|
|
619
962
|
return createComponent(View, {
|
|
620
963
|
className: "taro-picker__group",
|
|
621
964
|
get children() {
|
|
@@ -625,7 +968,7 @@ function PickerGroupRegion(props) {
|
|
|
625
968
|
className: "taro-picker__indicator"
|
|
626
969
|
}, indicatorStyle ? {
|
|
627
970
|
style: indicatorStyle
|
|
628
|
-
} : {})), createComponent(ScrollView, {
|
|
971
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
629
972
|
ref: scrollViewRef,
|
|
630
973
|
scrollY: true,
|
|
631
974
|
showScrollbar: false,
|
|
@@ -633,16 +976,16 @@ function PickerGroupRegion(props) {
|
|
|
633
976
|
style: {
|
|
634
977
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
635
978
|
},
|
|
636
|
-
scrollTop: targetScrollTop
|
|
979
|
+
scrollTop: targetScrollTop
|
|
980
|
+
}, clickScrollViewProps, {
|
|
637
981
|
onScroll: handleScroll,
|
|
638
982
|
onTouchStart: () => {
|
|
639
|
-
|
|
640
|
-
isUserBeginScrollRef.current = true;
|
|
983
|
+
isTouchingRef.current = true;
|
|
641
984
|
},
|
|
642
985
|
onScrollEnd: handleScrollEnd,
|
|
643
986
|
scrollWithAnimation: true,
|
|
644
987
|
children: realPickerItems
|
|
645
|
-
})];
|
|
988
|
+
}))];
|
|
646
989
|
}
|
|
647
990
|
});
|
|
648
991
|
}
|