@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.
- package/dist/components/picker/index.js +71 -34
- package/dist/components/picker/index.js.map +1 -1
- package/dist/components/picker/picker-group.js +329 -45
- package/dist/components/picker/picker-group.js.map +1 -1
- package/dist/components/scroll-view/index.js +29 -13
- package/dist/components/scroll-view/index.js.map +1 -1
- package/dist/original/components/picker/index.js +71 -34
- package/dist/original/components/picker/index.js.map +1 -1
- package/dist/original/components/picker/picker-group.js +329 -45
- package/dist/original/components/picker/picker-group.js.map +1 -1
- package/dist/original/components/scroll-view/index.js +29 -13
- package/dist/original/components/scroll-view/index.js.map +1 -1
- package/dist/solid/components/picker/index.js +75 -38
- package/dist/solid/components/picker/index.js.map +1 -1
- package/dist/solid/components/picker/picker-group.js +352 -69
- package/dist/solid/components/picker/picker-group.js.map +1 -1
- package/dist/solid/components/scroll-view/index.js +29 -13
- package/dist/solid/components/scroll-view/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -3,6 +3,32 @@ import Taro from '@tarojs/taro';
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
|
+
function requestAccessibilityFocusOnView(node) {
|
|
7
|
+
if (node == null) return;
|
|
8
|
+
const fn = Taro.setAccessibilityFocus;
|
|
9
|
+
if (typeof fn !== 'function') return;
|
|
10
|
+
fn({
|
|
11
|
+
viewRef: {
|
|
12
|
+
current: node
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** 部分端同 id 连续 scrollIntoView 不生效:先清空再在下一宏任务设回目标 id */
|
|
17
|
+
function usePickerItemScrollIntoView() {
|
|
18
|
+
const [scrollIntoView, setScrollIntoView] = React.useState('');
|
|
19
|
+
const pulseRef = React.useRef(0);
|
|
20
|
+
const scrollToItemId = React.useCallback(itemId => {
|
|
21
|
+
pulseRef.current += 1;
|
|
22
|
+
const token = pulseRef.current;
|
|
23
|
+
setScrollIntoView('');
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (pulseRef.current !== token) return;
|
|
26
|
+
setScrollIntoView(itemId);
|
|
27
|
+
}, 0);
|
|
28
|
+
}, []);
|
|
29
|
+
return [scrollIntoView, scrollToItemId];
|
|
30
|
+
}
|
|
31
|
+
// 定义常量
|
|
6
32
|
const PICKER_LINE_HEIGHT = 34; // px
|
|
7
33
|
const PICKER_VISIBLE_ITEMS = 7; // 可见行数
|
|
8
34
|
const PICKER_BLANK_ITEMS = 3; // 空白行数
|
|
@@ -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
|
|
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
|
-
//
|
|
221
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
190
222
|
React.useEffect(() => {
|
|
191
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
-
//
|
|
445
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
324
446
|
React.useEffect(() => {
|
|
325
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
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
|
-
//
|
|
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
|
-
|
|
346
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
-
//
|
|
669
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
456
670
|
React.useEffect(() => {
|
|
457
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
|
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
|
-
//
|
|
843
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
591
844
|
React.useEffect(() => {
|
|
592
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
676
|
-
isUserBeginScrollRef.current = true;
|
|
960
|
+
isTouchingRef.current = true;
|
|
677
961
|
},
|
|
678
962
|
onScrollEnd: handleScrollEnd,
|
|
679
963
|
scrollWithAnimation: true,
|