@tarojs/components-advanced 4.1.12-beta.7 → 4.1.12-beta.70
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/index.js +2 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/list/hooks/useItemSizeCache.d.ts +1 -1
- package/dist/components/list/hooks/useItemSizeCache.js +5 -5
- package/dist/components/list/hooks/useItemSizeCache.js.map +1 -1
- package/dist/components/list/hooks/useListNestedScroll.d.ts +18 -0
- package/dist/components/list/hooks/useListNestedScroll.js +61 -0
- package/dist/components/list/hooks/useListNestedScroll.js.map +1 -0
- package/dist/components/list/hooks/useListScrollElementAttach.d.ts +25 -0
- package/dist/components/list/hooks/useListScrollElementAttach.js +88 -0
- package/dist/components/list/hooks/useListScrollElementAttach.js.map +1 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.d.ts +15 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.js +135 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.js.map +1 -0
- package/dist/components/list/hooks/useMeasureStartOffset.d.ts +4 -2
- package/dist/components/list/hooks/useMeasureStartOffset.js +56 -15
- package/dist/components/list/hooks/useMeasureStartOffset.js.map +1 -1
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.d.ts +14 -0
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js +87 -0
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js.map +1 -0
- package/dist/components/list/hooks/useRefresher.d.ts +8 -2
- package/dist/components/list/hooks/useRefresher.js +125 -67
- package/dist/components/list/hooks/useRefresher.js.map +1 -1
- package/dist/components/list/hooks/useResizeObserver.d.ts +4 -1
- package/dist/components/list/hooks/useResizeObserver.js +8 -7
- package/dist/components/list/hooks/useResizeObserver.js.map +1 -1
- package/dist/components/list/hooks/useScrollCorrection.js +2 -1
- package/dist/components/list/hooks/useScrollCorrection.js.map +1 -1
- package/dist/components/list/hooks/useScrollParentAutoFind.d.ts +20 -0
- package/dist/components/list/hooks/useScrollParentAutoFind.js +81 -0
- package/dist/components/list/hooks/useScrollParentAutoFind.js.map +1 -0
- package/dist/components/list/index.d.ts +12 -17
- package/dist/components/list/index.js +266 -286
- package/dist/components/list/index.js.map +1 -1
- package/dist/components/list/utils.d.ts +12 -0
- package/dist/components/list/utils.js +23 -1
- package/dist/components/list/utils.js.map +1 -1
- package/dist/components/virtual-list/vue/list.d.ts +10 -10
- package/dist/components/virtual-waterfall/vue/waterfall.d.ts +10 -10
- package/dist/components/water-flow/feed-column.d.ts +2 -0
- package/dist/components/water-flow/feed-column.js +10 -0
- package/dist/components/water-flow/feed-column.js.map +1 -0
- package/dist/components/water-flow/flow-item.js +18 -11
- package/dist/components/water-flow/flow-item.js.map +1 -1
- package/dist/components/water-flow/flow-section.js +1 -1
- package/dist/components/water-flow/flow-section.js.map +1 -1
- package/dist/components/water-flow/index.d.ts +1 -1
- package/dist/components/water-flow/interface.d.ts +31 -4
- package/dist/components/water-flow/node.d.ts +3 -0
- package/dist/components/water-flow/node.js +34 -1
- package/dist/components/water-flow/node.js.map +1 -1
- package/dist/components/water-flow/root.d.ts +36 -3
- package/dist/components/water-flow/root.js +143 -44
- package/dist/components/water-flow/root.js.map +1 -1
- package/dist/components/water-flow/section.d.ts +23 -1
- package/dist/components/water-flow/section.js +120 -20
- package/dist/components/water-flow/section.js.map +1 -1
- package/dist/components/water-flow/utils.d.ts +2 -0
- package/dist/components/water-flow/utils.js +3 -1
- package/dist/components/water-flow/utils.js.map +1 -1
- package/dist/components/water-flow/water-flow-node-cache.d.ts +24 -0
- package/dist/components/water-flow/water-flow-node-cache.js +161 -0
- package/dist/components/water-flow/water-flow-node-cache.js.map +1 -0
- package/dist/components/water-flow/water-flow.d.ts +2 -3
- package/dist/components/water-flow/water-flow.js +257 -77
- package/dist/components/water-flow/water-flow.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/dom.d.ts +2 -2
- package/dist/utils/dom.js +7 -6
- package/dist/utils/dom.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scrollElementContext.d.ts +15 -0
- package/dist/utils/scrollElementContext.js +14 -0
- package/dist/utils/scrollElementContext.js.map +1 -0
- package/dist/utils/scrollParent.d.ts +33 -0
- package/dist/utils/scrollParent.js +88 -0
- package/dist/utils/scrollParent.js.map +1 -0
- package/dist/utils/weapp-scope.d.ts +7 -0
- package/dist/utils/weapp-scope.js +20 -0
- package/dist/utils/weapp-scope.js.map +1 -0
- package/package.json +9 -9
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { View, ScrollView } from '@tarojs/components';
|
|
3
3
|
import Taro from '@tarojs/taro';
|
|
4
|
-
import React
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import '../../utils/index.js';
|
|
6
|
+
import { ScrollElementContextOrFallback } from '../../utils/scrollElementContext.js';
|
|
5
7
|
import { useItemSizeCache } from './hooks/useItemSizeCache.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { useListNestedScroll } from './hooks/useListNestedScroll.js';
|
|
9
|
+
import { useListScrollElementAttach } from './hooks/useListScrollElementAttach.js';
|
|
10
|
+
import { useListScrollElementAttachWeapp } from './hooks/useListScrollElementAttachWeapp.js';
|
|
11
|
+
import { useRefresher } from './hooks/useRefresher.js';
|
|
8
12
|
import { useResizeObserver } from './hooks/useResizeObserver.js';
|
|
9
13
|
import { useScrollCorrection } from './hooks/useScrollCorrection.js';
|
|
10
14
|
import { ListItem } from './ListItem.js';
|
|
11
15
|
import { NoMore } from './NoMore.js';
|
|
12
16
|
import { StickyHeader } from './StickyHeader.js';
|
|
13
17
|
import { StickySection } from './StickySection.js';
|
|
14
|
-
import {
|
|
18
|
+
import { isWeapp, createSelectorQueryForRef, isH5, supportsNativeRefresher } from './utils.js';
|
|
19
|
+
import { getScrollViewContextNode } from '../../utils/dom.js';
|
|
15
20
|
|
|
16
|
-
// 工具:累加数组
|
|
17
21
|
function accumulate(arr) {
|
|
18
22
|
const result = [0];
|
|
19
23
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -21,19 +25,15 @@ function accumulate(arr) {
|
|
|
21
25
|
}
|
|
22
26
|
return result;
|
|
23
27
|
}
|
|
24
|
-
// 检测抖动
|
|
25
28
|
function isShaking(diffList) {
|
|
26
29
|
if (diffList.length < 3)
|
|
27
30
|
return false;
|
|
28
|
-
// 检查是否有连续的正负交替
|
|
29
31
|
const signs = diffList.map(diff => Math.sign(diff));
|
|
30
32
|
let alternations = 0;
|
|
31
33
|
for (let i = 1; i < signs.length; i++) {
|
|
32
|
-
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
34
|
+
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
33
35
|
alternations++;
|
|
34
|
-
}
|
|
35
36
|
}
|
|
36
|
-
// 如果交替次数过多,认为是抖动
|
|
37
37
|
return alternations >= 2;
|
|
38
38
|
}
|
|
39
39
|
// 小程序端:判断 item 是否应该执行 SelectorQuery 测量(仅检查是否已测量过)
|
|
@@ -43,37 +43,53 @@ function shouldMeasureWeappItem(index, measuredSet) {
|
|
|
43
43
|
}
|
|
44
44
|
// 小程序端暂不外抛 onItemSizeChange,避免父层重渲染导致 List remount 引发回顶或空白
|
|
45
45
|
function weappDeferItemSizeChange(_index, _size, _onItemSizeChange) { }
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
/** 从 scroll 选项解析目标偏移量 */
|
|
47
|
+
function resolveScrollTargetOffset(options, isHorizontal) {
|
|
48
|
+
const opts = options !== null && options !== void 0 ? options : {};
|
|
49
|
+
const top = typeof opts.top === 'number' ? opts.top : undefined;
|
|
50
|
+
const left = typeof opts.left === 'number' ? opts.left : undefined;
|
|
51
|
+
let result = 0;
|
|
52
|
+
if (isHorizontal) {
|
|
53
|
+
if (typeof left === 'number')
|
|
54
|
+
result = left;
|
|
55
|
+
else if (typeof top === 'number')
|
|
56
|
+
result = top;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (typeof top === 'number')
|
|
60
|
+
result = top;
|
|
61
|
+
else if (typeof left === 'number')
|
|
62
|
+
result = left;
|
|
61
63
|
}
|
|
62
|
-
return
|
|
64
|
+
return Number.isFinite(result) ? result : 0;
|
|
63
65
|
}
|
|
66
|
+
// eslint-disable-next-line complexity -- List 多端/多模式逻辑集中,已抽离 useListNestedScroll、useListScrollElementAttach、resolveScrollTargetOffset 等
|
|
64
67
|
const InnerList = (props, ref) => {
|
|
65
|
-
var _a, _b
|
|
66
|
-
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 50, lowerThreshold = 50, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children,
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 50, lowerThreshold = 50, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children, nestedScroll, scrollElement, scrollRef: scrollRefProp, } = props;
|
|
67
70
|
const isHorizontal = scrollX === true;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const measuredStartOffset = useMeasureStartOffset((_a = result1.effectiveScrollElement) !== null && _a !== void 0 ? _a : EMPTY_SCROLL_REF, contentWrapperRef, {
|
|
71
|
-
enabled: result1.useScrollElementMode && !result1.hasListScrollCtx && !!result1.effectiveScrollElement && isH5,
|
|
72
|
-
isHorizontal,
|
|
73
|
-
});
|
|
74
|
-
const { effectiveScrollElement, effectiveStartOffset, useScrollElementMode } = useListScrollElement(listType, scrollElement, measuredStartOffset);
|
|
71
|
+
const listType = nestedScroll === true ? 'nested' : 'default';
|
|
72
|
+
const { effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, useScrollElementMode, needAutoFind, autoFindStatus, contentWrapperRef, contentId, } = useListNestedScroll(listType, scrollElement, undefined, isHorizontal);
|
|
75
73
|
const DEFAULT_ITEM_WIDTH = 120;
|
|
76
74
|
const DEFAULT_ITEM_HEIGHT = 40;
|
|
75
|
+
const defaultItemSize = isHorizontal ? DEFAULT_ITEM_WIDTH : DEFAULT_ITEM_HEIGHT;
|
|
76
|
+
const normalizeSize = React.useCallback((value) => {
|
|
77
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
|
78
|
+
return null;
|
|
79
|
+
return value;
|
|
80
|
+
}, []);
|
|
81
|
+
const resolveItemSizeByIndex = React.useCallback((index, fallback) => {
|
|
82
|
+
const { itemSize, itemData } = props;
|
|
83
|
+
const numberSize = normalizeSize(itemSize);
|
|
84
|
+
if (numberSize != null)
|
|
85
|
+
return numberSize;
|
|
86
|
+
if (typeof itemSize === 'function') {
|
|
87
|
+
const functionSize = normalizeSize(itemSize(index, itemData));
|
|
88
|
+
if (functionSize != null)
|
|
89
|
+
return functionSize;
|
|
90
|
+
}
|
|
91
|
+
return fallback;
|
|
92
|
+
}, [props.itemSize, props.itemData, normalizeSize]);
|
|
77
93
|
// 滚动状态管理
|
|
78
94
|
const containerRef = React.useRef(null);
|
|
79
95
|
// 生成唯一 List ID(用于小程序 ResizeObserver)
|
|
@@ -104,6 +120,22 @@ const InnerList = (props, ref) => {
|
|
|
104
120
|
ro.observe(el);
|
|
105
121
|
return () => ro.disconnect();
|
|
106
122
|
}, [isHorizontal]);
|
|
123
|
+
// WeChat 小程序:没有原生 ResizeObserver,用 SelectorQuery 一次性测量容器高度
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (!isWeapp)
|
|
126
|
+
return;
|
|
127
|
+
Taro.nextTick(() => {
|
|
128
|
+
createSelectorQueryForRef(containerRef)
|
|
129
|
+
.select(`#${listId}`)
|
|
130
|
+
.boundingClientRect((rect) => {
|
|
131
|
+
const measured = isHorizontal ? rect === null || rect === void 0 ? void 0 : rect.width : rect === null || rect === void 0 ? void 0 : rect.height;
|
|
132
|
+
if (typeof measured === 'number' && measured > 0) {
|
|
133
|
+
setContainerLength(measured);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.exec();
|
|
137
|
+
});
|
|
138
|
+
}, [isWeapp, isHorizontal, listId]);
|
|
107
139
|
// 滚动追踪相关refs
|
|
108
140
|
const isScrollingRef = React.useRef(false);
|
|
109
141
|
const lastScrollTopRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
@@ -115,6 +147,10 @@ const InnerList = (props, ref) => {
|
|
|
115
147
|
const programmaticCooldownRef = React.useRef(false);
|
|
116
148
|
const programmaticCooldownTimerRef = React.useRef(null);
|
|
117
149
|
const scrollViewOffsetRef = React.useRef(0);
|
|
150
|
+
// 用户滚动期间挂起的 reflow 标记(用于滚动结束后补触发)
|
|
151
|
+
const pendingWeappReflowRef = React.useRef(false);
|
|
152
|
+
// ref 持有最新版 scheduleWeappDynamicReflow,供 updateRenderOffset 内的 setTimeout 闭包安全访问
|
|
153
|
+
const scheduleWeappDynamicReflowRef = React.useRef(null);
|
|
118
154
|
// 处理渲染偏移量更新。
|
|
119
155
|
// syncToScrollView=true:程序性滚动,立即同步 scrollViewOffset。
|
|
120
156
|
// syncToScrollView=false:用户滑动,仅更新 renderOffset。weapp 采用 recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset,避免「传滞后值拉回」和「从有到无归顶」。
|
|
@@ -135,7 +171,10 @@ const InnerList = (props, ref) => {
|
|
|
135
171
|
if (needForce) {
|
|
136
172
|
const intermediate = newOffset > 0 ? newOffset - 0.01 : 0.01;
|
|
137
173
|
setScrollViewOffset(intermediate);
|
|
174
|
+
programmaticScrollRef.current = true;
|
|
138
175
|
requestAnimationFrame(() => {
|
|
176
|
+
// 第二帧也需要标记为程序性滚动,否则 else 分支不会传 scrollTop
|
|
177
|
+
programmaticScrollRef.current = true;
|
|
139
178
|
setScrollViewOffset(newOffset);
|
|
140
179
|
});
|
|
141
180
|
}
|
|
@@ -166,37 +205,17 @@ const InnerList = (props, ref) => {
|
|
|
166
205
|
if (!isWeapp) {
|
|
167
206
|
setScrollViewOffset(lastScrollTopRef.current);
|
|
168
207
|
}
|
|
208
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
209
|
+
if (isWeapp && pendingWeappReflowRef.current) {
|
|
210
|
+
scheduleWeappDynamicReflowRef.current();
|
|
211
|
+
}
|
|
169
212
|
}
|
|
170
213
|
}, isWeapp ? 200 : 150);
|
|
171
214
|
}, []);
|
|
172
215
|
// 暴露给外部的实例方法:通过 ref.scroll({ top / left }) 进行程序性滚动
|
|
173
216
|
React.useImperativeHandle(ref, () => ({
|
|
174
217
|
scroll(options) {
|
|
175
|
-
const
|
|
176
|
-
const isHorizontal = scrollX === true;
|
|
177
|
-
const top = typeof opts.top === 'number' ? opts.top : undefined;
|
|
178
|
-
const left = typeof opts.left === 'number' ? opts.left : undefined;
|
|
179
|
-
let targetOffset = 0;
|
|
180
|
-
if (isHorizontal) {
|
|
181
|
-
if (typeof left === 'number') {
|
|
182
|
-
targetOffset = left;
|
|
183
|
-
}
|
|
184
|
-
else if (typeof top === 'number') {
|
|
185
|
-
targetOffset = top;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
if (typeof top === 'number') {
|
|
190
|
-
targetOffset = top;
|
|
191
|
-
}
|
|
192
|
-
else if (typeof left === 'number') {
|
|
193
|
-
targetOffset = left;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (!Number.isFinite(targetOffset)) {
|
|
197
|
-
targetOffset = 0;
|
|
198
|
-
}
|
|
199
|
-
// 任务 2.2:scrollElement 模式下直接操作外部容器(内嵌时需加上 startOffset)
|
|
218
|
+
const targetOffset = resolveScrollTargetOffset(options, isHorizontal);
|
|
200
219
|
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
201
220
|
if (el && isH5) {
|
|
202
221
|
const scrollTarget = targetOffset + effectiveStartOffset;
|
|
@@ -208,11 +227,30 @@ const InnerList = (props, ref) => {
|
|
|
208
227
|
}
|
|
209
228
|
updateRenderOffset(targetOffset, false, 'scrollElement');
|
|
210
229
|
}
|
|
230
|
+
else if (el && isWeapp && useScrollElementMode) {
|
|
231
|
+
// 小程序 scrollElement 模式:需通过 getScrollViewContextNode + node.scrollTo 真正滚动外部 scroll-view
|
|
232
|
+
// updateRenderOffset 必须在 scrollTo 之后调用,否则 getScrollViewContextNode 异步期间会先更新 renderOffset 导致闪一下
|
|
233
|
+
const startOff = effectiveStartOffsetRef.current;
|
|
234
|
+
const scrollTarget = targetOffset + startOff;
|
|
235
|
+
const scrollViewId = el.id || `_ls_${listId}`;
|
|
236
|
+
if (!el.id)
|
|
237
|
+
el.id = scrollViewId;
|
|
238
|
+
getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
|
|
239
|
+
var _a, _b;
|
|
240
|
+
if (isHorizontal) {
|
|
241
|
+
(_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
(_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
|
|
245
|
+
}
|
|
246
|
+
updateRenderOffset(targetOffset, true, 'imperative');
|
|
247
|
+
});
|
|
248
|
+
}
|
|
211
249
|
else {
|
|
212
250
|
updateRenderOffset(targetOffset, true, 'imperative');
|
|
213
251
|
}
|
|
214
252
|
},
|
|
215
|
-
}), [scrollX, effectiveScrollElement, effectiveStartOffset, isH5, isHorizontal, updateRenderOffset]);
|
|
253
|
+
}), [scrollX, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId, useScrollElementMode, updateRenderOffset]);
|
|
216
254
|
// 提取 Refresher 配置(List 属性为 base,Refresher 子组件覆盖)
|
|
217
255
|
const refresherConfig = React.useMemo(() => {
|
|
218
256
|
const listRefresherEnabled = props.refresherEnabled !== false && (props.refresherEnabled === true || props.onRefresherRefresh != null);
|
|
@@ -294,9 +332,9 @@ const InnerList = (props, ref) => {
|
|
|
294
332
|
return config;
|
|
295
333
|
}, [children, props.showNoMore, props.noMoreText, props.noMoreStyle, props.renderNoMore]);
|
|
296
334
|
// Refresher 平台适配:H5 用 addImperativeTouchListeners
|
|
297
|
-
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
|
|
335
|
+
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, slotHeight: h5RefresherSlotHeight, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
|
|
298
336
|
? (ev) => { var _a, _b; return (_b = (ev.target != null && ((_a = contentWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(ev.target)))) !== null && _b !== void 0 ? _b : false; }
|
|
299
|
-
: undefined);
|
|
337
|
+
: undefined, !!(isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false));
|
|
300
338
|
const refresherTeardownRef = React.useRef(null);
|
|
301
339
|
const addImperativeTouchListenersRef = React.useRef(addImperativeTouchListeners);
|
|
302
340
|
addImperativeTouchListenersRef.current = addImperativeTouchListeners;
|
|
@@ -304,16 +342,16 @@ const InnerList = (props, ref) => {
|
|
|
304
342
|
if (refresherTeardownRef.current)
|
|
305
343
|
refresherTeardownRef.current();
|
|
306
344
|
}, []);
|
|
307
|
-
// H5
|
|
308
|
-
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false)
|
|
309
|
-
|
|
310
|
-
|
|
345
|
+
// H5 下拉刷新顶栏高度:自定义内容由 useRefresher 内 ResizeObserver 测量,否则默认 50
|
|
346
|
+
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false)
|
|
347
|
+
? h5RefresherSlotHeight
|
|
348
|
+
: 0;
|
|
349
|
+
// 解析分组结构:StickySection、ListItem 为直接子组件,过滤 Refresher/NoMore
|
|
311
350
|
const sections = React.useMemo(() => {
|
|
312
351
|
const result = [];
|
|
313
352
|
const defaultItems = [];
|
|
314
353
|
React.Children.forEach(children, (child, idx) => {
|
|
315
354
|
if (React.isValidElement(child) && child.type === StickySection) {
|
|
316
|
-
// 分组模式
|
|
317
355
|
const sectionProps = child.props;
|
|
318
356
|
let header = null;
|
|
319
357
|
const items = [];
|
|
@@ -326,10 +364,8 @@ const InnerList = (props, ref) => {
|
|
|
326
364
|
result.push({ header, items, key: child.key || String(idx) });
|
|
327
365
|
}
|
|
328
366
|
else if (React.isValidElement(child) && child.type === ListItem) {
|
|
329
|
-
// 普通 ListItem
|
|
330
367
|
defaultItems.push(child);
|
|
331
368
|
}
|
|
332
|
-
// 忽略 Refresher 和 NoMore 组件(已在上面提取配置)
|
|
333
369
|
});
|
|
334
370
|
if (defaultItems.length > 0) {
|
|
335
371
|
result.push({ header: null, items: defaultItems, key: 'default' });
|
|
@@ -337,8 +373,7 @@ const InnerList = (props, ref) => {
|
|
|
337
373
|
return result;
|
|
338
374
|
}, [children]);
|
|
339
375
|
// 动态尺寸管理
|
|
340
|
-
const
|
|
341
|
-
const estimatedSize = (_b = props.estimatedItemSize) !== null && _b !== void 0 ? _b : defaultEstimatedSize;
|
|
376
|
+
const estimatedSize = resolveItemSizeByIndex(0, defaultItemSize);
|
|
342
377
|
// 计算总 item 数量(跨所有 section)
|
|
343
378
|
const totalItemCount = React.useMemo(() => {
|
|
344
379
|
return sections.reduce((sum, section) => sum + section.items.length, 0);
|
|
@@ -352,17 +387,17 @@ const InnerList = (props, ref) => {
|
|
|
352
387
|
// 动态尺寸缓存更新版本:setItemSize 后递增,用于驱动 sectionOffsets/totalLength 与 item 定位重算
|
|
353
388
|
const [sizeCacheVersion, setSizeCacheVersion] = React.useState(0);
|
|
354
389
|
const sizeCacheRafRef = React.useRef(null);
|
|
355
|
-
//
|
|
356
|
-
|
|
390
|
+
// (measureScrollProtectRef 已移除:scrollTop + 内容变更同帧 setData 会与 scrollAnchoring
|
|
391
|
+
// 冲突导致归顶;改由 scrollAnchoring=true 独立处理内容重排,无需显式传 scrollTop 保护)
|
|
357
392
|
// 动态尺寸缓存
|
|
358
393
|
const sizeCache = useItemSizeCache({
|
|
359
394
|
isHorizontal,
|
|
360
|
-
|
|
395
|
+
estimatedSize,
|
|
361
396
|
itemCount: totalItemCount
|
|
362
397
|
});
|
|
363
398
|
// header 动态尺寸缓存(sectionIndex -> size)
|
|
364
399
|
const headerSizeCacheRef = React.useRef(new Map());
|
|
365
|
-
//
|
|
400
|
+
// 滚动修正的可见起始索引
|
|
366
401
|
const visibleStartIndexRef = React.useRef(0);
|
|
367
402
|
// ScrollTop 修正(仅 H5):动高时尺寸变化自动修正 scrollTop
|
|
368
403
|
const scrollCorrectionEnabled = !isWeapp && props.useResizeObserver === true;
|
|
@@ -386,23 +421,36 @@ const InnerList = (props, ref) => {
|
|
|
386
421
|
onScrollToLowerRef.current = onScrollToLower;
|
|
387
422
|
const thresholdRef = React.useRef({ upper: upperThreshold, lower: lowerThreshold });
|
|
388
423
|
thresholdRef.current = { upper: upperThreshold, lower: lowerThreshold };
|
|
389
|
-
|
|
424
|
+
const listContentLengthRef = React.useRef(0);
|
|
425
|
+
const inUpperZoneRef = React.useRef(true);
|
|
426
|
+
const inLowerZoneRef = React.useRef(false);
|
|
427
|
+
// 小程序 + 动高(virtual-list 风格):测量变化后同帧重排,不做程序性 scrollTop 回拉。
|
|
428
|
+
// 若用户正在滚动(惯性未结束),推迟到滚动停止后再触发,防止内容重排 + setData 同帧
|
|
429
|
+
// 导致 WeChat scrollAnchoring 失效或传滞后位置造成归顶。
|
|
390
430
|
const scheduleWeappDynamicReflow = React.useCallback(() => {
|
|
391
431
|
if (!isWeapp || props.useResizeObserver !== true)
|
|
392
432
|
return;
|
|
433
|
+
// 滚动中:仅标记为待处理,等 onScrollEnd / 200ms 超时触发
|
|
434
|
+
if (isUserScrollingRef.current) {
|
|
435
|
+
pendingWeappReflowRef.current = true;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
393
438
|
if (sizeCacheRafRef.current != null)
|
|
394
439
|
return;
|
|
395
440
|
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
396
441
|
sizeCacheRafRef.current = null;
|
|
397
|
-
|
|
442
|
+
pendingWeappReflowRef.current = false;
|
|
398
443
|
setSizeCacheVersion((v) => v + 1);
|
|
399
444
|
});
|
|
400
445
|
}, [isWeapp, props.useResizeObserver]);
|
|
446
|
+
// 每次渲染时更新 ref,保证 setTimeout 闭包内拿到最新版本
|
|
447
|
+
scheduleWeappDynamicReflowRef.current = scheduleWeappDynamicReflow;
|
|
401
448
|
// ResizeObserver(当启用动态测量时)
|
|
402
449
|
const resizeObserver = useResizeObserver({
|
|
403
450
|
enabled: props.useResizeObserver === true,
|
|
404
451
|
isHorizontal,
|
|
405
452
|
listId,
|
|
453
|
+
listViewportRef: containerRef,
|
|
406
454
|
onResize: (index, size) => {
|
|
407
455
|
var _a;
|
|
408
456
|
const oldSize = sizeCache.getItemSize(index);
|
|
@@ -430,85 +478,63 @@ const InnerList = (props, ref) => {
|
|
|
430
478
|
}
|
|
431
479
|
}
|
|
432
480
|
});
|
|
433
|
-
//
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
481
|
+
// 嵌套滚动:内层高度变化上报
|
|
482
|
+
const handleReportNestedHeightChange = React.useCallback((height, index) => {
|
|
483
|
+
if (height <= 0)
|
|
484
|
+
return;
|
|
485
|
+
const estimatedHeader = estimatedSize * 0.5;
|
|
486
|
+
const fullHeight = height + estimatedHeader;
|
|
487
|
+
const oldSize = sizeCache.getItemSize(index);
|
|
488
|
+
if (Math.abs(oldSize - fullHeight) < 1)
|
|
489
|
+
return;
|
|
490
|
+
sizeCache.setItemSize(index, fullHeight);
|
|
491
|
+
scrollCorrection.recordSizeChange(index, oldSize, fullHeight);
|
|
492
|
+
if (isWeapp && props.useResizeObserver === true) {
|
|
493
|
+
scheduleWeappDynamicReflow();
|
|
445
494
|
}
|
|
446
|
-
else {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (typeof props.itemSize === 'number')
|
|
452
|
-
return props.itemSize;
|
|
453
|
-
if (typeof props.itemSize === 'function')
|
|
454
|
-
return props.itemSize(0, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
455
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
495
|
+
else if (sizeCacheRafRef.current == null) {
|
|
496
|
+
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
497
|
+
sizeCacheRafRef.current = null;
|
|
498
|
+
setSizeCacheVersion((v) => v + 1);
|
|
499
|
+
});
|
|
456
500
|
}
|
|
457
|
-
};
|
|
458
|
-
|
|
501
|
+
}, [sizeCache, scrollCorrection, estimatedSize, isWeapp, props.useResizeObserver, scheduleWeappDynamicReflow]);
|
|
502
|
+
const getDefaultHeaderSize = React.useCallback(() => {
|
|
503
|
+
const headerSize = normalizeSize(props.headerSize);
|
|
504
|
+
if (headerSize != null)
|
|
505
|
+
return headerSize;
|
|
506
|
+
return resolveItemSizeByIndex(0, defaultItemSize);
|
|
507
|
+
}, [props.headerSize, resolveItemSizeByIndex, defaultItemSize, normalizeSize]);
|
|
508
|
+
// 获取 header 尺寸(支持动态测量)
|
|
459
509
|
const getHeaderSize = React.useCallback((sectionIndex) => {
|
|
460
|
-
// 如果启用动态测量,优先从缓存读取
|
|
461
510
|
if (props.useResizeObserver === true) {
|
|
462
511
|
const cached = headerSizeCacheRef.current.get(sectionIndex);
|
|
463
512
|
if (cached != null && cached > 0)
|
|
464
513
|
return cached;
|
|
465
514
|
}
|
|
466
|
-
// 否则返回默认尺寸
|
|
467
515
|
return getDefaultHeaderSize();
|
|
468
|
-
}, [props.useResizeObserver,
|
|
469
|
-
// 工具:获取 item 尺寸,支持函数/props/默认值/动态测量
|
|
516
|
+
}, [props.useResizeObserver, getDefaultHeaderSize]);
|
|
470
517
|
const getItemSize = React.useCallback((index) => {
|
|
471
|
-
// 优先级1:如果启用动态测量,从缓存读取
|
|
472
518
|
if (props.useResizeObserver === true) {
|
|
473
519
|
return sizeCache.getItemSize(index);
|
|
474
520
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (typeof props.itemWidth === 'number')
|
|
478
|
-
return props.itemWidth;
|
|
479
|
-
if (typeof props.itemSize === 'number')
|
|
480
|
-
return props.itemSize;
|
|
481
|
-
if (typeof props.itemSize === 'function')
|
|
482
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_WIDTH;
|
|
483
|
-
return DEFAULT_ITEM_WIDTH;
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
if (typeof props.itemHeight === 'number')
|
|
487
|
-
return props.itemHeight;
|
|
488
|
-
if (typeof props.itemSize === 'number')
|
|
489
|
-
return props.itemSize;
|
|
490
|
-
if (typeof props.itemSize === 'function')
|
|
491
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
492
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
493
|
-
}
|
|
494
|
-
}, [props.useResizeObserver, props.itemWidth, props.itemHeight, props.itemSize, props.itemData, isHorizontal, sizeCache]);
|
|
521
|
+
return resolveItemSizeByIndex(index, defaultItemSize);
|
|
522
|
+
}, [props.useResizeObserver, sizeCache, resolveItemSizeByIndex, defaultItemSize]);
|
|
495
523
|
// 分组累积高度/宽度,sizeCacheVersion 变化时重算
|
|
496
524
|
const sectionOffsets = React.useMemo(() => {
|
|
497
525
|
const offsets = [0];
|
|
498
|
-
let globalItemIndex = 0;
|
|
526
|
+
let globalItemIndex = 0;
|
|
499
527
|
sections.forEach((section, sectionIdx) => {
|
|
500
528
|
const headerSize = getHeaderSize(sectionIdx);
|
|
501
|
-
// 使用全局索引计算每个 item 的尺寸
|
|
502
529
|
const itemSizes = section.items.map((_, localIdx) => getItemSize(globalItemIndex + localIdx));
|
|
503
530
|
const groupSize = (section.header ? headerSize : 0) +
|
|
504
531
|
itemSizes.reduce((a, b) => a + b, 0) +
|
|
505
532
|
Math.max(0, section.items.length) * space;
|
|
506
533
|
offsets.push(offsets[offsets.length - 1] + groupSize);
|
|
507
|
-
// 累加当前 section 的 item 数量
|
|
508
534
|
globalItemIndex += section.items.length;
|
|
509
535
|
});
|
|
510
536
|
return offsets;
|
|
511
|
-
}, [sections, space,
|
|
537
|
+
}, [sections, space, getItemSize, getHeaderSize, sizeCacheVersion]);
|
|
512
538
|
// 外层虚拟滚动:可见分组
|
|
513
539
|
const [startSection, endSection] = React.useMemo(() => {
|
|
514
540
|
let start = 0;
|
|
@@ -557,11 +583,9 @@ const InnerList = (props, ref) => {
|
|
|
557
583
|
return [0, 0];
|
|
558
584
|
return [firstVisible, lastVisible];
|
|
559
585
|
}, [renderOffset, containerLength, sections, sectionOffsets, getHeaderSize, getItemSize, space]);
|
|
560
|
-
// 触发 onScrollIndex 回调(带防重复)
|
|
561
586
|
const lastVisibleRangeRef = React.useRef({ start: -1, end: -1 });
|
|
562
587
|
React.useEffect(() => {
|
|
563
588
|
if (props.onScrollIndex) {
|
|
564
|
-
// 避免重复触发
|
|
565
589
|
if (lastVisibleRangeRef.current.start !== visibleStartItem ||
|
|
566
590
|
lastVisibleRangeRef.current.end !== visibleEndItem) {
|
|
567
591
|
lastVisibleRangeRef.current = { start: visibleStartItem, end: visibleEndItem };
|
|
@@ -570,6 +594,7 @@ const InnerList = (props, ref) => {
|
|
|
570
594
|
}
|
|
571
595
|
}, [visibleStartItem, visibleEndItem, props.onScrollIndex]);
|
|
572
596
|
const handleScroll = React.useCallback((e) => {
|
|
597
|
+
var _a, _b, _c, _d;
|
|
573
598
|
let newOffset;
|
|
574
599
|
if (e.detail) {
|
|
575
600
|
newOffset = isHorizontal ? e.detail.scrollLeft : e.detail.scrollTop;
|
|
@@ -578,28 +603,51 @@ const InnerList = (props, ref) => {
|
|
|
578
603
|
newOffset = isHorizontal ? e.scrollLeft : e.scrollTop;
|
|
579
604
|
}
|
|
580
605
|
const effectiveOffset = newOffset;
|
|
606
|
+
const currentThreshold = thresholdRef.current;
|
|
607
|
+
const { upper, lower } = currentThreshold;
|
|
608
|
+
// WeChat 小程序节点不具备 clientHeight/clientWidth,需显式回退到 containerLength state
|
|
609
|
+
const rawContainerSize = containerRef.current
|
|
610
|
+
? (isHorizontal ? containerRef.current.clientWidth : containerRef.current.clientHeight)
|
|
611
|
+
: null;
|
|
612
|
+
const currentContainerLength = (typeof rawContainerSize === 'number' && rawContainerSize > 0)
|
|
613
|
+
? rawContainerSize
|
|
614
|
+
: containerLength;
|
|
615
|
+
const nowInUpper = effectiveOffset <= upper;
|
|
616
|
+
const currentContentLength = listContentLengthRef.current;
|
|
617
|
+
const nowInLower = currentContentLength > 0 && effectiveOffset + currentContainerLength >= currentContentLength - lower;
|
|
581
618
|
const diff = effectiveOffset - lastScrollTopRef.current;
|
|
582
619
|
scrollDiffListRef.current.shift();
|
|
583
620
|
scrollDiffListRef.current.push(diff);
|
|
584
621
|
const shaking = isScrollingRef.current && isShaking(scrollDiffListRef.current);
|
|
585
|
-
if (shaking)
|
|
622
|
+
if (shaking) {
|
|
586
623
|
return;
|
|
624
|
+
}
|
|
587
625
|
if (programmaticCooldownRef.current) {
|
|
588
626
|
lastScrollTopRef.current = effectiveOffset;
|
|
589
627
|
setRenderOffset(effectiveOffset);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
628
|
+
const scrollTop = isHorizontal ? 0 : newOffset;
|
|
629
|
+
const scrollLeft = isHorizontal ? newOffset : 0;
|
|
630
|
+
onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
|
|
631
|
+
if (nowInUpper && !inUpperZoneRef.current)
|
|
632
|
+
(_a = onScrollToUpperRef.current) === null || _a === void 0 ? void 0 : _a.call(onScrollToUpperRef);
|
|
633
|
+
if (nowInLower && !inLowerZoneRef.current)
|
|
634
|
+
(_b = onScrollToLowerRef.current) === null || _b === void 0 ? void 0 : _b.call(onScrollToLowerRef);
|
|
635
|
+
inUpperZoneRef.current = nowInUpper;
|
|
636
|
+
inLowerZoneRef.current = nowInLower;
|
|
594
637
|
return;
|
|
595
638
|
}
|
|
596
639
|
scrollCorrection.markUserScrolling();
|
|
597
640
|
updateRenderOffset(effectiveOffset, false, 'onScroll');
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
641
|
+
const scrollTop = isHorizontal ? 0 : newOffset;
|
|
642
|
+
const scrollLeft = isHorizontal ? newOffset : 0;
|
|
643
|
+
onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
|
|
644
|
+
if (nowInUpper && !inUpperZoneRef.current)
|
|
645
|
+
(_c = onScrollToUpperRef.current) === null || _c === void 0 ? void 0 : _c.call(onScrollToUpperRef);
|
|
646
|
+
if (nowInLower && !inLowerZoneRef.current)
|
|
647
|
+
(_d = onScrollToLowerRef.current) === null || _d === void 0 ? void 0 : _d.call(onScrollToLowerRef);
|
|
648
|
+
inUpperZoneRef.current = nowInUpper;
|
|
649
|
+
inLowerZoneRef.current = nowInLower;
|
|
650
|
+
}, [isHorizontal, onScroll, updateRenderOffset, scrollCorrection, props.useResizeObserver, containerLength]);
|
|
603
651
|
// 小程序:onScrollEnd 优先结束 isUserScrolling,timeout 兜底
|
|
604
652
|
const handleNativeScrollEnd = React.useCallback(() => {
|
|
605
653
|
if (isWeapp) {
|
|
@@ -611,78 +659,22 @@ const InnerList = (props, ref) => {
|
|
|
611
659
|
isScrollingRef.current = false;
|
|
612
660
|
isUserScrollingRef.current = false;
|
|
613
661
|
setIsUserScrolling(false);
|
|
614
|
-
//
|
|
662
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
663
|
+
if (pendingWeappReflowRef.current) {
|
|
664
|
+
scheduleWeappDynamicReflowRef.current();
|
|
665
|
+
}
|
|
615
666
|
}
|
|
616
667
|
}
|
|
617
668
|
onScrollEnd === null || onScrollEnd === void 0 ? void 0 : onScrollEnd();
|
|
618
669
|
}, [isWeapp, onScrollEnd]);
|
|
619
|
-
//
|
|
620
|
-
React.useEffect(() => {
|
|
621
|
-
if (!effectiveScrollElement || !isH5)
|
|
622
|
-
return;
|
|
623
|
-
const el = effectiveScrollElement.current;
|
|
624
|
-
if (!el || typeof ResizeObserver === 'undefined')
|
|
625
|
-
return;
|
|
626
|
-
const update = () => {
|
|
627
|
-
const measured = isHorizontal ? el.clientWidth : el.clientHeight;
|
|
628
|
-
if (measured > 0) {
|
|
629
|
-
setContainerLength(measured);
|
|
630
|
-
}
|
|
631
|
-
// scrollElement 模式下 useLayoutEffect 可能早于 ref 挂载,ResizeObserver 作为兜底确保 scrollRef 正确
|
|
632
|
-
if (scrollRefProp)
|
|
633
|
-
scrollRefProp.current = el;
|
|
634
|
-
};
|
|
635
|
-
update();
|
|
636
|
-
const ro = new ResizeObserver(update);
|
|
637
|
-
ro.observe(el);
|
|
638
|
-
return () => ro.disconnect();
|
|
639
|
-
}, [effectiveScrollElement, isH5, isHorizontal]);
|
|
640
|
-
// scrollElement 模式:监听外部滚动,驱动 renderOffset
|
|
641
|
-
// 使用 ref 保存 scrollCorrection/onScroll,避免每次渲染 deps 变化导致 effect 反复执行形成循环
|
|
642
|
-
const effectiveStartOffsetRef = React.useRef(effectiveStartOffset);
|
|
643
|
-
effectiveStartOffsetRef.current = effectiveStartOffset;
|
|
644
|
-
React.useEffect(() => {
|
|
645
|
-
if (!effectiveScrollElement || !isH5)
|
|
646
|
-
return;
|
|
647
|
-
const el = effectiveScrollElement.current;
|
|
648
|
-
if (!el)
|
|
649
|
-
return;
|
|
650
|
-
const handler = () => {
|
|
651
|
-
var _a, _b, _c;
|
|
652
|
-
const scrollPos = isHorizontal ? el.scrollLeft : el.scrollTop;
|
|
653
|
-
const adjustedPos = scrollPos - effectiveStartOffsetRef.current;
|
|
654
|
-
const clientSize = isHorizontal ? el.clientWidth : el.clientHeight;
|
|
655
|
-
const scrollSize = isHorizontal ? el.scrollWidth : el.scrollHeight;
|
|
656
|
-
scrollCorrectionRef.current.markUserScrolling();
|
|
657
|
-
updateRenderOffset(adjustedPos, false, 'scrollElement');
|
|
658
|
-
(_a = onScrollRef.current) === null || _a === void 0 ? void 0 : _a.call(onScrollRef, {
|
|
659
|
-
scrollTop: isHorizontal ? 0 : scrollPos,
|
|
660
|
-
scrollLeft: isHorizontal ? scrollPos : 0
|
|
661
|
-
});
|
|
662
|
-
// 任务 2.1:onScrollToUpper / onScrollToLower(scrollElement 模式用 adjustedPos,使有上方混排时以 List 顶为基准)
|
|
663
|
-
const { upper, lower } = thresholdRef.current;
|
|
664
|
-
const effectiveAdjusted = Math.max(0, adjustedPos);
|
|
665
|
-
if (effectiveAdjusted <= upper) {
|
|
666
|
-
(_b = onScrollToUpperRef.current) === null || _b === void 0 ? void 0 : _b.call(onScrollToUpperRef);
|
|
667
|
-
}
|
|
668
|
-
if (scrollPos + clientSize >= scrollSize - lower) {
|
|
669
|
-
(_c = onScrollToLowerRef.current) === null || _c === void 0 ? void 0 : _c.call(onScrollToLowerRef);
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
// 同步初始 scroll 位置(内嵌时需减去 startOffset)
|
|
673
|
-
const initialScroll = isHorizontal ? el.scrollLeft : el.scrollTop;
|
|
674
|
-
updateRenderOffset(initialScroll - effectiveStartOffsetRef.current, false, 'scrollElement');
|
|
675
|
-
el.addEventListener('scroll', handler, { passive: true });
|
|
676
|
-
return () => el.removeEventListener('scroll', handler);
|
|
677
|
-
}, [effectiveScrollElement, isH5, isHorizontal, updateRenderOffset]);
|
|
678
|
-
// 任务 2.3:暴露 scrollRef 给父组件(供内层 List/WaterFlow 传入 scrollElement)
|
|
679
|
-
// 收敛到此处统一赋值,不再在 ResizeObserver 中重复
|
|
670
|
+
// 暴露 scrollRef 给父组件,供内层 List/WaterFlow 传入 scrollElement
|
|
680
671
|
React.useLayoutEffect(() => {
|
|
681
672
|
if (!scrollRefProp)
|
|
682
673
|
return;
|
|
683
674
|
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
684
|
-
if (el)
|
|
675
|
+
if (el) {
|
|
685
676
|
scrollRefProp.current = el;
|
|
677
|
+
}
|
|
686
678
|
}, [scrollRefProp, useScrollElementMode, effectiveScrollElement]);
|
|
687
679
|
// controlledScrollTop 变化时同步到 ScrollView
|
|
688
680
|
React.useEffect(() => {
|
|
@@ -760,20 +752,42 @@ const InnerList = (props, ref) => {
|
|
|
760
752
|
currentGlobalIndex += section.items.length;
|
|
761
753
|
}
|
|
762
754
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
755
|
+
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
756
|
+
if (useScrollElementMode && el) {
|
|
757
|
+
const scrollTarget = targetOffset + (isWeapp ? effectiveStartOffsetRef.current : effectiveStartOffset);
|
|
758
|
+
if (isH5) {
|
|
759
|
+
if (isHorizontal) {
|
|
760
|
+
el.scrollTo({ left: scrollTarget });
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
el.scrollTo({ top: scrollTarget });
|
|
764
|
+
}
|
|
765
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
766
|
+
}
|
|
767
|
+
else if (isWeapp) {
|
|
768
|
+
const scrollViewId = el.id || `_ls_${listId}`;
|
|
769
|
+
if (!el.id)
|
|
770
|
+
el.id = scrollViewId;
|
|
771
|
+
getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
|
|
772
|
+
var _a, _b;
|
|
773
|
+
if (isHorizontal) {
|
|
774
|
+
(_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
(_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
|
|
778
|
+
}
|
|
779
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
780
|
+
});
|
|
770
781
|
}
|
|
771
782
|
else {
|
|
772
|
-
|
|
783
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
773
784
|
}
|
|
774
785
|
}
|
|
786
|
+
else {
|
|
787
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
788
|
+
}
|
|
775
789
|
}
|
|
776
|
-
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, isH5, isHorizontal]);
|
|
790
|
+
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId]);
|
|
777
791
|
// 容器样式;H5 刷新中禁止滚动
|
|
778
792
|
const containerStyle = Object.assign(Object.assign({ position: 'relative', boxSizing: 'border-box', height,
|
|
779
793
|
width }, style), (isH5 && refresherConfig && !supportsNativeRefresher && h5RefresherProps.isRefreshing
|
|
@@ -782,9 +796,7 @@ const InnerList = (props, ref) => {
|
|
|
782
796
|
// ScrollView 属性
|
|
783
797
|
const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign({ scrollY: !scrollX && scrollY, scrollX, style: containerStyle, className, enhanced: true, showScrollbar,
|
|
784
798
|
upperThreshold,
|
|
785
|
-
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll,
|
|
786
|
-
onScrollToLower,
|
|
787
|
-
onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), scrollViewRefresherProps), scrollViewRefresherHandlers);
|
|
799
|
+
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll, onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), scrollViewRefresherProps), scrollViewRefresherHandlers);
|
|
788
800
|
// H5 对齐小程序:refresherTriggered=true 时顶部立即显示加载指示器,滚到顶部并锁定滚动直至设为 false
|
|
789
801
|
React.useEffect(() => {
|
|
790
802
|
if (!isH5 || !refresherConfig || supportsNativeRefresher || !h5RefresherProps.isRefreshing)
|
|
@@ -815,30 +827,23 @@ const InnerList = (props, ref) => {
|
|
|
815
827
|
};
|
|
816
828
|
}, [useScrollElementMode, effectiveScrollElement]);
|
|
817
829
|
scrollViewOffsetRef.current = scrollViewOffset;
|
|
818
|
-
// scrollTop
|
|
830
|
+
// scrollTop 传递策略(对齐 virtual-list enhanced=true 的做法):
|
|
831
|
+
// - 程序性滚动帧(programmaticScrollRef=true):传 scrollViewOffset(目标位置)
|
|
832
|
+
// - 其余所有帧(用户滚动 / 测量重排 / 空闲):完全不传,依赖微信原生 + scrollAnchoring
|
|
833
|
+
// scrollAnchoring=true 负责在内容重排时自动维持视口位置,避免与显式 scrollTop
|
|
834
|
+
// 同帧冲突(冲突会导致 scrollAnchoring 失去锚点后归顶)
|
|
819
835
|
if (isWeapp) {
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
const eff = lastScrollTopRef.current;
|
|
823
|
-
setScrollViewOffset(eff);
|
|
836
|
+
if (programmaticScrollRef.current) {
|
|
837
|
+
// 程序性滚动(imperative / scrollCorrection / scrollIntoView 等):传目标位置
|
|
824
838
|
if (isHorizontal) {
|
|
825
|
-
scrollViewProps.scrollLeft =
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
scrollViewProps.scrollTop = eff;
|
|
829
|
-
}
|
|
830
|
-
programmaticScrollRef.current = false;
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
const sv = scrollViewOffset;
|
|
834
|
-
if (isHorizontal) {
|
|
835
|
-
scrollViewProps.scrollLeft = sv;
|
|
839
|
+
scrollViewProps.scrollLeft = scrollViewOffset;
|
|
836
840
|
}
|
|
837
841
|
else {
|
|
838
|
-
scrollViewProps.scrollTop =
|
|
842
|
+
scrollViewProps.scrollTop = scrollViewOffset;
|
|
839
843
|
}
|
|
840
844
|
programmaticScrollRef.current = false;
|
|
841
845
|
}
|
|
846
|
+
// else:用户滚动 / 空闲帧不传 scrollTop,让微信原生 + scrollAnchoring 完全接管
|
|
842
847
|
}
|
|
843
848
|
else {
|
|
844
849
|
// H5:非用户滑动时传
|
|
@@ -872,6 +877,19 @@ const InnerList = (props, ref) => {
|
|
|
872
877
|
const noMoreHeight = (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible) ? (noMoreConfig.height || 60) : 0;
|
|
873
878
|
const listContentLength = sectionOffsets[sectionOffsets.length - 1] + noMoreHeight;
|
|
874
879
|
const totalLength = listContentLength;
|
|
880
|
+
// scrollElement 模式下 onScrollToLower 需用内层内容高度判断,供 scroll handler 读取
|
|
881
|
+
listContentLengthRef.current = listContentLength;
|
|
882
|
+
const scrollAttachRefsRef = React.useRef(null);
|
|
883
|
+
scrollAttachRefsRef.current = {
|
|
884
|
+
scrollCorrection: scrollCorrectionRef.current,
|
|
885
|
+
onScroll: onScrollRef.current,
|
|
886
|
+
onScrollToUpper: onScrollToUpperRef.current,
|
|
887
|
+
onScrollToLower: onScrollToLowerRef.current,
|
|
888
|
+
threshold: thresholdRef.current,
|
|
889
|
+
listContentLength: listContentLengthRef.current,
|
|
890
|
+
};
|
|
891
|
+
useListScrollElementAttach(useScrollElementMode && isH5, effectiveScrollElement, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef);
|
|
892
|
+
useListScrollElementAttachWeapp(useScrollElementMode && isWeapp, effectiveScrollElement, effectiveStartOffsetRef, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef, initialContainerLength);
|
|
875
893
|
// 吸顶/吸左 header
|
|
876
894
|
const stickyHeaderNode = React.useMemo(() => {
|
|
877
895
|
if (!stickyHeader)
|
|
@@ -897,7 +915,7 @@ const InnerList = (props, ref) => {
|
|
|
897
915
|
}
|
|
898
916
|
}
|
|
899
917
|
return null;
|
|
900
|
-
}, [stickyHeader, renderOffset, sectionOffsets, sections
|
|
918
|
+
}, [stickyHeader, renderOffset, sectionOffsets, sections]);
|
|
901
919
|
// 渲染分组+item双层虚拟滚动
|
|
902
920
|
const renderSections = () => {
|
|
903
921
|
const nodes = [];
|
|
@@ -953,7 +971,7 @@ const InnerList = (props, ref) => {
|
|
|
953
971
|
Taro.nextTick(() => {
|
|
954
972
|
if (!headerRefsRef.current.has(sectionIndex))
|
|
955
973
|
return;
|
|
956
|
-
|
|
974
|
+
createSelectorQueryForRef({ current: el })
|
|
957
975
|
.select(`#${listId}-list-header-inner-${sectionIndex}`)
|
|
958
976
|
.boundingClientRect((rect) => {
|
|
959
977
|
if (rect) {
|
|
@@ -1099,7 +1117,7 @@ const InnerList = (props, ref) => {
|
|
|
1099
1117
|
Taro.nextTick(() => {
|
|
1100
1118
|
if (!itemRefsRef.current.has(capturedIndex))
|
|
1101
1119
|
return;
|
|
1102
|
-
|
|
1120
|
+
createSelectorQueryForRef({ current: el })
|
|
1103
1121
|
// 页面上可能同时存在多个 List,inner id 必须带 listId 前缀避免跨列表误命中
|
|
1104
1122
|
.select(`#${listId}-list-item-inner-${capturedIndex}`)
|
|
1105
1123
|
.boundingClientRect((rect) => {
|
|
@@ -1152,34 +1170,14 @@ const InnerList = (props, ref) => {
|
|
|
1152
1170
|
const itemNode = React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i]));
|
|
1153
1171
|
// 任务 4.1:当 List 暴露 scrollRef 时,为每个 item 提供 Context,供内层 WaterFlow 使用
|
|
1154
1172
|
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1155
|
-
? React.createElement(
|
|
1173
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1156
1174
|
key: outerItemProps.key,
|
|
1157
1175
|
value: {
|
|
1158
1176
|
scrollRef: scrollRefProp,
|
|
1159
1177
|
containerHeight: containerLength,
|
|
1160
1178
|
startOffset: offset + itemOffsets[i],
|
|
1161
1179
|
reportNestedHeightChange: props.useResizeObserver
|
|
1162
|
-
? (
|
|
1163
|
-
if (height <= 0)
|
|
1164
|
-
return;
|
|
1165
|
-
const idx = currentGlobalIndex;
|
|
1166
|
-
const estimatedHeader = estimatedSize * 0.5;
|
|
1167
|
-
const fullHeight = height + estimatedHeader;
|
|
1168
|
-
const oldSize = sizeCache.getItemSize(idx);
|
|
1169
|
-
if (Math.abs(oldSize - fullHeight) < 1)
|
|
1170
|
-
return;
|
|
1171
|
-
sizeCache.setItemSize(idx, fullHeight);
|
|
1172
|
-
scrollCorrection.recordSizeChange(idx, oldSize, fullHeight);
|
|
1173
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
1174
|
-
scheduleWeappDynamicReflow();
|
|
1175
|
-
}
|
|
1176
|
-
else if (sizeCacheRafRef.current == null) {
|
|
1177
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1178
|
-
sizeCacheRafRef.current = null;
|
|
1179
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1180
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1183
1181
|
: undefined,
|
|
1184
1182
|
},
|
|
1185
1183
|
}, itemNode)
|
|
@@ -1189,34 +1187,14 @@ const InnerList = (props, ref) => {
|
|
|
1189
1187
|
else {
|
|
1190
1188
|
const itemNode = React.createElement(View, outerItemProps, section.items[i]);
|
|
1191
1189
|
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1192
|
-
? React.createElement(
|
|
1190
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1193
1191
|
key: outerItemProps.key,
|
|
1194
1192
|
value: {
|
|
1195
1193
|
scrollRef: scrollRefProp,
|
|
1196
1194
|
containerHeight: containerLength,
|
|
1197
1195
|
startOffset: offset + itemOffsets[i],
|
|
1198
1196
|
reportNestedHeightChange: props.useResizeObserver
|
|
1199
|
-
? (
|
|
1200
|
-
if (height <= 0)
|
|
1201
|
-
return;
|
|
1202
|
-
const idx = currentGlobalIndex;
|
|
1203
|
-
const estimatedHeader = estimatedSize * 0.5;
|
|
1204
|
-
const fullHeight = height + estimatedHeader;
|
|
1205
|
-
const oldSize = sizeCache.getItemSize(idx);
|
|
1206
|
-
if (Math.abs(oldSize - fullHeight) < 1)
|
|
1207
|
-
return;
|
|
1208
|
-
sizeCache.setItemSize(idx, fullHeight);
|
|
1209
|
-
scrollCorrection.recordSizeChange(idx, oldSize, fullHeight);
|
|
1210
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
1211
|
-
scheduleWeappDynamicReflow();
|
|
1212
|
-
}
|
|
1213
|
-
else if (sizeCacheRafRef.current == null) {
|
|
1214
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1215
|
-
sizeCacheRafRef.current = null;
|
|
1216
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1197
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1220
1198
|
: undefined,
|
|
1221
1199
|
},
|
|
1222
1200
|
}, itemNode)
|
|
@@ -1269,9 +1247,11 @@ const InnerList = (props, ref) => {
|
|
|
1269
1247
|
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1270
1248
|
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [!supportsNativeRefresher && renderRefresherContent(), stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1271
1249
|
};
|
|
1272
|
-
|
|
1250
|
+
// useScrollElementMode 或 needAutoFind&&pending:渲染 View 以便监听外部滚动或 probe 阶段查找滚动父节点
|
|
1251
|
+
const renderView = useScrollElementMode || (!!needAutoFind && autoFindStatus === 'pending');
|
|
1252
|
+
if (renderView) {
|
|
1273
1253
|
// 任务 2.4:恢复 refresher DOM 结构(refresher 层 + listWrapperStyle)
|
|
1274
|
-
return (jsx(View, { ref: contentWrapperRef, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1254
|
+
return (jsx(View, { ref: contentWrapperRef, id: contentId, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1275
1255
|
position: 'absolute',
|
|
1276
1256
|
top: 0,
|
|
1277
1257
|
left: 0,
|
|
@@ -1279,11 +1259,11 @@ const InnerList = (props, ref) => {
|
|
|
1279
1259
|
height: refresherHeightForH5,
|
|
1280
1260
|
zIndex: 0,
|
|
1281
1261
|
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1282
|
-
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (
|
|
1262
|
+
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1283
1263
|
}
|
|
1284
1264
|
return (jsxs(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { id: listId, children: [supportsNativeRefresher && renderRefresherContent(), renderContentArea()] })));
|
|
1285
1265
|
};
|
|
1286
1266
|
const List = React.forwardRef(InnerList);
|
|
1287
1267
|
|
|
1288
|
-
export { List, ListItem, ListScrollElementContext, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1268
|
+
export { List, ListItem, ScrollElementContextOrFallback as ListScrollElementContext, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1289
1269
|
//# sourceMappingURL=index.js.map
|