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