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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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; // 可见行数
@@ -157,16 +182,22 @@ function PickerGroupBasic(props) {
157
182
  onColumnChange,
158
183
  selectedIndex = 0,
159
184
  // 使用selectedIndex参数,默认为0
160
- colors = {}
185
+ colors = {},
186
+ enableClickItemScroll = true
161
187
  } = props;
162
188
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
163
189
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
190
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
164
191
  const scrollViewRef = React.useRef(null);
165
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);
166
197
  // 使用selectedIndex初始化当前索引
167
198
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
168
199
  // 触摸状态用于优化用户体验
169
- const [isTouching, setIsTouching] = React.useState(false);
200
+ const isTouchingRef = React.useRef(false);
170
201
  const lengthScaleRatioRef = React.useRef(1);
171
202
  const useMeasuredScaleRef = React.useRef(false);
172
203
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
@@ -187,29 +218,36 @@ function PickerGroupBasic(props) {
187
218
  React.useEffect(() => {
188
219
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
189
220
  }, [range.length]); // 只在range长度变化时重新计算
190
- // selectedIndex变化时,调整滚动位置
221
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
191
222
  React.useEffect(() => {
192
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
223
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
224
+ syncScrollFromPropsRef.current = true;
193
225
  const baseValue = selectedIndex * itemHeightRef.current;
194
226
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
195
227
  setCurrentIndex(selectedIndex);
228
+ const tid = setTimeout(() => {
229
+ syncScrollFromPropsRef.current = false;
230
+ }, 400);
231
+ return () => clearTimeout(tid);
196
232
  }
197
233
  }, [selectedIndex, range]);
198
234
  // 是否处于归中状态
199
235
  const isCenterTimerId = React.useRef(null);
200
- // 简化为直接在滚动结束时通知父组件
236
+ // 滚动静止后归中并同步选中
201
237
  const handleScrollEnd = () => {
202
238
  if (!scrollViewRef.current) return;
203
239
  if (isCenterTimerId.current) {
204
240
  clearTimeout(isCenterTimerId.current);
205
241
  isCenterTimerId.current = null;
206
242
  }
207
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
243
+ // 100ms 内无新滚动则归中并提交索引
208
244
  isCenterTimerId.current = setTimeout(() => {
209
245
  if (!scrollViewRef.current) return;
210
246
  const scrollTop = scrollViewRef.current.scrollTop;
211
247
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
212
- setIsTouching(false);
248
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
249
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
250
+ isTouchingRef.current = false;
213
251
  const baseValue = newIndex * itemHeightRef.current;
214
252
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
215
253
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -218,10 +256,15 @@ function PickerGroupBasic(props) {
218
256
  columnId,
219
257
  index: newIndex
220
258
  });
259
+ pendingScrollSettleFocusRef.current = false;
260
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
261
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
262
+ }
263
+ syncScrollFromPropsRef.current = false;
221
264
  isCenterTimerId.current = null;
222
265
  }, 100);
223
266
  };
224
- // 滚动处理 - 在滚动时计算索引然后更新选中项样式
267
+ // 滚动中:按 scrollTop 更新高亮索引
225
268
  const handleScroll = () => {
226
269
  if (!scrollViewRef.current) return;
227
270
  if (isCenterTimerId.current) {
@@ -229,7 +272,17 @@ function PickerGroupBasic(props) {
229
272
  isCenterTimerId.current = null;
230
273
  }
231
274
  const scrollTop = scrollViewRef.current.scrollTop;
232
- 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
+ }
233
286
  if (newIndex !== currentIndex) {
234
287
  setCurrentIndex(newIndex);
235
288
  }
@@ -237,11 +290,14 @@ function PickerGroupBasic(props) {
237
290
  // 渲染选项
238
291
  const pickerItem = range.map((item, index) => {
239
292
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
240
- return createComponent(View, {
293
+ return createComponent(View, mergeProps({
241
294
  id: `picker-item-${columnId}-${index}`,
242
295
  key: index,
243
296
  ref: el => itemRefs.current[index] = el,
244
- className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
297
+ className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
298
+ }, enableClickItemScroll ? {
299
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
300
+ } : {}, {
245
301
  get style() {
246
302
  return {
247
303
  height: PICKER_LINE_HEIGHT,
@@ -249,7 +305,7 @@ function PickerGroupBasic(props) {
249
305
  };
250
306
  },
251
307
  children: content
252
- });
308
+ }));
253
309
  });
254
310
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
255
311
  key: `blank-top-${idx}`,
@@ -264,6 +320,10 @@ function PickerGroupBasic(props) {
264
320
  height: PICKER_LINE_HEIGHT
265
321
  }
266
322
  }))];
323
+ const clickScrollViewProps = enableClickItemScroll ? {
324
+ scrollIntoView,
325
+ scrollIntoViewAlignment: 'center'
326
+ } : {};
267
327
  return createComponent(View, {
268
328
  className: "taro-picker__group",
269
329
  get children() {
@@ -273,7 +333,7 @@ function PickerGroupBasic(props) {
273
333
  className: "taro-picker__indicator"
274
334
  }, indicatorStyle ? {
275
335
  style: indicatorStyle
276
- } : {})), createComponent(ScrollView, {
336
+ } : {})), createComponent(ScrollView, mergeProps({
277
337
  ref: scrollViewRef,
278
338
  scrollY: true,
279
339
  showScrollbar: false,
@@ -281,13 +341,16 @@ function PickerGroupBasic(props) {
281
341
  style: {
282
342
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
283
343
  },
284
- scrollTop: targetScrollTop,
344
+ scrollTop: targetScrollTop
345
+ }, clickScrollViewProps, {
285
346
  onScroll: handleScroll,
286
- onTouchStart: () => setIsTouching(true),
347
+ onTouchStart: () => {
348
+ isTouchingRef.current = true;
349
+ },
287
350
  onScrollEnd: handleScrollEnd,
288
351
  scrollWithAnimation: true,
289
352
  children: realPickerItems
290
- })];
353
+ }))];
291
354
  }
292
355
  });
293
356
  }
@@ -299,17 +362,75 @@ function PickerGroupTime(props) {
299
362
  columnId,
300
363
  updateIndex,
301
364
  selectedIndex = 0,
302
- colors = {}
365
+ colors = {},
366
+ timeA11yLimitFocus,
367
+ onTimeA11yLimitFocusConsumed,
368
+ timeA11yLimitEventNonce,
369
+ enableClickItemScroll = true
303
370
  } = props;
304
371
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
305
372
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
373
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
306
374
  const scrollViewRef = React.useRef(null);
307
375
  const itemRefs = React.useRef([]);
376
+ const selectedIndexPropRef = React.useRef(selectedIndex);
377
+ selectedIndexPropRef.current = selectedIndex;
378
+ const syncScrollFromPropsRef = React.useRef(false);
379
+ const prevLimitEventNonceRef = React.useRef(undefined);
380
+ const suppressScrollSettleFocusNonceRef = React.useRef(null);
381
+ const pendingScrollSettleFocusRef = React.useRef(false);
382
+ const pendingLimitFocusRef = React.useRef(null);
383
+ const limitFocusTimerRef = React.useRef(null);
308
384
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
309
- const [isTouching, setIsTouching] = React.useState(false);
385
+ const isTouchingRef = React.useRef(false);
310
386
  const lengthScaleRatioRef = React.useRef(1);
311
387
  const useMeasuredScaleRef = React.useRef(false);
312
388
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
389
+ const clearLimitFocusTimer = React.useCallback(() => {
390
+ if (limitFocusTimerRef.current) {
391
+ clearTimeout(limitFocusTimerRef.current);
392
+ limitFocusTimerRef.current = null;
393
+ }
394
+ }, []);
395
+ const tryFocusPendingLimit = React.useCallback(() => {
396
+ const pending = pendingLimitFocusRef.current;
397
+ const scrollView = scrollViewRef.current;
398
+ if (!pending || !scrollView) return;
399
+ const visualIndex = getSelectedIndex(scrollView.scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
400
+ const isStable = visualIndex === pending.index;
401
+ if (!isStable) {
402
+ if (pending.attempts >= 20) {
403
+ pendingLimitFocusRef.current = null;
404
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
405
+ suppressScrollSettleFocusNonceRef.current = null;
406
+ }
407
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
408
+ return;
409
+ }
410
+ pending.attempts += 1;
411
+ clearLimitFocusTimer();
412
+ limitFocusTimerRef.current = setTimeout(() => {
413
+ tryFocusPendingLimit();
414
+ }, 50);
415
+ return;
416
+ }
417
+ const node = itemRefs.current[pending.index];
418
+ pendingLimitFocusRef.current = null;
419
+ if (suppressScrollSettleFocusNonceRef.current === pending.nonce) {
420
+ suppressScrollSettleFocusNonceRef.current = null;
421
+ }
422
+ clearLimitFocusTimer();
423
+ requestAccessibilityFocusOnView(node);
424
+ onTimeA11yLimitFocusConsumed === null || onTimeA11yLimitFocusConsumed === void 0 ? void 0 : onTimeA11yLimitFocusConsumed();
425
+ }, [clearLimitFocusTimer, onTimeA11yLimitFocusConsumed]);
426
+ const schedulePendingLimitFocus = React.useCallback(() => {
427
+ if (!pendingLimitFocusRef.current) return;
428
+ clearLimitFocusTimer();
429
+ limitFocusTimerRef.current = setTimeout(() => {
430
+ tryFocusPendingLimit();
431
+ }, 100);
432
+ }, [clearLimitFocusTimer, tryFocusPendingLimit]);
433
+ React.useEffect(() => clearLimitFocusTimer, [clearLimitFocusTimer]);
313
434
  // 初始化时计算 lengthScaleRatio 并判定缩放模式
314
435
  React.useEffect(() => {
315
436
  Taro.getSystemInfo({
@@ -327,41 +448,103 @@ function PickerGroupTime(props) {
327
448
  React.useEffect(() => {
328
449
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
329
450
  }, [range.length]); // 只在range长度变化时重新计算
330
- // selectedIndex变化时,调整滚动位置
451
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
331
452
  React.useEffect(() => {
332
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
453
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
454
+ syncScrollFromPropsRef.current = true;
333
455
  const baseValue = selectedIndex * itemHeightRef.current;
334
456
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
335
457
  setCurrentIndex(selectedIndex);
458
+ const tid = setTimeout(() => {
459
+ syncScrollFromPropsRef.current = false;
460
+ }, 400);
461
+ return () => clearTimeout(tid);
336
462
  }
337
463
  }, [selectedIndex, range]);
464
+ // time 限位 nonce 变更:强制对齐 scrollTop(避免 remount 打断读屏焦点)
465
+ React.useEffect(() => {
466
+ if (!timeA11yLimitEventNonce) return;
467
+ if (!scrollViewRef.current || range.length === 0 || isTouchingRef.current) return;
468
+ syncScrollFromPropsRef.current = true;
469
+ const baseValue = selectedIndex * itemHeightRef.current;
470
+ setTargetScrollTopWithScale(setTargetScrollTop, baseValue, Math.random() * 0.001, lengthScaleRatioRef.current);
471
+ setCurrentIndex(selectedIndex);
472
+ const tid = setTimeout(() => {
473
+ syncScrollFromPropsRef.current = false;
474
+ }, 400);
475
+ return () => clearTimeout(tid);
476
+ }, [timeA11yLimitEventNonce]); // 仅依赖 nonce,其余用 ref
477
+ React.useLayoutEffect(() => {
478
+ if (timeA11yLimitEventNonce === undefined) return;
479
+ const prev = prevLimitEventNonceRef.current;
480
+ prevLimitEventNonceRef.current = timeA11yLimitEventNonce;
481
+ if (timeA11yLimitEventNonce > 0 && (prev === undefined || timeA11yLimitEventNonce > prev)) {
482
+ suppressScrollSettleFocusNonceRef.current = timeA11yLimitEventNonce;
483
+ }
484
+ }, [timeA11yLimitEventNonce]);
485
+ // 限位后登记本列待聚焦项,稳定后再 requestAccessibilityFocus
486
+ React.useEffect(() => {
487
+ const req = timeA11yLimitFocus;
488
+ if (!req || req.columnId !== columnId) return;
489
+ const idx = selectedIndex;
490
+ const nonce = req.nonce;
491
+ pendingScrollSettleFocusRef.current = false;
492
+ pendingLimitFocusRef.current = {
493
+ index: idx,
494
+ nonce,
495
+ attempts: 0
496
+ };
497
+ schedulePendingLimitFocus();
498
+ }, [timeA11yLimitFocus, columnId, selectedIndex, schedulePendingLimitFocus]);
338
499
  // 是否处于归中状态
339
500
  const isCenterTimerId = React.useRef(null);
340
- // 简化为直接在滚动结束时通知父组件
501
+ // 滚动静止后归中并同步选中
341
502
  const handleScrollEnd = () => {
342
503
  if (!scrollViewRef.current) return;
343
504
  if (isCenterTimerId.current) {
344
505
  clearTimeout(isCenterTimerId.current);
345
506
  isCenterTimerId.current = null;
346
507
  }
347
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
508
+ // 100ms 内无新滚动则归中并提交索引
348
509
  isCenterTimerId.current = setTimeout(() => {
349
510
  if (!scrollViewRef.current) return;
350
511
  const scrollTop = scrollViewRef.current.scrollTop;
351
512
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
352
- setIsTouching(false);
353
- // 调用updateIndex执行限位逻辑,获取是否触发了限位
513
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
514
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
515
+ isTouchingRef.current = false;
354
516
  const isLimited = Boolean(updateIndex(newIndex, columnId, true));
355
- // 如果没有触发限位,才执行归中逻辑
517
+ const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
518
+ if (isLimited) {
519
+ pendingScrollSettleFocusRef.current = false;
520
+ }
356
521
  if (!isLimited) {
357
522
  const baseValue = newIndex * itemHeightRef.current;
358
523
  const randomOffset = Math.random() * 0.001;
359
524
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
360
525
  }
526
+ if (!isLimited && hasPendingLimitFocus) {
527
+ pendingScrollSettleFocusRef.current = false;
528
+ schedulePendingLimitFocus();
529
+ syncScrollFromPropsRef.current = false;
530
+ isCenterTimerId.current = null;
531
+ return;
532
+ }
533
+ if (!isLimited && pendingLimitFocusRef.current == null && suppressScrollSettleFocusNonceRef.current != null) {
534
+ suppressScrollSettleFocusNonceRef.current = null;
535
+ }
536
+ const suppressByLimitNonce = suppressScrollSettleFocusNonceRef.current != null;
537
+ if (!isLimited && allowA11yFocus && shouldFocusOnScrollSettle) {
538
+ pendingScrollSettleFocusRef.current = false;
539
+ if (!suppressByLimitNonce) {
540
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
541
+ }
542
+ }
543
+ syncScrollFromPropsRef.current = false;
361
544
  isCenterTimerId.current = null;
362
545
  }, 100);
363
546
  };
364
- // 滚动处理
547
+ // 滚动中:按 scrollTop 更新高亮索引
365
548
  const handleScroll = () => {
366
549
  if (!scrollViewRef.current) return;
367
550
  if (isCenterTimerId.current) {
@@ -369,19 +552,35 @@ function PickerGroupTime(props) {
369
552
  isCenterTimerId.current = null;
370
553
  }
371
554
  const scrollTop = scrollViewRef.current.scrollTop;
372
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
555
+ const ih = itemHeightRef.current;
556
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
557
+ const spi = selectedIndexPropRef.current;
558
+ if (newIndex !== spi) {
559
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
560
+ pendingScrollSettleFocusRef.current = true;
561
+ }
562
+ if (isTouchingRef.current) {
563
+ syncScrollFromPropsRef.current = false;
564
+ }
565
+ }
373
566
  if (newIndex !== currentIndex) {
374
567
  setCurrentIndex(newIndex);
375
568
  }
569
+ if (pendingLimitFocusRef.current) {
570
+ schedulePendingLimitFocus();
571
+ }
376
572
  };
377
573
  // 渲染选项
378
574
  const pickerItem = range.map((item, index) => {
379
575
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
380
- return createComponent(View, {
576
+ return createComponent(View, mergeProps({
381
577
  id: `picker-item-${columnId}-${index}`,
382
578
  key: index,
383
579
  ref: el => itemRefs.current[index] = el,
384
- className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
580
+ className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
581
+ }, enableClickItemScroll ? {
582
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
583
+ } : {}, {
385
584
  get style() {
386
585
  return {
387
586
  height: PICKER_LINE_HEIGHT,
@@ -389,7 +588,7 @@ function PickerGroupTime(props) {
389
588
  };
390
589
  },
391
590
  children: content
392
- });
591
+ }));
393
592
  });
394
593
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
395
594
  key: `blank-top-${idx}`,
@@ -404,6 +603,10 @@ function PickerGroupTime(props) {
404
603
  height: PICKER_LINE_HEIGHT
405
604
  }
406
605
  }))];
606
+ const clickScrollViewProps = enableClickItemScroll ? {
607
+ scrollIntoView,
608
+ scrollIntoViewAlignment: 'center'
609
+ } : {};
407
610
  return createComponent(View, {
408
611
  className: "taro-picker__group",
409
612
  get children() {
@@ -413,7 +616,7 @@ function PickerGroupTime(props) {
413
616
  className: "taro-picker__indicator"
414
617
  }, indicatorStyle ? {
415
618
  style: indicatorStyle
416
- } : {})), createComponent(ScrollView, {
619
+ } : {})), createComponent(ScrollView, mergeProps({
417
620
  ref: scrollViewRef,
418
621
  scrollY: true,
419
622
  showScrollbar: false,
@@ -421,13 +624,16 @@ function PickerGroupTime(props) {
421
624
  style: {
422
625
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
423
626
  },
424
- scrollTop: targetScrollTop,
627
+ scrollTop: targetScrollTop
628
+ }, clickScrollViewProps, {
425
629
  onScroll: handleScroll,
426
- onTouchStart: () => setIsTouching(true),
630
+ onTouchStart: () => {
631
+ isTouchingRef.current = true;
632
+ },
427
633
  onScrollEnd: handleScrollEnd,
428
634
  scrollWithAnimation: true,
429
635
  children: realPickerItems
430
- })];
636
+ }))];
431
637
  }
432
638
  });
433
639
  }
@@ -438,13 +644,20 @@ function PickerGroupDate(props) {
438
644
  columnId,
439
645
  updateDay,
440
646
  selectedIndex = 0,
441
- colors = {}
647
+ colors = {},
648
+ enableClickItemScroll = true
442
649
  } = props;
443
650
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
444
651
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
652
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
445
653
  const scrollViewRef = React.useRef(null);
654
+ const itemRefs = React.useRef([]);
655
+ const selectedIndexPropRef = React.useRef(selectedIndex);
656
+ selectedIndexPropRef.current = selectedIndex;
657
+ const syncScrollFromPropsRef = React.useRef(false);
658
+ const pendingScrollSettleFocusRef = React.useRef(false);
446
659
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
447
- const [isTouching, setIsTouching] = React.useState(false);
660
+ const isTouchingRef = React.useRef(false);
448
661
  const lengthScaleRatioRef = React.useRef(1);
449
662
  const useMeasuredScaleRef = React.useRef(false);
450
663
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
@@ -465,29 +678,36 @@ function PickerGroupDate(props) {
465
678
  React.useEffect(() => {
466
679
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
467
680
  }, [range.length]); // 只在range长度变化时重新计算
468
- // selectedIndex变化时,调整滚动位置
681
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
469
682
  React.useEffect(() => {
470
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
683
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
684
+ syncScrollFromPropsRef.current = true;
471
685
  const baseValue = selectedIndex * itemHeightRef.current;
472
686
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
473
687
  setCurrentIndex(selectedIndex);
688
+ const tid = setTimeout(() => {
689
+ syncScrollFromPropsRef.current = false;
690
+ }, 400);
691
+ return () => clearTimeout(tid);
474
692
  }
475
693
  }, [selectedIndex, range]);
476
694
  // 是否处于归中状态
477
695
  const isCenterTimerId = React.useRef(null);
478
- // 简化为直接在滚动结束时通知父组件
696
+ // 滚动静止后归中并同步选中
479
697
  const handleScrollEnd = () => {
480
698
  if (!scrollViewRef.current) return;
481
699
  if (isCenterTimerId.current) {
482
700
  clearTimeout(isCenterTimerId.current);
483
701
  isCenterTimerId.current = null;
484
702
  }
485
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
703
+ // 100ms 内无新滚动则归中并提交索引
486
704
  isCenterTimerId.current = setTimeout(() => {
487
705
  if (!scrollViewRef.current) return;
488
706
  const scrollTop = scrollViewRef.current.scrollTop;
489
707
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
490
- setIsTouching(false);
708
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
709
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
710
+ isTouchingRef.current = false;
491
711
  const baseValue = newIndex * itemHeightRef.current;
492
712
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
493
713
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
@@ -498,10 +718,15 @@ function PickerGroupDate(props) {
498
718
  const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
499
719
  updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
500
720
  }
721
+ pendingScrollSettleFocusRef.current = false;
722
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
723
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
724
+ }
725
+ syncScrollFromPropsRef.current = false;
501
726
  isCenterTimerId.current = null;
502
727
  }, 100);
503
728
  };
504
- // 滚动处理
729
+ // 滚动中:按 scrollTop 更新高亮索引
505
730
  const handleScroll = () => {
506
731
  if (!scrollViewRef.current) return;
507
732
  if (isCenterTimerId.current) {
@@ -509,17 +734,31 @@ function PickerGroupDate(props) {
509
734
  isCenterTimerId.current = null;
510
735
  }
511
736
  const scrollTop = scrollViewRef.current.scrollTop;
512
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
737
+ const ih = itemHeightRef.current;
738
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
739
+ const spi = selectedIndexPropRef.current;
740
+ if (newIndex !== spi) {
741
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
742
+ pendingScrollSettleFocusRef.current = true;
743
+ }
744
+ if (isTouchingRef.current) {
745
+ syncScrollFromPropsRef.current = false;
746
+ }
747
+ }
513
748
  if (newIndex !== currentIndex) {
514
749
  setCurrentIndex(newIndex);
515
750
  }
516
751
  };
517
752
  // 渲染选项
518
753
  const pickerItem = range.map((item, index) => {
519
- return createComponent(View, {
754
+ return createComponent(View, mergeProps({
520
755
  id: `picker-item-${columnId}-${index}`,
521
756
  key: index,
522
- className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
757
+ ref: el => itemRefs.current[index] = el,
758
+ className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
759
+ }, enableClickItemScroll ? {
760
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
761
+ } : {}, {
523
762
  get style() {
524
763
  return {
525
764
  height: PICKER_LINE_HEIGHT,
@@ -527,7 +766,7 @@ function PickerGroupDate(props) {
527
766
  };
528
767
  },
529
768
  children: item
530
- });
769
+ }));
531
770
  });
532
771
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
533
772
  key: `blank-top-${idx}`,
@@ -542,6 +781,10 @@ function PickerGroupDate(props) {
542
781
  height: PICKER_LINE_HEIGHT
543
782
  }
544
783
  }))];
784
+ const clickScrollViewProps = enableClickItemScroll ? {
785
+ scrollIntoView,
786
+ scrollIntoViewAlignment: 'center'
787
+ } : {};
545
788
  return createComponent(View, {
546
789
  className: "taro-picker__group",
547
790
  get children() {
@@ -551,7 +794,7 @@ function PickerGroupDate(props) {
551
794
  className: "taro-picker__indicator"
552
795
  }, indicatorStyle ? {
553
796
  style: indicatorStyle
554
- } : {})), createComponent(ScrollView, {
797
+ } : {})), createComponent(ScrollView, mergeProps({
555
798
  ref: scrollViewRef,
556
799
  scrollY: true,
557
800
  showScrollbar: false,
@@ -559,13 +802,16 @@ function PickerGroupDate(props) {
559
802
  style: {
560
803
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
561
804
  },
562
- scrollTop: targetScrollTop,
805
+ scrollTop: targetScrollTop
806
+ }, clickScrollViewProps, {
563
807
  onScroll: handleScroll,
564
- onTouchStart: () => setIsTouching(true),
808
+ onTouchStart: () => {
809
+ isTouchingRef.current = true;
810
+ },
565
811
  onScrollEnd: handleScrollEnd,
566
812
  scrollWithAnimation: true,
567
813
  children: realPickerItems
568
- })];
814
+ }))];
569
815
  }
570
816
  });
571
817
  }
@@ -578,17 +824,23 @@ function PickerGroupRegion(props) {
578
824
  updateIndex,
579
825
  selectedIndex = 0,
580
826
  // 使用selectedIndex参数,默认为0
581
- colors = {}
827
+ colors = {},
828
+ enableClickItemScroll = true
582
829
  } = props;
583
830
  const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
584
831
  const scrollViewRef = React.useRef(null);
585
832
  const [targetScrollTop, setTargetScrollTop] = React.useState(0);
833
+ const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
586
834
  const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
587
- const [isTouching, setIsTouching] = React.useState(false);
835
+ const isTouchingRef = React.useRef(false);
588
836
  const lengthScaleRatioRef = React.useRef(1);
589
837
  const useMeasuredScaleRef = React.useRef(false);
590
838
  const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
591
- const isUserBeginScrollRef = React.useRef(false);
839
+ const itemRefs = React.useRef([]);
840
+ const selectedIndexPropRef = React.useRef(selectedIndex);
841
+ selectedIndexPropRef.current = selectedIndex;
842
+ const syncScrollFromPropsRef = React.useRef(false);
843
+ const pendingScrollSettleFocusRef = React.useRef(false);
592
844
  // 初始化时计算 lengthScaleRatio 并判定缩放模式
593
845
  React.useEffect(() => {
594
846
  Taro.getSystemInfo({
@@ -606,15 +858,20 @@ function PickerGroupRegion(props) {
606
858
  React.useEffect(() => {
607
859
  itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
608
860
  }, [range.length]); // 只在range长度变化时重新计算
609
- // selectedIndex变化时,调整滚动位置
861
+ // props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
610
862
  React.useEffect(() => {
611
- if (scrollViewRef.current && range.length > 0 && !isTouching) {
863
+ if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
864
+ syncScrollFromPropsRef.current = true;
612
865
  const baseValue = selectedIndex * itemHeightRef.current;
613
866
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
614
867
  setCurrentIndex(selectedIndex);
868
+ const tid = setTimeout(() => {
869
+ syncScrollFromPropsRef.current = false;
870
+ }, 400);
871
+ return () => clearTimeout(tid);
615
872
  }
616
873
  }, [selectedIndex, range]);
617
- // 滚动结束处理
874
+ // 滚动静止后归中(debounce)
618
875
  const isCenterTimerId = React.useRef(null);
619
876
  const handleScrollEnd = () => {
620
877
  if (!scrollViewRef.current) return;
@@ -622,19 +879,27 @@ function PickerGroupRegion(props) {
622
879
  clearTimeout(isCenterTimerId.current);
623
880
  isCenterTimerId.current = null;
624
881
  }
625
- // 做一个0.1s延时 0.1s之内没有新的滑动 则把选项归到中间 然后更新选中项
882
+ // 100ms 内无新滚动则归中并提交索引
626
883
  isCenterTimerId.current = setTimeout(() => {
627
884
  if (!scrollViewRef.current) return;
628
885
  const scrollTop = scrollViewRef.current.scrollTop;
629
886
  const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
630
- setIsTouching(false);
887
+ const allowA11yFocus = !syncScrollFromPropsRef.current;
888
+ const shouldFocusOnScrollSettle = pendingScrollSettleFocusRef.current;
889
+ isTouchingRef.current = false;
631
890
  const baseValue = newIndex * itemHeightRef.current;
632
891
  const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
633
892
  setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
634
- updateIndex(newIndex, columnId, false, isUserBeginScrollRef.current);
893
+ updateIndex(newIndex, columnId, false, allowA11yFocus);
894
+ pendingScrollSettleFocusRef.current = false;
895
+ if (allowA11yFocus && shouldFocusOnScrollSettle) {
896
+ requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
897
+ }
898
+ syncScrollFromPropsRef.current = false;
899
+ isCenterTimerId.current = null;
635
900
  }, 100);
636
901
  };
637
- // 滚动处理 - 在滚动时计算索引
902
+ // 滚动中:按 scrollTop 更新高亮索引
638
903
  const handleScroll = () => {
639
904
  if (!scrollViewRef.current) return;
640
905
  if (isCenterTimerId.current) {
@@ -642,7 +907,17 @@ function PickerGroupRegion(props) {
642
907
  isCenterTimerId.current = null;
643
908
  }
644
909
  const scrollTop = scrollViewRef.current.scrollTop;
645
- const newIndex = getSelectedIndex(scrollTop, itemHeightRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
910
+ const ih = itemHeightRef.current;
911
+ const newIndex = getSelectedIndex(scrollTop, ih, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
912
+ const spi = selectedIndexPropRef.current;
913
+ if (newIndex !== spi) {
914
+ if (!syncScrollFromPropsRef.current || isTouchingRef.current) {
915
+ pendingScrollSettleFocusRef.current = true;
916
+ }
917
+ if (isTouchingRef.current) {
918
+ syncScrollFromPropsRef.current = false;
919
+ }
920
+ }
646
921
  if (newIndex !== currentIndex) {
647
922
  setCurrentIndex(newIndex);
648
923
  }
@@ -650,10 +925,14 @@ function PickerGroupRegion(props) {
650
925
  // 渲染选项
651
926
  const pickerItem = range.map((item, index) => {
652
927
  const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
653
- return createComponent(View, {
928
+ return createComponent(View, mergeProps({
654
929
  id: `picker-item-${columnId}-${index}`,
655
930
  key: index,
656
- className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`,
931
+ ref: el => itemRefs.current[index] = el,
932
+ className: `taro-picker__item${index === currentIndex ? ' taro-picker__item--selected' : ''}`
933
+ }, enableClickItemScroll ? {
934
+ onClick: () => scrollToItemId(`picker-item-${columnId}-${index}`)
935
+ } : {}, {
657
936
  get style() {
658
937
  return {
659
938
  height: PICKER_LINE_HEIGHT,
@@ -661,7 +940,7 @@ function PickerGroupRegion(props) {
661
940
  };
662
941
  },
663
942
  children: content
664
- });
943
+ }));
665
944
  });
666
945
  const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
667
946
  key: `blank-top-${idx}`,
@@ -676,6 +955,10 @@ function PickerGroupRegion(props) {
676
955
  height: PICKER_LINE_HEIGHT
677
956
  }
678
957
  }))];
958
+ const clickScrollViewProps = enableClickItemScroll ? {
959
+ scrollIntoView,
960
+ scrollIntoViewAlignment: 'center'
961
+ } : {};
679
962
  return createComponent(View, {
680
963
  className: "taro-picker__group",
681
964
  get children() {
@@ -685,7 +968,7 @@ function PickerGroupRegion(props) {
685
968
  className: "taro-picker__indicator"
686
969
  }, indicatorStyle ? {
687
970
  style: indicatorStyle
688
- } : {})), createComponent(ScrollView, {
971
+ } : {})), createComponent(ScrollView, mergeProps({
689
972
  ref: scrollViewRef,
690
973
  scrollY: true,
691
974
  showScrollbar: false,
@@ -693,16 +976,16 @@ function PickerGroupRegion(props) {
693
976
  style: {
694
977
  height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
695
978
  },
696
- scrollTop: targetScrollTop,
979
+ scrollTop: targetScrollTop
980
+ }, clickScrollViewProps, {
697
981
  onScroll: handleScroll,
698
982
  onTouchStart: () => {
699
- setIsTouching(true);
700
- isUserBeginScrollRef.current = true;
983
+ isTouchingRef.current = true;
701
984
  },
702
985
  onScrollEnd: handleScrollEnd,
703
986
  scrollWithAnimation: true,
704
987
  children: realPickerItems
705
- })];
988
+ }))];
706
989
  }
707
990
  });
708
991
  }