@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,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,42 +38,85 @@ 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
+ };
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
+ };
16
79
  // 辅助函数:获取系统信息的 lengthScaleRatio 并设置 targetScrollTop
17
- const setTargetScrollTopWithScale = (setTargetScrollTop, baseValue, randomOffset) => {
80
+ const setTargetScrollTopWithScale = function (setTargetScrollTop, baseValue, randomOffset) {
81
+ let lengthScaleRatio = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
18
82
  // H5 和 weapp 不参与放大计算,直接使用 baseValue
19
83
  if (process.env.TARO_ENV === 'h5' || process.env.TARO_ENV === 'weapp') {
20
84
  const finalValue = randomOffset !== undefined ? baseValue + randomOffset : baseValue;
21
85
  setTargetScrollTop(finalValue);
22
86
  return;
23
87
  }
24
- Taro.getSystemInfo({
25
- success: res => {
26
- let lengthScaleRatio = res === null || res === void 0 ? void 0 : res.lengthScaleRatio;
27
- if (lengthScaleRatio == null || lengthScaleRatio === 0) {
28
- console.warn('Taro.getSystemInfo: lengthScaleRatio 不存在,使用计算值');
29
- lengthScaleRatio = 1;
30
- if (res.windowWidth < 320) {
31
- lengthScaleRatio = res.windowWidth / 320;
32
- } else if (res.windowWidth >= 400 && res.windowWidth < 600) {
33
- lengthScaleRatio = res.windowWidth / 400;
34
- }
35
- const shortSide = res.windowWidth < res.windowHeight ? res.windowWidth : res.windowHeight;
36
- const isBigScreen = shortSide >= 600;
37
- if (isBigScreen) {
38
- lengthScaleRatio = shortSide / 720;
39
- }
40
- }
41
- const scaledValue = baseValue * lengthScaleRatio;
42
- const finalValue = randomOffset !== undefined ? scaledValue + randomOffset : scaledValue;
43
- setTargetScrollTop(finalValue);
44
- },
45
- fail: err => {
46
- console.error('获取系统信息失败:', err);
47
- // 失败时使用默认值 1
48
- const finalValue = randomOffset !== undefined ? baseValue + randomOffset : baseValue;
49
- setTargetScrollTop(finalValue);
50
- }
51
- });
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;
52
120
  };
53
121
  function PickerGroupBasic(props) {
54
122
  const {
@@ -59,66 +127,89 @@ function PickerGroupBasic(props) {
59
127
  onColumnChange,
60
128
  selectedIndex = 0,
61
129
  // 使用selectedIndex参数,默认为0
62
- colors = {}
130
+ colors = {},
131
+ enableClickItemScroll = true
63
132
  } = props;
64
133
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
65
134
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
135
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
66
136
  const scrollViewRef = React.useRef(null);
67
137
  const itemRefs = React.useRef([]);
138
+ const selectedIndexPropRef = React.useRef(selectedIndex);
139
+ selectedIndexPropRef.current = selectedIndex;
140
+ const syncScrollFromPropsRef = React.useRef(false);
141
+ const pendingScrollSettleFocusRef = React.useRef(false);
68
142
  // 使用selectedIndex初始化当前索引
69
143
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
70
144
  // 触摸状态用于优化用户体验
71
- const [isTouching, setIsTouching] = React.useState(false);
145
+ const isTouchingRef = React.useRef(false);
146
+ const lengthScaleRatioRef = React.useRef(1);
147
+ const useMeasuredScaleRef = React.useRef(false);
72
148
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
149
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
73
150
  React.useEffect(() => {
74
- var _a;
75
- if (process.env.TARO_PLATFORM !== 'harmony') {
76
- if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
77
- itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
78
- } else {
79
- 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;
80
160
  }
81
- }
161
+ });
162
+ }, []);
163
+ React.useEffect(() => {
164
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
82
165
  }, [range.length]); // 只在range长度变化时重新计算
83
- // 获取选中的索引
84
- const getSelectedIndex = scrollTop => {
85
- return Math.round(scrollTop / itemHeightRef.current);
86
- };
87
- // 当selectedIndex变化时,调整滚动位置
166
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
88
167
  React.useEffect(() => {
89
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
168
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
169
+ syncScrollFromPropsRef.current = true;
90
170
  const baseValue = selectedIndex * itemHeightRef.current;
91
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
171
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
92
172
  setCurrentIndex(selectedIndex);
173
+ const tid = setTimeout(() => {
174
+ syncScrollFromPropsRef.current = false;
175
+ }, 400);
176
+ return () => clearTimeout(tid);
93
177
  }
94
178
  }, [selectedIndex, range]);
95
179
  // 是否处于归中状态
96
180
  const isCenterTimerId = React.useRef(null);
97
- // 简化为直接在滚动结束时通知父组件
181
+ // 滚动静止后归中并同步选中
98
182
  const handleScrollEnd = () => {
99
183
  if (!scrollViewRef.current) return;
100
184
  if (isCenterTimerId.current) {
101
185
  clearTimeout(isCenterTimerId.current);
102
186
  isCenterTimerId.current = null;
103
187
  }
104
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
188
+ // 100ms 内无新滚动则归中并提交索引
105
189
  isCenterTimerId.current = setTimeout(() => {
106
190
  if (!scrollViewRef.current) return;
107
191
  const scrollTop = scrollViewRef.current.scrollTop;
108
- const newIndex = getSelectedIndex(scrollTop);
109
- 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;
110
196
  const baseValue = newIndex * itemHeightRef.current;
111
197
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
112
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset);
198
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
113
199
  updateIndex(newIndex, columnId);
114
200
  onColumnChange === null || onColumnChange === void 0 ? void 0 : onColumnChange({
115
201
  columnId,
116
202
  index: newIndex
117
203
  });
204
+ pendingScrollSettleFocusRef.current = false;
205
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
206
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
207
+ }
208
+ syncScrollFromPropsRef.current = false;
118
209
  isCenterTimerId.current = null;
119
210
  }, 100);
120
211
  };
121
- // 滚动处理 - 在滚动时计算索引然后更新选中项样式
212
+ // 滚动中:按 scrollTop 更新高亮索引
122
213
  const handleScroll = () => {
123
214
  if (!scrollViewRef.current) return;
124
215
  if (isCenterTimerId.current) {
@@ -126,7 +217,17 @@ function PickerGroupBasic(props) {
126
217
  isCenterTimerId.current = null;
127
218
  }
128
219
  const scrollTop = scrollViewRef.current.scrollTop;
129
- 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
+ }
130
231
  if (newIndex !== currentIndex) {
131
232
  setCurrentIndex(newIndex);
132
233
  }
@@ -134,11 +235,14 @@ function PickerGroupBasic(props) {
134
235
  // 渲染选项
135
236
  const pickerItem = range.map((item, index) => {
136
237
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
137
- return createComponent(View, {
238
+ return createComponent(View, mergeProps({
138
239
  id: `picker-item-${columnId}-${index}`,
139
240
  key: index,
140
241
  ref: el => itemRefs.current[index] = el,
141
- 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
+ } : {}, {
142
246
  get style() {
143
247
  return {
144
248
  height: PICKER_LINE_HEIGHT,
@@ -146,7 +250,7 @@ function PickerGroupBasic(props) {
146
250
  };
147
251
  },
148
252
  children: content
149
- });
253
+ }));
150
254
  });
151
255
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
152
256
  key: `blank-top-${idx}`,
@@ -161,6 +265,10 @@ function PickerGroupBasic(props) {
161
265
  height: PICKER_LINE_HEIGHT
162
266
  }
163
267
  }))];
268
+ const clickScrollViewProps = enableClickItemScroll ? {
269
+ scrollIntoView,
270
+ scrollIntoViewAlignment: 'center'
271
+ } : {};
164
272
  return createComponent(View, {
165
273
  className: "taro-picker__group",
166
274
  get children() {
@@ -170,7 +278,7 @@ function PickerGroupBasic(props) {
170
278
  className: "taro-picker__indicator"
171
279
  }, indicatorStyle ? {
172
280
  style: indicatorStyle
173
- } : {})), createComponent(ScrollView, {
281
+ } : {})), createComponent(ScrollView, mergeProps({
174
282
  ref: scrollViewRef,
175
283
  scrollY: true,
176
284
  showScrollbar: false,
@@ -178,13 +286,16 @@ function PickerGroupBasic(props) {
178
286
  style: {
179
287
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
180
288
  },
181
- scrollTop: targetScrollTop,
289
+ scrollTop: targetScrollTop
290
+ }, clickScrollViewProps, {
182
291
  onScroll: handleScroll,
183
- onTouchStart: () => setIsTouching(true),
292
+ onTouchStart: () => {
293
+ isTouchingRef.current = true;
294
+ },
184
295
  onScrollEnd: handleScrollEnd,
185
296
  scrollWithAnimation: true,
186
297
  children: realPickerItems
187
- })];
298
+ }))];
188
299
  }
189
300
  });
190
301
  }
@@ -196,63 +307,189 @@ function PickerGroupTime(props) {
196
307
  columnId,
197
308
  updateIndex,
198
309
  selectedIndex = 0,
199
- colors = {}
310
+ colors = {},
311
+ timeA11yLimitFocus,
312
+ onTimeA11yLimitFocusConsumed,
313
+ timeA11yLimitEventNonce,
314
+ enableClickItemScroll = true
200
315
  } = props;
201
316
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
202
317
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
318
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
203
319
  const scrollViewRef = React.useRef(null);
204
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);
205
329
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
206
- const [isTouching, setIsTouching] = React.useState(false);
330
+ const isTouchingRef = React.useRef(false);
331
+ const lengthScaleRatioRef = React.useRef(1);
332
+ const useMeasuredScaleRef = React.useRef(false);
207
333
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
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 并判定缩放模式
208
380
  React.useEffect(() => {
209
- var _a;
210
- if (process.env.TARO_PLATFORM !== 'harmony') {
211
- if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
212
- itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
213
- } else {
214
- console.warn('Height measurement anomaly');
381
+ Taro.getSystemInfo({
382
+ success: res => {
383
+ lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
384
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
385
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
386
+ },
387
+ fail: () => {
388
+ lengthScaleRatioRef.current = 1;
389
+ useMeasuredScaleRef.current = false;
215
390
  }
216
- }
391
+ });
392
+ }, []);
393
+ React.useEffect(() => {
394
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
217
395
  }, [range.length]); // 只在range长度变化时重新计算
218
- const getSelectedIndex = scrollTop => {
219
- return Math.round(scrollTop / itemHeightRef.current);
220
- };
221
- // 当selectedIndex变化时,调整滚动位置
396
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
222
397
  React.useEffect(() => {
223
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
398
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
399
+ syncScrollFromPropsRef.current = true;
224
400
  const baseValue = selectedIndex * itemHeightRef.current;
225
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
401
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
226
402
  setCurrentIndex(selectedIndex);
403
+ const tid = setTimeout(() => {
404
+ syncScrollFromPropsRef.current = false;
405
+ }, 400);
406
+ return () => clearTimeout(tid);
227
407
  }
228
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]);
229
444
  // 是否处于归中状态
230
445
  const isCenterTimerId = React.useRef(null);
231
- // 简化为直接在滚动结束时通知父组件
446
+ // 滚动静止后归中并同步选中
232
447
  const handleScrollEnd = () => {
233
448
  if (!scrollViewRef.current) return;
234
449
  if (isCenterTimerId.current) {
235
450
  clearTimeout(isCenterTimerId.current);
236
451
  isCenterTimerId.current = null;
237
452
  }
238
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
453
+ // 100ms 内无新滚动则归中并提交索引
239
454
  isCenterTimerId.current = setTimeout(() => {
240
455
  if (!scrollViewRef.current) return;
241
456
  const scrollTop = scrollViewRef.current.scrollTop;
242
- const newIndex = getSelectedIndex(scrollTop);
243
- setIsTouching(false);
244
- // 调用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;
245
461
  const isLimited = Boolean(updateIndex(newIndex, columnId, true));
246
- // 如果没有触发限位,才执行归中逻辑
462
+ const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
463
+ if (isLimited) {
464
+ pendingScrollSettleFocusRef.current = false;
465
+ }
247
466
  if (!isLimited) {
248
467
  const baseValue = newIndex * itemHeightRef.current;
249
468
  const randomOffset = Math.random() * 0.001;
250
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset);
469
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
470
+ }
471
+ if (!isLimited && hasPendingLimitFocus) {
472
+ pendingScrollSettleFocusRef.current = false;
473
+ schedulePendingLimitFocus();
474
+ syncScrollFromPropsRef.current = false;
475
+ isCenterTimerId.current = null;
476
+ return;
251
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;
252
489
  isCenterTimerId.current = null;
253
490
  }, 100);
254
491
  };
255
- // 滚动处理
492
+ // 滚动中:按 scrollTop 更新高亮索引
256
493
  const handleScroll = () => {
257
494
  if (!scrollViewRef.current) return;
258
495
  if (isCenterTimerId.current) {
@@ -260,19 +497,35 @@ function PickerGroupTime(props) {
260
497
  isCenterTimerId.current = null;
261
498
  }
262
499
  const scrollTop = scrollViewRef.current.scrollTop;
263
- 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
+ }
264
511
  if (newIndex !== currentIndex) {
265
512
  setCurrentIndex(newIndex);
266
513
  }
514
+ if (pendingLimitFocusRef.current) {
515
+ schedulePendingLimitFocus();
516
+ }
267
517
  };
268
518
  // 渲染选项
269
519
  const pickerItem = range.map((item, index) => {
270
520
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
271
- return createComponent(View, {
521
+ return createComponent(View, mergeProps({
272
522
  id: `picker-item-${columnId}-${index}`,
273
523
  key: index,
274
524
  ref: el => itemRefs.current[index] = el,
275
- 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
+ } : {}, {
276
529
  get style() {
277
530
  return {
278
531
  height: PICKER_LINE_HEIGHT,
@@ -280,7 +533,7 @@ function PickerGroupTime(props) {
280
533
  };
281
534
  },
282
535
  children: content
283
- });
536
+ }));
284
537
  });
285
538
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
286
539
  key: `blank-top-${idx}`,
@@ -295,6 +548,10 @@ function PickerGroupTime(props) {
295
548
  height: PICKER_LINE_HEIGHT
296
549
  }
297
550
  }))];
551
+ const clickScrollViewProps = enableClickItemScroll ? {
552
+ scrollIntoView,
553
+ scrollIntoViewAlignment: 'center'
554
+ } : {};
298
555
  return createComponent(View, {
299
556
  className: "taro-picker__group",
300
557
  get children() {
@@ -304,7 +561,7 @@ function PickerGroupTime(props) {
304
561
  className: "taro-picker__indicator"
305
562
  }, indicatorStyle ? {
306
563
  style: indicatorStyle
307
- } : {})), createComponent(ScrollView, {
564
+ } : {})), createComponent(ScrollView, mergeProps({
308
565
  ref: scrollViewRef,
309
566
  scrollY: true,
310
567
  showScrollbar: false,
@@ -312,13 +569,16 @@ function PickerGroupTime(props) {
312
569
  style: {
313
570
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
314
571
  },
315
- scrollTop: targetScrollTop,
572
+ scrollTop: targetScrollTop
573
+ }, clickScrollViewProps, {
316
574
  onScroll: handleScroll,
317
- onTouchStart: () => setIsTouching(true),
575
+ onTouchStart: () => {
576
+ isTouchingRef.current = true;
577
+ },
318
578
  onScrollEnd: handleScrollEnd,
319
579
  scrollWithAnimation: true,
320
580
  children: realPickerItems
321
- })];
581
+ }))];
322
582
  }
323
583
  });
324
584
  }
@@ -329,53 +589,73 @@ function PickerGroupDate(props) {
329
589
  columnId,
330
590
  updateDay,
331
591
  selectedIndex = 0,
332
- colors = {}
592
+ colors = {},
593
+ enableClickItemScroll = true
333
594
  } = props;
334
595
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
335
596
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
597
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
336
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);
337
604
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
338
- const [isTouching, setIsTouching] = React.useState(false);
605
+ const isTouchingRef = React.useRef(false);
606
+ const lengthScaleRatioRef = React.useRef(1);
607
+ const useMeasuredScaleRef = React.useRef(false);
339
608
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
609
+ // 初始化时计算 lengthScaleRatio 并判定缩放模式
340
610
  React.useEffect(() => {
341
- var _a;
342
- if (process.env.TARO_PLATFORM !== 'harmony') {
343
- if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
344
- itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
345
- } else {
346
- console.warn('Height measurement anomaly');
611
+ Taro.getSystemInfo({
612
+ success: res => {
613
+ lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
614
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
615
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
616
+ },
617
+ fail: () => {
618
+ lengthScaleRatioRef.current = 1;
619
+ useMeasuredScaleRef.current = false;
347
620
  }
348
- }
621
+ });
622
+ }, []);
623
+ React.useEffect(() => {
624
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
349
625
  }, [range.length]); // 只在range长度变化时重新计算
350
- const getSelectedIndex = scrollTop => {
351
- return Math.round(scrollTop / itemHeightRef.current);
352
- };
353
- // 当selectedIndex变化时,调整滚动位置
626
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
354
627
  React.useEffect(() => {
355
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
628
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
629
+ syncScrollFromPropsRef.current = true;
356
630
  const baseValue = selectedIndex * itemHeightRef.current;
357
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
631
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
358
632
  setCurrentIndex(selectedIndex);
633
+ const tid = setTimeout(() => {
634
+ syncScrollFromPropsRef.current = false;
635
+ }, 400);
636
+ return () => clearTimeout(tid);
359
637
  }
360
638
  }, [selectedIndex, range]);
361
639
  // 是否处于归中状态
362
640
  const isCenterTimerId = React.useRef(null);
363
- // 简化为直接在滚动结束时通知父组件
641
+ // 滚动静止后归中并同步选中
364
642
  const handleScrollEnd = () => {
365
643
  if (!scrollViewRef.current) return;
366
644
  if (isCenterTimerId.current) {
367
645
  clearTimeout(isCenterTimerId.current);
368
646
  isCenterTimerId.current = null;
369
647
  }
370
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
648
+ // 100ms 内无新滚动则归中并提交索引
371
649
  isCenterTimerId.current = setTimeout(() => {
372
650
  if (!scrollViewRef.current) return;
373
651
  const scrollTop = scrollViewRef.current.scrollTop;
374
- const newIndex = getSelectedIndex(scrollTop);
375
- 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;
376
656
  const baseValue = newIndex * itemHeightRef.current;
377
657
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
378
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset);
658
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
379
659
  // 更新日期值
380
660
  if (updateDay) {
381
661
  // 解析文本中的数字(移除年、月、日等后缀)
@@ -383,10 +663,15 @@ function PickerGroupDate(props) {
383
663
  const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
384
664
  updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
385
665
  }
666
+ pendingScrollSettleFocusRef.current = false;
667
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
668
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
669
+ }
670
+ syncScrollFromPropsRef.current = false;
386
671
  isCenterTimerId.current = null;
387
672
  }, 100);
388
673
  };
389
- // 滚动处理
674
+ // 滚动中:按 scrollTop 更新高亮索引
390
675
  const handleScroll = () => {
391
676
  if (!scrollViewRef.current) return;
392
677
  if (isCenterTimerId.current) {
@@ -394,17 +679,31 @@ function PickerGroupDate(props) {
394
679
  isCenterTimerId.current = null;
395
680
  }
396
681
  const scrollTop = scrollViewRef.current.scrollTop;
397
- 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
+ }
398
693
  if (newIndex !== currentIndex) {
399
694
  setCurrentIndex(newIndex);
400
695
  }
401
696
  };
402
697
  // 渲染选项
403
698
  const pickerItem = range.map((item, index) => {
404
- return createComponent(View, {
699
+ return createComponent(View, mergeProps({
405
700
  id: `picker-item-${columnId}-${index}`,
406
701
  key: index,
407
- 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
+ } : {}, {
408
707
  get style() {
409
708
  return {
410
709
  height: PICKER_LINE_HEIGHT,
@@ -412,7 +711,7 @@ function PickerGroupDate(props) {
412
711
  };
413
712
  },
414
713
  children: item
415
- });
714
+ }));
416
715
  });
417
716
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
418
717
  key: `blank-top-${idx}`,
@@ -427,6 +726,10 @@ function PickerGroupDate(props) {
427
726
  height: PICKER_LINE_HEIGHT
428
727
  }
429
728
  }))];
729
+ const clickScrollViewProps = enableClickItemScroll ? {
730
+ scrollIntoView,
731
+ scrollIntoViewAlignment: 'center'
732
+ } : {};
430
733
  return createComponent(View, {
431
734
  className: "taro-picker__group",
432
735
  get children() {
@@ -436,7 +739,7 @@ function PickerGroupDate(props) {
436
739
  className: "taro-picker__indicator"
437
740
  }, indicatorStyle ? {
438
741
  style: indicatorStyle
439
- } : {})), createComponent(ScrollView, {
742
+ } : {})), createComponent(ScrollView, mergeProps({
440
743
  ref: scrollViewRef,
441
744
  scrollY: true,
442
745
  showScrollbar: false,
@@ -444,13 +747,16 @@ function PickerGroupDate(props) {
444
747
  style: {
445
748
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
446
749
  },
447
- scrollTop: targetScrollTop,
750
+ scrollTop: targetScrollTop
751
+ }, clickScrollViewProps, {
448
752
  onScroll: handleScroll,
449
- onTouchStart: () => setIsTouching(true),
753
+ onTouchStart: () => {
754
+ isTouchingRef.current = true;
755
+ },
450
756
  onScrollEnd: handleScrollEnd,
451
757
  scrollWithAnimation: true,
452
758
  children: realPickerItems
453
- })];
759
+ }))];
454
760
  }
455
761
  });
456
762
  }
@@ -463,37 +769,54 @@ function PickerGroupRegion(props) {
463
769
  updateIndex,
464
770
  selectedIndex = 0,
465
771
  // 使用selectedIndex参数,默认为0
466
- colors = {}
772
+ colors = {},
773
+ enableClickItemScroll = true
467
774
  } = props;
468
775
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
469
776
  const scrollViewRef = React.useRef(null);
470
777
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
778
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
471
779
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
472
- const [isTouching, setIsTouching] = React.useState(false);
780
+ const isTouchingRef = React.useRef(false);
781
+ const lengthScaleRatioRef = React.useRef(1);
782
+ const useMeasuredScaleRef = React.useRef(false);
473
783
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
474
- const isUserBeginScrollRef = React.useRef(false);
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 并判定缩放模式
475
790
  React.useEffect(() => {
476
- var _a;
477
- if (process.env.TARO_PLATFORM !== 'harmony') {
478
- if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
479
- itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
480
- } else {
481
- console.warn('Height measurement anomaly');
791
+ Taro.getSystemInfo({
792
+ success: res => {
793
+ lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
794
+ useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
795
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
796
+ },
797
+ fail: () => {
798
+ lengthScaleRatioRef.current = 1;
799
+ useMeasuredScaleRef.current = false;
482
800
  }
483
- }
801
+ });
802
+ }, []);
803
+ React.useEffect(() => {
804
+ itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
484
805
  }, [range.length]); // 只在range长度变化时重新计算
485
- const getSelectedIndex = scrollTop => {
486
- return Math.round(scrollTop / itemHeightRef.current);
487
- };
488
- // 当selectedIndex变化时,调整滚动位置
806
+ // props selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
489
807
  React.useEffect(() => {
490
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
808
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
809
+ syncScrollFromPropsRef.current = true;
491
810
  const baseValue = selectedIndex * itemHeightRef.current;
492
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
811
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
493
812
  setCurrentIndex(selectedIndex);
813
+ const tid = setTimeout(() => {
814
+ syncScrollFromPropsRef.current = false;
815
+ }, 400);
816
+ return () => clearTimeout(tid);
494
817
  }
495
818
  }, [selectedIndex, range]);
496
- // 滚动结束处理
819
+ // 滚动静止后归中(debounce)
497
820
  const isCenterTimerId = React.useRef(null);
498
821
  const handleScrollEnd = () => {
499
822
  if (!scrollViewRef.current) return;
@@ -501,19 +824,27 @@ function PickerGroupRegion(props) {
501
824
  clearTimeout(isCenterTimerId.current);
502
825
  isCenterTimerId.current = null;
503
826
  }
504
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
827
+ // 100ms 内无新滚动则归中并提交索引
505
828
  isCenterTimerId.current = setTimeout(() => {
506
829
  if (!scrollViewRef.current) return;
507
830
  const scrollTop = scrollViewRef.current.scrollTop;
508
- const newIndex = getSelectedIndex(scrollTop);
509
- 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;
510
835
  const baseValue = newIndex * itemHeightRef.current;
511
836
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
512
- setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset);
513
- updateIndex(newIndex, columnId, false, isUserBeginScrollRef.current);
837
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.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;
514
845
  }, 100);
515
846
  };
516
- // 滚动处理 - 在滚动时计算索引
847
+ // 滚动中:按 scrollTop 更新高亮索引
517
848
  const handleScroll = () => {
518
849
  if (!scrollViewRef.current) return;
519
850
  if (isCenterTimerId.current) {
@@ -521,7 +852,17 @@ function PickerGroupRegion(props) {
521
852
  isCenterTimerId.current = null;
522
853
  }
523
854
  const scrollTop = scrollViewRef.current.scrollTop;
524
- 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
+ }
525
866
  if (newIndex !== currentIndex) {
526
867
  setCurrentIndex(newIndex);
527
868
  }
@@ -529,10 +870,14 @@ function PickerGroupRegion(props) {
529
870
  // 渲染选项
530
871
  const pickerItem = range.map((item, index) => {
531
872
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
532
- return createComponent(View, {
873
+ return createComponent(View, mergeProps({
533
874
  id: `picker-item-${columnId}-${index}`,
534
875
  key: index,
535
- 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
+ } : {}, {
536
881
  get style() {
537
882
  return {
538
883
  height: PICKER_LINE_HEIGHT,
@@ -540,7 +885,7 @@ function PickerGroupRegion(props) {
540
885
  };
541
886
  },
542
887
  children: content
543
- });
888
+ }));
544
889
  });
545
890
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
546
891
  key: `blank-top-${idx}`,
@@ -555,6 +900,10 @@ function PickerGroupRegion(props) {
555
900
  height: PICKER_LINE_HEIGHT
556
901
  }
557
902
  }))];
903
+ const clickScrollViewProps = enableClickItemScroll ? {
904
+ scrollIntoView,
905
+ scrollIntoViewAlignment: 'center'
906
+ } : {};
558
907
  return createComponent(View, {
559
908
  className: "taro-picker__group",
560
909
  get children() {
@@ -564,7 +913,7 @@ function PickerGroupRegion(props) {
564
913
  className: "taro-picker__indicator"
565
914
  }, indicatorStyle ? {
566
915
  style: indicatorStyle
567
- } : {})), createComponent(ScrollView, {
916
+ } : {})), createComponent(ScrollView, mergeProps({
568
917
  ref: scrollViewRef,
569
918
  scrollY: true,
570
919
  showScrollbar: false,
@@ -572,16 +921,16 @@ function PickerGroupRegion(props) {
572
921
  style: {
573
922
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
574
923
  },
575
- scrollTop: targetScrollTop,
924
+ scrollTop: targetScrollTop
925
+ }, clickScrollViewProps, {
576
926
  onScroll: handleScroll,
577
927
  onTouchStart: () => {
578
- setIsTouching(true);
579
- isUserBeginScrollRef.current = true;
928
+ isTouchingRef.current = true;
580
929
  },
581
930
  onScrollEnd: handleScrollEnd,
582
931
  scrollWithAnimation: true,
583
932
  children: realPickerItems
584
- })];
933
+ }))];
585
934
  }
586
935
  });
587
936
  }