@tarojs/components-react 4.1.12-beta.5 → 4.1.12-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/image/index.js +5 -3
  2. package/dist/components/image/index.js.map +1 -1
  3. package/dist/components/map/MapContext.js +628 -0
  4. package/dist/components/map/MapContext.js.map +1 -0
  5. package/dist/components/map/MapCustomCallout.js +91 -0
  6. package/dist/components/map/MapCustomCallout.js.map +1 -0
  7. package/dist/components/map/common.js +4 -0
  8. package/dist/components/map/common.js.map +1 -0
  9. package/dist/components/map/createMapContext.js +34 -0
  10. package/dist/components/map/createMapContext.js.map +1 -0
  11. package/dist/components/map/handler.js +50 -0
  12. package/dist/components/map/handler.js.map +1 -0
  13. package/dist/components/map/index.js +329 -0
  14. package/dist/components/map/index.js.map +1 -0
  15. package/dist/components/picker/index.js +110 -38
  16. package/dist/components/picker/index.js.map +1 -1
  17. package/dist/components/picker/picker-group.js +400 -111
  18. package/dist/components/picker/picker-group.js.map +1 -1
  19. package/dist/components/scroll-view/index.js +80 -36
  20. package/dist/components/scroll-view/index.js.map +1 -1
  21. package/dist/components/switch/index.js +125 -0
  22. package/dist/components/switch/index.js.map +1 -0
  23. package/dist/components/switch/style/index.scss.js +4 -0
  24. package/dist/components/switch/style/index.scss.js.map +1 -0
  25. package/dist/contexts/ScrollElementContext.js +6 -0
  26. package/dist/contexts/ScrollElementContext.js.map +1 -0
  27. package/dist/index.css +1 -1
  28. package/dist/index.js +5 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/original/components/image/index.js +5 -3
  31. package/dist/original/components/image/index.js.map +1 -1
  32. package/dist/original/components/image/style/index.scss +5 -1
  33. package/dist/original/components/map/MapContext.js +628 -0
  34. package/dist/original/components/map/MapContext.js.map +1 -0
  35. package/dist/original/components/map/MapCustomCallout.js +91 -0
  36. package/dist/original/components/map/MapCustomCallout.js.map +1 -0
  37. package/dist/original/components/map/common.js +4 -0
  38. package/dist/original/components/map/common.js.map +1 -0
  39. package/dist/original/components/map/createMapContext.js +34 -0
  40. package/dist/original/components/map/createMapContext.js.map +1 -0
  41. package/dist/original/components/map/handler.js +50 -0
  42. package/dist/original/components/map/handler.js.map +1 -0
  43. package/dist/original/components/map/index.js +329 -0
  44. package/dist/original/components/map/index.js.map +1 -0
  45. package/dist/original/components/picker/index.js +110 -38
  46. package/dist/original/components/picker/index.js.map +1 -1
  47. package/dist/original/components/picker/picker-group.js +400 -111
  48. package/dist/original/components/picker/picker-group.js.map +1 -1
  49. package/dist/original/components/picker/style/index.scss +9 -8
  50. package/dist/original/components/scroll-view/index.js +80 -36
  51. package/dist/original/components/scroll-view/index.js.map +1 -1
  52. package/dist/original/components/switch/index.js +125 -0
  53. package/dist/original/components/switch/index.js.map +1 -0
  54. package/dist/original/components/switch/style/index.scss +35 -0
  55. package/dist/original/contexts/ScrollElementContext.js +6 -0
  56. package/dist/original/contexts/ScrollElementContext.js.map +1 -0
  57. package/dist/original/index.js +5 -1
  58. package/dist/original/index.js.map +1 -1
  59. package/dist/solid/components/image/index.js +5 -3
  60. package/dist/solid/components/image/index.js.map +1 -1
  61. package/dist/solid/components/picker/index.js +121 -45
  62. package/dist/solid/components/picker/index.js.map +1 -1
  63. package/dist/solid/components/picker/picker-group.js +423 -135
  64. package/dist/solid/components/picker/picker-group.js.map +1 -1
  65. package/dist/solid/components/scroll-view/index.js +84 -40
  66. package/dist/solid/components/scroll-view/index.js.map +1 -1
  67. package/dist/solid/contexts/ScrollElementContext.js +6 -0
  68. package/dist/solid/contexts/ScrollElementContext.js.map +1 -0
  69. package/dist/solid/index.css +1 -1
  70. 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,25 @@ const getIndicatorStyle = lineColor => {
12
38
  borderBottomColor: lineColor
13
39
  };
14
40
  };
41
+ // 大屏方案版本要求
42
+ const MIN_DESIGN_APP_VERSION = 16;
43
+ // 判断是否启用测量值缩放适配(true=启用, false=使用系统侧缩放)
44
+ const resolveUseMeasuredScale = res => {
45
+ // H5/weapp 不参与大屏系数
46
+ if (process.env.TARO_ENV === 'h5' || process.env.TARO_ENV === 'weapp') {
47
+ return false;
48
+ }
49
+ const designAppVersionRaw = res.designAppVersion;
50
+ const designAppVersionMajor = designAppVersionRaw != null ? parseInt(String(designAppVersionRaw).trim(), 10) : Number.NaN;
51
+ if (!Number.isFinite(designAppVersionMajor) || designAppVersionMajor < MIN_DESIGN_APP_VERSION) {
52
+ return false;
53
+ }
54
+ const platform = String(res.platform || '').toLowerCase();
55
+ if (platform === 'harmony' || platform === 'android' || platform === 'ios') {
56
+ return true;
57
+ }
58
+ return false;
59
+ };
15
60
  // 辅助函数:计算 lengthScaleRatio
16
61
  const calculateLengthScaleRatio = res => {
17
62
  let lengthScaleRatio = res === null || res === void 0 ? void 0 : res.lengthScaleRatio;
@@ -50,6 +95,29 @@ const setTargetScrollTopWithScale = function (setTargetScrollTop, baseValue, ran
50
95
  setTargetScrollTop(finalValue);
51
96
  }
52
97
  };
98
+ // 根据 scrollTop 计算选中索引
99
+ // 系统侧缩放模式:scrollHeight 已被系统缩放,直接相除即可
100
+ // 自行缩放模式:需要除以 ratio 换算(harmony 特殊处理,ratio 已内嵌在 itemHeight 中)
101
+ const getSelectedIndex = function (scrollTop, itemHeight) {
102
+ let lengthScaleRatio = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
103
+ let useMeasuredScale = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
104
+ if (!useMeasuredScale || process.env.TARO_PLATFORM === 'harmony') {
105
+ return Math.round(scrollTop / itemHeight);
106
+ }
107
+ return Math.round(scrollTop / lengthScaleRatio / itemHeight);
108
+ };
109
+ // 计算单项高度(返回设计稿值)
110
+ const calculateItemHeight = function (scrollView, lengthScaleRatio) {
111
+ let useMeasuredScale = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
112
+ if (process.env.TARO_PLATFORM === 'harmony') {
113
+ return useMeasuredScale ? PICKER_LINE_HEIGHT * lengthScaleRatio : PICKER_LINE_HEIGHT;
114
+ }
115
+ if (scrollView && (scrollView === null || scrollView === void 0 ? void 0 : scrollView.scrollHeight)) {
116
+ return useMeasuredScale ? scrollView.scrollHeight / lengthScaleRatio / scrollView.childNodes.length : scrollView.scrollHeight / scrollView.childNodes.length;
117
+ }
118
+ console.warn('Height measurement anomaly');
119
+ return PICKER_LINE_HEIGHT;
120
+ };
53
121
  function PickerGroupBasic(props) {
54
122
  const {
55
123
  range = [],
@@ -59,69 +127,72 @@ function PickerGroupBasic(props) {
59
127
  onColumnChange,
60
128
  selectedIndex = 0,
61
129
  // 使用selectedIndex参数,默认为0
62
- colors = {}
130
+ colors = {},
131
+ enableClickItemScroll = true
63
132
  } = props;
64
133
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
65
134
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
135
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
66
136
  const scrollViewRef = React.useRef(null);
67
137
  const itemRefs = React.useRef([]);
138
+ const selectedIndexPropRef = React.useRef(selectedIndex);
139
+ selectedIndexPropRef.current = selectedIndex;
140
+ const syncScrollFromPropsRef = React.useRef(false);
141
+ const pendingScrollSettleFocusRef = React.useRef(false);
68
142
  // 使用selectedIndex初始化当前索引
69
143
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
70
144
  // 触摸状态用于优化用户体验
71
- const [isTouching, setIsTouching] = React.useState(false);
145
+ const isTouchingRef = React.useRef(false);
72
146
  const lengthScaleRatioRef = React.useRef(1);
147
+ const useMeasuredScaleRef = React.useRef(false);
73
148
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
74
- // 初始化时计算 lengthScaleRatio
149
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
75
150
  React.useEffect(() => {
76
151
  Taro.getSystemInfo({
77
152
  success: res => {
78
153
  lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
154
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
155
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
79
156
  },
80
157
  fail: () => {
81
- // 失败时使用默认值 1
82
158
  lengthScaleRatioRef.current = 1;
159
+ useMeasuredScaleRef.current = false;
83
160
  }
84
161
  });
85
162
  }, []);
86
163
  React.useEffect(() => {
87
- var _a;
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
- }
164
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
97
165
  }, [range.length]); // 只在range长度变化时重新计算
98
- // 获取选中的索引
99
- const getSelectedIndex = scrollTop => {
100
- return Math.round(scrollTop / itemHeightRef.current);
101
- };
102
- // 当selectedIndex变化时,调整滚动位置
166
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
103
167
  React.useEffect(() => {
104
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
168
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
169
+ syncScrollFromPropsRef.current = true;
105
170
  const baseValue = selectedIndex * itemHeightRef.current;
106
171
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
107
172
  setCurrentIndex(selectedIndex);
173
+ const tid = setTimeout(() => {
174
+ syncScrollFromPropsRef.current = false;
175
+ }, 400);
176
+ return () => clearTimeout(tid);
108
177
  }
109
178
  }, [selectedIndex, range]);
110
179
  // 是否处于归中状态
111
180
  const isCenterTimerId = React.useRef(null);
112
- // 简化为直接在滚动结束时通知父组件
181
+ // 滚动静止后归中并同步选中
113
182
  const handleScrollEnd = () => {
114
183
  if (!scrollViewRef.current) return;
115
184
  if (isCenterTimerId.current) {
116
185
  clearTimeout(isCenterTimerId.current);
117
186
  isCenterTimerId.current = null;
118
187
  }
119
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
188
+ // 100ms 内无新滚动则归中并提交索引
120
189
  isCenterTimerId.current = setTimeout(() => {
121
190
  if (!scrollViewRef.current) return;
122
191
  const scrollTop = scrollViewRef.current.scrollTop;
123
- const newIndex = getSelectedIndex(scrollTop);
124
- setIsTouching(false);
192
+ const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
193
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
194
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
195
+ isTouchingRef.current = false;
125
196
  const baseValue = newIndex * itemHeightRef.current;
126
197
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
127
198
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -130,10 +201,15 @@ function PickerGroupBasic(props) {
130
201
  columnId,
131
202
  index: newIndex
132
203
  });
204
+ pendingScrollSettleFocusRef.current = false;
205
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
206
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
207
+ }
208
+ syncScrollFromPropsRef.current = false;
133
209
  isCenterTimerId.current = null;
134
210
  }, 100);
135
211
  };
136
- // 滚动处理 - 在滚动时计算索引然后更新选中项样式
212
+ // 滚动中:按 scrollTop 更新高亮索引
137
213
  const handleScroll = () => {
138
214
  if (!scrollViewRef.current) return;
139
215
  if (isCenterTimerId.current) {
@@ -141,7 +217,17 @@ function PickerGroupBasic(props) {
141
217
  isCenterTimerId.current = null;
142
218
  }
143
219
  const scrollTop = scrollViewRef.current.scrollTop;
144
- const newIndex = getSelectedIndex(scrollTop);
220
+ const ih = itemHeightRef.current;
221
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
222
+ const spi = selectedIndexPropRef.current;
223
+ if (newIndex !== spi) {
224
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
225
+ pendingScrollSettleFocusRef.current = true;
226
+ }
227
+ if (isTouchingRef.current) {
228
+ syncScrollFromPropsRef.current = false;
229
+ }
230
+ }
145
231
  if (newIndex !== currentIndex) {
146
232
  setCurrentIndex(newIndex);
147
233
  }
@@ -153,6 +239,9 @@ function PickerGroupBasic(props) {
153
239
  id: `picker-item-${columnId}-${index}`,
154
240
  ref: el => itemRefs.current[index] = el,
155
241
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
242
+ ...(enableClickItemScroll ? {
243
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
244
+ } : {}),
156
245
  style: {
157
246
  height: PICKER_LINE_HEIGHT,
158
247
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -171,6 +260,10 @@ function PickerGroupBasic(props) {
171
260
  height: PICKER_LINE_HEIGHT
172
261
  }
173
262
  }, `blank-bottom-${idx}`))];
263
+ const clickScrollViewProps = enableClickItemScroll ? {
264
+ scrollIntoView,
265
+ scrollIntoViewAlignment: 'center'
266
+ } : {};
174
267
  return /*#__PURE__*/jsxs(View, {
175
268
  className: "taro-picker__group",
176
269
  children: [/*#__PURE__*/jsx(View, {
@@ -189,8 +282,11 @@ function PickerGroupBasic(props) {
189
282
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
190
283
  },
191
284
  scrollTop: targetScrollTop,
285
+ ...clickScrollViewProps,
192
286
  onScroll: handleScroll,
193
- onTouchStart: () => setIsTouching(true),
287
+ onTouchStart: () => {
288
+ isTouchingRef.current = true;
289
+ },
194
290
  onScrollEnd: handleScrollEnd,
195
291
  scrollWithAnimation: true,
196
292
  children: realPickerItems
@@ -205,78 +301,189 @@ function PickerGroupTime(props) {
205
301
  columnId,
206
302
  updateIndex,
207
303
  selectedIndex = 0,
208
- colors = {}
304
+ colors = {},
305
+ timeA11yLimitFocus,
306
+ onTimeA11yLimitFocusConsumed,
307
+ timeA11yLimitEventNonce,
308
+ enableClickItemScroll = true
209
309
  } = props;
210
310
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
211
311
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
312
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
212
313
  const scrollViewRef = React.useRef(null);
213
314
  const itemRefs = React.useRef([]);
315
+ const selectedIndexPropRef = React.useRef(selectedIndex);
316
+ selectedIndexPropRef.current = selectedIndex;
317
+ const syncScrollFromPropsRef = React.useRef(false);
318
+ const prevLimitEventNonceRef = React.useRef(undefined);
319
+ const suppressScrollSettleFocusNonceRef = React.useRef(null);
320
+ const pendingScrollSettleFocusRef = React.useRef(false);
321
+ const pendingLimitFocusRef = React.useRef(null);
322
+ const limitFocusTimerRef = React.useRef(null);
214
323
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
215
- const [isTouching, setIsTouching] = React.useState(false);
324
+ const isTouchingRef = React.useRef(false);
216
325
  const lengthScaleRatioRef = React.useRef(1);
326
+ const useMeasuredScaleRef = React.useRef(false);
217
327
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
218
- // 初始化时计算 lengthScaleRatio
328
+ const clearLimitFocusTimer = React.useCallback(() => {
329
+ if (limitFocusTimerRef.current) {
330
+ clearTimeout(limitFocusTimerRef.current);
331
+ limitFocusTimerRef.current = null;
332
+ }
333
+ }, []);
334
+ const tryFocusPendingLimit = React.useCallback(() => {
335
+ const pending = pendingLimitFocusRef.current;
336
+ const scrollView = scrollViewRef.current;
337
+ if (!pending || !scrollView) return;
338
+ const visualIndex = getSelectedIndex(scrollView.scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
339
+ const isStable = visualIndex === pending.index;
340
+ if (!isStable) {
341
+ if (pending.attempts >= 20) {
342
+ pendingLimitFocusRef.current = null;
343
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
344
+ suppressScrollSettleFocusNonceRef.current = null;
345
+ }
346
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
347
+ return;
348
+ }
349
+ pending.attempts += 1;
350
+ clearLimitFocusTimer();
351
+ limitFocusTimerRef.current = setTimeout(() => {
352
+ tryFocusPendingLimit();
353
+ }, 50);
354
+ return;
355
+ }
356
+ const node = itemRefs.current[pending.index];
357
+ pendingLimitFocusRef.current = null;
358
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
359
+ suppressScrollSettleFocusNonceRef.current = null;
360
+ }
361
+ clearLimitFocusTimer();
362
+ requestAccessibilityFocusOnView(node);
363
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
364
+ }, [clearLimitFocusTimer, onTimeA11yLimitFocusConsumed]);
365
+ const schedulePendingLimitFocus = React.useCallback(() => {
366
+ if (!pendingLimitFocusRef.current) return;
367
+ clearLimitFocusTimer();
368
+ limitFocusTimerRef.current = setTimeout(() => {
369
+ tryFocusPendingLimit();
370
+ }, 100);
371
+ }, [clearLimitFocusTimer, tryFocusPendingLimit]);
372
+ React.useEffect(() => clearLimitFocusTimer, [clearLimitFocusTimer]);
373
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
219
374
  React.useEffect(() => {
220
375
  Taro.getSystemInfo({
221
376
  success: res => {
222
377
  lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
378
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
379
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
223
380
  },
224
381
  fail: () => {
225
- // 失败时使用默认值 1
226
382
  lengthScaleRatioRef.current = 1;
383
+ useMeasuredScaleRef.current = false;
227
384
  }
228
385
  });
229
386
  }, []);
230
387
  React.useEffect(() => {
231
- var _a;
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
- }
388
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
241
389
  }, [range.length]); // 只在range长度变化时重新计算
242
- const getSelectedIndex = scrollTop => {
243
- return Math.round(scrollTop / itemHeightRef.current);
244
- };
245
- // 当selectedIndex变化时,调整滚动位置
390
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
246
391
  React.useEffect(() => {
247
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
392
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
393
+ syncScrollFromPropsRef.current = true;
248
394
  const baseValue = selectedIndex * itemHeightRef.current;
249
395
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
250
396
  setCurrentIndex(selectedIndex);
397
+ const tid = setTimeout(() => {
398
+ syncScrollFromPropsRef.current = false;
399
+ }, 400);
400
+ return () => clearTimeout(tid);
251
401
  }
252
402
  }, [selectedIndex, range]);
403
+ // time 限位 nonce 变更:强制对齐 scrollTop(避免 remount 打断读屏焦点)
404
+ React.useEffect(() => {
405
+ if (!timeA11yLimitEventNonce) return;
406
+ if (!scrollViewRef.current || range.length === 0 || isTouchingRef.current) return;
407
+ syncScrollFromPropsRef.current = true;
408
+ const baseValue = selectedIndex * itemHeightRef.current;
409
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, Math.random() * 0.001, lengthScaleRatioRef.current);
410
+ setCurrentIndex(selectedIndex);
411
+ const tid = setTimeout(() => {
412
+ syncScrollFromPropsRef.current = false;
413
+ }, 400);
414
+ return () => clearTimeout(tid);
415
+ }, [timeA11yLimitEventNonce]); // 仅依赖 nonce,其余用 ref
416
+ React.useLayoutEffect(() => {
417
+ if (timeA11yLimitEventNonce === undefined) return;
418
+ const prev = prevLimitEventNonceRef.current;
419
+ prevLimitEventNonceRef.current = timeA11yLimitEventNonce;
420
+ if (timeA11yLimitEventNonce > 0 && (prev === undefined || timeA11yLimitEventNonce > prev)) {
421
+ suppressScrollSettleFocusNonceRef.current = timeA11yLimitEventNonce;
422
+ }
423
+ }, [timeA11yLimitEventNonce]);
424
+ // 限位后登记本列待聚焦项,稳定后再 requestAccessibilityFocus
425
+ React.useEffect(() => {
426
+ const req = timeA11yLimitFocus;
427
+ if (!req || req.columnId !== columnId) return;
428
+ const idx = selectedIndex;
429
+ const nonce = req.nonce;
430
+ pendingScrollSettleFocusRef.current = false;
431
+ pendingLimitFocusRef.current = {
432
+ index: idx,
433
+ nonce,
434
+ attempts: 0
435
+ };
436
+ schedulePendingLimitFocus();
437
+ }, [timeA11yLimitFocus, columnId, selectedIndex, schedulePendingLimitFocus]);
253
438
  // 是否处于归中状态
254
439
  const isCenterTimerId = React.useRef(null);
255
- // 简化为直接在滚动结束时通知父组件
440
+ // 滚动静止后归中并同步选中
256
441
  const handleScrollEnd = () => {
257
442
  if (!scrollViewRef.current) return;
258
443
  if (isCenterTimerId.current) {
259
444
  clearTimeout(isCenterTimerId.current);
260
445
  isCenterTimerId.current = null;
261
446
  }
262
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
447
+ // 100ms 内无新滚动则归中并提交索引
263
448
  isCenterTimerId.current = setTimeout(() => {
264
449
  if (!scrollViewRef.current) return;
265
450
  const scrollTop = scrollViewRef.current.scrollTop;
266
- const newIndex = getSelectedIndex(scrollTop);
267
- setIsTouching(false);
268
- // 调用updateIndex执行限位逻辑,获取是否触发了限位
451
+ const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
452
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
453
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
454
+ isTouchingRef.current = false;
269
455
  const isLimited = Boolean(updateIndex(newIndex, columnId, true));
270
- // 如果没有触发限位,才执行归中逻辑
456
+ const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
457
+ if (isLimited) {
458
+ pendingScrollSettleFocusRef.current = false;
459
+ }
271
460
  if (!isLimited) {
272
461
  const baseValue = newIndex * itemHeightRef.current;
273
462
  const randomOffset = Math.random() * 0.001;
274
463
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
275
464
  }
465
+ if (!isLimited && hasPendingLimitFocus) {
466
+ pendingScrollSettleFocusRef.current = false;
467
+ schedulePendingLimitFocus();
468
+ syncScrollFromPropsRef.current = false;
469
+ isCenterTimerId.current = null;
470
+ return;
471
+ }
472
+ if (!isLimited && pendingLimitFocusRef.current == null && suppressScrollSettleFocusNonceRef.current != null) {
473
+ suppressScrollSettleFocusNonceRef.current = null;
474
+ }
475
+ const suppressByLimitNonce = suppressScrollSettleFocusNonceRef.current != null;
476
+ if (!isLimited && allowA11yFocus && shouldFocusOnScrollSettle) {
477
+ pendingScrollSettleFocusRef.current = false;
478
+ if (!suppressByLimitNonce) {
479
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
480
+ }
481
+ }
482
+ syncScrollFromPropsRef.current = false;
276
483
  isCenterTimerId.current = null;
277
484
  }, 100);
278
485
  };
279
- // 滚动处理
486
+ // 滚动中:按 scrollTop 更新高亮索引
280
487
  const handleScroll = () => {
281
488
  if (!scrollViewRef.current) return;
282
489
  if (isCenterTimerId.current) {
@@ -284,10 +491,23 @@ function PickerGroupTime(props) {
284
491
  isCenterTimerId.current = null;
285
492
  }
286
493
  const scrollTop = scrollViewRef.current.scrollTop;
287
- const newIndex = getSelectedIndex(scrollTop);
494
+ const ih = itemHeightRef.current;
495
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
496
+ const spi = selectedIndexPropRef.current;
497
+ if (newIndex !== spi) {
498
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
499
+ pendingScrollSettleFocusRef.current = true;
500
+ }
501
+ if (isTouchingRef.current) {
502
+ syncScrollFromPropsRef.current = false;
503
+ }
504
+ }
288
505
  if (newIndex !== currentIndex) {
289
506
  setCurrentIndex(newIndex);
290
507
  }
508
+ if (pendingLimitFocusRef.current) {
509
+ schedulePendingLimitFocus();
510
+ }
291
511
  };
292
512
  // 渲染选项
293
513
  const pickerItem = range.map((item, index) => {
@@ -296,6 +516,9 @@ function PickerGroupTime(props) {
296
516
  id: `picker-item-${columnId}-${index}`,
297
517
  ref: el => itemRefs.current[index] = el,
298
518
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
519
+ ...(enableClickItemScroll ? {
520
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
521
+ } : {}),
299
522
  style: {
300
523
  height: PICKER_LINE_HEIGHT,
301
524
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -314,6 +537,10 @@ function PickerGroupTime(props) {
314
537
  height: PICKER_LINE_HEIGHT
315
538
  }
316
539
  }, `blank-bottom-${idx}`))];
540
+ const clickScrollViewProps = enableClickItemScroll ? {
541
+ scrollIntoView,
542
+ scrollIntoViewAlignment: 'center'
543
+ } : {};
317
544
  return /*#__PURE__*/jsxs(View, {
318
545
  className: "taro-picker__group",
319
546
  children: [/*#__PURE__*/jsx(View, {
@@ -332,8 +559,11 @@ function PickerGroupTime(props) {
332
559
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
333
560
  },
334
561
  scrollTop: targetScrollTop,
562
+ ...clickScrollViewProps,
335
563
  onScroll: handleScroll,
336
- onTouchStart: () => setIsTouching(true),
564
+ onTouchStart: () => {
565
+ isTouchingRef.current = true;
566
+ },
337
567
  onScrollEnd: handleScrollEnd,
338
568
  scrollWithAnimation: true,
339
569
  children: realPickerItems
@@ -347,65 +577,70 @@ function PickerGroupDate(props) {
347
577
  columnId,
348
578
  updateDay,
349
579
  selectedIndex = 0,
350
- colors = {}
580
+ colors = {},
581
+ enableClickItemScroll = true
351
582
  } = props;
352
583
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
353
584
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
585
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
354
586
  const scrollViewRef = React.useRef(null);
587
+ const itemRefs = React.useRef([]);
588
+ const selectedIndexPropRef = React.useRef(selectedIndex);
589
+ selectedIndexPropRef.current = selectedIndex;
590
+ const syncScrollFromPropsRef = React.useRef(false);
591
+ const pendingScrollSettleFocusRef = React.useRef(false);
355
592
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
356
- const [isTouching, setIsTouching] = React.useState(false);
593
+ const isTouchingRef = React.useRef(false);
357
594
  const lengthScaleRatioRef = React.useRef(1);
595
+ const useMeasuredScaleRef = React.useRef(false);
358
596
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
359
- // 初始化时计算 lengthScaleRatio
597
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
360
598
  React.useEffect(() => {
361
599
  Taro.getSystemInfo({
362
600
  success: res => {
363
601
  lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
602
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
603
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
364
604
  },
365
605
  fail: () => {
366
- // 失败时使用默认值 1
367
606
  lengthScaleRatioRef.current = 1;
607
+ useMeasuredScaleRef.current = false;
368
608
  }
369
609
  });
370
610
  }, []);
371
611
  React.useEffect(() => {
372
- var _a;
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
- }
612
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
382
613
  }, [range.length]); // 只在range长度变化时重新计算
383
- const getSelectedIndex = scrollTop => {
384
- return Math.round(scrollTop / itemHeightRef.current);
385
- };
386
- // 当selectedIndex变化时,调整滚动位置
614
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
387
615
  React.useEffect(() => {
388
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
616
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
617
+ syncScrollFromPropsRef.current = true;
389
618
  const baseValue = selectedIndex * itemHeightRef.current;
390
619
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
391
620
  setCurrentIndex(selectedIndex);
621
+ const tid = setTimeout(() => {
622
+ syncScrollFromPropsRef.current = false;
623
+ }, 400);
624
+ return () => clearTimeout(tid);
392
625
  }
393
626
  }, [selectedIndex, range]);
394
627
  // 是否处于归中状态
395
628
  const isCenterTimerId = React.useRef(null);
396
- // 简化为直接在滚动结束时通知父组件
629
+ // 滚动静止后归中并同步选中
397
630
  const handleScrollEnd = () => {
398
631
  if (!scrollViewRef.current) return;
399
632
  if (isCenterTimerId.current) {
400
633
  clearTimeout(isCenterTimerId.current);
401
634
  isCenterTimerId.current = null;
402
635
  }
403
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
636
+ // 100ms 内无新滚动则归中并提交索引
404
637
  isCenterTimerId.current = setTimeout(() => {
405
638
  if (!scrollViewRef.current) return;
406
639
  const scrollTop = scrollViewRef.current.scrollTop;
407
- const newIndex = getSelectedIndex(scrollTop);
408
- setIsTouching(false);
640
+ const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
641
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
642
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
643
+ isTouchingRef.current = false;
409
644
  const baseValue = newIndex * itemHeightRef.current;
410
645
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
411
646
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -416,10 +651,15 @@ function PickerGroupDate(props) {
416
651
  const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
417
652
  updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
418
653
  }
654
+ pendingScrollSettleFocusRef.current = false;
655
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
656
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
657
+ }
658
+ syncScrollFromPropsRef.current = false;
419
659
  isCenterTimerId.current = null;
420
660
  }, 100);
421
661
  };
422
- // 滚动处理
662
+ // 滚动中:按 scrollTop 更新高亮索引
423
663
  const handleScroll = () => {
424
664
  if (!scrollViewRef.current) return;
425
665
  if (isCenterTimerId.current) {
@@ -427,7 +667,17 @@ function PickerGroupDate(props) {
427
667
  isCenterTimerId.current = null;
428
668
  }
429
669
  const scrollTop = scrollViewRef.current.scrollTop;
430
- const newIndex = getSelectedIndex(scrollTop);
670
+ const ih = itemHeightRef.current;
671
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
672
+ const spi = selectedIndexPropRef.current;
673
+ if (newIndex !== spi) {
674
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
675
+ pendingScrollSettleFocusRef.current = true;
676
+ }
677
+ if (isTouchingRef.current) {
678
+ syncScrollFromPropsRef.current = false;
679
+ }
680
+ }
431
681
  if (newIndex !== currentIndex) {
432
682
  setCurrentIndex(newIndex);
433
683
  }
@@ -436,7 +686,11 @@ function PickerGroupDate(props) {
436
686
  const pickerItem = range.map((item, index) => {
437
687
  return /*#__PURE__*/jsx(View, {
438
688
  id: `picker-item-${columnId}-${index}`,
689
+ ref: el => itemRefs.current[index] = el,
439
690
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
691
+ ...(enableClickItemScroll ? {
692
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
693
+ } : {}),
440
694
  style: {
441
695
  height: PICKER_LINE_HEIGHT,
442
696
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -455,6 +709,10 @@ function PickerGroupDate(props) {
455
709
  height: PICKER_LINE_HEIGHT
456
710
  }
457
711
  }, `blank-bottom-${idx}`))];
712
+ const clickScrollViewProps = enableClickItemScroll ? {
713
+ scrollIntoView,
714
+ scrollIntoViewAlignment: 'center'
715
+ } : {};
458
716
  return /*#__PURE__*/jsxs(View, {
459
717
  className: "taro-picker__group",
460
718
  children: [/*#__PURE__*/jsx(View, {
@@ -473,8 +731,11 @@ function PickerGroupDate(props) {
473
731
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
474
732
  },
475
733
  scrollTop: targetScrollTop,
734
+ ...clickScrollViewProps,
476
735
  onScroll: handleScroll,
477
- onTouchStart: () => setIsTouching(true),
736
+ onTouchStart: () => {
737
+ isTouchingRef.current = true;
738
+ },
478
739
  onScrollEnd: handleScrollEnd,
479
740
  scrollWithAnimation: true,
480
741
  children: realPickerItems
@@ -490,52 +751,54 @@ function PickerGroupRegion(props) {
490
751
  updateIndex,
491
752
  selectedIndex = 0,
492
753
  // 使用selectedIndex参数,默认为0
493
- colors = {}
754
+ colors = {},
755
+ enableClickItemScroll = true
494
756
  } = props;
495
757
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
496
758
  const scrollViewRef = React.useRef(null);
497
759
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
760
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
498
761
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
499
- const [isTouching, setIsTouching] = React.useState(false);
762
+ const isTouchingRef = React.useRef(false);
500
763
  const lengthScaleRatioRef = React.useRef(1);
764
+ const useMeasuredScaleRef = React.useRef(false);
501
765
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
502
- const isUserBeginScrollRef = React.useRef(false);
503
- // 初始化时计算 lengthScaleRatio
766
+ const itemRefs = React.useRef([]);
767
+ const selectedIndexPropRef = React.useRef(selectedIndex);
768
+ selectedIndexPropRef.current = selectedIndex;
769
+ const syncScrollFromPropsRef = React.useRef(false);
770
+ const pendingScrollSettleFocusRef = React.useRef(false);
771
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
504
772
  React.useEffect(() => {
505
773
  Taro.getSystemInfo({
506
774
  success: res => {
507
775
  lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
776
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
777
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
508
778
  },
509
779
  fail: () => {
510
- // 失败时使用默认值 1
511
780
  lengthScaleRatioRef.current = 1;
781
+ useMeasuredScaleRef.current = false;
512
782
  }
513
783
  });
514
784
  }, []);
515
785
  React.useEffect(() => {
516
- var _a;
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
- }
786
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
526
787
  }, [range.length]); // 只在range长度变化时重新计算
527
- const getSelectedIndex = scrollTop => {
528
- return Math.round(scrollTop / itemHeightRef.current);
529
- };
530
- // 当selectedIndex变化时,调整滚动位置
788
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
531
789
  React.useEffect(() => {
532
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
790
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
791
+ syncScrollFromPropsRef.current = true;
533
792
  const baseValue = selectedIndex * itemHeightRef.current;
534
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
793
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
535
794
  setCurrentIndex(selectedIndex);
795
+ const tid = setTimeout(() => {
796
+ syncScrollFromPropsRef.current = false;
797
+ }, 400);
798
+ return () => clearTimeout(tid);
536
799
  }
537
800
  }, [selectedIndex, range]);
538
- // 滚动结束处理
801
+ // 滚动静止后归中(debounce)
539
802
  const isCenterTimerId = React.useRef(null);
540
803
  const handleScrollEnd = () => {
541
804
  if (!scrollViewRef.current) return;
@@ -543,19 +806,27 @@ function PickerGroupRegion(props) {
543
806
  clearTimeout(isCenterTimerId.current);
544
807
  isCenterTimerId.current = null;
545
808
  }
546
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
809
+ // 100ms 内无新滚动则归中并提交索引
547
810
  isCenterTimerId.current = setTimeout(() => {
548
811
  if (!scrollViewRef.current) return;
549
812
  const scrollTop = scrollViewRef.current.scrollTop;
550
- const newIndex = getSelectedIndex(scrollTop);
551
- setIsTouching(false);
813
+ const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
814
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
815
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
816
+ isTouchingRef.current = false;
552
817
  const baseValue = newIndex * itemHeightRef.current;
553
818
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
554
819
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
555
- updateIndex(newIndex, columnId, false, isUserBeginScrollRef.current);
820
+ updateIndex(newIndex, columnId, false, allowA11yFocus);
821
+ pendingScrollSettleFocusRef.current = false;
822
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
823
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
824
+ }
825
+ syncScrollFromPropsRef.current = false;
826
+ isCenterTimerId.current = null;
556
827
  }, 100);
557
828
  };
558
- // 滚动处理 - 在滚动时计算索引
829
+ // 滚动中:按 scrollTop 更新高亮索引
559
830
  const handleScroll = () => {
560
831
  if (!scrollViewRef.current) return;
561
832
  if (isCenterTimerId.current) {
@@ -563,7 +834,17 @@ function PickerGroupRegion(props) {
563
834
  isCenterTimerId.current = null;
564
835
  }
565
836
  const scrollTop = scrollViewRef.current.scrollTop;
566
- const newIndex = getSelectedIndex(scrollTop);
837
+ const ih = itemHeightRef.current;
838
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
839
+ const spi = selectedIndexPropRef.current;
840
+ if (newIndex !== spi) {
841
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
842
+ pendingScrollSettleFocusRef.current = true;
843
+ }
844
+ if (isTouchingRef.current) {
845
+ syncScrollFromPropsRef.current = false;
846
+ }
847
+ }
567
848
  if (newIndex !== currentIndex) {
568
849
  setCurrentIndex(newIndex);
569
850
  }
@@ -573,7 +854,11 @@ function PickerGroupRegion(props) {
573
854
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
574
855
  return /*#__PURE__*/jsx(View, {
575
856
  id: `picker-item-${columnId}-${index}`,
857
+ ref: el => itemRefs.current[index] = el,
576
858
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
859
+ ...(enableClickItemScroll ? {
860
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
861
+ } : {}),
577
862
  style: {
578
863
  height: PICKER_LINE_HEIGHT,
579
864
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -592,6 +877,10 @@ function PickerGroupRegion(props) {
592
877
  height: PICKER_LINE_HEIGHT
593
878
  }
594
879
  }, `blank-bottom-${idx}`))];
880
+ const clickScrollViewProps = enableClickItemScroll ? {
881
+ scrollIntoView,
882
+ scrollIntoViewAlignment: 'center'
883
+ } : {};
595
884
  return /*#__PURE__*/jsxs(View, {
596
885
  className: "taro-picker__group",
597
886
  children: [/*#__PURE__*/jsx(View, {
@@ -610,10 +899,10 @@ function PickerGroupRegion(props) {
610
899
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
611
900
  },
612
901
  scrollTop: targetScrollTop,
902
+ ...clickScrollViewProps,
613
903
  onScroll: handleScroll,
614
904
  onTouchStart: () => {
615
- setIsTouching(true);
616
- isUserBeginScrollRef.current = true;
905
+ isTouchingRef.current = true;
617
906
  },
618
907
  onScrollEnd: handleScrollEnd,
619
908
  scrollWithAnimation: true,