@tarojs/components-react 4.1.12-beta.40 → 4.1.12-beta.42

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.
@@ -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; // 空白行数
@@ -156,16 +182,22 @@ function PickerGroupBasic(props) {
156
182
  onColumnChange,
157
183
  selectedIndex = 0,
158
184
  // 使用selectedIndex参数,默认为0
159
- colors = {}
185
+ colors = {},
186
+ enableClickItemScroll = true
160
187
  } = props;
161
188
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
162
189
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
190
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
163
191
  const scrollViewRef = React.useRef(null);
164
192
  const itemRefs = React.useRef([]);
193
+ const selectedIndexPropRef = React.useRef(selectedIndex);
194
+ selectedIndexPropRef.current = selectedIndex;
195
+ const syncScrollFromPropsRef = React.useRef(false);
196
+ const pendingScrollSettleFocusRef = React.useRef(false);
165
197
  // 使用selectedIndex初始化当前索引
166
198
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
167
199
  // 触摸状态用于优化用户体验
168
- const [isTouching, setIsTouching] = React.useState(false);
200
+ const isTouchingRef = React.useRef(false);
169
201
  const lengthScaleRatioRef = React.useRef(1);
170
202
  const useMeasuredScaleRef = React.useRef(false);
171
203
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
@@ -186,29 +218,36 @@ function PickerGroupBasic(props) {
186
218
  React.useEffect(() => {
187
219
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
188
220
  }, [range.length]); // 只在range长度变化时重新计算
189
- // selectedIndex变化时,调整滚动位置
221
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
190
222
  React.useEffect(() => {
191
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
223
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
224
+ syncScrollFromPropsRef.current = true;
192
225
  const baseValue = selectedIndex * itemHeightRef.current;
193
226
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
194
227
  setCurrentIndex(selectedIndex);
228
+ const tid = setTimeout(() => {
229
+ syncScrollFromPropsRef.current = false;
230
+ }, 400);
231
+ return () => clearTimeout(tid);
195
232
  }
196
233
  }, [selectedIndex, range]);
197
234
  // 是否处于归中状态
198
235
  const isCenterTimerId = React.useRef(null);
199
- // 简化为直接在滚动结束时通知父组件
236
+ // 滚动静止后归中并同步选中
200
237
  const handleScrollEnd = () => {
201
238
  if (!scrollViewRef.current) return;
202
239
  if (isCenterTimerId.current) {
203
240
  clearTimeout(isCenterTimerId.current);
204
241
  isCenterTimerId.current = null;
205
242
  }
206
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
243
+ // 100ms 内无新滚动则归中并提交索引
207
244
  isCenterTimerId.current = setTimeout(() => {
208
245
  if (!scrollViewRef.current) return;
209
246
  const scrollTop = scrollViewRef.current.scrollTop;
210
247
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
211
- setIsTouching(false);
248
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
249
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
250
+ isTouchingRef.current = false;
212
251
  const baseValue = newIndex * itemHeightRef.current;
213
252
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
214
253
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -217,10 +256,15 @@ function PickerGroupBasic(props) {
217
256
  columnId,
218
257
  index: newIndex
219
258
  });
259
+ pendingScrollSettleFocusRef.current = false;
260
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
261
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
262
+ }
263
+ syncScrollFromPropsRef.current = false;
220
264
  isCenterTimerId.current = null;
221
265
  }, 100);
222
266
  };
223
- // 滚动处理 - 在滚动时计算索引然后更新选中项样式
267
+ // 滚动中:按 scrollTop 更新高亮索引
224
268
  const handleScroll = () => {
225
269
  if (!scrollViewRef.current) return;
226
270
  if (isCenterTimerId.current) {
@@ -228,7 +272,17 @@ function PickerGroupBasic(props) {
228
272
  isCenterTimerId.current = null;
229
273
  }
230
274
  const scrollTop = scrollViewRef.current.scrollTop;
231
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
275
+ const ih = itemHeightRef.current;
276
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
277
+ const spi = selectedIndexPropRef.current;
278
+ if (newIndex !== spi) {
279
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
280
+ pendingScrollSettleFocusRef.current = true;
281
+ }
282
+ if (isTouchingRef.current) {
283
+ syncScrollFromPropsRef.current = false;
284
+ }
285
+ }
232
286
  if (newIndex !== currentIndex) {
233
287
  setCurrentIndex(newIndex);
234
288
  }
@@ -240,6 +294,9 @@ function PickerGroupBasic(props) {
240
294
  id: `picker-item-${columnId}-${index}`,
241
295
  ref: el => itemRefs.current[index] = el,
242
296
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
297
+ ...(enableClickItemScroll ? {
298
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
299
+ } : {}),
243
300
  style: {
244
301
  height: PICKER_LINE_HEIGHT,
245
302
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -258,6 +315,10 @@ function PickerGroupBasic(props) {
258
315
  height: PICKER_LINE_HEIGHT
259
316
  }
260
317
  }, `blank-bottom-${idx}`))];
318
+ const clickScrollViewProps = enableClickItemScroll ? {
319
+ scrollIntoView,
320
+ scrollIntoViewAlignment: 'center'
321
+ } : {};
261
322
  return /*#__PURE__*/jsxs(View, {
262
323
  className: "taro-picker__group",
263
324
  children: [/*#__PURE__*/jsx(View, {
@@ -276,8 +337,11 @@ function PickerGroupBasic(props) {
276
337
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
277
338
  },
278
339
  scrollTop: targetScrollTop,
340
+ ...clickScrollViewProps,
279
341
  onScroll: handleScroll,
280
- onTouchStart: () => setIsTouching(true),
342
+ onTouchStart: () => {
343
+ isTouchingRef.current = true;
344
+ },
281
345
  onScrollEnd: handleScrollEnd,
282
346
  scrollWithAnimation: true,
283
347
  children: realPickerItems
@@ -292,17 +356,75 @@ function PickerGroupTime(props) {
292
356
  columnId,
293
357
  updateIndex,
294
358
  selectedIndex = 0,
295
- colors = {}
359
+ colors = {},
360
+ timeA11yLimitFocus,
361
+ onTimeA11yLimitFocusConsumed,
362
+ timeA11yLimitEventNonce,
363
+ enableClickItemScroll = true
296
364
  } = props;
297
365
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
298
366
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
367
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
299
368
  const scrollViewRef = React.useRef(null);
300
369
  const itemRefs = React.useRef([]);
370
+ const selectedIndexPropRef = React.useRef(selectedIndex);
371
+ selectedIndexPropRef.current = selectedIndex;
372
+ const syncScrollFromPropsRef = React.useRef(false);
373
+ const prevLimitEventNonceRef = React.useRef(undefined);
374
+ const suppressScrollSettleFocusNonceRef = React.useRef(null);
375
+ const pendingScrollSettleFocusRef = React.useRef(false);
376
+ const pendingLimitFocusRef = React.useRef(null);
377
+ const limitFocusTimerRef = React.useRef(null);
301
378
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
302
- const [isTouching, setIsTouching] = React.useState(false);
379
+ const isTouchingRef = React.useRef(false);
303
380
  const lengthScaleRatioRef = React.useRef(1);
304
381
  const useMeasuredScaleRef = React.useRef(false);
305
382
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
383
+ const clearLimitFocusTimer = React.useCallback(() => {
384
+ if (limitFocusTimerRef.current) {
385
+ clearTimeout(limitFocusTimerRef.current);
386
+ limitFocusTimerRef.current = null;
387
+ }
388
+ }, []);
389
+ const tryFocusPendingLimit = React.useCallback(() => {
390
+ const pending = pendingLimitFocusRef.current;
391
+ const scrollView = scrollViewRef.current;
392
+ if (!pending || !scrollView) return;
393
+ const visualIndex = getSelectedIndex(scrollView.scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
394
+ const isStable = visualIndex === pending.index;
395
+ if (!isStable) {
396
+ if (pending.attempts >= 20) {
397
+ pendingLimitFocusRef.current = null;
398
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
399
+ suppressScrollSettleFocusNonceRef.current = null;
400
+ }
401
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
402
+ return;
403
+ }
404
+ pending.attempts += 1;
405
+ clearLimitFocusTimer();
406
+ limitFocusTimerRef.current = setTimeout(() => {
407
+ tryFocusPendingLimit();
408
+ }, 50);
409
+ return;
410
+ }
411
+ const node = itemRefs.current[pending.index];
412
+ pendingLimitFocusRef.current = null;
413
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
414
+ suppressScrollSettleFocusNonceRef.current = null;
415
+ }
416
+ clearLimitFocusTimer();
417
+ requestAccessibilityFocusOnView(node);
418
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
419
+ }, [clearLimitFocusTimer, onTimeA11yLimitFocusConsumed]);
420
+ const schedulePendingLimitFocus = React.useCallback(() => {
421
+ if (!pendingLimitFocusRef.current) return;
422
+ clearLimitFocusTimer();
423
+ limitFocusTimerRef.current = setTimeout(() => {
424
+ tryFocusPendingLimit();
425
+ }, 100);
426
+ }, [clearLimitFocusTimer, tryFocusPendingLimit]);
427
+ React.useEffect(() => clearLimitFocusTimer, [clearLimitFocusTimer]);
306
428
  // 初始化时计算 lengthScaleRatio 并判定缩放模式
307
429
  React.useEffect(() => {
308
430
  Taro.getSystemInfo({
@@ -320,41 +442,103 @@ function PickerGroupTime(props) {
320
442
  React.useEffect(() => {
321
443
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
322
444
  }, [range.length]); // 只在range长度变化时重新计算
323
- // selectedIndex变化时,调整滚动位置
445
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
324
446
  React.useEffect(() => {
325
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
447
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
448
+ syncScrollFromPropsRef.current = true;
326
449
  const baseValue = selectedIndex * itemHeightRef.current;
327
450
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
328
451
  setCurrentIndex(selectedIndex);
452
+ const tid = setTimeout(() => {
453
+ syncScrollFromPropsRef.current = false;
454
+ }, 400);
455
+ return () => clearTimeout(tid);
329
456
  }
330
457
  }, [selectedIndex, range]);
458
+ // time 限位 nonce 变更:强制对齐 scrollTop(避免 remount 打断读屏焦点)
459
+ React.useEffect(() => {
460
+ if (!timeA11yLimitEventNonce) return;
461
+ if (!scrollViewRef.current || range.length === 0 || isTouchingRef.current) return;
462
+ syncScrollFromPropsRef.current = true;
463
+ const baseValue = selectedIndex * itemHeightRef.current;
464
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, Math.random() * 0.001, lengthScaleRatioRef.current);
465
+ setCurrentIndex(selectedIndex);
466
+ const tid = setTimeout(() => {
467
+ syncScrollFromPropsRef.current = false;
468
+ }, 400);
469
+ return () => clearTimeout(tid);
470
+ }, [timeA11yLimitEventNonce]); // 仅依赖 nonce,其余用 ref
471
+ React.useLayoutEffect(() => {
472
+ if (timeA11yLimitEventNonce === undefined) return;
473
+ const prev = prevLimitEventNonceRef.current;
474
+ prevLimitEventNonceRef.current = timeA11yLimitEventNonce;
475
+ if (timeA11yLimitEventNonce > 0 && (prev === undefined || timeA11yLimitEventNonce > prev)) {
476
+ suppressScrollSettleFocusNonceRef.current = timeA11yLimitEventNonce;
477
+ }
478
+ }, [timeA11yLimitEventNonce]);
479
+ // 限位后登记本列待聚焦项,稳定后再 requestAccessibilityFocus
480
+ React.useEffect(() => {
481
+ const req = timeA11yLimitFocus;
482
+ if (!req || req.columnId !== columnId) return;
483
+ const idx = selectedIndex;
484
+ const nonce = req.nonce;
485
+ pendingScrollSettleFocusRef.current = false;
486
+ pendingLimitFocusRef.current = {
487
+ index: idx,
488
+ nonce,
489
+ attempts: 0
490
+ };
491
+ schedulePendingLimitFocus();
492
+ }, [timeA11yLimitFocus, columnId, selectedIndex, schedulePendingLimitFocus]);
331
493
  // 是否处于归中状态
332
494
  const isCenterTimerId = React.useRef(null);
333
- // 简化为直接在滚动结束时通知父组件
495
+ // 滚动静止后归中并同步选中
334
496
  const handleScrollEnd = () => {
335
497
  if (!scrollViewRef.current) return;
336
498
  if (isCenterTimerId.current) {
337
499
  clearTimeout(isCenterTimerId.current);
338
500
  isCenterTimerId.current = null;
339
501
  }
340
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
502
+ // 100ms 内无新滚动则归中并提交索引
341
503
  isCenterTimerId.current = setTimeout(() => {
342
504
  if (!scrollViewRef.current) return;
343
505
  const scrollTop = scrollViewRef.current.scrollTop;
344
506
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
345
- setIsTouching(false);
346
- // 调用updateIndex执行限位逻辑,获取是否触发了限位
507
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
508
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
509
+ isTouchingRef.current = false;
347
510
  const isLimited = Boolean(updateIndex(newIndex, columnId, true));
348
- // 如果没有触发限位,才执行归中逻辑
511
+ const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
512
+ if (isLimited) {
513
+ pendingScrollSettleFocusRef.current = false;
514
+ }
349
515
  if (!isLimited) {
350
516
  const baseValue = newIndex * itemHeightRef.current;
351
517
  const randomOffset = Math.random() * 0.001;
352
518
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
353
519
  }
520
+ if (!isLimited && hasPendingLimitFocus) {
521
+ pendingScrollSettleFocusRef.current = false;
522
+ schedulePendingLimitFocus();
523
+ syncScrollFromPropsRef.current = false;
524
+ isCenterTimerId.current = null;
525
+ return;
526
+ }
527
+ if (!isLimited && pendingLimitFocusRef.current == null && suppressScrollSettleFocusNonceRef.current != null) {
528
+ suppressScrollSettleFocusNonceRef.current = null;
529
+ }
530
+ const suppressByLimitNonce = suppressScrollSettleFocusNonceRef.current != null;
531
+ if (!isLimited && allowA11yFocus && shouldFocusOnScrollSettle) {
532
+ pendingScrollSettleFocusRef.current = false;
533
+ if (!suppressByLimitNonce) {
534
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
535
+ }
536
+ }
537
+ syncScrollFromPropsRef.current = false;
354
538
  isCenterTimerId.current = null;
355
539
  }, 100);
356
540
  };
357
- // 滚动处理
541
+ // 滚动中:按 scrollTop 更新高亮索引
358
542
  const handleScroll = () => {
359
543
  if (!scrollViewRef.current) return;
360
544
  if (isCenterTimerId.current) {
@@ -362,10 +546,23 @@ function PickerGroupTime(props) {
362
546
  isCenterTimerId.current = null;
363
547
  }
364
548
  const scrollTop = scrollViewRef.current.scrollTop;
365
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
549
+ const ih = itemHeightRef.current;
550
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
551
+ const spi = selectedIndexPropRef.current;
552
+ if (newIndex !== spi) {
553
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
554
+ pendingScrollSettleFocusRef.current = true;
555
+ }
556
+ if (isTouchingRef.current) {
557
+ syncScrollFromPropsRef.current = false;
558
+ }
559
+ }
366
560
  if (newIndex !== currentIndex) {
367
561
  setCurrentIndex(newIndex);
368
562
  }
563
+ if (pendingLimitFocusRef.current) {
564
+ schedulePendingLimitFocus();
565
+ }
369
566
  };
370
567
  // 渲染选项
371
568
  const pickerItem = range.map((item, index) => {
@@ -374,6 +571,9 @@ function PickerGroupTime(props) {
374
571
  id: `picker-item-${columnId}-${index}`,
375
572
  ref: el => itemRefs.current[index] = el,
376
573
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
574
+ ...(enableClickItemScroll ? {
575
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
576
+ } : {}),
377
577
  style: {
378
578
  height: PICKER_LINE_HEIGHT,
379
579
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -392,6 +592,10 @@ function PickerGroupTime(props) {
392
592
  height: PICKER_LINE_HEIGHT
393
593
  }
394
594
  }, `blank-bottom-${idx}`))];
595
+ const clickScrollViewProps = enableClickItemScroll ? {
596
+ scrollIntoView,
597
+ scrollIntoViewAlignment: 'center'
598
+ } : {};
395
599
  return /*#__PURE__*/jsxs(View, {
396
600
  className: "taro-picker__group",
397
601
  children: [/*#__PURE__*/jsx(View, {
@@ -410,8 +614,11 @@ function PickerGroupTime(props) {
410
614
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
411
615
  },
412
616
  scrollTop: targetScrollTop,
617
+ ...clickScrollViewProps,
413
618
  onScroll: handleScroll,
414
- onTouchStart: () => setIsTouching(true),
619
+ onTouchStart: () => {
620
+ isTouchingRef.current = true;
621
+ },
415
622
  onScrollEnd: handleScrollEnd,
416
623
  scrollWithAnimation: true,
417
624
  children: realPickerItems
@@ -425,13 +632,20 @@ function PickerGroupDate(props) {
425
632
  columnId,
426
633
  updateDay,
427
634
  selectedIndex = 0,
428
- colors = {}
635
+ colors = {},
636
+ enableClickItemScroll = true
429
637
  } = props;
430
638
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
431
639
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
640
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
432
641
  const scrollViewRef = React.useRef(null);
642
+ const itemRefs = React.useRef([]);
643
+ const selectedIndexPropRef = React.useRef(selectedIndex);
644
+ selectedIndexPropRef.current = selectedIndex;
645
+ const syncScrollFromPropsRef = React.useRef(false);
646
+ const pendingScrollSettleFocusRef = React.useRef(false);
433
647
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
434
- const [isTouching, setIsTouching] = React.useState(false);
648
+ const isTouchingRef = React.useRef(false);
435
649
  const lengthScaleRatioRef = React.useRef(1);
436
650
  const useMeasuredScaleRef = React.useRef(false);
437
651
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
@@ -452,29 +666,36 @@ function PickerGroupDate(props) {
452
666
  React.useEffect(() => {
453
667
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
454
668
  }, [range.length]); // 只在range长度变化时重新计算
455
- // selectedIndex变化时,调整滚动位置
669
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
456
670
  React.useEffect(() => {
457
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
671
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
672
+ syncScrollFromPropsRef.current = true;
458
673
  const baseValue = selectedIndex * itemHeightRef.current;
459
674
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
460
675
  setCurrentIndex(selectedIndex);
676
+ const tid = setTimeout(() => {
677
+ syncScrollFromPropsRef.current = false;
678
+ }, 400);
679
+ return () => clearTimeout(tid);
461
680
  }
462
681
  }, [selectedIndex, range]);
463
682
  // 是否处于归中状态
464
683
  const isCenterTimerId = React.useRef(null);
465
- // 简化为直接在滚动结束时通知父组件
684
+ // 滚动静止后归中并同步选中
466
685
  const handleScrollEnd = () => {
467
686
  if (!scrollViewRef.current) return;
468
687
  if (isCenterTimerId.current) {
469
688
  clearTimeout(isCenterTimerId.current);
470
689
  isCenterTimerId.current = null;
471
690
  }
472
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
691
+ // 100ms 内无新滚动则归中并提交索引
473
692
  isCenterTimerId.current = setTimeout(() => {
474
693
  if (!scrollViewRef.current) return;
475
694
  const scrollTop = scrollViewRef.current.scrollTop;
476
695
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
477
- setIsTouching(false);
696
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
697
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
698
+ isTouchingRef.current = false;
478
699
  const baseValue = newIndex * itemHeightRef.current;
479
700
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
480
701
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -485,10 +706,15 @@ function PickerGroupDate(props) {
485
706
  const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
486
707
  updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
487
708
  }
709
+ pendingScrollSettleFocusRef.current = false;
710
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
711
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
712
+ }
713
+ syncScrollFromPropsRef.current = false;
488
714
  isCenterTimerId.current = null;
489
715
  }, 100);
490
716
  };
491
- // 滚动处理
717
+ // 滚动中:按 scrollTop 更新高亮索引
492
718
  const handleScroll = () => {
493
719
  if (!scrollViewRef.current) return;
494
720
  if (isCenterTimerId.current) {
@@ -496,7 +722,17 @@ function PickerGroupDate(props) {
496
722
  isCenterTimerId.current = null;
497
723
  }
498
724
  const scrollTop = scrollViewRef.current.scrollTop;
499
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
725
+ const ih = itemHeightRef.current;
726
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
727
+ const spi = selectedIndexPropRef.current;
728
+ if (newIndex !== spi) {
729
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
730
+ pendingScrollSettleFocusRef.current = true;
731
+ }
732
+ if (isTouchingRef.current) {
733
+ syncScrollFromPropsRef.current = false;
734
+ }
735
+ }
500
736
  if (newIndex !== currentIndex) {
501
737
  setCurrentIndex(newIndex);
502
738
  }
@@ -505,7 +741,11 @@ function PickerGroupDate(props) {
505
741
  const pickerItem = range.map((item, index) => {
506
742
  return /*#__PURE__*/jsx(View, {
507
743
  id: `picker-item-${columnId}-${index}`,
744
+ ref: el => itemRefs.current[index] = el,
508
745
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
746
+ ...(enableClickItemScroll ? {
747
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
748
+ } : {}),
509
749
  style: {
510
750
  height: PICKER_LINE_HEIGHT,
511
751
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -524,6 +764,10 @@ function PickerGroupDate(props) {
524
764
  height: PICKER_LINE_HEIGHT
525
765
  }
526
766
  }, `blank-bottom-${idx}`))];
767
+ const clickScrollViewProps = enableClickItemScroll ? {
768
+ scrollIntoView,
769
+ scrollIntoViewAlignment: 'center'
770
+ } : {};
527
771
  return /*#__PURE__*/jsxs(View, {
528
772
  className: "taro-picker__group",
529
773
  children: [/*#__PURE__*/jsx(View, {
@@ -542,8 +786,11 @@ function PickerGroupDate(props) {
542
786
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
543
787
  },
544
788
  scrollTop: targetScrollTop,
789
+ ...clickScrollViewProps,
545
790
  onScroll: handleScroll,
546
- onTouchStart: () => setIsTouching(true),
791
+ onTouchStart: () => {
792
+ isTouchingRef.current = true;
793
+ },
547
794
  onScrollEnd: handleScrollEnd,
548
795
  scrollWithAnimation: true,
549
796
  children: realPickerItems
@@ -559,17 +806,23 @@ function PickerGroupRegion(props) {
559
806
  updateIndex,
560
807
  selectedIndex = 0,
561
808
  // 使用selectedIndex参数,默认为0
562
- colors = {}
809
+ colors = {},
810
+ enableClickItemScroll = true
563
811
  } = props;
564
812
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
565
813
  const scrollViewRef = React.useRef(null);
566
814
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
815
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
567
816
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
568
- const [isTouching, setIsTouching] = React.useState(false);
817
+ const isTouchingRef = React.useRef(false);
569
818
  const lengthScaleRatioRef = React.useRef(1);
570
819
  const useMeasuredScaleRef = React.useRef(false);
571
820
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
572
- const isUserBeginScrollRef = React.useRef(false);
821
+ const itemRefs = React.useRef([]);
822
+ const selectedIndexPropRef = React.useRef(selectedIndex);
823
+ selectedIndexPropRef.current = selectedIndex;
824
+ const syncScrollFromPropsRef = React.useRef(false);
825
+ const pendingScrollSettleFocusRef = React.useRef(false);
573
826
  // 初始化时计算 lengthScaleRatio 并判定缩放模式
574
827
  React.useEffect(() => {
575
828
  Taro.getSystemInfo({
@@ -587,15 +840,20 @@ function PickerGroupRegion(props) {
587
840
  React.useEffect(() => {
588
841
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
589
842
  }, [range.length]); // 只在range长度变化时重新计算
590
- // selectedIndex变化时,调整滚动位置
843
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
591
844
  React.useEffect(() => {
592
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
845
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
846
+ syncScrollFromPropsRef.current = true;
593
847
  const baseValue = selectedIndex * itemHeightRef.current;
594
848
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
595
849
  setCurrentIndex(selectedIndex);
850
+ const tid = setTimeout(() => {
851
+ syncScrollFromPropsRef.current = false;
852
+ }, 400);
853
+ return () => clearTimeout(tid);
596
854
  }
597
855
  }, [selectedIndex, range]);
598
- // 滚动结束处理
856
+ // 滚动静止后归中(debounce)
599
857
  const isCenterTimerId = React.useRef(null);
600
858
  const handleScrollEnd = () => {
601
859
  if (!scrollViewRef.current) return;
@@ -603,19 +861,27 @@ function PickerGroupRegion(props) {
603
861
  clearTimeout(isCenterTimerId.current);
604
862
  isCenterTimerId.current = null;
605
863
  }
606
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
864
+ // 100ms 内无新滚动则归中并提交索引
607
865
  isCenterTimerId.current = setTimeout(() => {
608
866
  if (!scrollViewRef.current) return;
609
867
  const scrollTop = scrollViewRef.current.scrollTop;
610
868
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
611
- setIsTouching(false);
869
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
870
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
871
+ isTouchingRef.current = false;
612
872
  const baseValue = newIndex * itemHeightRef.current;
613
873
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
614
874
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
615
- updateIndex(newIndex, columnId, false, isUserBeginScrollRef.current);
875
+ updateIndex(newIndex, columnId, false, allowA11yFocus);
876
+ pendingScrollSettleFocusRef.current = false;
877
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
878
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
879
+ }
880
+ syncScrollFromPropsRef.current = false;
881
+ isCenterTimerId.current = null;
616
882
  }, 100);
617
883
  };
618
- // 滚动处理 - 在滚动时计算索引
884
+ // 滚动中:按 scrollTop 更新高亮索引
619
885
  const handleScroll = () => {
620
886
  if (!scrollViewRef.current) return;
621
887
  if (isCenterTimerId.current) {
@@ -623,7 +889,17 @@ function PickerGroupRegion(props) {
623
889
  isCenterTimerId.current = null;
624
890
  }
625
891
  const scrollTop = scrollViewRef.current.scrollTop;
626
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
892
+ const ih = itemHeightRef.current;
893
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
894
+ const spi = selectedIndexPropRef.current;
895
+ if (newIndex !== spi) {
896
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
897
+ pendingScrollSettleFocusRef.current = true;
898
+ }
899
+ if (isTouchingRef.current) {
900
+ syncScrollFromPropsRef.current = false;
901
+ }
902
+ }
627
903
  if (newIndex !== currentIndex) {
628
904
  setCurrentIndex(newIndex);
629
905
  }
@@ -633,7 +909,11 @@ function PickerGroupRegion(props) {
633
909
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
634
910
  return /*#__PURE__*/jsx(View, {
635
911
  id: `picker-item-${columnId}-${index}`,
912
+ ref: el => itemRefs.current[index] = el,
636
913
  className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
914
+ ...(enableClickItemScroll ? {
915
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
916
+ } : {}),
637
917
  style: {
638
918
  height: PICKER_LINE_HEIGHT,
639
919
  color: index === currentIndex ? colors.itemSelectedColor || undefined : colors.itemDefaultColor || undefined
@@ -652,6 +932,10 @@ function PickerGroupRegion(props) {
652
932
  height: PICKER_LINE_HEIGHT
653
933
  }
654
934
  }, `blank-bottom-${idx}`))];
935
+ const clickScrollViewProps = enableClickItemScroll ? {
936
+ scrollIntoView,
937
+ scrollIntoViewAlignment: 'center'
938
+ } : {};
655
939
  return /*#__PURE__*/jsxs(View, {
656
940
  className: "taro-picker__group",
657
941
  children: [/*#__PURE__*/jsx(View, {
@@ -670,10 +954,10 @@ function PickerGroupRegion(props) {
670
954
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
671
955
  },
672
956
  scrollTop: targetScrollTop,
957
+ ...clickScrollViewProps,
673
958
  onScroll: handleScroll,
674
959
  onTouchStart: () => {
675
- setIsTouching(true);
676
- isUserBeginScrollRef.current = true;
960
+ isTouchingRef.current = true;
677
961
  },
678
962
  onScrollEnd: handleScrollEnd,
679
963
  scrollWithAnimation: true,