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