@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.
Files changed (69) 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 +327 -0
  14. package/dist/components/map/index.js.map +1 -0
  15. package/dist/components/picker/index.js +71 -34
  16. package/dist/components/picker/index.js.map +1 -1
  17. package/dist/components/picker/picker-group.js +455 -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 +327 -0
  44. package/dist/original/components/map/index.js.map +1 -0
  45. package/dist/original/components/picker/index.js +71 -34
  46. package/dist/original/components/picker/index.js.map +1 -1
  47. package/dist/original/components/picker/picker-group.js +455 -111
  48. package/dist/original/components/picker/picker-group.js.map +1 -1
  49. package/dist/original/components/scroll-view/index.js +80 -36
  50. package/dist/original/components/scroll-view/index.js.map +1 -1
  51. package/dist/original/components/switch/index.js +125 -0
  52. package/dist/original/components/switch/index.js.map +1 -0
  53. package/dist/original/components/switch/style/index.scss +35 -0
  54. package/dist/original/contexts/ScrollElementContext.js +6 -0
  55. package/dist/original/contexts/ScrollElementContext.js.map +1 -0
  56. package/dist/original/index.js +5 -1
  57. package/dist/original/index.js.map +1 -1
  58. package/dist/solid/components/image/index.js +5 -3
  59. package/dist/solid/components/image/index.js.map +1 -1
  60. package/dist/solid/components/picker/index.js +75 -38
  61. package/dist/solid/components/picker/index.js.map +1 -1
  62. package/dist/solid/components/picker/picker-group.js +478 -135
  63. package/dist/solid/components/picker/picker-group.js.map +1 -1
  64. package/dist/solid/components/scroll-view/index.js +84 -40
  65. package/dist/solid/components/scroll-view/index.js.map +1 -1
  66. package/dist/solid/contexts/ScrollElementContext.js +6 -0
  67. package/dist/solid/contexts/ScrollElementContext.js.map +1 -0
  68. package/dist/solid/index.css +1 -1
  69. 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 [isTouching, setIsTouching] = React.useState(false);
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
- 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
- }
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 && !isTouching) {
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
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
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
- setIsTouching(false);
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 newIndex = getSelectedIndex(scrollTop);
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: () => setIsTouching(true),
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 [isTouching, setIsTouching] = React.useState(false);
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
- // 初始化时计算 lengthScaleRatio
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
- 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
- }
443
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
241
444
  }, [range.length]); // 只在range长度变化时重新计算
242
- const getSelectedIndex = scrollTop => {
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 && !isTouching) {
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
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
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
- setIsTouching(false);
268
- // 调用updateIndex执行限位逻辑,获取是否触发了限位
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 newIndex = getSelectedIndex(scrollTop);
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: () => setIsTouching(true),
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 [isTouching, setIsTouching] = React.useState(false);
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
- 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
- }
667
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
382
668
  }, [range.length]); // 只在range长度变化时重新计算
383
- const getSelectedIndex = scrollTop => {
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 && !isTouching) {
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
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
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
- setIsTouching(false);
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 newIndex = getSelectedIndex(scrollTop);
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: () => setIsTouching(true),
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 [isTouching, setIsTouching] = React.useState(false);
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 isUserBeginScrollRef = React.useRef(false);
503
- // 初始化时计算 lengthScaleRatio
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
- 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
- }
841
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
526
842
  }, [range.length]); // 只在range长度变化时重新计算
527
- const getSelectedIndex = scrollTop => {
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 && !isTouching) {
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
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
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
- setIsTouching(false);
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, isUserBeginScrollRef.current);
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 newIndex = getSelectedIndex(scrollTop);
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
- setIsTouching(true);
616
- isUserBeginScrollRef.current = true;
960
+ isTouchingRef.current = true;
617
961
  },
618
962
  onScrollEnd: handleScrollEnd,
619
963
  scrollWithAnimation: true,