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

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