@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,32 @@ import Taro from '@tarojs/taro';
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
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
|
+
}
|
|
31
|
+
// 定义常量
|
|
6
32
|
const PICKER_LINE_HEIGHT = 34; // px
|
|
7
33
|
const PICKER_VISIBLE_ITEMS = 7; // 可见行数
|
|
8
34
|
const PICKER_BLANK_ITEMS = 3; // 空白行数
|
|
@@ -12,6 +38,80 @@ const getIndicatorStyle = lineColor => {
|
|
|
12
38
|
borderBottomColor: lineColor
|
|
13
39
|
};
|
|
14
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
|
+
};
|
|
15
115
|
// 辅助函数:计算 lengthScaleRatio
|
|
16
116
|
const calculateLengthScaleRatio = res => {
|
|
17
117
|
let lengthScaleRatio = res === null || res === void 0 ? void 0 : res.lengthScaleRatio;
|
|
@@ -50,6 +150,29 @@ const setTargetScrollTopWithScale = function (setTargetScrollTop, baseValue, ran
|
|
|
50
150
|
setTargetScrollTop(finalValue);
|
|
51
151
|
}
|
|
52
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
|
+
};
|
|
53
176
|
function PickerGroupBasic(props) {
|
|
54
177
|
const {
|
|
55
178
|
range = [],
|
|
@@ -59,69 +182,72 @@ function PickerGroupBasic(props) {
|
|
|
59
182
|
onColumnChange,
|
|
60
183
|
selectedIndex = 0,
|
|
61
184
|
// 使用selectedIndex参数,默认为0
|
|
62
|
-
colors = {}
|
|
185
|
+
colors = {},
|
|
186
|
+
enableClickItemScroll = true
|
|
63
187
|
} = props;
|
|
64
188
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
65
189
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
190
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
66
191
|
const scrollViewRef = React.useRef(null);
|
|
67
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);
|
|
68
197
|
// 使用selectedIndex初始化当前索引
|
|
69
198
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
70
199
|
// 触摸状态用于优化用户体验
|
|
71
|
-
const
|
|
200
|
+
const isTouchingRef = React.useRef(false);
|
|
72
201
|
const lengthScaleRatioRef = React.useRef(1);
|
|
202
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
73
203
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
74
|
-
// 初始化时计算 lengthScaleRatio
|
|
204
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
75
205
|
React.useEffect(() => {
|
|
76
206
|
Taro.getSystemInfo({
|
|
77
207
|
success: res => {
|
|
78
208
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
209
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
210
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
79
211
|
},
|
|
80
212
|
fail: () => {
|
|
81
|
-
// 失败时使用默认值 1
|
|
82
213
|
lengthScaleRatioRef.current = 1;
|
|
214
|
+
useMeasuredScaleRef.current = false;
|
|
83
215
|
}
|
|
84
216
|
});
|
|
85
217
|
}, []);
|
|
86
218
|
React.useEffect(() => {
|
|
87
|
-
|
|
88
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
89
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
90
|
-
} else {
|
|
91
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
92
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
93
|
-
} else {
|
|
94
|
-
console.warn('Height measurement anomaly');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
219
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
97
220
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
98
|
-
//
|
|
99
|
-
const getSelectedIndex = scrollTop => {
|
|
100
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
101
|
-
};
|
|
102
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
221
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
103
222
|
React.useEffect(() => {
|
|
104
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
223
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
224
|
+
syncScrollFromPropsRef.current = true;
|
|
105
225
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
106
226
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
107
227
|
setCurrentIndex(selectedIndex);
|
|
228
|
+
const tid = setTimeout(() => {
|
|
229
|
+
syncScrollFromPropsRef.current = false;
|
|
230
|
+
}, 400);
|
|
231
|
+
return () => clearTimeout(tid);
|
|
108
232
|
}
|
|
109
233
|
}, [selectedIndex, range]);
|
|
110
234
|
// 是否处于归中状态
|
|
111
235
|
const isCenterTimerId = React.useRef(null);
|
|
112
|
-
//
|
|
236
|
+
// 滚动静止后归中并同步选中
|
|
113
237
|
const handleScrollEnd = () => {
|
|
114
238
|
if (!scrollViewRef.current) return;
|
|
115
239
|
if (isCenterTimerId.current) {
|
|
116
240
|
clearTimeout(isCenterTimerId.current);
|
|
117
241
|
isCenterTimerId.current = null;
|
|
118
242
|
}
|
|
119
|
-
//
|
|
243
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
120
244
|
isCenterTimerId.current = setTimeout(() => {
|
|
121
245
|
if (!scrollViewRef.current) return;
|
|
122
246
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
123
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
124
|
-
|
|
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;
|
|
125
251
|
const baseValue = newIndex * itemHeightRef.current;
|
|
126
252
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
127
253
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -130,10 +256,15 @@ function PickerGroupBasic(props) {
|
|
|
130
256
|
columnId,
|
|
131
257
|
index: newIndex
|
|
132
258
|
});
|
|
259
|
+
pendingScrollSettleFocusRef.current = false;
|
|
260
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
261
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
262
|
+
}
|
|
263
|
+
syncScrollFromPropsRef.current = false;
|
|
133
264
|
isCenterTimerId.current = null;
|
|
134
265
|
}, 100);
|
|
135
266
|
};
|
|
136
|
-
//
|
|
267
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
137
268
|
const handleScroll = () => {
|
|
138
269
|
if (!scrollViewRef.current) return;
|
|
139
270
|
if (isCenterTimerId.current) {
|
|
@@ -141,7 +272,17 @@ function PickerGroupBasic(props) {
|
|
|
141
272
|
isCenterTimerId.current = null;
|
|
142
273
|
}
|
|
143
274
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
144
|
-
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
|
+
}
|
|
145
286
|
if (newIndex !== currentIndex) {
|
|
146
287
|
setCurrentIndex(newIndex);
|
|
147
288
|
}
|
|
@@ -153,6 +294,9 @@ function PickerGroupBasic(props) {
|
|
|
153
294
|
id: `picker-item-${columnId}-${index}`,
|
|
154
295
|
ref: el => itemRefs.current[index] = el,
|
|
155
296
|
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
|
|
297
|
+
...(enableClickItemScroll ? {
|
|
298
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
299
|
+
} : {}),
|
|
156
300
|
style: {
|
|
157
301
|
height: PICKER_LINE_HEIGHT,
|
|
158
302
|
color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
|
|
@@ -171,6 +315,10 @@ function PickerGroupBasic(props) {
|
|
|
171
315
|
height: PICKER_LINE_HEIGHT
|
|
172
316
|
}
|
|
173
317
|
}, `blank-bottom-${idx}`))];
|
|
318
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
319
|
+
scrollIntoView,
|
|
320
|
+
scrollIntoViewAlignment: 'center'
|
|
321
|
+
} : {};
|
|
174
322
|
return /*#__PURE__*/jsxs(View, {
|
|
175
323
|
className: "taro-picker__group",
|
|
176
324
|
children: [/*#__PURE__*/jsx(View, {
|
|
@@ -189,8 +337,11 @@ function PickerGroupBasic(props) {
|
|
|
189
337
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
190
338
|
},
|
|
191
339
|
scrollTop: targetScrollTop,
|
|
340
|
+
...clickScrollViewProps,
|
|
192
341
|
onScroll: handleScroll,
|
|
193
|
-
onTouchStart: () =>
|
|
342
|
+
onTouchStart: () => {
|
|
343
|
+
isTouchingRef.current = true;
|
|
344
|
+
},
|
|
194
345
|
onScrollEnd: handleScrollEnd,
|
|
195
346
|
scrollWithAnimation: true,
|
|
196
347
|
children: realPickerItems
|
|
@@ -205,78 +356,189 @@ function PickerGroupTime(props) {
|
|
|
205
356
|
columnId,
|
|
206
357
|
updateIndex,
|
|
207
358
|
selectedIndex = 0,
|
|
208
|
-
colors = {}
|
|
359
|
+
colors = {},
|
|
360
|
+
timeA11yLimitFocus,
|
|
361
|
+
onTimeA11yLimitFocusConsumed,
|
|
362
|
+
timeA11yLimitEventNonce,
|
|
363
|
+
enableClickItemScroll = true
|
|
209
364
|
} = props;
|
|
210
365
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
211
366
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
367
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
212
368
|
const scrollViewRef = React.useRef(null);
|
|
213
369
|
const itemRefs = React.useRef([]);
|
|
370
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
371
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
372
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
373
|
+
const prevLimitEventNonceRef = React.useRef(undefined);
|
|
374
|
+
const suppressScrollSettleFocusNonceRef = React.useRef(null);
|
|
375
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
376
|
+
const pendingLimitFocusRef = React.useRef(null);
|
|
377
|
+
const limitFocusTimerRef = React.useRef(null);
|
|
214
378
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
215
|
-
const
|
|
379
|
+
const isTouchingRef = React.useRef(false);
|
|
216
380
|
const lengthScaleRatioRef = React.useRef(1);
|
|
381
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
217
382
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
218
|
-
|
|
383
|
+
const clearLimitFocusTimer = React.useCallback(() => {
|
|
384
|
+
if (limitFocusTimerRef.current) {
|
|
385
|
+
clearTimeout(limitFocusTimerRef.current);
|
|
386
|
+
limitFocusTimerRef.current = null;
|
|
387
|
+
}
|
|
388
|
+
}, []);
|
|
389
|
+
const tryFocusPendingLimit = React.useCallback(() => {
|
|
390
|
+
const pending = pendingLimitFocusRef.current;
|
|
391
|
+
const scrollView = scrollViewRef.current;
|
|
392
|
+
if (!pending || !scrollView) return;
|
|
393
|
+
const visualIndex = getSelectedIndex(scrollView.scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
394
|
+
const isStable = visualIndex === pending.index;
|
|
395
|
+
if (!isStable) {
|
|
396
|
+
if (pending.attempts >= 20) {
|
|
397
|
+
pendingLimitFocusRef.current = null;
|
|
398
|
+
if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
|
|
399
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
400
|
+
}
|
|
401
|
+
onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
pending.attempts += 1;
|
|
405
|
+
clearLimitFocusTimer();
|
|
406
|
+
limitFocusTimerRef.current = setTimeout(() => {
|
|
407
|
+
tryFocusPendingLimit();
|
|
408
|
+
}, 50);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const node = itemRefs.current[pending.index];
|
|
412
|
+
pendingLimitFocusRef.current = null;
|
|
413
|
+
if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
|
|
414
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
415
|
+
}
|
|
416
|
+
clearLimitFocusTimer();
|
|
417
|
+
requestAccessibilityFocusOnView(node);
|
|
418
|
+
onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
|
|
419
|
+
}, [clearLimitFocusTimer, onTimeA11yLimitFocusConsumed]);
|
|
420
|
+
const schedulePendingLimitFocus = React.useCallback(() => {
|
|
421
|
+
if (!pendingLimitFocusRef.current) return;
|
|
422
|
+
clearLimitFocusTimer();
|
|
423
|
+
limitFocusTimerRef.current = setTimeout(() => {
|
|
424
|
+
tryFocusPendingLimit();
|
|
425
|
+
}, 100);
|
|
426
|
+
}, [clearLimitFocusTimer, tryFocusPendingLimit]);
|
|
427
|
+
React.useEffect(() => clearLimitFocusTimer, [clearLimitFocusTimer]);
|
|
428
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
219
429
|
React.useEffect(() => {
|
|
220
430
|
Taro.getSystemInfo({
|
|
221
431
|
success: res => {
|
|
222
432
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
433
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
434
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
223
435
|
},
|
|
224
436
|
fail: () => {
|
|
225
|
-
// 失败时使用默认值 1
|
|
226
437
|
lengthScaleRatioRef.current = 1;
|
|
438
|
+
useMeasuredScaleRef.current = false;
|
|
227
439
|
}
|
|
228
440
|
});
|
|
229
441
|
}, []);
|
|
230
442
|
React.useEffect(() => {
|
|
231
|
-
|
|
232
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
233
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
234
|
-
} else {
|
|
235
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
236
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
237
|
-
} else {
|
|
238
|
-
console.warn('Height measurement anomaly');
|
|
239
|
-
}
|
|
240
|
-
}
|
|
443
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
241
444
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
242
|
-
|
|
243
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
244
|
-
};
|
|
245
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
445
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
246
446
|
React.useEffect(() => {
|
|
247
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
447
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
448
|
+
syncScrollFromPropsRef.current = true;
|
|
248
449
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
249
450
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
250
451
|
setCurrentIndex(selectedIndex);
|
|
452
|
+
const tid = setTimeout(() => {
|
|
453
|
+
syncScrollFromPropsRef.current = false;
|
|
454
|
+
}, 400);
|
|
455
|
+
return () => clearTimeout(tid);
|
|
251
456
|
}
|
|
252
457
|
}, [selectedIndex, range]);
|
|
458
|
+
// time 限位 nonce 变更:强制对齐 scrollTop(避免 remount 打断读屏焦点)
|
|
459
|
+
React.useEffect(() => {
|
|
460
|
+
if (!timeA11yLimitEventNonce) return;
|
|
461
|
+
if (!scrollViewRef.current || range.length === 0 || isTouchingRef.current) return;
|
|
462
|
+
syncScrollFromPropsRef.current = true;
|
|
463
|
+
const baseValue = selectedIndex * itemHeightRef.current;
|
|
464
|
+
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, Math.random() * 0.001, lengthScaleRatioRef.current);
|
|
465
|
+
setCurrentIndex(selectedIndex);
|
|
466
|
+
const tid = setTimeout(() => {
|
|
467
|
+
syncScrollFromPropsRef.current = false;
|
|
468
|
+
}, 400);
|
|
469
|
+
return () => clearTimeout(tid);
|
|
470
|
+
}, [timeA11yLimitEventNonce]); // 仅依赖 nonce,其余用 ref
|
|
471
|
+
React.useLayoutEffect(() => {
|
|
472
|
+
if (timeA11yLimitEventNonce === undefined) return;
|
|
473
|
+
const prev = prevLimitEventNonceRef.current;
|
|
474
|
+
prevLimitEventNonceRef.current = timeA11yLimitEventNonce;
|
|
475
|
+
if (timeA11yLimitEventNonce > 0 && (prev === undefined || timeA11yLimitEventNonce > prev)) {
|
|
476
|
+
suppressScrollSettleFocusNonceRef.current = timeA11yLimitEventNonce;
|
|
477
|
+
}
|
|
478
|
+
}, [timeA11yLimitEventNonce]);
|
|
479
|
+
// 限位后登记本列待聚焦项,稳定后再 requestAccessibilityFocus
|
|
480
|
+
React.useEffect(() => {
|
|
481
|
+
const req = timeA11yLimitFocus;
|
|
482
|
+
if (!req || req.columnId !== columnId) return;
|
|
483
|
+
const idx = selectedIndex;
|
|
484
|
+
const nonce = req.nonce;
|
|
485
|
+
pendingScrollSettleFocusRef.current = false;
|
|
486
|
+
pendingLimitFocusRef.current = {
|
|
487
|
+
index: idx,
|
|
488
|
+
nonce,
|
|
489
|
+
attempts: 0
|
|
490
|
+
};
|
|
491
|
+
schedulePendingLimitFocus();
|
|
492
|
+
}, [timeA11yLimitFocus, columnId, selectedIndex, schedulePendingLimitFocus]);
|
|
253
493
|
// 是否处于归中状态
|
|
254
494
|
const isCenterTimerId = React.useRef(null);
|
|
255
|
-
//
|
|
495
|
+
// 滚动静止后归中并同步选中
|
|
256
496
|
const handleScrollEnd = () => {
|
|
257
497
|
if (!scrollViewRef.current) return;
|
|
258
498
|
if (isCenterTimerId.current) {
|
|
259
499
|
clearTimeout(isCenterTimerId.current);
|
|
260
500
|
isCenterTimerId.current = null;
|
|
261
501
|
}
|
|
262
|
-
//
|
|
502
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
263
503
|
isCenterTimerId.current = setTimeout(() => {
|
|
264
504
|
if (!scrollViewRef.current) return;
|
|
265
505
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
266
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
267
|
-
|
|
268
|
-
|
|
506
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
507
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
508
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
509
|
+
isTouchingRef.current = false;
|
|
269
510
|
const isLimited = Boolean(updateIndex(newIndex, columnId, true));
|
|
270
|
-
|
|
511
|
+
const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
|
|
512
|
+
if (isLimited) {
|
|
513
|
+
pendingScrollSettleFocusRef.current = false;
|
|
514
|
+
}
|
|
271
515
|
if (!isLimited) {
|
|
272
516
|
const baseValue = newIndex * itemHeightRef.current;
|
|
273
517
|
const randomOffset = Math.random() * 0.001;
|
|
274
518
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
275
519
|
}
|
|
520
|
+
if (!isLimited && hasPendingLimitFocus) {
|
|
521
|
+
pendingScrollSettleFocusRef.current = false;
|
|
522
|
+
schedulePendingLimitFocus();
|
|
523
|
+
syncScrollFromPropsRef.current = false;
|
|
524
|
+
isCenterTimerId.current = null;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (!isLimited && pendingLimitFocusRef.current == null && suppressScrollSettleFocusNonceRef.current != null) {
|
|
528
|
+
suppressScrollSettleFocusNonceRef.current = null;
|
|
529
|
+
}
|
|
530
|
+
const suppressByLimitNonce = suppressScrollSettleFocusNonceRef.current != null;
|
|
531
|
+
if (!isLimited && allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
532
|
+
pendingScrollSettleFocusRef.current = false;
|
|
533
|
+
if (!suppressByLimitNonce) {
|
|
534
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
syncScrollFromPropsRef.current = false;
|
|
276
538
|
isCenterTimerId.current = null;
|
|
277
539
|
}, 100);
|
|
278
540
|
};
|
|
279
|
-
//
|
|
541
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
280
542
|
const handleScroll = () => {
|
|
281
543
|
if (!scrollViewRef.current) return;
|
|
282
544
|
if (isCenterTimerId.current) {
|
|
@@ -284,10 +546,23 @@ function PickerGroupTime(props) {
|
|
|
284
546
|
isCenterTimerId.current = null;
|
|
285
547
|
}
|
|
286
548
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
287
|
-
const
|
|
549
|
+
const ih = itemHeightRef.current;
|
|
550
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
551
|
+
const spi = selectedIndexPropRef.current;
|
|
552
|
+
if (newIndex !== spi) {
|
|
553
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
554
|
+
pendingScrollSettleFocusRef.current = true;
|
|
555
|
+
}
|
|
556
|
+
if (isTouchingRef.current) {
|
|
557
|
+
syncScrollFromPropsRef.current = false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
288
560
|
if (newIndex !== currentIndex) {
|
|
289
561
|
setCurrentIndex(newIndex);
|
|
290
562
|
}
|
|
563
|
+
if (pendingLimitFocusRef.current) {
|
|
564
|
+
schedulePendingLimitFocus();
|
|
565
|
+
}
|
|
291
566
|
};
|
|
292
567
|
// 渲染选项
|
|
293
568
|
const pickerItem = range.map((item, index) => {
|
|
@@ -296,6 +571,9 @@ function PickerGroupTime(props) {
|
|
|
296
571
|
id: `picker-item-${columnId}-${index}`,
|
|
297
572
|
ref: el => itemRefs.current[index] = el,
|
|
298
573
|
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
|
|
574
|
+
...(enableClickItemScroll ? {
|
|
575
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
576
|
+
} : {}),
|
|
299
577
|
style: {
|
|
300
578
|
height: PICKER_LINE_HEIGHT,
|
|
301
579
|
color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
|
|
@@ -314,6 +592,10 @@ function PickerGroupTime(props) {
|
|
|
314
592
|
height: PICKER_LINE_HEIGHT
|
|
315
593
|
}
|
|
316
594
|
}, `blank-bottom-${idx}`))];
|
|
595
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
596
|
+
scrollIntoView,
|
|
597
|
+
scrollIntoViewAlignment: 'center'
|
|
598
|
+
} : {};
|
|
317
599
|
return /*#__PURE__*/jsxs(View, {
|
|
318
600
|
className: "taro-picker__group",
|
|
319
601
|
children: [/*#__PURE__*/jsx(View, {
|
|
@@ -332,8 +614,11 @@ function PickerGroupTime(props) {
|
|
|
332
614
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
333
615
|
},
|
|
334
616
|
scrollTop: targetScrollTop,
|
|
617
|
+
...clickScrollViewProps,
|
|
335
618
|
onScroll: handleScroll,
|
|
336
|
-
onTouchStart: () =>
|
|
619
|
+
onTouchStart: () => {
|
|
620
|
+
isTouchingRef.current = true;
|
|
621
|
+
},
|
|
337
622
|
onScrollEnd: handleScrollEnd,
|
|
338
623
|
scrollWithAnimation: true,
|
|
339
624
|
children: realPickerItems
|
|
@@ -347,65 +632,70 @@ function PickerGroupDate(props) {
|
|
|
347
632
|
columnId,
|
|
348
633
|
updateDay,
|
|
349
634
|
selectedIndex = 0,
|
|
350
|
-
colors = {}
|
|
635
|
+
colors = {},
|
|
636
|
+
enableClickItemScroll = true
|
|
351
637
|
} = props;
|
|
352
638
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
353
639
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
640
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
354
641
|
const scrollViewRef = React.useRef(null);
|
|
642
|
+
const itemRefs = React.useRef([]);
|
|
643
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
644
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
645
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
646
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
355
647
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
356
|
-
const
|
|
648
|
+
const isTouchingRef = React.useRef(false);
|
|
357
649
|
const lengthScaleRatioRef = React.useRef(1);
|
|
650
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
358
651
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
359
|
-
// 初始化时计算 lengthScaleRatio
|
|
652
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
360
653
|
React.useEffect(() => {
|
|
361
654
|
Taro.getSystemInfo({
|
|
362
655
|
success: res => {
|
|
363
656
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
657
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
658
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
364
659
|
},
|
|
365
660
|
fail: () => {
|
|
366
|
-
// 失败时使用默认值 1
|
|
367
661
|
lengthScaleRatioRef.current = 1;
|
|
662
|
+
useMeasuredScaleRef.current = false;
|
|
368
663
|
}
|
|
369
664
|
});
|
|
370
665
|
}, []);
|
|
371
666
|
React.useEffect(() => {
|
|
372
|
-
|
|
373
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
374
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
375
|
-
} else {
|
|
376
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
377
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
378
|
-
} else {
|
|
379
|
-
console.warn('Height measurement anomaly');
|
|
380
|
-
}
|
|
381
|
-
}
|
|
667
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
382
668
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
383
|
-
|
|
384
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
385
|
-
};
|
|
386
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
669
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
387
670
|
React.useEffect(() => {
|
|
388
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
671
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
672
|
+
syncScrollFromPropsRef.current = true;
|
|
389
673
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
390
674
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
391
675
|
setCurrentIndex(selectedIndex);
|
|
676
|
+
const tid = setTimeout(() => {
|
|
677
|
+
syncScrollFromPropsRef.current = false;
|
|
678
|
+
}, 400);
|
|
679
|
+
return () => clearTimeout(tid);
|
|
392
680
|
}
|
|
393
681
|
}, [selectedIndex, range]);
|
|
394
682
|
// 是否处于归中状态
|
|
395
683
|
const isCenterTimerId = React.useRef(null);
|
|
396
|
-
//
|
|
684
|
+
// 滚动静止后归中并同步选中
|
|
397
685
|
const handleScrollEnd = () => {
|
|
398
686
|
if (!scrollViewRef.current) return;
|
|
399
687
|
if (isCenterTimerId.current) {
|
|
400
688
|
clearTimeout(isCenterTimerId.current);
|
|
401
689
|
isCenterTimerId.current = null;
|
|
402
690
|
}
|
|
403
|
-
//
|
|
691
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
404
692
|
isCenterTimerId.current = setTimeout(() => {
|
|
405
693
|
if (!scrollViewRef.current) return;
|
|
406
694
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
407
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
408
|
-
|
|
695
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
696
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
697
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
698
|
+
isTouchingRef.current = false;
|
|
409
699
|
const baseValue = newIndex * itemHeightRef.current;
|
|
410
700
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
411
701
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -416,10 +706,15 @@ function PickerGroupDate(props) {
|
|
|
416
706
|
const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
|
|
417
707
|
updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
|
|
418
708
|
}
|
|
709
|
+
pendingScrollSettleFocusRef.current = false;
|
|
710
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
711
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
712
|
+
}
|
|
713
|
+
syncScrollFromPropsRef.current = false;
|
|
419
714
|
isCenterTimerId.current = null;
|
|
420
715
|
}, 100);
|
|
421
716
|
};
|
|
422
|
-
//
|
|
717
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
423
718
|
const handleScroll = () => {
|
|
424
719
|
if (!scrollViewRef.current) return;
|
|
425
720
|
if (isCenterTimerId.current) {
|
|
@@ -427,7 +722,17 @@ function PickerGroupDate(props) {
|
|
|
427
722
|
isCenterTimerId.current = null;
|
|
428
723
|
}
|
|
429
724
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
430
|
-
const
|
|
725
|
+
const ih = itemHeightRef.current;
|
|
726
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
727
|
+
const spi = selectedIndexPropRef.current;
|
|
728
|
+
if (newIndex !== spi) {
|
|
729
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
730
|
+
pendingScrollSettleFocusRef.current = true;
|
|
731
|
+
}
|
|
732
|
+
if (isTouchingRef.current) {
|
|
733
|
+
syncScrollFromPropsRef.current = false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
431
736
|
if (newIndex !== currentIndex) {
|
|
432
737
|
setCurrentIndex(newIndex);
|
|
433
738
|
}
|
|
@@ -436,7 +741,11 @@ function PickerGroupDate(props) {
|
|
|
436
741
|
const pickerItem = range.map((item, index) => {
|
|
437
742
|
return /*#__PURE__*/jsx(View, {
|
|
438
743
|
id: `picker-item-${columnId}-${index}`,
|
|
744
|
+
ref: el => itemRefs.current[index] = el,
|
|
439
745
|
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
|
|
746
|
+
...(enableClickItemScroll ? {
|
|
747
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
748
|
+
} : {}),
|
|
440
749
|
style: {
|
|
441
750
|
height: PICKER_LINE_HEIGHT,
|
|
442
751
|
color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
|
|
@@ -455,6 +764,10 @@ function PickerGroupDate(props) {
|
|
|
455
764
|
height: PICKER_LINE_HEIGHT
|
|
456
765
|
}
|
|
457
766
|
}, `blank-bottom-${idx}`))];
|
|
767
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
768
|
+
scrollIntoView,
|
|
769
|
+
scrollIntoViewAlignment: 'center'
|
|
770
|
+
} : {};
|
|
458
771
|
return /*#__PURE__*/jsxs(View, {
|
|
459
772
|
className: "taro-picker__group",
|
|
460
773
|
children: [/*#__PURE__*/jsx(View, {
|
|
@@ -473,8 +786,11 @@ function PickerGroupDate(props) {
|
|
|
473
786
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
474
787
|
},
|
|
475
788
|
scrollTop: targetScrollTop,
|
|
789
|
+
...clickScrollViewProps,
|
|
476
790
|
onScroll: handleScroll,
|
|
477
|
-
onTouchStart: () =>
|
|
791
|
+
onTouchStart: () => {
|
|
792
|
+
isTouchingRef.current = true;
|
|
793
|
+
},
|
|
478
794
|
onScrollEnd: handleScrollEnd,
|
|
479
795
|
scrollWithAnimation: true,
|
|
480
796
|
children: realPickerItems
|
|
@@ -490,52 +806,54 @@ function PickerGroupRegion(props) {
|
|
|
490
806
|
updateIndex,
|
|
491
807
|
selectedIndex = 0,
|
|
492
808
|
// 使用selectedIndex参数,默认为0
|
|
493
|
-
colors = {}
|
|
809
|
+
colors = {},
|
|
810
|
+
enableClickItemScroll = true
|
|
494
811
|
} = props;
|
|
495
812
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
496
813
|
const scrollViewRef = React.useRef(null);
|
|
497
814
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
815
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
498
816
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
499
|
-
const
|
|
817
|
+
const isTouchingRef = React.useRef(false);
|
|
500
818
|
const lengthScaleRatioRef = React.useRef(1);
|
|
819
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
501
820
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
502
|
-
const
|
|
503
|
-
|
|
821
|
+
const itemRefs = React.useRef([]);
|
|
822
|
+
const selectedIndexPropRef = React.useRef(selectedIndex);
|
|
823
|
+
selectedIndexPropRef.current = selectedIndex;
|
|
824
|
+
const syncScrollFromPropsRef = React.useRef(false);
|
|
825
|
+
const pendingScrollSettleFocusRef = React.useRef(false);
|
|
826
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
504
827
|
React.useEffect(() => {
|
|
505
828
|
Taro.getSystemInfo({
|
|
506
829
|
success: res => {
|
|
507
830
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
831
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
832
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
508
833
|
},
|
|
509
834
|
fail: () => {
|
|
510
|
-
// 失败时使用默认值 1
|
|
511
835
|
lengthScaleRatioRef.current = 1;
|
|
836
|
+
useMeasuredScaleRef.current = false;
|
|
512
837
|
}
|
|
513
838
|
});
|
|
514
839
|
}, []);
|
|
515
840
|
React.useEffect(() => {
|
|
516
|
-
|
|
517
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
518
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
519
|
-
} else {
|
|
520
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
521
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
522
|
-
} else {
|
|
523
|
-
console.warn('Height measurement anomaly');
|
|
524
|
-
}
|
|
525
|
-
}
|
|
841
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
526
842
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
527
|
-
|
|
528
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
529
|
-
};
|
|
530
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
843
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
531
844
|
React.useEffect(() => {
|
|
532
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
845
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
846
|
+
syncScrollFromPropsRef.current = true;
|
|
533
847
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
534
|
-
setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
|
|
848
|
+
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
535
849
|
setCurrentIndex(selectedIndex);
|
|
850
|
+
const tid = setTimeout(() => {
|
|
851
|
+
syncScrollFromPropsRef.current = false;
|
|
852
|
+
}, 400);
|
|
853
|
+
return () => clearTimeout(tid);
|
|
536
854
|
}
|
|
537
855
|
}, [selectedIndex, range]);
|
|
538
|
-
//
|
|
856
|
+
// 滚动静止后归中(debounce)
|
|
539
857
|
const isCenterTimerId = React.useRef(null);
|
|
540
858
|
const handleScrollEnd = () => {
|
|
541
859
|
if (!scrollViewRef.current) return;
|
|
@@ -543,19 +861,27 @@ function PickerGroupRegion(props) {
|
|
|
543
861
|
clearTimeout(isCenterTimerId.current);
|
|
544
862
|
isCenterTimerId.current = null;
|
|
545
863
|
}
|
|
546
|
-
//
|
|
864
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
547
865
|
isCenterTimerId.current = setTimeout(() => {
|
|
548
866
|
if (!scrollViewRef.current) return;
|
|
549
867
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
550
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
551
|
-
|
|
868
|
+
const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
869
|
+
const allowA11yFocus = !syncScrollFromPropsRef.current;
|
|
870
|
+
const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
|
|
871
|
+
isTouchingRef.current = false;
|
|
552
872
|
const baseValue = newIndex * itemHeightRef.current;
|
|
553
873
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
554
874
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
555
|
-
updateIndex(newIndex, columnId, false,
|
|
875
|
+
updateIndex(newIndex, columnId, false, allowA11yFocus);
|
|
876
|
+
pendingScrollSettleFocusRef.current = false;
|
|
877
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
878
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
879
|
+
}
|
|
880
|
+
syncScrollFromPropsRef.current = false;
|
|
881
|
+
isCenterTimerId.current = null;
|
|
556
882
|
}, 100);
|
|
557
883
|
};
|
|
558
|
-
//
|
|
884
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
559
885
|
const handleScroll = () => {
|
|
560
886
|
if (!scrollViewRef.current) return;
|
|
561
887
|
if (isCenterTimerId.current) {
|
|
@@ -563,7 +889,17 @@ function PickerGroupRegion(props) {
|
|
|
563
889
|
isCenterTimerId.current = null;
|
|
564
890
|
}
|
|
565
891
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
566
|
-
const
|
|
892
|
+
const ih = itemHeightRef.current;
|
|
893
|
+
const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
894
|
+
const spi = selectedIndexPropRef.current;
|
|
895
|
+
if (newIndex !== spi) {
|
|
896
|
+
if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
|
|
897
|
+
pendingScrollSettleFocusRef.current = true;
|
|
898
|
+
}
|
|
899
|
+
if (isTouchingRef.current) {
|
|
900
|
+
syncScrollFromPropsRef.current = false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
567
903
|
if (newIndex !== currentIndex) {
|
|
568
904
|
setCurrentIndex(newIndex);
|
|
569
905
|
}
|
|
@@ -573,7 +909,11 @@ function PickerGroupRegion(props) {
|
|
|
573
909
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
574
910
|
return /*#__PURE__*/jsx(View, {
|
|
575
911
|
id: `picker-item-${columnId}-${index}`,
|
|
912
|
+
ref: el => itemRefs.current[index] = el,
|
|
576
913
|
className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
|
|
914
|
+
...(enableClickItemScroll ? {
|
|
915
|
+
onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
|
|
916
|
+
} : {}),
|
|
577
917
|
style: {
|
|
578
918
|
height: PICKER_LINE_HEIGHT,
|
|
579
919
|
color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
|
|
@@ -592,6 +932,10 @@ function PickerGroupRegion(props) {
|
|
|
592
932
|
height: PICKER_LINE_HEIGHT
|
|
593
933
|
}
|
|
594
934
|
}, `blank-bottom-${idx}`))];
|
|
935
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
936
|
+
scrollIntoView,
|
|
937
|
+
scrollIntoViewAlignment: 'center'
|
|
938
|
+
} : {};
|
|
595
939
|
return /*#__PURE__*/jsxs(View, {
|
|
596
940
|
className: "taro-picker__group",
|
|
597
941
|
children: [/*#__PURE__*/jsx(View, {
|
|
@@ -610,10 +954,10 @@ function PickerGroupRegion(props) {
|
|
|
610
954
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
611
955
|
},
|
|
612
956
|
scrollTop: targetScrollTop,
|
|
957
|
+
...clickScrollViewProps,
|
|
613
958
|
onScroll: handleScroll,
|
|
614
959
|
onTouchStart: () => {
|
|
615
|
-
|
|
616
|
-
isUserBeginScrollRef.current = true;
|
|
960
|
+
isTouchingRef.current = true;
|
|
617
961
|
},
|
|
618
962
|
onScrollEnd: handleScrollEnd,
|
|
619
963
|
scrollWithAnimation: true,
|