@tarojs/components-advanced 4.1.12-beta.7 → 4.1.12-beta.70

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