@tarojs/components-advanced 4.1.12-beta.5 → 4.1.12-beta.50
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 +1 -0
- 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 +12 -0
- package/dist/components/list/hooks/useMeasureStartOffset.js +84 -0
- package/dist/components/list/hooks/useMeasureStartOffset.js.map +1 -0
- 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 +10 -2
- package/dist/components/list/hooks/useRefresher.js +150 -69
- 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 +14 -5
- package/dist/components/list/index.js +340 -147
- 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/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 +32 -2
- 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 +39 -4
- package/dist/components/water-flow/root.js +144 -44
- package/dist/components/water-flow/root.js.map +1 -1
- package/dist/components/water-flow/section.d.ts +11 -1
- package/dist/components/water-flow/section.js +81 -19
- package/dist/components/water-flow/section.js.map +1 -1
- package/dist/components/water-flow/utils.d.ts +4 -0
- package/dist/components/water-flow/utils.js +5 -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 +316 -36
- package/dist/components/water-flow/water-flow.js.map +1 -1
- package/dist/index.js +3 -0
- 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
|
@@ -2,17 +2,22 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { View, ScrollView } from '@tarojs/components';
|
|
3
3
|
import Taro from '@tarojs/taro';
|
|
4
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 {
|
|
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';
|
|
7
12
|
import { useResizeObserver } from './hooks/useResizeObserver.js';
|
|
8
13
|
import { useScrollCorrection } from './hooks/useScrollCorrection.js';
|
|
9
14
|
import { ListItem } from './ListItem.js';
|
|
10
15
|
import { NoMore } from './NoMore.js';
|
|
11
16
|
import { StickyHeader } from './StickyHeader.js';
|
|
12
17
|
import { StickySection } from './StickySection.js';
|
|
13
|
-
import { isWeapp, isH5, supportsNativeRefresher } from './utils.js';
|
|
18
|
+
import { isWeapp, createSelectorQueryForRef, isH5, supportsNativeRefresher } from './utils.js';
|
|
19
|
+
import { getScrollViewContextNode } from '../../utils/dom.js';
|
|
14
20
|
|
|
15
|
-
// 工具:累加数组
|
|
16
21
|
function accumulate(arr) {
|
|
17
22
|
const result = [0];
|
|
18
23
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -20,19 +25,15 @@ function accumulate(arr) {
|
|
|
20
25
|
}
|
|
21
26
|
return result;
|
|
22
27
|
}
|
|
23
|
-
// 检测抖动
|
|
24
28
|
function isShaking(diffList) {
|
|
25
29
|
if (diffList.length < 3)
|
|
26
30
|
return false;
|
|
27
|
-
// 检查是否有连续的正负交替
|
|
28
31
|
const signs = diffList.map(diff => Math.sign(diff));
|
|
29
32
|
let alternations = 0;
|
|
30
33
|
for (let i = 1; i < signs.length; i++) {
|
|
31
|
-
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
34
|
+
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
32
35
|
alternations++;
|
|
33
|
-
}
|
|
34
36
|
}
|
|
35
|
-
// 如果交替次数过多,认为是抖动
|
|
36
37
|
return alternations >= 2;
|
|
37
38
|
}
|
|
38
39
|
// 小程序端:判断 item 是否应该执行 SelectorQuery 测量(仅检查是否已测量过)
|
|
@@ -42,12 +43,53 @@ function shouldMeasureWeappItem(index, measuredSet) {
|
|
|
42
43
|
}
|
|
43
44
|
// 小程序端暂不外抛 onItemSizeChange,避免父层重渲染导致 List remount 引发回顶或空白
|
|
44
45
|
function weappDeferItemSizeChange(_index, _size, _onItemSizeChange) { }
|
|
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;
|
|
63
|
+
}
|
|
64
|
+
return Number.isFinite(result) ? result : 0;
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line complexity -- List 多端/多模式逻辑集中,已抽离 useListNestedScroll、useListScrollElementAttach、resolveScrollTargetOffset 等
|
|
45
67
|
const InnerList = (props, ref) => {
|
|
46
|
-
var _a;
|
|
47
|
-
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, } = props;
|
|
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;
|
|
48
70
|
const isHorizontal = scrollX === true;
|
|
71
|
+
const listType = nestedScroll === true ? 'nested' : 'default';
|
|
72
|
+
const { effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, useScrollElementMode, needAutoFind, autoFindStatus, contentWrapperRef, contentId, } = useListNestedScroll(listType, scrollElement, undefined, isHorizontal);
|
|
49
73
|
const DEFAULT_ITEM_WIDTH = 120;
|
|
50
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]);
|
|
51
93
|
// 滚动状态管理
|
|
52
94
|
const containerRef = React.useRef(null);
|
|
53
95
|
// 生成唯一 List ID(用于小程序 ResizeObserver)
|
|
@@ -78,6 +120,22 @@ const InnerList = (props, ref) => {
|
|
|
78
120
|
ro.observe(el);
|
|
79
121
|
return () => ro.disconnect();
|
|
80
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]);
|
|
81
139
|
// 滚动追踪相关refs
|
|
82
140
|
const isScrollingRef = React.useRef(false);
|
|
83
141
|
const lastScrollTopRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
@@ -89,6 +147,10 @@ const InnerList = (props, ref) => {
|
|
|
89
147
|
const programmaticCooldownRef = React.useRef(false);
|
|
90
148
|
const programmaticCooldownTimerRef = React.useRef(null);
|
|
91
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);
|
|
92
154
|
// 处理渲染偏移量更新。
|
|
93
155
|
// syncToScrollView=true:程序性滚动,立即同步 scrollViewOffset。
|
|
94
156
|
// syncToScrollView=false:用户滑动,仅更新 renderOffset。weapp 采用 recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset,避免「传滞后值拉回」和「从有到无归顶」。
|
|
@@ -109,7 +171,10 @@ const InnerList = (props, ref) => {
|
|
|
109
171
|
if (needForce) {
|
|
110
172
|
const intermediate = newOffset > 0 ? newOffset - 0.01 : 0.01;
|
|
111
173
|
setScrollViewOffset(intermediate);
|
|
174
|
+
programmaticScrollRef.current = true;
|
|
112
175
|
requestAnimationFrame(() => {
|
|
176
|
+
// 第二帧也需要标记为程序性滚动,否则 else 分支不会传 scrollTop
|
|
177
|
+
programmaticScrollRef.current = true;
|
|
113
178
|
setScrollViewOffset(newOffset);
|
|
114
179
|
});
|
|
115
180
|
}
|
|
@@ -140,39 +205,52 @@ const InnerList = (props, ref) => {
|
|
|
140
205
|
if (!isWeapp) {
|
|
141
206
|
setScrollViewOffset(lastScrollTopRef.current);
|
|
142
207
|
}
|
|
208
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
209
|
+
if (isWeapp && pendingWeappReflowRef.current) {
|
|
210
|
+
scheduleWeappDynamicReflowRef.current();
|
|
211
|
+
}
|
|
143
212
|
}
|
|
144
213
|
}, isWeapp ? 200 : 150);
|
|
145
214
|
}, []);
|
|
146
215
|
// 暴露给外部的实例方法:通过 ref.scroll({ top / left }) 进行程序性滚动
|
|
147
216
|
React.useImperativeHandle(ref, () => ({
|
|
148
217
|
scroll(options) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (typeof left === 'number') {
|
|
156
|
-
targetOffset = left;
|
|
218
|
+
const targetOffset = resolveScrollTargetOffset(options, isHorizontal);
|
|
219
|
+
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
220
|
+
if (el && isH5) {
|
|
221
|
+
const scrollTarget = targetOffset + effectiveStartOffset;
|
|
222
|
+
if (isHorizontal) {
|
|
223
|
+
el.scrollTo({ left: scrollTarget });
|
|
157
224
|
}
|
|
158
|
-
else
|
|
159
|
-
|
|
225
|
+
else {
|
|
226
|
+
el.scrollTo({ top: scrollTarget });
|
|
160
227
|
}
|
|
228
|
+
updateRenderOffset(targetOffset, false, 'scrollElement');
|
|
161
229
|
}
|
|
162
|
-
else {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
});
|
|
169
248
|
}
|
|
170
|
-
|
|
171
|
-
targetOffset
|
|
249
|
+
else {
|
|
250
|
+
updateRenderOffset(targetOffset, true, 'imperative');
|
|
172
251
|
}
|
|
173
|
-
updateRenderOffset(targetOffset, true, 'imperative');
|
|
174
252
|
},
|
|
175
|
-
}), [scrollX, updateRenderOffset]);
|
|
253
|
+
}), [scrollX, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId, useScrollElementMode, updateRenderOffset]);
|
|
176
254
|
// 提取 Refresher 配置(List 属性为 base,Refresher 子组件覆盖)
|
|
177
255
|
const refresherConfig = React.useMemo(() => {
|
|
178
256
|
const listRefresherEnabled = props.refresherEnabled !== false && (props.refresherEnabled === true || props.onRefresherRefresh != null);
|
|
@@ -254,7 +332,9 @@ const InnerList = (props, ref) => {
|
|
|
254
332
|
return config;
|
|
255
333
|
}, [children, props.showNoMore, props.noMoreText, props.noMoreStyle, props.renderNoMore]);
|
|
256
334
|
// Refresher 平台适配:H5 用 addImperativeTouchListeners
|
|
257
|
-
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset
|
|
335
|
+
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, slotHeight: h5RefresherSlotHeight, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
|
|
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; }
|
|
337
|
+
: undefined, !!(isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false));
|
|
258
338
|
const refresherTeardownRef = React.useRef(null);
|
|
259
339
|
const addImperativeTouchListenersRef = React.useRef(addImperativeTouchListeners);
|
|
260
340
|
addImperativeTouchListenersRef.current = addImperativeTouchListeners;
|
|
@@ -262,16 +342,16 @@ const InnerList = (props, ref) => {
|
|
|
262
342
|
if (refresherTeardownRef.current)
|
|
263
343
|
refresherTeardownRef.current();
|
|
264
344
|
}, []);
|
|
265
|
-
// H5
|
|
266
|
-
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false)
|
|
267
|
-
|
|
268
|
-
|
|
345
|
+
// H5 下拉刷新顶栏高度:自定义内容由 useRefresher 内 ResizeObserver 测量,否则默认 50
|
|
346
|
+
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false)
|
|
347
|
+
? h5RefresherSlotHeight
|
|
348
|
+
: 0;
|
|
349
|
+
// 解析分组结构:StickySection、ListItem 为直接子组件,过滤 Refresher/NoMore
|
|
269
350
|
const sections = React.useMemo(() => {
|
|
270
351
|
const result = [];
|
|
271
352
|
const defaultItems = [];
|
|
272
353
|
React.Children.forEach(children, (child, idx) => {
|
|
273
354
|
if (React.isValidElement(child) && child.type === StickySection) {
|
|
274
|
-
// 分组模式
|
|
275
355
|
const sectionProps = child.props;
|
|
276
356
|
let header = null;
|
|
277
357
|
const items = [];
|
|
@@ -284,10 +364,8 @@ const InnerList = (props, ref) => {
|
|
|
284
364
|
result.push({ header, items, key: child.key || String(idx) });
|
|
285
365
|
}
|
|
286
366
|
else if (React.isValidElement(child) && child.type === ListItem) {
|
|
287
|
-
// 普通 ListItem
|
|
288
367
|
defaultItems.push(child);
|
|
289
368
|
}
|
|
290
|
-
// 忽略 Refresher 和 NoMore 组件(已在上面提取配置)
|
|
291
369
|
});
|
|
292
370
|
if (defaultItems.length > 0) {
|
|
293
371
|
result.push({ header: null, items: defaultItems, key: 'default' });
|
|
@@ -295,8 +373,7 @@ const InnerList = (props, ref) => {
|
|
|
295
373
|
return result;
|
|
296
374
|
}, [children]);
|
|
297
375
|
// 动态尺寸管理
|
|
298
|
-
const
|
|
299
|
-
const estimatedSize = (_a = props.estimatedItemSize) !== null && _a !== void 0 ? _a : defaultEstimatedSize;
|
|
376
|
+
const estimatedSize = resolveItemSizeByIndex(0, defaultItemSize);
|
|
300
377
|
// 计算总 item 数量(跨所有 section)
|
|
301
378
|
const totalItemCount = React.useMemo(() => {
|
|
302
379
|
return sections.reduce((sum, section) => sum + section.items.length, 0);
|
|
@@ -310,17 +387,17 @@ const InnerList = (props, ref) => {
|
|
|
310
387
|
// 动态尺寸缓存更新版本:setItemSize 后递增,用于驱动 sectionOffsets/totalLength 与 item 定位重算
|
|
311
388
|
const [sizeCacheVersion, setSizeCacheVersion] = React.useState(0);
|
|
312
389
|
const sizeCacheRafRef = React.useRef(null);
|
|
313
|
-
//
|
|
314
|
-
|
|
390
|
+
// (measureScrollProtectRef 已移除:scrollTop + 内容变更同帧 setData 会与 scrollAnchoring
|
|
391
|
+
// 冲突导致归顶;改由 scrollAnchoring=true 独立处理内容重排,无需显式传 scrollTop 保护)
|
|
315
392
|
// 动态尺寸缓存
|
|
316
393
|
const sizeCache = useItemSizeCache({
|
|
317
394
|
isHorizontal,
|
|
318
|
-
|
|
395
|
+
estimatedSize,
|
|
319
396
|
itemCount: totalItemCount
|
|
320
397
|
});
|
|
321
398
|
// header 动态尺寸缓存(sectionIndex -> size)
|
|
322
399
|
const headerSizeCacheRef = React.useRef(new Map());
|
|
323
|
-
//
|
|
400
|
+
// 滚动修正的可见起始索引
|
|
324
401
|
const visibleStartIndexRef = React.useRef(0);
|
|
325
402
|
// ScrollTop 修正(仅 H5):动高时尺寸变化自动修正 scrollTop
|
|
326
403
|
const scrollCorrectionEnabled = !isWeapp && props.useResizeObserver === true;
|
|
@@ -334,23 +411,46 @@ const InnerList = (props, ref) => {
|
|
|
334
411
|
updateRenderOffset(newOffset, true, 'scrollCorrection'); // 程序性修正需同步到 ScrollView
|
|
335
412
|
}
|
|
336
413
|
});
|
|
337
|
-
|
|
414
|
+
const scrollCorrectionRef = React.useRef(scrollCorrection);
|
|
415
|
+
scrollCorrectionRef.current = scrollCorrection;
|
|
416
|
+
const onScrollRef = React.useRef(onScroll);
|
|
417
|
+
onScrollRef.current = onScroll;
|
|
418
|
+
const onScrollToUpperRef = React.useRef(onScrollToUpper);
|
|
419
|
+
onScrollToUpperRef.current = onScrollToUpper;
|
|
420
|
+
const onScrollToLowerRef = React.useRef(onScrollToLower);
|
|
421
|
+
onScrollToLowerRef.current = onScrollToLower;
|
|
422
|
+
const thresholdRef = React.useRef({ upper: upperThreshold, lower: lowerThreshold });
|
|
423
|
+
thresholdRef.current = { upper: upperThreshold, lower: lowerThreshold };
|
|
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 失效或传滞后位置造成归顶。
|
|
338
430
|
const scheduleWeappDynamicReflow = React.useCallback(() => {
|
|
339
431
|
if (!isWeapp || props.useResizeObserver !== true)
|
|
340
432
|
return;
|
|
433
|
+
// 滚动中:仅标记为待处理,等 onScrollEnd / 200ms 超时触发
|
|
434
|
+
if (isUserScrollingRef.current) {
|
|
435
|
+
pendingWeappReflowRef.current = true;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
341
438
|
if (sizeCacheRafRef.current != null)
|
|
342
439
|
return;
|
|
343
440
|
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
344
441
|
sizeCacheRafRef.current = null;
|
|
345
|
-
|
|
442
|
+
pendingWeappReflowRef.current = false;
|
|
346
443
|
setSizeCacheVersion((v) => v + 1);
|
|
347
444
|
});
|
|
348
445
|
}, [isWeapp, props.useResizeObserver]);
|
|
446
|
+
// 每次渲染时更新 ref,保证 setTimeout 闭包内拿到最新版本
|
|
447
|
+
scheduleWeappDynamicReflowRef.current = scheduleWeappDynamicReflow;
|
|
349
448
|
// ResizeObserver(当启用动态测量时)
|
|
350
449
|
const resizeObserver = useResizeObserver({
|
|
351
450
|
enabled: props.useResizeObserver === true,
|
|
352
451
|
isHorizontal,
|
|
353
452
|
listId,
|
|
453
|
+
listViewportRef: containerRef,
|
|
354
454
|
onResize: (index, size) => {
|
|
355
455
|
var _a;
|
|
356
456
|
const oldSize = sizeCache.getItemSize(index);
|
|
@@ -378,85 +478,63 @@ const InnerList = (props, ref) => {
|
|
|
378
478
|
}
|
|
379
479
|
}
|
|
380
480
|
});
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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();
|
|
393
494
|
}
|
|
394
|
-
else {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (typeof props.itemSize === 'number')
|
|
400
|
-
return props.itemSize;
|
|
401
|
-
if (typeof props.itemSize === 'function')
|
|
402
|
-
return props.itemSize(0, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
403
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
495
|
+
else if (sizeCacheRafRef.current == null) {
|
|
496
|
+
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
497
|
+
sizeCacheRafRef.current = null;
|
|
498
|
+
setSizeCacheVersion((v) => v + 1);
|
|
499
|
+
});
|
|
404
500
|
}
|
|
405
|
-
};
|
|
406
|
-
|
|
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 尺寸(支持动态测量)
|
|
407
509
|
const getHeaderSize = React.useCallback((sectionIndex) => {
|
|
408
|
-
// 如果启用动态测量,优先从缓存读取
|
|
409
510
|
if (props.useResizeObserver === true) {
|
|
410
511
|
const cached = headerSizeCacheRef.current.get(sectionIndex);
|
|
411
512
|
if (cached != null && cached > 0)
|
|
412
513
|
return cached;
|
|
413
514
|
}
|
|
414
|
-
// 否则返回默认尺寸
|
|
415
515
|
return getDefaultHeaderSize();
|
|
416
|
-
}, [props.useResizeObserver,
|
|
417
|
-
// 工具:获取 item 尺寸,支持函数/props/默认值/动态测量
|
|
516
|
+
}, [props.useResizeObserver, getDefaultHeaderSize]);
|
|
418
517
|
const getItemSize = React.useCallback((index) => {
|
|
419
|
-
// 优先级1:如果启用动态测量,从缓存读取
|
|
420
518
|
if (props.useResizeObserver === true) {
|
|
421
519
|
return sizeCache.getItemSize(index);
|
|
422
520
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (typeof props.itemWidth === 'number')
|
|
426
|
-
return props.itemWidth;
|
|
427
|
-
if (typeof props.itemSize === 'number')
|
|
428
|
-
return props.itemSize;
|
|
429
|
-
if (typeof props.itemSize === 'function')
|
|
430
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_WIDTH;
|
|
431
|
-
return DEFAULT_ITEM_WIDTH;
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
if (typeof props.itemHeight === 'number')
|
|
435
|
-
return props.itemHeight;
|
|
436
|
-
if (typeof props.itemSize === 'number')
|
|
437
|
-
return props.itemSize;
|
|
438
|
-
if (typeof props.itemSize === 'function')
|
|
439
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
440
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
441
|
-
}
|
|
442
|
-
}, [props.useResizeObserver, props.itemWidth, props.itemHeight, props.itemSize, props.itemData, isHorizontal, sizeCache]);
|
|
521
|
+
return resolveItemSizeByIndex(index, defaultItemSize);
|
|
522
|
+
}, [props.useResizeObserver, sizeCache, resolveItemSizeByIndex, defaultItemSize]);
|
|
443
523
|
// 分组累积高度/宽度,sizeCacheVersion 变化时重算
|
|
444
524
|
const sectionOffsets = React.useMemo(() => {
|
|
445
525
|
const offsets = [0];
|
|
446
|
-
let globalItemIndex = 0;
|
|
526
|
+
let globalItemIndex = 0;
|
|
447
527
|
sections.forEach((section, sectionIdx) => {
|
|
448
528
|
const headerSize = getHeaderSize(sectionIdx);
|
|
449
|
-
// 使用全局索引计算每个 item 的尺寸
|
|
450
529
|
const itemSizes = section.items.map((_, localIdx) => getItemSize(globalItemIndex + localIdx));
|
|
451
530
|
const groupSize = (section.header ? headerSize : 0) +
|
|
452
531
|
itemSizes.reduce((a, b) => a + b, 0) +
|
|
453
532
|
Math.max(0, section.items.length) * space;
|
|
454
533
|
offsets.push(offsets[offsets.length - 1] + groupSize);
|
|
455
|
-
// 累加当前 section 的 item 数量
|
|
456
534
|
globalItemIndex += section.items.length;
|
|
457
535
|
});
|
|
458
536
|
return offsets;
|
|
459
|
-
}, [sections, space,
|
|
537
|
+
}, [sections, space, getItemSize, getHeaderSize, sizeCacheVersion]);
|
|
460
538
|
// 外层虚拟滚动:可见分组
|
|
461
539
|
const [startSection, endSection] = React.useMemo(() => {
|
|
462
540
|
let start = 0;
|
|
@@ -505,11 +583,9 @@ const InnerList = (props, ref) => {
|
|
|
505
583
|
return [0, 0];
|
|
506
584
|
return [firstVisible, lastVisible];
|
|
507
585
|
}, [renderOffset, containerLength, sections, sectionOffsets, getHeaderSize, getItemSize, space]);
|
|
508
|
-
// 触发 onScrollIndex 回调(带防重复)
|
|
509
586
|
const lastVisibleRangeRef = React.useRef({ start: -1, end: -1 });
|
|
510
587
|
React.useEffect(() => {
|
|
511
588
|
if (props.onScrollIndex) {
|
|
512
|
-
// 避免重复触发
|
|
513
589
|
if (lastVisibleRangeRef.current.start !== visibleStartItem ||
|
|
514
590
|
lastVisibleRangeRef.current.end !== visibleEndItem) {
|
|
515
591
|
lastVisibleRangeRef.current = { start: visibleStartItem, end: visibleEndItem };
|
|
@@ -518,6 +594,7 @@ const InnerList = (props, ref) => {
|
|
|
518
594
|
}
|
|
519
595
|
}, [visibleStartItem, visibleEndItem, props.onScrollIndex]);
|
|
520
596
|
const handleScroll = React.useCallback((e) => {
|
|
597
|
+
var _a, _b, _c, _d;
|
|
521
598
|
let newOffset;
|
|
522
599
|
if (e.detail) {
|
|
523
600
|
newOffset = isHorizontal ? e.detail.scrollLeft : e.detail.scrollTop;
|
|
@@ -526,28 +603,51 @@ const InnerList = (props, ref) => {
|
|
|
526
603
|
newOffset = isHorizontal ? e.scrollLeft : e.scrollTop;
|
|
527
604
|
}
|
|
528
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;
|
|
529
618
|
const diff = effectiveOffset - lastScrollTopRef.current;
|
|
530
619
|
scrollDiffListRef.current.shift();
|
|
531
620
|
scrollDiffListRef.current.push(diff);
|
|
532
621
|
const shaking = isScrollingRef.current && isShaking(scrollDiffListRef.current);
|
|
533
|
-
if (shaking)
|
|
622
|
+
if (shaking) {
|
|
534
623
|
return;
|
|
624
|
+
}
|
|
535
625
|
if (programmaticCooldownRef.current) {
|
|
536
626
|
lastScrollTopRef.current = effectiveOffset;
|
|
537
627
|
setRenderOffset(effectiveOffset);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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;
|
|
542
637
|
return;
|
|
543
638
|
}
|
|
544
639
|
scrollCorrection.markUserScrolling();
|
|
545
640
|
updateRenderOffset(effectiveOffset, false, 'onScroll');
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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]);
|
|
551
651
|
// 小程序:onScrollEnd 优先结束 isUserScrolling,timeout 兜底
|
|
552
652
|
const handleNativeScrollEnd = React.useCallback(() => {
|
|
553
653
|
if (isWeapp) {
|
|
@@ -559,11 +659,23 @@ const InnerList = (props, ref) => {
|
|
|
559
659
|
isScrollingRef.current = false;
|
|
560
660
|
isUserScrollingRef.current = false;
|
|
561
661
|
setIsUserScrolling(false);
|
|
562
|
-
//
|
|
662
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
663
|
+
if (pendingWeappReflowRef.current) {
|
|
664
|
+
scheduleWeappDynamicReflowRef.current();
|
|
665
|
+
}
|
|
563
666
|
}
|
|
564
667
|
}
|
|
565
668
|
onScrollEnd === null || onScrollEnd === void 0 ? void 0 : onScrollEnd();
|
|
566
669
|
}, [isWeapp, onScrollEnd]);
|
|
670
|
+
// 暴露 scrollRef 给父组件,供内层 List/WaterFlow 传入 scrollElement
|
|
671
|
+
React.useLayoutEffect(() => {
|
|
672
|
+
if (!scrollRefProp)
|
|
673
|
+
return;
|
|
674
|
+
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
675
|
+
if (el) {
|
|
676
|
+
scrollRefProp.current = el;
|
|
677
|
+
}
|
|
678
|
+
}, [scrollRefProp, useScrollElementMode, effectiveScrollElement]);
|
|
567
679
|
// controlledScrollTop 变化时同步到 ScrollView
|
|
568
680
|
React.useEffect(() => {
|
|
569
681
|
if (typeof controlledScrollTop === 'number') {
|
|
@@ -640,20 +752,51 @@ const InnerList = (props, ref) => {
|
|
|
640
752
|
currentGlobalIndex += section.items.length;
|
|
641
753
|
}
|
|
642
754
|
}
|
|
643
|
-
|
|
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
|
+
});
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
788
|
+
}
|
|
644
789
|
}
|
|
645
|
-
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset]);
|
|
790
|
+
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId]);
|
|
646
791
|
// 容器样式;H5 刷新中禁止滚动
|
|
647
792
|
const containerStyle = Object.assign(Object.assign({ position: 'relative', boxSizing: 'border-box', height,
|
|
648
793
|
width }, style), (isH5 && refresherConfig && !supportsNativeRefresher && h5RefresherProps.isRefreshing
|
|
649
794
|
? { overflow: 'hidden' }
|
|
650
795
|
: {}));
|
|
651
796
|
// ScrollView 属性
|
|
652
|
-
const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign(
|
|
797
|
+
const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign({ scrollY: !scrollX && scrollY, scrollX, style: containerStyle, className, enhanced: true, showScrollbar,
|
|
653
798
|
upperThreshold,
|
|
654
|
-
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll,
|
|
655
|
-
onScrollToLower,
|
|
656
|
-
onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), { 'data-testid': 'taro-list-container' }), scrollViewRefresherProps), scrollViewRefresherHandlers);
|
|
799
|
+
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll, onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), scrollViewRefresherProps), scrollViewRefresherHandlers);
|
|
657
800
|
// H5 对齐小程序:refresherTriggered=true 时顶部立即显示加载指示器,滚到顶部并锁定滚动直至设为 false
|
|
658
801
|
React.useEffect(() => {
|
|
659
802
|
if (!isH5 || !refresherConfig || supportsNativeRefresher || !h5RefresherProps.isRefreshing)
|
|
@@ -667,7 +810,7 @@ const InnerList = (props, ref) => {
|
|
|
667
810
|
return;
|
|
668
811
|
let teardown = null;
|
|
669
812
|
const tryAttach = () => {
|
|
670
|
-
const el = containerRef.current;
|
|
813
|
+
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
671
814
|
if (el && !refresherTeardownRef.current) {
|
|
672
815
|
teardown = attach(el);
|
|
673
816
|
refresherTeardownRef.current = teardown;
|
|
@@ -682,32 +825,25 @@ const InnerList = (props, ref) => {
|
|
|
682
825
|
}
|
|
683
826
|
refresherTeardownRef.current = null;
|
|
684
827
|
};
|
|
685
|
-
}, []);
|
|
828
|
+
}, [useScrollElementMode, effectiveScrollElement]);
|
|
686
829
|
scrollViewOffsetRef.current = scrollViewOffset;
|
|
687
|
-
// scrollTop
|
|
830
|
+
// scrollTop 传递策略(对齐 virtual-list enhanced=true 的做法):
|
|
831
|
+
// - 程序性滚动帧(programmaticScrollRef=true):传 scrollViewOffset(目标位置)
|
|
832
|
+
// - 其余所有帧(用户滚动 / 测量重排 / 空闲):完全不传,依赖微信原生 + scrollAnchoring
|
|
833
|
+
// scrollAnchoring=true 负责在内容重排时自动维持视口位置,避免与显式 scrollTop
|
|
834
|
+
// 同帧冲突(冲突会导致 scrollAnchoring 失去锚点后归顶)
|
|
688
835
|
if (isWeapp) {
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
const eff = lastScrollTopRef.current;
|
|
692
|
-
setScrollViewOffset(eff);
|
|
693
|
-
if (isHorizontal) {
|
|
694
|
-
scrollViewProps.scrollLeft = eff;
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
scrollViewProps.scrollTop = eff;
|
|
698
|
-
}
|
|
699
|
-
programmaticScrollRef.current = false;
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
const sv = scrollViewOffset;
|
|
836
|
+
if (programmaticScrollRef.current) {
|
|
837
|
+
// 程序性滚动(imperative / scrollCorrection / scrollIntoView 等):传目标位置
|
|
703
838
|
if (isHorizontal) {
|
|
704
|
-
scrollViewProps.scrollLeft =
|
|
839
|
+
scrollViewProps.scrollLeft = scrollViewOffset;
|
|
705
840
|
}
|
|
706
841
|
else {
|
|
707
|
-
scrollViewProps.scrollTop =
|
|
842
|
+
scrollViewProps.scrollTop = scrollViewOffset;
|
|
708
843
|
}
|
|
709
844
|
programmaticScrollRef.current = false;
|
|
710
845
|
}
|
|
846
|
+
// else:用户滚动 / 空闲帧不传 scrollTop,让微信原生 + scrollAnchoring 完全接管
|
|
711
847
|
}
|
|
712
848
|
else {
|
|
713
849
|
// H5:非用户滑动时传
|
|
@@ -741,6 +877,19 @@ const InnerList = (props, ref) => {
|
|
|
741
877
|
const noMoreHeight = (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible) ? (noMoreConfig.height || 60) : 0;
|
|
742
878
|
const listContentLength = sectionOffsets[sectionOffsets.length - 1] + noMoreHeight;
|
|
743
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);
|
|
744
893
|
// 吸顶/吸左 header
|
|
745
894
|
const stickyHeaderNode = React.useMemo(() => {
|
|
746
895
|
if (!stickyHeader)
|
|
@@ -766,7 +915,7 @@ const InnerList = (props, ref) => {
|
|
|
766
915
|
}
|
|
767
916
|
}
|
|
768
917
|
return null;
|
|
769
|
-
}, [stickyHeader, renderOffset, sectionOffsets, sections
|
|
918
|
+
}, [stickyHeader, renderOffset, sectionOffsets, sections]);
|
|
770
919
|
// 渲染分组+item双层虚拟滚动
|
|
771
920
|
const renderSections = () => {
|
|
772
921
|
const nodes = [];
|
|
@@ -822,7 +971,7 @@ const InnerList = (props, ref) => {
|
|
|
822
971
|
Taro.nextTick(() => {
|
|
823
972
|
if (!headerRefsRef.current.has(sectionIndex))
|
|
824
973
|
return;
|
|
825
|
-
|
|
974
|
+
createSelectorQueryForRef({ current: el })
|
|
826
975
|
.select(`#${listId}-list-header-inner-${sectionIndex}`)
|
|
827
976
|
.boundingClientRect((rect) => {
|
|
828
977
|
if (rect) {
|
|
@@ -968,7 +1117,7 @@ const InnerList = (props, ref) => {
|
|
|
968
1117
|
Taro.nextTick(() => {
|
|
969
1118
|
if (!itemRefsRef.current.has(capturedIndex))
|
|
970
1119
|
return;
|
|
971
|
-
|
|
1120
|
+
createSelectorQueryForRef({ current: el })
|
|
972
1121
|
// 页面上可能同时存在多个 List,inner id 必须带 listId 前缀避免跨列表误命中
|
|
973
1122
|
.select(`#${listId}-list-item-inner-${capturedIndex}`)
|
|
974
1123
|
.boundingClientRect((rect) => {
|
|
@@ -1018,10 +1167,39 @@ const InnerList = (props, ref) => {
|
|
|
1018
1167
|
innerProps.id = `${listId}-list-item-inner-${currentGlobalIndex}`;
|
|
1019
1168
|
}
|
|
1020
1169
|
innerProps['data-index'] = String(currentGlobalIndex);
|
|
1021
|
-
|
|
1170
|
+
const itemNode = React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i]));
|
|
1171
|
+
// 任务 4.1:当 List 暴露 scrollRef 时,为每个 item 提供 Context,供内层 WaterFlow 使用
|
|
1172
|
+
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1173
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1174
|
+
key: outerItemProps.key,
|
|
1175
|
+
value: {
|
|
1176
|
+
scrollRef: scrollRefProp,
|
|
1177
|
+
containerHeight: containerLength,
|
|
1178
|
+
startOffset: offset + itemOffsets[i],
|
|
1179
|
+
reportNestedHeightChange: props.useResizeObserver
|
|
1180
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1181
|
+
: undefined,
|
|
1182
|
+
},
|
|
1183
|
+
}, itemNode)
|
|
1184
|
+
: itemNode;
|
|
1185
|
+
nodes.push(itemWithContext);
|
|
1022
1186
|
}
|
|
1023
1187
|
else {
|
|
1024
|
-
|
|
1188
|
+
const itemNode = React.createElement(View, outerItemProps, section.items[i]);
|
|
1189
|
+
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1190
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1191
|
+
key: outerItemProps.key,
|
|
1192
|
+
value: {
|
|
1193
|
+
scrollRef: scrollRefProp,
|
|
1194
|
+
containerHeight: containerLength,
|
|
1195
|
+
startOffset: offset + itemOffsets[i],
|
|
1196
|
+
reportNestedHeightChange: props.useResizeObserver
|
|
1197
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1198
|
+
: undefined,
|
|
1199
|
+
},
|
|
1200
|
+
}, itemNode)
|
|
1201
|
+
: itemNode;
|
|
1202
|
+
nodes.push(itemWithContext);
|
|
1025
1203
|
}
|
|
1026
1204
|
}
|
|
1027
1205
|
globalItemIndex += section.items.length;
|
|
@@ -1038,16 +1216,17 @@ const InnerList = (props, ref) => {
|
|
|
1038
1216
|
const defaultStyle = Object.assign(Object.assign(Object.assign({ position: 'absolute' }, (isHorizontal
|
|
1039
1217
|
? { left: listContentEnd, top: 0, width: noMoreHeightValue, height: '100%' }
|
|
1040
1218
|
: { top: listContentEnd, left: 0, width: '100%', height: noMoreHeightValue })), { display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: '#999', fontSize: '14px', boxSizing: 'border-box' }), noMoreConfig.style);
|
|
1041
|
-
return (jsx(View, { style: defaultStyle,
|
|
1219
|
+
return (jsx(View, { style: defaultStyle, children: noMoreConfig.children || noMoreConfig.text || '没有更多了' }));
|
|
1042
1220
|
};
|
|
1043
1221
|
// 空列表场景:仅显示 NoMore
|
|
1044
1222
|
if (sections.length === 0 && (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible)) {
|
|
1045
1223
|
return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsx(View, { style: Object.assign({ minHeight: containerLength, display: 'flex', alignItems: 'center', justifyContent: 'center' }, containerStyle), children: renderNoMoreContent() }) })));
|
|
1046
1224
|
}
|
|
1047
1225
|
// 可滚区域总尺寸
|
|
1226
|
+
// H5 refresher 用负 translateY 隐藏,有上方混排时可能延伸到 sibling 区域;overflow:hidden 裁剪避免重叠
|
|
1227
|
+
const needsRefresherClip = refresherHeightForH5 > 0;
|
|
1048
1228
|
const contentWrapperStyle = isHorizontal
|
|
1049
|
-
? { width: totalLength, position: 'relative', height: '100%' }
|
|
1050
|
-
: { height: totalLength, position: 'relative', width: '100%' };
|
|
1229
|
+
? Object.assign({ width: totalLength, position: 'relative', height: '100%' }, (needsRefresherClip && { overflow: 'hidden' })) : Object.assign({ height: totalLength, position: 'relative', width: '100%' }, (needsRefresherClip && { overflow: 'hidden' }));
|
|
1051
1230
|
const listWrapperStyle = isHorizontal
|
|
1052
1231
|
? { width: listContentLength, position: 'relative', height: '100%' }
|
|
1053
1232
|
: { height: listContentLength, position: 'relative', width: '100%' };
|
|
@@ -1068,9 +1247,23 @@ const InnerList = (props, ref) => {
|
|
|
1068
1247
|
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1069
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()] })) }));
|
|
1070
1249
|
};
|
|
1250
|
+
// useScrollElementMode 或 needAutoFind&&pending:渲染 View 以便监听外部滚动或 probe 阶段查找滚动父节点
|
|
1251
|
+
const renderView = useScrollElementMode || (!!needAutoFind && autoFindStatus === 'pending');
|
|
1252
|
+
if (renderView) {
|
|
1253
|
+
// 任务 2.4:恢复 refresher DOM 结构(refresher 层 + listWrapperStyle)
|
|
1254
|
+
return (jsx(View, { ref: contentWrapperRef, id: contentId, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1255
|
+
position: 'absolute',
|
|
1256
|
+
top: 0,
|
|
1257
|
+
left: 0,
|
|
1258
|
+
right: 0,
|
|
1259
|
+
height: refresherHeightForH5,
|
|
1260
|
+
zIndex: 0,
|
|
1261
|
+
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
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()] })) }));
|
|
1263
|
+
}
|
|
1071
1264
|
return (jsxs(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { id: listId, children: [supportsNativeRefresher && renderRefresherContent(), renderContentArea()] })));
|
|
1072
1265
|
};
|
|
1073
1266
|
const List = React.forwardRef(InnerList);
|
|
1074
1267
|
|
|
1075
|
-
export { List, ListItem, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1268
|
+
export { List, ListItem, ScrollElementContextOrFallback as ListScrollElementContext, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1076
1269
|
//# sourceMappingURL=index.js.map
|