@tarojs/components-advanced 4.1.12-beta.5 → 4.1.12-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/components/index.js +1 -0
  2. package/dist/components/index.js.map +1 -1
  3. package/dist/components/list/hooks/useItemSizeCache.d.ts +1 -1
  4. package/dist/components/list/hooks/useItemSizeCache.js +5 -5
  5. package/dist/components/list/hooks/useItemSizeCache.js.map +1 -1
  6. package/dist/components/list/hooks/useListNestedScroll.d.ts +18 -0
  7. package/dist/components/list/hooks/useListNestedScroll.js +61 -0
  8. package/dist/components/list/hooks/useListNestedScroll.js.map +1 -0
  9. package/dist/components/list/hooks/useListScrollElementAttach.d.ts +25 -0
  10. package/dist/components/list/hooks/useListScrollElementAttach.js +88 -0
  11. package/dist/components/list/hooks/useListScrollElementAttach.js.map +1 -0
  12. package/dist/components/list/hooks/useListScrollElementAttachWeapp.d.ts +15 -0
  13. package/dist/components/list/hooks/useListScrollElementAttachWeapp.js +135 -0
  14. package/dist/components/list/hooks/useListScrollElementAttachWeapp.js.map +1 -0
  15. package/dist/components/list/hooks/useMeasureStartOffset.d.ts +12 -0
  16. package/dist/components/list/hooks/useMeasureStartOffset.js +84 -0
  17. package/dist/components/list/hooks/useMeasureStartOffset.js.map +1 -0
  18. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.d.ts +14 -0
  19. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js +87 -0
  20. package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js.map +1 -0
  21. package/dist/components/list/hooks/useRefresher.d.ts +10 -2
  22. package/dist/components/list/hooks/useRefresher.js +150 -69
  23. package/dist/components/list/hooks/useRefresher.js.map +1 -1
  24. package/dist/components/list/hooks/useResizeObserver.d.ts +4 -1
  25. package/dist/components/list/hooks/useResizeObserver.js +8 -7
  26. package/dist/components/list/hooks/useResizeObserver.js.map +1 -1
  27. package/dist/components/list/hooks/useScrollCorrection.js +2 -1
  28. package/dist/components/list/hooks/useScrollCorrection.js.map +1 -1
  29. package/dist/components/list/hooks/useScrollParentAutoFind.d.ts +20 -0
  30. package/dist/components/list/hooks/useScrollParentAutoFind.js +81 -0
  31. package/dist/components/list/hooks/useScrollParentAutoFind.js.map +1 -0
  32. package/dist/components/list/index.d.ts +14 -5
  33. package/dist/components/list/index.js +340 -147
  34. package/dist/components/list/index.js.map +1 -1
  35. package/dist/components/list/utils.d.ts +12 -0
  36. package/dist/components/list/utils.js +23 -1
  37. package/dist/components/list/utils.js.map +1 -1
  38. package/dist/components/virtual-list/vue/list.d.ts +10 -10
  39. package/dist/components/virtual-waterfall/vue/waterfall.d.ts +10 -10
  40. package/dist/components/water-flow/flow-item.js +18 -11
  41. package/dist/components/water-flow/flow-item.js.map +1 -1
  42. package/dist/components/water-flow/flow-section.js +1 -1
  43. package/dist/components/water-flow/flow-section.js.map +1 -1
  44. package/dist/components/water-flow/index.d.ts +1 -1
  45. package/dist/components/water-flow/interface.d.ts +32 -2
  46. package/dist/components/water-flow/node.d.ts +3 -0
  47. package/dist/components/water-flow/node.js +34 -1
  48. package/dist/components/water-flow/node.js.map +1 -1
  49. package/dist/components/water-flow/root.d.ts +39 -4
  50. package/dist/components/water-flow/root.js +144 -44
  51. package/dist/components/water-flow/root.js.map +1 -1
  52. package/dist/components/water-flow/section.d.ts +11 -1
  53. package/dist/components/water-flow/section.js +81 -19
  54. package/dist/components/water-flow/section.js.map +1 -1
  55. package/dist/components/water-flow/utils.d.ts +4 -0
  56. package/dist/components/water-flow/utils.js +5 -1
  57. package/dist/components/water-flow/utils.js.map +1 -1
  58. package/dist/components/water-flow/water-flow-node-cache.d.ts +24 -0
  59. package/dist/components/water-flow/water-flow-node-cache.js +161 -0
  60. package/dist/components/water-flow/water-flow-node-cache.js.map +1 -0
  61. package/dist/components/water-flow/water-flow.d.ts +2 -3
  62. package/dist/components/water-flow/water-flow.js +316 -36
  63. package/dist/components/water-flow/water-flow.js.map +1 -1
  64. package/dist/index.js +3 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/utils/dom.d.ts +2 -2
  67. package/dist/utils/dom.js +7 -6
  68. package/dist/utils/dom.js.map +1 -1
  69. package/dist/utils/index.d.ts +2 -0
  70. package/dist/utils/index.js +2 -0
  71. package/dist/utils/index.js.map +1 -1
  72. package/dist/utils/scrollElementContext.d.ts +15 -0
  73. package/dist/utils/scrollElementContext.js +14 -0
  74. package/dist/utils/scrollElementContext.js.map +1 -0
  75. package/dist/utils/scrollParent.d.ts +33 -0
  76. package/dist/utils/scrollParent.js +88 -0
  77. package/dist/utils/scrollParent.js.map +1 -0
  78. package/dist/utils/weapp-scope.d.ts +7 -0
  79. package/dist/utils/weapp-scope.js +20 -0
  80. package/dist/utils/weapp-scope.js.map +1 -0
  81. 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 { useRefresher, DEFAULT_REFRESHER_HEIGHT } from './hooks/useRefresher.js';
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 opts = options !== null && options !== void 0 ? options : {};
150
- const isHorizontal = scrollX === true;
151
- const top = typeof opts.top === 'number' ? opts.top : undefined;
152
- const left = typeof opts.left === 'number' ? opts.left : undefined;
153
- let targetOffset = 0;
154
- if (isHorizontal) {
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 if (typeof top === 'number') {
159
- targetOffset = top;
225
+ else {
226
+ el.scrollTo({ top: scrollTarget });
160
227
  }
228
+ updateRenderOffset(targetOffset, false, 'scrollElement');
161
229
  }
162
- else {
163
- if (typeof top === 'number') {
164
- targetOffset = top;
165
- }
166
- else if (typeof left === 'number') {
167
- targetOffset = left;
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
- if (!Number.isFinite(targetOffset)) {
171
- targetOffset = 0;
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 下拉刷新顶栏高度(默认 50px,来自 useRefresher)
266
- const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false) ? DEFAULT_REFRESHER_HEIGHT : 0;
267
- // 解析分组结构,只支持 StickySection 和 ListItem 作为直接子组件
268
- // 过滤掉 Refresher 和 NoMore 组件
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 defaultEstimatedSize = isHorizontal ? DEFAULT_ITEM_WIDTH : DEFAULT_ITEM_HEIGHT;
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
- // 小程序端:测量触发 re-render 时,reflow 帧传 eff 并同步 scrollViewOffset,避免下一帧传 0 导致跳变
314
- const measureScrollProtectRef = React.useRef(false);
390
+ // (measureScrollProtectRef 已移除:scrollTop + 内容变更同帧 setData 会与 scrollAnchoring
391
+ // 冲突导致归顶;改由 scrollAnchoring=true 独立处理内容重排,无需显式传 scrollTop 保护)
315
392
  // 动态尺寸缓存
316
393
  const sizeCache = useItemSizeCache({
317
394
  isHorizontal,
318
- estimatedItemSize: estimatedSize,
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
- // 小程序 + 动高(virtual-list 风格):测量变化后同帧重排,不做程序性 scrollTop 回拉
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
- measureScrollProtectRef.current = true;
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
- // 工具:获取 header 默认/估算尺寸
382
- const getDefaultHeaderSize = () => {
383
- if (isHorizontal) {
384
- if (typeof props.headerWidth === 'number')
385
- return props.headerWidth;
386
- if (typeof props.itemWidth === 'number')
387
- return props.itemWidth;
388
- if (typeof props.itemSize === 'number')
389
- return props.itemSize;
390
- if (typeof props.itemSize === 'function')
391
- return props.itemSize(0, props.itemData) || DEFAULT_ITEM_WIDTH;
392
- return DEFAULT_ITEM_WIDTH;
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
- if (typeof props.headerHeight === 'number')
396
- return props.headerHeight;
397
- if (typeof props.itemHeight === 'number')
398
- return props.itemHeight;
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
- // 工具:获取 header 尺寸(支持动态测量)
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, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData, isHorizontal]);
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
- // 优先级2:固定尺寸或函数计算
424
- if (isHorizontal) {
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, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData, getItemSize, getHeaderSize, sizeCacheVersion]);
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
- onScroll === null || onScroll === void 0 ? void 0 : onScroll({
539
- scrollTop: isHorizontal ? 0 : newOffset,
540
- scrollLeft: isHorizontal ? newOffset : 0
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
- onScroll === null || onScroll === void 0 ? void 0 : onScroll({
547
- scrollTop: isHorizontal ? 0 : newOffset,
548
- scrollLeft: isHorizontal ? newOffset : 0
549
- });
550
- }, [isHorizontal, onScroll, updateRenderOffset, scrollCorrection, props.useResizeObserver]);
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
- // recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset
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
- updateRenderOffset(targetOffset, true, 'scrollIntoView');
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(Object.assign({ scrollY: !scrollX && scrollY, scrollX, style: containerStyle, className, enhanced: true, showScrollbar,
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, onScrollToUpper,
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:weapp scrollViewOffset(reflow 帧传 eff 防跳变);H5 仅非用户滑动时传
830
+ // scrollTop 传递策略(对齐 virtual-list enhanced=true 的做法):
831
+ // - 程序性滚动帧(programmaticScrollRef=true):传 scrollViewOffset(目标位置)
832
+ // - 其余所有帧(用户滚动 / 测量重排 / 空闲):完全不传,依赖微信原生 + scrollAnchoring
833
+ // scrollAnchoring=true 负责在内容重排时自动维持视口位置,避免与显式 scrollTop
834
+ // 同帧冲突(冲突会导致 scrollAnchoring 失去锚点后归顶)
688
835
  if (isWeapp) {
689
- if (measureScrollProtectRef.current) {
690
- measureScrollProtectRef.current = false;
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 = sv;
839
+ scrollViewProps.scrollLeft = scrollViewOffset;
705
840
  }
706
841
  else {
707
- scrollViewProps.scrollTop = sv;
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, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData]);
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
- Taro.createSelectorQuery()
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
- Taro.createSelectorQuery()
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
- nodes.push(React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i])));
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
- nodes.push(React.createElement(View, outerItemProps, section.items[i]));
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, "data-testid": "list-nomore", children: noMoreConfig.children || noMoreConfig.text || '没有更多了' }));
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