@tarojs/components-react 4.2.0 → 4.2.1-beta.1

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