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