@onekeyfe/react-native-native-list 6.7.0-alpha.378 → 6.7.0-alpha.383

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 (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
@@ -32,7 +32,11 @@ import type {
32
32
  EndReachedEvent,
33
33
  VisibleRangeChangedEvent,
34
34
  } from './models';
35
- import type { NativeListProps, NativeListRef } from './NativeList.types';
35
+ import type {
36
+ NativeListProps,
37
+ NativeListRef,
38
+ StructuralTransitionSettledEvent,
39
+ } from './NativeList.types';
36
40
  import { isSelectableRow } from './selection';
37
41
  import {
38
42
  normalizeIndexScroll,
@@ -44,15 +48,19 @@ import {
44
48
  type NormalizedPositionScroll,
45
49
  } from './scrolling';
46
50
  import {
47
- applyRowPatches,
51
+ applyOwnedNativePatches,
52
+ captureNativeListInput,
48
53
  containNativeListInput,
49
54
  emptyNativeListSnapshot,
50
- serializePatches,
51
- validatePatches,
55
+ ownValidatedNativeSnapshot,
52
56
  validateSnapshot,
53
57
  } from './validation';
54
58
 
55
- export type { NativeListProps, NativeListRef } from './NativeList.types';
59
+ export type {
60
+ NativeListProps,
61
+ NativeListRef,
62
+ StructuralTransitionSettledEvent,
63
+ } from './NativeList.types';
56
64
  export type {
57
65
  ActionAnchorState,
58
66
  NativeListKeyboardDismissMode,
@@ -74,6 +82,8 @@ const NativeListHost = getHostComponent<
74
82
  NativeListNativeProps,
75
83
  NativeListMethods
76
84
  >('NativeList', () => NativeListConfig);
85
+ const clearedStructuralTransitionSettled = { f: undefined } as const;
86
+ const disclosureSlotMeasurementStyle = { alignSelf: 'stretch' } as const;
77
87
 
78
88
  export function preloadNativeListAvatarImages(
79
89
  sources: readonly ImageSource[]
@@ -107,6 +117,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
107
117
  listHeader,
108
118
  listFooter,
109
119
  listEmpty,
120
+ listSidecar,
121
+ listExpandedContent,
122
+ listStructureToggle,
123
+ sidecarLayout,
110
124
  topOverlay,
111
125
  topScrollEdgeEffect,
112
126
  scrollPositionThresholds,
@@ -122,6 +136,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
122
136
  onReorder,
123
137
  onEndReached,
124
138
  onVisibleRangeChanged,
139
+ onStructuralTransitionSettled,
125
140
  onScrollPositionThresholdChange,
126
141
  onRefresh,
127
142
  onScrollToIndexFailed,
@@ -134,27 +149,171 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
134
149
  forwardedRef
135
150
  ) {
136
151
  const snapshotRef = useRef<NativeListSnapshot>(emptyNativeListSnapshot());
152
+ const renderedSnapshotRef = useRef<NativeListSnapshot | undefined>(
153
+ undefined
154
+ );
137
155
  const initializedSnapshot = useRef(false);
138
- const hasContainerSlots = Boolean(listHeader || listEmpty || listFooter);
139
- const prepareSnapshot = useCallback(
140
- (next: NativeListSnapshot) => {
141
- const accepted = validateSnapshot(next);
156
+ const pendingDisclosureSnapshotRef = useRef<
157
+ | {
158
+ source: 'prop' | 'imperative';
159
+ input: NativeListSnapshot;
160
+ owned: NativeListSnapshot;
161
+ expandedEpoch: number;
162
+ toggleEpoch: number;
163
+ }
164
+ | undefined
165
+ >(undefined);
166
+ const previousPropInputRef = useRef(inputSnapshot);
167
+ const [pendingDisclosureRevision, setPendingDisclosureRevision] =
168
+ useState(0);
169
+ const disclosureSlotMounts = useRef({
170
+ expandedPresent: false,
171
+ togglePresent: false,
172
+ expandedEpoch: 0,
173
+ toggleEpoch: 0,
174
+ });
175
+ const expandedPresent = listExpandedContent != null;
176
+ const togglePresent = listStructureToggle != null;
177
+ if (disclosureSlotMounts.current.expandedPresent !== expandedPresent) {
178
+ disclosureSlotMounts.current.expandedPresent = expandedPresent;
179
+ disclosureSlotMounts.current.expandedEpoch += 1;
180
+ }
181
+ if (disclosureSlotMounts.current.togglePresent !== togglePresent) {
182
+ disclosureSlotMounts.current.togglePresent = togglePresent;
183
+ disclosureSlotMounts.current.toggleEpoch += 1;
184
+ }
185
+ const expandedEpoch = disclosureSlotMounts.current.expandedEpoch;
186
+ const toggleEpoch = disclosureSlotMounts.current.toggleEpoch;
187
+ const [slotMeasurements, setSlotMeasurements] = useState({
188
+ heights: [0, 0, 0, 0, 0, 0],
189
+ readyEpochs: [-1, -1, -1, -1, -1, -1],
190
+ });
191
+ const hasSidecar = listSidecar !== undefined;
192
+ const sidecarWidth = sidecarLayout?.width;
193
+ const sidecarGap = sidecarLayout?.gap;
194
+ const sidecarMaxContentWidth = sidecarLayout?.maxContentWidth;
195
+ const hasContainerSlots = Boolean(
196
+ listHeader ||
197
+ listEmpty ||
198
+ listFooter ||
199
+ listSidecar ||
200
+ listExpandedContent ||
201
+ listStructureToggle
202
+ );
203
+ const validateContainerConfiguration = useCallback(
204
+ (accepted: NativeListSnapshot) => {
142
205
  if (hasContainerSlots && accepted.layout.orientation === 'horizontal')
143
206
  throw new Error(
144
207
  'NativeList container slots require vertical orientation'
145
208
  );
146
- return accepted;
209
+ if (hasSidecar !== (sidecarWidth !== undefined))
210
+ throw new Error(
211
+ 'listSidecar and sidecarLayout must be supplied together'
212
+ );
213
+ if (sidecarWidth !== undefined) {
214
+ const width = sidecarWidth;
215
+ const gap = sidecarGap ?? NaN;
216
+ const maxContentWidth = sidecarMaxContentWidth ?? NaN;
217
+ if (
218
+ accepted.layout.kind !== 'linear' ||
219
+ !Number.isFinite(width) ||
220
+ width <= 0 ||
221
+ !Number.isFinite(gap) ||
222
+ gap < 0 ||
223
+ !Number.isFinite(maxContentWidth) ||
224
+ maxContentWidth <= width + gap
225
+ )
226
+ throw new Error('Invalid NativeList sidecar layout');
227
+ }
147
228
  },
148
- [hasContainerSlots]
229
+ [
230
+ hasContainerSlots,
231
+ hasSidecar,
232
+ sidecarWidth,
233
+ sidecarGap,
234
+ sidecarMaxContentWidth,
235
+ ]
149
236
  );
150
- const snapshot = useMemo(
151
- () =>
152
- containNativeListInput(() => prepareSnapshot(inputSnapshot)) ??
153
- snapshotRef.current,
154
- [inputSnapshot, prepareSnapshot]
237
+ const prepareSnapshot = useCallback(
238
+ (next: NativeListSnapshot) => {
239
+ const accepted = validateSnapshot(captureNativeListInput(next));
240
+ validateContainerConfiguration(accepted);
241
+ return ownValidatedNativeSnapshot(accepted);
242
+ },
243
+ [validateContainerConfiguration]
155
244
  );
245
+ const expandedReadyEpoch = slotMeasurements.readyEpochs[4];
246
+ const toggleReadyEpoch = slotMeasurements.readyEpochs[5];
247
+ const snapshot = useMemo(() => {
248
+ // Imperative pending snapshots use this revision to retry the readiness gate.
249
+ // eslint-disable-next-line no-void -- Reading this revision invalidates the readiness memo.
250
+ void pendingDisclosureRevision;
251
+ const propReplaced = previousPropInputRef.current !== inputSnapshot;
252
+ previousPropInputRef.current = inputSnapshot;
253
+ const pending = pendingDisclosureSnapshotRef.current;
254
+ const retained =
255
+ pending &&
256
+ !propReplaced &&
257
+ (pending.source === 'imperative' || pending.input === inputSnapshot) &&
258
+ pending.expandedEpoch === expandedEpoch &&
259
+ pending.toggleEpoch === toggleEpoch
260
+ ? pending
261
+ : undefined;
262
+ const candidate =
263
+ retained?.owned ??
264
+ (!propReplaced && initializedSnapshot.current
265
+ ? renderedSnapshotRef.current ?? snapshotRef.current
266
+ : containNativeListInput(() => prepareSnapshot(inputSnapshot)) ??
267
+ snapshotRef.current);
268
+ const acceptedCandidate =
269
+ !propReplaced && initializedSnapshot.current
270
+ ? containNativeListInput(() => {
271
+ validateContainerConfiguration(candidate);
272
+ return candidate;
273
+ })
274
+ : candidate;
275
+ if (!acceptedCandidate) {
276
+ pendingDisclosureSnapshotRef.current = undefined;
277
+ return renderedSnapshotRef.current ?? snapshotRef.current;
278
+ }
279
+ const transitionId = acceptedCandidate.structuralTransition?.id;
280
+ const newDisclosureCommand =
281
+ initializedSnapshot.current &&
282
+ acceptedCandidate.disclosure !== undefined &&
283
+ transitionId !== undefined &&
284
+ transitionId !== snapshotRef.current.structuralTransition?.id;
285
+ if (
286
+ newDisclosureCommand &&
287
+ ((expandedPresent && expandedReadyEpoch !== expandedEpoch) ||
288
+ (togglePresent && toggleReadyEpoch !== toggleEpoch))
289
+ ) {
290
+ pendingDisclosureSnapshotRef.current = retained ?? {
291
+ source: 'prop',
292
+ input: inputSnapshot,
293
+ owned: acceptedCandidate,
294
+ expandedEpoch,
295
+ toggleEpoch,
296
+ };
297
+ return snapshotRef.current;
298
+ }
299
+ pendingDisclosureSnapshotRef.current = undefined;
300
+ return acceptedCandidate;
301
+ }, [
302
+ inputSnapshot,
303
+ prepareSnapshot,
304
+ validateContainerConfiguration,
305
+ expandedPresent,
306
+ togglePresent,
307
+ expandedEpoch,
308
+ toggleEpoch,
309
+ expandedReadyEpoch,
310
+ toggleReadyEpoch,
311
+ pendingDisclosureRevision,
312
+ ]);
313
+ renderedSnapshotRef.current = snapshot;
156
314
  const fontFamiliesJson = useNativeListFontFamiliesJson();
157
- const [slotHeights, setSlotHeights] = useState([0, 0, 0]);
315
+ const slotHeights = slotMeasurements.heights;
316
+ const [viewportWidth, setViewportWidth] = useState(0);
158
317
  const [topOverlayHeight, setTopOverlayHeight] = useState(0);
159
318
  const hasTopOverlay =
160
319
  Platform?.OS === 'ios' &&
@@ -162,11 +321,51 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
162
321
  (topScrollEdgeEffect === 'hard' || topScrollEdgeEffect === 'soft') &&
163
322
  topOverlay != null;
164
323
  const containerSlotHeightsJson = JSON.stringify(slotHeights);
165
- const slotInset =
324
+ const defaultSlotInset =
166
325
  snapshot.layout.contentPaddingHorizontal ??
167
326
  snapshot.layout.contentPadding ??
168
327
  0;
169
- const slotContents = [listHeader, listEmpty, listFooter];
328
+ const bandWidth = sidecarLayout
329
+ ? Math.min(viewportWidth, sidecarLayout.maxContentWidth)
330
+ : 0;
331
+ const slotInset = sidecarLayout
332
+ ? Math.max(0, (viewportWidth - bandWidth) / 2) + defaultSlotInset
333
+ : defaultSlotInset;
334
+ const sidecarSpace = sidecarLayout
335
+ ? sidecarLayout.width + sidecarLayout.gap
336
+ : 0;
337
+ const slotContents = [
338
+ listHeader,
339
+ listEmpty,
340
+ listFooter,
341
+ listSidecar,
342
+ listExpandedContent,
343
+ listStructureToggle,
344
+ ];
345
+ const recordSlotLayout = (index: number, height: number, epoch: number) => {
346
+ if (
347
+ (index === 4 && epoch !== disclosureSlotMounts.current.expandedEpoch) ||
348
+ (index === 5 && epoch !== disclosureSlotMounts.current.toggleEpoch)
349
+ ) {
350
+ return;
351
+ }
352
+ setSlotMeasurements((previous) => {
353
+ if (
354
+ previous.heights[index] === height &&
355
+ previous.readyEpochs[index] === epoch
356
+ ) {
357
+ return previous;
358
+ }
359
+ const heights = [...previous.heights];
360
+ const readyEpochs = [...previous.readyEpochs];
361
+ heights[index] = height;
362
+ readyEpochs[index] = epoch;
363
+ return { heights, readyEpochs };
364
+ });
365
+ };
366
+ const sidecarLayoutJson = sidecarLayout
367
+ ? JSON.stringify(sidecarLayout)
368
+ : '';
170
369
  const normalizedKeyboardShouldPersistTaps =
171
370
  keyboardShouldPersistTaps === true
172
371
  ? 'always'
@@ -181,6 +380,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
181
380
  onReorder,
182
381
  onEndReached,
183
382
  onVisibleRangeChanged,
383
+ onStructuralTransitionSettled,
184
384
  onScrollPositionThresholdChange,
185
385
  onRefresh,
186
386
  onScrollToIndexFailed,
@@ -192,6 +392,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
192
392
  onReorder,
193
393
  onEndReached,
194
394
  onVisibleRangeChanged,
395
+ onStructuralTransitionSettled,
195
396
  onScrollPositionThresholdChange,
196
397
  onRefresh,
197
398
  onScrollToIndexFailed,
@@ -367,6 +568,26 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
367
568
  prepareSnapshot(nextSnapshot)
368
569
  );
369
570
  if (!accepted) return;
571
+ const transitionId = accepted.structuralTransition?.id;
572
+ const waitsForSlots =
573
+ accepted.disclosure !== undefined &&
574
+ transitionId !== undefined &&
575
+ transitionId !== snapshotRef.current.structuralTransition?.id &&
576
+ ((expandedPresent &&
577
+ slotMeasurements.readyEpochs[4] !== expandedEpoch) ||
578
+ (togglePresent && slotMeasurements.readyEpochs[5] !== toggleEpoch));
579
+ if (waitsForSlots) {
580
+ pendingDisclosureSnapshotRef.current = {
581
+ source: 'imperative',
582
+ input: nextSnapshot,
583
+ owned: accepted,
584
+ expandedEpoch,
585
+ toggleEpoch,
586
+ };
587
+ setPendingDisclosureRevision((value) => value + 1);
588
+ return;
589
+ }
590
+ pendingDisclosureSnapshotRef.current = undefined;
370
591
  const nextSnapshotJson = JSON.stringify(accepted);
371
592
  nativeRef.current?.applySnapshot(nextSnapshotJson);
372
593
  snapshotRef.current = accepted;
@@ -376,16 +597,22 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
376
597
  }
377
598
  },
378
599
  applyPatches(patches) {
379
- const accepted = containNativeListInput(() => {
380
- const normalized = validatePatches(patches);
381
- return {
382
- patches: normalized,
383
- snapshot: applyRowPatches(snapshotRef.current, normalized),
384
- json: serializePatches(normalized),
385
- };
386
- });
600
+ const pending = pendingDisclosureSnapshotRef.current;
601
+ const accepted = containNativeListInput(() =>
602
+ applyOwnedNativePatches(
603
+ pending?.owned ?? snapshotRef.current,
604
+ patches
605
+ )
606
+ );
387
607
  if (!accepted || accepted.patches.length === 0) return;
388
608
  const nextSnapshot = accepted.snapshot;
609
+ if (pending) {
610
+ pendingDisclosureSnapshotRef.current = {
611
+ ...pending,
612
+ owned: nextSnapshot,
613
+ };
614
+ return;
615
+ }
389
616
  const native = nativeRef.current;
390
617
  const dispatch = native
391
618
  ? () => {
@@ -401,19 +628,25 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
401
628
  updateAvatarPrefetchRef.current();
402
629
  },
403
630
  reconcileSelection(selectedKeys) {
404
- nativeRef.current?.reconcileSelection(JSON.stringify(selectedKeys));
405
- const current = snapshotRef.current;
631
+ const capturedKeys = captureNativeListInput(selectedKeys);
632
+ const pending = pendingDisclosureSnapshotRef.current;
633
+ if (!pending)
634
+ nativeRef.current?.reconcileSelection(JSON.stringify(capturedKeys));
635
+ const current = pending?.owned ?? snapshotRef.current;
406
636
  if (
407
637
  current.selection &&
408
- (current.selection.mode !== 'single' || selectedKeys.length <= 1) &&
409
- selectedKeys.every((key) =>
638
+ (current.selection.mode !== 'single' || capturedKeys.length <= 1) &&
639
+ capturedKeys.every((key) =>
410
640
  current.rows.some((row) => row.key === key && isSelectableRow(row))
411
641
  )
412
642
  ) {
413
- snapshotRef.current = {
643
+ const next = {
414
644
  ...current,
415
- selection: { ...current.selection, selectedKeys },
645
+ selection: { ...current.selection, selectedKeys: capturedKeys },
416
646
  };
647
+ if (pending)
648
+ pendingDisclosureSnapshotRef.current = { ...pending, owned: next };
649
+ else snapshotRef.current = next;
417
650
  }
418
651
  },
419
652
  scrollToKey(paramsOrKey, animated, alignment) {
@@ -546,6 +779,11 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
546
779
  updateAvatarPrefetchRef.current();
547
780
  callbacksRef.current.onVisibleRangeChanged?.(payload);
548
781
  }),
782
+ onStructuralTransitionSettled: callback((payloadJson: string) => {
783
+ callbacksRef.current.onStructuralTransitionSettled?.(
784
+ parsePayload<StructuralTransitionSettledEvent>(payloadJson)
785
+ );
786
+ }),
549
787
  }),
550
788
  []
551
789
  );
@@ -553,10 +791,20 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
553
791
  return (
554
792
  <NativeListHost
555
793
  {...viewProps}
794
+ onLayout={(event) => {
795
+ setViewportWidth(event.nativeEvent.layout.width);
796
+ viewProps.onLayout?.(event);
797
+ }}
556
798
  {...nativeCallbacks}
799
+ onStructuralTransitionSettled={
800
+ onStructuralTransitionSettled
801
+ ? nativeCallbacks.onStructuralTransitionSettled
802
+ : clearedStructuralTransitionSettled
803
+ }
557
804
  fontFamiliesJson={fontFamiliesJson}
558
805
  snapshotJson={snapshotJson}
559
806
  containerSlotHeightsJson={containerSlotHeightsJson}
807
+ sidecarLayoutJson={sidecarLayoutJson}
560
808
  topScrollEdgeEffect={
561
809
  hasTopOverlay ? topScrollEdgeEffect ?? 'none' : 'none'
562
810
  }
@@ -571,22 +819,43 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
571
819
  collapsable={false}
572
820
  style={{
573
821
  position: 'absolute',
574
- left: slotInset,
575
- right: slotInset,
822
+ start: index === 3 ? 0 : slotInset,
823
+ end: index === 3 ? undefined : slotInset + sidecarSpace,
824
+ width: index === 3 ? sidecarLayout?.width : undefined,
576
825
  top: 0,
577
826
  }}
578
827
  onLayout={(event) => {
579
- const height = Math.ceil(event.nativeEvent.layout.height);
580
- setSlotHeights((previous) =>
581
- previous[index] === height
582
- ? previous
583
- : previous.map((value, slot) =>
584
- slot === index ? height : value
585
- )
586
- );
828
+ if (index < 4) {
829
+ recordSlotLayout(
830
+ index,
831
+ Math.ceil(event.nativeEvent.layout.height),
832
+ 0
833
+ );
834
+ }
587
835
  }}
588
836
  >
589
- {content}
837
+ {index >= 4 ? (
838
+ <View
839
+ key={
840
+ index === 4
841
+ ? `expanded-${expandedEpoch}`
842
+ : `toggle-${toggleEpoch}`
843
+ }
844
+ collapsable={false}
845
+ style={disclosureSlotMeasurementStyle}
846
+ onLayout={(event) =>
847
+ recordSlotLayout(
848
+ index,
849
+ Math.ceil(event.nativeEvent.layout.height),
850
+ index === 4 ? expandedEpoch : toggleEpoch
851
+ )
852
+ }
853
+ >
854
+ {content}
855
+ </View>
856
+ ) : (
857
+ content
858
+ )}
590
859
  </View>
591
860
  ))}
592
861
  {hasTopOverlay ? (
@@ -78,6 +78,18 @@ export type NativeListWebScrollHost = Readonly<{
78
78
  scrollTop: number;
79
79
  }>;
80
80
 
81
+ export type NativeListSidecarLayout = Readonly<{
82
+ width: number;
83
+ gap: number;
84
+ maxContentWidth: number;
85
+ }>;
86
+
87
+ export type StructuralTransitionSettledEvent = Readonly<{
88
+ transitionId: string;
89
+ status: 'completed' | 'interrupted';
90
+ settledExpanded: boolean;
91
+ }>;
92
+
81
93
  type InitialScrollProps =
82
94
  | Readonly<{
83
95
  initialScrollIndex?: never;
@@ -131,6 +143,13 @@ export type NativeListProps = Omit<ViewProps, 'children'> &
131
143
  listHeader?: ReactNode;
132
144
  listFooter?: ReactNode;
133
145
  listEmpty?: ReactNode;
146
+ /** Native disclosure content, retained until its transition settles. */
147
+ listExpandedContent?: ReactNode;
148
+ /** Stable native disclosure toggle after expanded content. */
149
+ listStructureToggle?: ReactNode;
150
+ /** Scrolling right column for a vertical linear list. Requires sidecarLayout. */
151
+ listSidecar?: ReactNode;
152
+ sidecarLayout?: NativeListSidecarLayout;
134
153
  /** Fixed overlay above scrolling rows on iOS 26 and newer. */
135
154
  topOverlay?: ReactNode;
136
155
  /** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
@@ -156,6 +175,9 @@ export type NativeListProps = Omit<ViewProps, 'children'> &
156
175
  onReorder?: (event: ReorderEvent) => void;
157
176
  onEndReached?: (event: EndReachedEvent) => void;
158
177
  onVisibleRangeChanged?: (event: VisibleRangeChangedEvent) => void;
178
+ onStructuralTransitionSettled?: (
179
+ event: StructuralTransitionSettledEvent
180
+ ) => void;
159
181
  /** Native only. Emits initial state and hysteresis transitions, not scroll frames. */
160
182
  scrollPositionThresholds?: Readonly<{
161
183
  start: number;