@tarojs/components-react 4.1.12-beta.5 → 4.1.12-beta.51
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 +110 -38
- package/dist/components/picker/index.js.map +1 -1
- package/dist/components/picker/picker-group.js +400 -111
- package/dist/components/picker/picker-group.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 +5 -1
- 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 +110 -38
- package/dist/original/components/picker/index.js.map +1 -1
- package/dist/original/components/picker/picker-group.js +400 -111
- 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/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 +5 -1
- 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 +121 -45
- package/dist/solid/components/picker/index.js.map +1 -1
- package/dist/solid/components/picker/picker-group.js +423 -135
- package/dist/solid/components/picker/picker-group.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/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,6 +38,25 @@ 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
|
+
};
|
|
16
60
|
// 辅助函数:计算 lengthScaleRatio
|
|
17
61
|
const calculateLengthScaleRatio = res => {
|
|
18
62
|
let lengthScaleRatio = res === null || res === void 0 ? void 0 : res.lengthScaleRatio;
|
|
@@ -51,6 +95,29 @@ const setTargetScrollTopWithScale = function (setTargetScrollTop, baseValue, ran
|
|
|
51
95
|
setTargetScrollTop(finalValue);
|
|
52
96
|
}
|
|
53
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;
|
|
120
|
+
};
|
|
54
121
|
function PickerGroupBasic(props) {
|
|
55
122
|
const {
|
|
56
123
|
range = [],
|
|
@@ -60,69 +127,72 @@ function PickerGroupBasic(props) {
|
|
|
60
127
|
onColumnChange,
|
|
61
128
|
selectedIndex = 0,
|
|
62
129
|
// 使用selectedIndex参数,默认为0
|
|
63
|
-
colors = {}
|
|
130
|
+
colors = {},
|
|
131
|
+
enableClickItemScroll = true
|
|
64
132
|
} = props;
|
|
65
133
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
66
134
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
135
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
67
136
|
const scrollViewRef = React.useRef(null);
|
|
68
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);
|
|
69
142
|
// 使用selectedIndex初始化当前索引
|
|
70
143
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
71
144
|
// 触摸状态用于优化用户体验
|
|
72
|
-
const
|
|
145
|
+
const isTouchingRef = React.useRef(false);
|
|
73
146
|
const lengthScaleRatioRef = React.useRef(1);
|
|
147
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
74
148
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
75
|
-
// 初始化时计算 lengthScaleRatio
|
|
149
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
76
150
|
React.useEffect(() => {
|
|
77
151
|
Taro.getSystemInfo({
|
|
78
152
|
success: res => {
|
|
79
153
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
154
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
155
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
80
156
|
},
|
|
81
157
|
fail: () => {
|
|
82
|
-
// 失败时使用默认值 1
|
|
83
158
|
lengthScaleRatioRef.current = 1;
|
|
159
|
+
useMeasuredScaleRef.current = false;
|
|
84
160
|
}
|
|
85
161
|
});
|
|
86
162
|
}, []);
|
|
87
163
|
React.useEffect(() => {
|
|
88
|
-
|
|
89
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
90
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
91
|
-
} else {
|
|
92
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
93
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
94
|
-
} else {
|
|
95
|
-
console.warn('Height measurement anomaly');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
164
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
98
165
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
99
|
-
//
|
|
100
|
-
const getSelectedIndex = scrollTop => {
|
|
101
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
102
|
-
};
|
|
103
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
166
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
104
167
|
React.useEffect(() => {
|
|
105
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
168
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
169
|
+
syncScrollFromPropsRef.current = true;
|
|
106
170
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
107
171
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
108
172
|
setCurrentIndex(selectedIndex);
|
|
173
|
+
const tid = setTimeout(() => {
|
|
174
|
+
syncScrollFromPropsRef.current = false;
|
|
175
|
+
}, 400);
|
|
176
|
+
return () => clearTimeout(tid);
|
|
109
177
|
}
|
|
110
178
|
}, [selectedIndex, range]);
|
|
111
179
|
// 是否处于归中状态
|
|
112
180
|
const isCenterTimerId = React.useRef(null);
|
|
113
|
-
//
|
|
181
|
+
// 滚动静止后归中并同步选中
|
|
114
182
|
const handleScrollEnd = () => {
|
|
115
183
|
if (!scrollViewRef.current) return;
|
|
116
184
|
if (isCenterTimerId.current) {
|
|
117
185
|
clearTimeout(isCenterTimerId.current);
|
|
118
186
|
isCenterTimerId.current = null;
|
|
119
187
|
}
|
|
120
|
-
//
|
|
188
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
121
189
|
isCenterTimerId.current = setTimeout(() => {
|
|
122
190
|
if (!scrollViewRef.current) return;
|
|
123
191
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
124
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
125
|
-
|
|
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;
|
|
126
196
|
const baseValue = newIndex * itemHeightRef.current;
|
|
127
197
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
128
198
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -131,10 +201,15 @@ function PickerGroupBasic(props) {
|
|
|
131
201
|
columnId,
|
|
132
202
|
index: newIndex
|
|
133
203
|
});
|
|
204
|
+
pendingScrollSettleFocusRef.current = false;
|
|
205
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
206
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
207
|
+
}
|
|
208
|
+
syncScrollFromPropsRef.current = false;
|
|
134
209
|
isCenterTimerId.current = null;
|
|
135
210
|
}, 100);
|
|
136
211
|
};
|
|
137
|
-
//
|
|
212
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
138
213
|
const handleScroll = () => {
|
|
139
214
|
if (!scrollViewRef.current) return;
|
|
140
215
|
if (isCenterTimerId.current) {
|
|
@@ -142,7 +217,17 @@ function PickerGroupBasic(props) {
|
|
|
142
217
|
isCenterTimerId.current = null;
|
|
143
218
|
}
|
|
144
219
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
145
|
-
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
|
+
}
|
|
146
231
|
if (newIndex !== currentIndex) {
|
|
147
232
|
setCurrentIndex(newIndex);
|
|
148
233
|
}
|
|
@@ -150,11 +235,14 @@ function PickerGroupBasic(props) {
|
|
|
150
235
|
// 渲染选项
|
|
151
236
|
const pickerItem = range.map((item, index) => {
|
|
152
237
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
153
|
-
return createComponent(View, {
|
|
238
|
+
return createComponent(View, mergeProps({
|
|
154
239
|
id: `picker-item-${columnId}-${index}`,
|
|
155
240
|
key: index,
|
|
156
241
|
ref: el => itemRefs.current[index] = el,
|
|
157
|
-
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
|
+
} : {}, {
|
|
158
246
|
get style() {
|
|
159
247
|
return {
|
|
160
248
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -162,7 +250,7 @@ function PickerGroupBasic(props) {
|
|
|
162
250
|
};
|
|
163
251
|
},
|
|
164
252
|
children: content
|
|
165
|
-
});
|
|
253
|
+
}));
|
|
166
254
|
});
|
|
167
255
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
168
256
|
key: `blank-top-${idx}`,
|
|
@@ -177,6 +265,10 @@ function PickerGroupBasic(props) {
|
|
|
177
265
|
height: PICKER_LINE_HEIGHT
|
|
178
266
|
}
|
|
179
267
|
}))];
|
|
268
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
269
|
+
scrollIntoView,
|
|
270
|
+
scrollIntoViewAlignment: 'center'
|
|
271
|
+
} : {};
|
|
180
272
|
return createComponent(View, {
|
|
181
273
|
className: "taro-picker__group",
|
|
182
274
|
get children() {
|
|
@@ -186,7 +278,7 @@ function PickerGroupBasic(props) {
|
|
|
186
278
|
className: "taro-picker__indicator"
|
|
187
279
|
}, indicatorStyle ? {
|
|
188
280
|
style: indicatorStyle
|
|
189
|
-
} : {})), createComponent(ScrollView, {
|
|
281
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
190
282
|
ref: scrollViewRef,
|
|
191
283
|
scrollY: true,
|
|
192
284
|
showScrollbar: false,
|
|
@@ -194,13 +286,16 @@ function PickerGroupBasic(props) {
|
|
|
194
286
|
style: {
|
|
195
287
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
196
288
|
},
|
|
197
|
-
scrollTop: targetScrollTop
|
|
289
|
+
scrollTop: targetScrollTop
|
|
290
|
+
}, clickScrollViewProps, {
|
|
198
291
|
onScroll: handleScroll,
|
|
199
|
-
onTouchStart: () =>
|
|
292
|
+
onTouchStart: () => {
|
|
293
|
+
isTouchingRef.current = true;
|
|
294
|
+
},
|
|
200
295
|
onScrollEnd: handleScrollEnd,
|
|
201
296
|
scrollWithAnimation: true,
|
|
202
297
|
children: realPickerItems
|
|
203
|
-
})];
|
|
298
|
+
}))];
|
|
204
299
|
}
|
|
205
300
|
});
|
|
206
301
|
}
|
|
@@ -212,78 +307,189 @@ function PickerGroupTime(props) {
|
|
|
212
307
|
columnId,
|
|
213
308
|
updateIndex,
|
|
214
309
|
selectedIndex = 0,
|
|
215
|
-
colors = {}
|
|
310
|
+
colors = {},
|
|
311
|
+
timeA11yLimitFocus,
|
|
312
|
+
onTimeA11yLimitFocusConsumed,
|
|
313
|
+
timeA11yLimitEventNonce,
|
|
314
|
+
enableClickItemScroll = true
|
|
216
315
|
} = props;
|
|
217
316
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
218
317
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
318
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
219
319
|
const scrollViewRef = React.useRef(null);
|
|
220
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);
|
|
221
329
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
222
|
-
const
|
|
330
|
+
const isTouchingRef = React.useRef(false);
|
|
223
331
|
const lengthScaleRatioRef = React.useRef(1);
|
|
332
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
224
333
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
225
|
-
|
|
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 并判定缩放模式
|
|
226
380
|
React.useEffect(() => {
|
|
227
381
|
Taro.getSystemInfo({
|
|
228
382
|
success: res => {
|
|
229
383
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
384
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
385
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
230
386
|
},
|
|
231
387
|
fail: () => {
|
|
232
|
-
// 失败时使用默认值 1
|
|
233
388
|
lengthScaleRatioRef.current = 1;
|
|
389
|
+
useMeasuredScaleRef.current = false;
|
|
234
390
|
}
|
|
235
391
|
});
|
|
236
392
|
}, []);
|
|
237
393
|
React.useEffect(() => {
|
|
238
|
-
|
|
239
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
240
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
241
|
-
} else {
|
|
242
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
243
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
244
|
-
} else {
|
|
245
|
-
console.warn('Height measurement anomaly');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
394
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
248
395
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
249
|
-
|
|
250
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
251
|
-
};
|
|
252
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
396
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
253
397
|
React.useEffect(() => {
|
|
254
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
398
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
399
|
+
syncScrollFromPropsRef.current = true;
|
|
255
400
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
256
401
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
257
402
|
setCurrentIndex(selectedIndex);
|
|
403
|
+
const tid = setTimeout(() => {
|
|
404
|
+
syncScrollFromPropsRef.current = false;
|
|
405
|
+
}, 400);
|
|
406
|
+
return () => clearTimeout(tid);
|
|
258
407
|
}
|
|
259
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]);
|
|
260
444
|
// 是否处于归中状态
|
|
261
445
|
const isCenterTimerId = React.useRef(null);
|
|
262
|
-
//
|
|
446
|
+
// 滚动静止后归中并同步选中
|
|
263
447
|
const handleScrollEnd = () => {
|
|
264
448
|
if (!scrollViewRef.current) return;
|
|
265
449
|
if (isCenterTimerId.current) {
|
|
266
450
|
clearTimeout(isCenterTimerId.current);
|
|
267
451
|
isCenterTimerId.current = null;
|
|
268
452
|
}
|
|
269
|
-
//
|
|
453
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
270
454
|
isCenterTimerId.current = setTimeout(() => {
|
|
271
455
|
if (!scrollViewRef.current) return;
|
|
272
456
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
273
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
461
|
const isLimited = Boolean(updateIndex(newIndex, columnId, true));
|
|
277
|
-
|
|
462
|
+
const hasPendingLimitFocus = pendingLimitFocusRef.current != null;
|
|
463
|
+
if (isLimited) {
|
|
464
|
+
pendingScrollSettleFocusRef.current = false;
|
|
465
|
+
}
|
|
278
466
|
if (!isLimited) {
|
|
279
467
|
const baseValue = newIndex * itemHeightRef.current;
|
|
280
468
|
const randomOffset = Math.random() * 0.001;
|
|
281
469
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
282
470
|
}
|
|
471
|
+
if (!isLimited && hasPendingLimitFocus) {
|
|
472
|
+
pendingScrollSettleFocusRef.current = false;
|
|
473
|
+
schedulePendingLimitFocus();
|
|
474
|
+
syncScrollFromPropsRef.current = false;
|
|
475
|
+
isCenterTimerId.current = null;
|
|
476
|
+
return;
|
|
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;
|
|
283
489
|
isCenterTimerId.current = null;
|
|
284
490
|
}, 100);
|
|
285
491
|
};
|
|
286
|
-
//
|
|
492
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
287
493
|
const handleScroll = () => {
|
|
288
494
|
if (!scrollViewRef.current) return;
|
|
289
495
|
if (isCenterTimerId.current) {
|
|
@@ -291,19 +497,35 @@ function PickerGroupTime(props) {
|
|
|
291
497
|
isCenterTimerId.current = null;
|
|
292
498
|
}
|
|
293
499
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
294
|
-
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
|
+
}
|
|
295
511
|
if (newIndex !== currentIndex) {
|
|
296
512
|
setCurrentIndex(newIndex);
|
|
297
513
|
}
|
|
514
|
+
if (pendingLimitFocusRef.current) {
|
|
515
|
+
schedulePendingLimitFocus();
|
|
516
|
+
}
|
|
298
517
|
};
|
|
299
518
|
// 渲染选项
|
|
300
519
|
const pickerItem = range.map((item, index) => {
|
|
301
520
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
302
|
-
return createComponent(View, {
|
|
521
|
+
return createComponent(View, mergeProps({
|
|
303
522
|
id: `picker-item-${columnId}-${index}`,
|
|
304
523
|
key: index,
|
|
305
524
|
ref: el => itemRefs.current[index] = el,
|
|
306
|
-
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
|
+
} : {}, {
|
|
307
529
|
get style() {
|
|
308
530
|
return {
|
|
309
531
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -311,7 +533,7 @@ function PickerGroupTime(props) {
|
|
|
311
533
|
};
|
|
312
534
|
},
|
|
313
535
|
children: content
|
|
314
|
-
});
|
|
536
|
+
}));
|
|
315
537
|
});
|
|
316
538
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
317
539
|
key: `blank-top-${idx}`,
|
|
@@ -326,6 +548,10 @@ function PickerGroupTime(props) {
|
|
|
326
548
|
height: PICKER_LINE_HEIGHT
|
|
327
549
|
}
|
|
328
550
|
}))];
|
|
551
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
552
|
+
scrollIntoView,
|
|
553
|
+
scrollIntoViewAlignment: 'center'
|
|
554
|
+
} : {};
|
|
329
555
|
return createComponent(View, {
|
|
330
556
|
className: "taro-picker__group",
|
|
331
557
|
get children() {
|
|
@@ -335,7 +561,7 @@ function PickerGroupTime(props) {
|
|
|
335
561
|
className: "taro-picker__indicator"
|
|
336
562
|
}, indicatorStyle ? {
|
|
337
563
|
style: indicatorStyle
|
|
338
|
-
} : {})), createComponent(ScrollView, {
|
|
564
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
339
565
|
ref: scrollViewRef,
|
|
340
566
|
scrollY: true,
|
|
341
567
|
showScrollbar: false,
|
|
@@ -343,13 +569,16 @@ function PickerGroupTime(props) {
|
|
|
343
569
|
style: {
|
|
344
570
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
345
571
|
},
|
|
346
|
-
scrollTop: targetScrollTop
|
|
572
|
+
scrollTop: targetScrollTop
|
|
573
|
+
}, clickScrollViewProps, {
|
|
347
574
|
onScroll: handleScroll,
|
|
348
|
-
onTouchStart: () =>
|
|
575
|
+
onTouchStart: () => {
|
|
576
|
+
isTouchingRef.current = true;
|
|
577
|
+
},
|
|
349
578
|
onScrollEnd: handleScrollEnd,
|
|
350
579
|
scrollWithAnimation: true,
|
|
351
580
|
children: realPickerItems
|
|
352
|
-
})];
|
|
581
|
+
}))];
|
|
353
582
|
}
|
|
354
583
|
});
|
|
355
584
|
}
|
|
@@ -360,65 +589,70 @@ function PickerGroupDate(props) {
|
|
|
360
589
|
columnId,
|
|
361
590
|
updateDay,
|
|
362
591
|
selectedIndex = 0,
|
|
363
|
-
colors = {}
|
|
592
|
+
colors = {},
|
|
593
|
+
enableClickItemScroll = true
|
|
364
594
|
} = props;
|
|
365
595
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
366
596
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
597
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
367
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);
|
|
368
604
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
369
|
-
const
|
|
605
|
+
const isTouchingRef = React.useRef(false);
|
|
370
606
|
const lengthScaleRatioRef = React.useRef(1);
|
|
607
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
371
608
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
372
|
-
// 初始化时计算 lengthScaleRatio
|
|
609
|
+
// 初始化时计算 lengthScaleRatio 并判定缩放模式
|
|
373
610
|
React.useEffect(() => {
|
|
374
611
|
Taro.getSystemInfo({
|
|
375
612
|
success: res => {
|
|
376
613
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
614
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
615
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
377
616
|
},
|
|
378
617
|
fail: () => {
|
|
379
|
-
// 失败时使用默认值 1
|
|
380
618
|
lengthScaleRatioRef.current = 1;
|
|
619
|
+
useMeasuredScaleRef.current = false;
|
|
381
620
|
}
|
|
382
621
|
});
|
|
383
622
|
}, []);
|
|
384
623
|
React.useEffect(() => {
|
|
385
|
-
|
|
386
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
387
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
388
|
-
} else {
|
|
389
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
390
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
391
|
-
} else {
|
|
392
|
-
console.warn('Height measurement anomaly');
|
|
393
|
-
}
|
|
394
|
-
}
|
|
624
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
395
625
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
396
|
-
|
|
397
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
398
|
-
};
|
|
399
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
626
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
400
627
|
React.useEffect(() => {
|
|
401
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
628
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
629
|
+
syncScrollFromPropsRef.current = true;
|
|
402
630
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
403
631
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
404
632
|
setCurrentIndex(selectedIndex);
|
|
633
|
+
const tid = setTimeout(() => {
|
|
634
|
+
syncScrollFromPropsRef.current = false;
|
|
635
|
+
}, 400);
|
|
636
|
+
return () => clearTimeout(tid);
|
|
405
637
|
}
|
|
406
638
|
}, [selectedIndex, range]);
|
|
407
639
|
// 是否处于归中状态
|
|
408
640
|
const isCenterTimerId = React.useRef(null);
|
|
409
|
-
//
|
|
641
|
+
// 滚动静止后归中并同步选中
|
|
410
642
|
const handleScrollEnd = () => {
|
|
411
643
|
if (!scrollViewRef.current) return;
|
|
412
644
|
if (isCenterTimerId.current) {
|
|
413
645
|
clearTimeout(isCenterTimerId.current);
|
|
414
646
|
isCenterTimerId.current = null;
|
|
415
647
|
}
|
|
416
|
-
//
|
|
648
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
417
649
|
isCenterTimerId.current = setTimeout(() => {
|
|
418
650
|
if (!scrollViewRef.current) return;
|
|
419
651
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
420
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
421
|
-
|
|
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;
|
|
422
656
|
const baseValue = newIndex * itemHeightRef.current;
|
|
423
657
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
424
658
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
@@ -429,10 +663,15 @@ function PickerGroupDate(props) {
|
|
|
429
663
|
const numericValue = parseInt(valueText.replace(/[^0-9]/g, ''));
|
|
430
664
|
updateDay(isNaN(numericValue) ? 0 : numericValue, parseInt(columnId));
|
|
431
665
|
}
|
|
666
|
+
pendingScrollSettleFocusRef.current = false;
|
|
667
|
+
if (allowA11yFocus && shouldFocusOnScrollSettle) {
|
|
668
|
+
requestAccessibilityFocusOnView(itemRefs.current[newIndex]);
|
|
669
|
+
}
|
|
670
|
+
syncScrollFromPropsRef.current = false;
|
|
432
671
|
isCenterTimerId.current = null;
|
|
433
672
|
}, 100);
|
|
434
673
|
};
|
|
435
|
-
//
|
|
674
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
436
675
|
const handleScroll = () => {
|
|
437
676
|
if (!scrollViewRef.current) return;
|
|
438
677
|
if (isCenterTimerId.current) {
|
|
@@ -440,17 +679,31 @@ function PickerGroupDate(props) {
|
|
|
440
679
|
isCenterTimerId.current = null;
|
|
441
680
|
}
|
|
442
681
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
443
|
-
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
|
+
}
|
|
444
693
|
if (newIndex !== currentIndex) {
|
|
445
694
|
setCurrentIndex(newIndex);
|
|
446
695
|
}
|
|
447
696
|
};
|
|
448
697
|
// 渲染选项
|
|
449
698
|
const pickerItem = range.map((item, index) => {
|
|
450
|
-
return createComponent(View, {
|
|
699
|
+
return createComponent(View, mergeProps({
|
|
451
700
|
id: `picker-item-${columnId}-${index}`,
|
|
452
701
|
key: index,
|
|
453
|
-
|
|
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
|
+
} : {}, {
|
|
454
707
|
get style() {
|
|
455
708
|
return {
|
|
456
709
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -458,7 +711,7 @@ function PickerGroupDate(props) {
|
|
|
458
711
|
};
|
|
459
712
|
},
|
|
460
713
|
children: item
|
|
461
|
-
});
|
|
714
|
+
}));
|
|
462
715
|
});
|
|
463
716
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
464
717
|
key: `blank-top-${idx}`,
|
|
@@ -473,6 +726,10 @@ function PickerGroupDate(props) {
|
|
|
473
726
|
height: PICKER_LINE_HEIGHT
|
|
474
727
|
}
|
|
475
728
|
}))];
|
|
729
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
730
|
+
scrollIntoView,
|
|
731
|
+
scrollIntoViewAlignment: 'center'
|
|
732
|
+
} : {};
|
|
476
733
|
return createComponent(View, {
|
|
477
734
|
className: "taro-picker__group",
|
|
478
735
|
get children() {
|
|
@@ -482,7 +739,7 @@ function PickerGroupDate(props) {
|
|
|
482
739
|
className: "taro-picker__indicator"
|
|
483
740
|
}, indicatorStyle ? {
|
|
484
741
|
style: indicatorStyle
|
|
485
|
-
} : {})), createComponent(ScrollView, {
|
|
742
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
486
743
|
ref: scrollViewRef,
|
|
487
744
|
scrollY: true,
|
|
488
745
|
showScrollbar: false,
|
|
@@ -490,13 +747,16 @@ function PickerGroupDate(props) {
|
|
|
490
747
|
style: {
|
|
491
748
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
492
749
|
},
|
|
493
|
-
scrollTop: targetScrollTop
|
|
750
|
+
scrollTop: targetScrollTop
|
|
751
|
+
}, clickScrollViewProps, {
|
|
494
752
|
onScroll: handleScroll,
|
|
495
|
-
onTouchStart: () =>
|
|
753
|
+
onTouchStart: () => {
|
|
754
|
+
isTouchingRef.current = true;
|
|
755
|
+
},
|
|
496
756
|
onScrollEnd: handleScrollEnd,
|
|
497
757
|
scrollWithAnimation: true,
|
|
498
758
|
children: realPickerItems
|
|
499
|
-
})];
|
|
759
|
+
}))];
|
|
500
760
|
}
|
|
501
761
|
});
|
|
502
762
|
}
|
|
@@ -509,52 +769,54 @@ function PickerGroupRegion(props) {
|
|
|
509
769
|
updateIndex,
|
|
510
770
|
selectedIndex = 0,
|
|
511
771
|
// 使用selectedIndex参数,默认为0
|
|
512
|
-
colors = {}
|
|
772
|
+
colors = {},
|
|
773
|
+
enableClickItemScroll = true
|
|
513
774
|
} = props;
|
|
514
775
|
const indicatorStyle = colors.lineColor ? getIndicatorStyle(colors.lineColor) : null;
|
|
515
776
|
const scrollViewRef = React.useRef(null);
|
|
516
777
|
const [targetScrollTop, setTargetScrollTop] = React.useState(0);
|
|
778
|
+
const [scrollIntoView, scrollToItemId] = usePickerItemScrollIntoView();
|
|
517
779
|
const [currentIndex, setCurrentIndex] = React.useState(selectedIndex);
|
|
518
|
-
const
|
|
780
|
+
const isTouchingRef = React.useRef(false);
|
|
519
781
|
const lengthScaleRatioRef = React.useRef(1);
|
|
782
|
+
const useMeasuredScaleRef = React.useRef(false);
|
|
520
783
|
const itemHeightRef = React.useRef(PICKER_LINE_HEIGHT);
|
|
521
|
-
const
|
|
522
|
-
|
|
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 并判定缩放模式
|
|
523
790
|
React.useEffect(() => {
|
|
524
791
|
Taro.getSystemInfo({
|
|
525
792
|
success: res => {
|
|
526
793
|
lengthScaleRatioRef.current = calculateLengthScaleRatio(res);
|
|
794
|
+
useMeasuredScaleRef.current = resolveUseMeasuredScale(res);
|
|
795
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
527
796
|
},
|
|
528
797
|
fail: () => {
|
|
529
|
-
// 失败时使用默认值 1
|
|
530
798
|
lengthScaleRatioRef.current = 1;
|
|
799
|
+
useMeasuredScaleRef.current = false;
|
|
531
800
|
}
|
|
532
801
|
});
|
|
533
802
|
}, []);
|
|
534
803
|
React.useEffect(() => {
|
|
535
|
-
|
|
536
|
-
if (process.env.TARO_PLATFORM === 'harmony') {
|
|
537
|
-
itemHeightRef.current = PICKER_LINE_HEIGHT * lengthScaleRatioRef.current;
|
|
538
|
-
} else {
|
|
539
|
-
if (scrollViewRef.current && ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollHeight)) {
|
|
540
|
-
itemHeightRef.current = scrollViewRef.current.scrollHeight / scrollViewRef.current.childNodes.length;
|
|
541
|
-
} else {
|
|
542
|
-
console.warn('Height measurement anomaly');
|
|
543
|
-
}
|
|
544
|
-
}
|
|
804
|
+
itemHeightRef.current = calculateItemHeight(scrollViewRef.current, lengthScaleRatioRef.current, useMeasuredScaleRef.current);
|
|
545
805
|
}, [range.length]); // 只在range长度变化时重新计算
|
|
546
|
-
|
|
547
|
-
return Math.round(scrollTop / itemHeightRef.current);
|
|
548
|
-
};
|
|
549
|
-
// 当selectedIndex变化时,调整滚动位置
|
|
806
|
+
// props 的 selectedIndex 变化:滚至对应项并短时标记程序化滚动(syncScrollFromPropsRef)
|
|
550
807
|
React.useEffect(() => {
|
|
551
|
-
if (scrollViewRef.current && range.length > 0 && !
|
|
808
|
+
if (scrollViewRef.current && range.length > 0 && !isTouchingRef.current) {
|
|
809
|
+
syncScrollFromPropsRef.current = true;
|
|
552
810
|
const baseValue = selectedIndex * itemHeightRef.current;
|
|
553
|
-
setTargetScrollTopWithScale(setTargetScrollTop, baseValue);
|
|
811
|
+
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, undefined, lengthScaleRatioRef.current);
|
|
554
812
|
setCurrentIndex(selectedIndex);
|
|
813
|
+
const tid = setTimeout(() => {
|
|
814
|
+
syncScrollFromPropsRef.current = false;
|
|
815
|
+
}, 400);
|
|
816
|
+
return () => clearTimeout(tid);
|
|
555
817
|
}
|
|
556
818
|
}, [selectedIndex, range]);
|
|
557
|
-
//
|
|
819
|
+
// 滚动静止后归中(debounce)
|
|
558
820
|
const isCenterTimerId = React.useRef(null);
|
|
559
821
|
const handleScrollEnd = () => {
|
|
560
822
|
if (!scrollViewRef.current) return;
|
|
@@ -562,19 +824,27 @@ function PickerGroupRegion(props) {
|
|
|
562
824
|
clearTimeout(isCenterTimerId.current);
|
|
563
825
|
isCenterTimerId.current = null;
|
|
564
826
|
}
|
|
565
|
-
//
|
|
827
|
+
// 100ms 内无新滚动则归中并提交索引
|
|
566
828
|
isCenterTimerId.current = setTimeout(() => {
|
|
567
829
|
if (!scrollViewRef.current) return;
|
|
568
830
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
569
|
-
const newIndex = getSelectedIndex(scrollTop);
|
|
570
|
-
|
|
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;
|
|
571
835
|
const baseValue = newIndex * itemHeightRef.current;
|
|
572
836
|
const randomOffset = Math.random() * 0.001; // 随机数为了在一个项内滚动时强制刷新
|
|
573
837
|
setTargetScrollTopWithScale(setTargetScrollTop, baseValue, randomOffset, lengthScaleRatioRef.current);
|
|
574
|
-
updateIndex(newIndex, columnId, false,
|
|
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;
|
|
575
845
|
}, 100);
|
|
576
846
|
};
|
|
577
|
-
//
|
|
847
|
+
// 滚动中:按 scrollTop 更新高亮索引
|
|
578
848
|
const handleScroll = () => {
|
|
579
849
|
if (!scrollViewRef.current) return;
|
|
580
850
|
if (isCenterTimerId.current) {
|
|
@@ -582,7 +852,17 @@ function PickerGroupRegion(props) {
|
|
|
582
852
|
isCenterTimerId.current = null;
|
|
583
853
|
}
|
|
584
854
|
const scrollTop = scrollViewRef.current.scrollTop;
|
|
585
|
-
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
|
+
}
|
|
586
866
|
if (newIndex !== currentIndex) {
|
|
587
867
|
setCurrentIndex(newIndex);
|
|
588
868
|
}
|
|
@@ -590,10 +870,14 @@ function PickerGroupRegion(props) {
|
|
|
590
870
|
// 渲染选项
|
|
591
871
|
const pickerItem = range.map((item, index) => {
|
|
592
872
|
const content = rangeKey && item && typeof item === 'object' ? item[rangeKey] : item;
|
|
593
|
-
return createComponent(View, {
|
|
873
|
+
return createComponent(View, mergeProps({
|
|
594
874
|
id: `picker-item-${columnId}-${index}`,
|
|
595
875
|
key: index,
|
|
596
|
-
|
|
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
|
+
} : {}, {
|
|
597
881
|
get style() {
|
|
598
882
|
return {
|
|
599
883
|
height: PICKER_LINE_HEIGHT,
|
|
@@ -601,7 +885,7 @@ function PickerGroupRegion(props) {
|
|
|
601
885
|
};
|
|
602
886
|
},
|
|
603
887
|
children: content
|
|
604
|
-
});
|
|
888
|
+
}));
|
|
605
889
|
});
|
|
606
890
|
const realPickerItems = [...new Array(PICKER_BLANK_ITEMS).fill(null).map((_, idx) => createComponent(View, {
|
|
607
891
|
key: `blank-top-${idx}`,
|
|
@@ -616,6 +900,10 @@ function PickerGroupRegion(props) {
|
|
|
616
900
|
height: PICKER_LINE_HEIGHT
|
|
617
901
|
}
|
|
618
902
|
}))];
|
|
903
|
+
const clickScrollViewProps = enableClickItemScroll ? {
|
|
904
|
+
scrollIntoView,
|
|
905
|
+
scrollIntoViewAlignment: 'center'
|
|
906
|
+
} : {};
|
|
619
907
|
return createComponent(View, {
|
|
620
908
|
className: "taro-picker__group",
|
|
621
909
|
get children() {
|
|
@@ -625,7 +913,7 @@ function PickerGroupRegion(props) {
|
|
|
625
913
|
className: "taro-picker__indicator"
|
|
626
914
|
}, indicatorStyle ? {
|
|
627
915
|
style: indicatorStyle
|
|
628
|
-
} : {})), createComponent(ScrollView, {
|
|
916
|
+
} : {})), createComponent(ScrollView, mergeProps({
|
|
629
917
|
ref: scrollViewRef,
|
|
630
918
|
scrollY: true,
|
|
631
919
|
showScrollbar: false,
|
|
@@ -633,16 +921,16 @@ function PickerGroupRegion(props) {
|
|
|
633
921
|
style: {
|
|
634
922
|
height: PICKER_LINE_HEIGHT * PICKER_VISIBLE_ITEMS
|
|
635
923
|
},
|
|
636
|
-
scrollTop: targetScrollTop
|
|
924
|
+
scrollTop: targetScrollTop
|
|
925
|
+
}, clickScrollViewProps, {
|
|
637
926
|
onScroll: handleScroll,
|
|
638
927
|
onTouchStart: () => {
|
|
639
|
-
|
|
640
|
-
isUserBeginScrollRef.current = true;
|
|
928
|
+
isTouchingRef.current = true;
|
|
641
929
|
},
|
|
642
930
|
onScrollEnd: handleScrollEnd,
|
|
643
931
|
scrollWithAnimation: true,
|
|
644
932
|
children: realPickerItems
|
|
645
|
-
})];
|
|
933
|
+
}))];
|
|
646
934
|
}
|
|
647
935
|
});
|
|
648
936
|
}
|