@legendapp/list 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,51 @@
1
+ ## 3.7.0
2
+
3
+ - Fix: List ref methods `scrollToIndex`, `scrollToOffset`, `scrollToItem`, `scrollToKey`, `scrollToEnd`, `scrollIndexIntoView`, and `scrollItemIntoView` resolve their promises once the scroll lands, or when it is interrupted by user input, canceled or replaced, or cannot reach its target (including removed targets, dataset replacement, and unmounting), instead of resolving early or never.
4
+
5
+ Suspense/Activity hide counts as unmounting for scroll requests. Requests made through a captured ref while hidden resolve without scrolling and are not replayed on reveal.
6
+
7
+ `scrollToIndex` and `scrollToOffset` clamp out-of-range targets and resolve once landed. Index-based navigation and `scrollToEnd` on empty data resolve without scrolling, including when a header or footer is present.
8
+
9
+ - Types: new required members on `LegendListRef` / state since 3.6.0. These stay required, and code that implements or mocks these types must add them:
10
+
11
+ - `LegendListRef` (native and web): `cancelScroll`, `scrollToKey`, `anchorVisibleContent`.
12
+ - `LegendListState`: `distanceFromEnd`, `isFollowingEnd`. `dataKey` is added as optional.
13
+ - `SectionListRef`: `cancelScroll`, `scrollToKey`, `anchorVisibleContent` (inherited from `LegendListRef`). Key-based methods use the flattened keys `${sectionKey}:item:${itemKey}`, `${sectionKey}:header`, `${sectionKey}:footer`, `${sectionKey}:separator:${itemIndex}`, and `${sectionKey}:section-separator`, where `sectionKey` is the section's key or `section-${sectionIndex}`.
14
+ - `LegendListMetrics`, the `onMetricsChange` payload: unchanged from 3.6.0, still `headerSize` and `footerSize`. Scroll position and end-distance fields added during the cycle were removed before release.
15
+
16
+ `getState().listen` accepts the new `isFollowingEnd` key. `distanceFromEnd` is not a listen key; read `getState().distanceFromEnd`, which is computed on demand. 3.6.0-shaped `listen` implementations remain assignable.
17
+
18
+ - Feat: Add ref methods `cancelScroll` to stop queued and active scrolling, `scrollToKey` to navigate by stable item key through prepends, and `anchorVisibleContent` to capture an item's key and viewport offset and hold it through layout changes (the first visible item by default). Pass `{ key }` to choose an item, or use `anchorVisibleContent({ hold: false })` for a snapshot without side effects. Returns `undefined` before initial placement, during dataset replacement, or if no item is found; at the end while following, it returns the anchor without interrupting end following.
19
+ - Feat: Add `viewportMargin: { start, end }` to `scrollIndexIntoView` to keep space clear at the start and end of the viewport along the scroll axis.
20
+ - Feat: Add `itemOffset` and `itemLength` to `scrollIndexIntoView` and `scrollItemIntoView` to reveal a region inside an item within `viewportMargin` on vertical and horizontal lists.
21
+ - Feat: Expose `distanceFromEnd`, `isFollowingEnd`, and `dataKey` through `getState()`, and accept `isFollowingEnd` in `getState().listen`.
22
+ - Feat: Add boolean `maintainScrollAtEnd.on.dataAppend` to follow only additions that preserve existing item keys in order; `on.dataChange` remains a boolean option for all data changes.
23
+ - Feat: `onUserScroll` fires once per accepted real user gesture (native drag with its momentum, web touch/wheel gesture, scroll key press, scrollbar input, or iOS status-bar tap), never for library or list-ref scrolls, and remains `@experimental` with heuristic wheel grouping and gaps in native pointer/wheel-only and web overlay scrollbar detection.
24
+ - Fix: With a native screen reader enabled, unowned accessibility scrolling toward the start releases end following and reports `onUserScroll` once per burst; assistive input during library animations or just after `cancelScroll()` can be missed, and scrolls bypassing the list (such as native-ref commands or Reanimated `scrollTo` worklets) cannot be distinguished from accessibility input, so call `cancelScroll()` on the JS thread before that navigation.
25
+ - Fix: Leftover trackpad momentum no longer cancels scroll commands; a new wheel gesture can interrupt them, while upward wheel motion releases automatic end following as in 3.6.0 and downward motion never does.
26
+ - Fix: iOS status-bar taps release end following and settle an interrupted list scroll.
27
+ - Fix: Native scrolls outside list-owned commands release end following when they move beyond its threshold, and appends while reading earlier rows leave no pending end measurements.
28
+ - Fix: Pausing `maintainScrollAtEnd` retains end intent for resuming; `isFollowingEnd` continues to report that intent while paused.
29
+ - Fix: Item-layout end following responds to every nonzero size change when its trigger is enabled.
30
+ - Fix: End thresholds (`isAtEnd`, `onEndReached`, and `maintainScrollAtEnd`) count trailing content and keyboard insets once. With a trailing inset, they now activate closer to the physical end instead of firing early by the inset amount.
31
+ - Fix: Row measurements and header/footer size changes recheck `onStartReached` and `onEndReached` thresholds, so pagination callbacks can fire without a scroll when layout brings an edge within its threshold.
32
+ - Fix: Animated end following stays aligned through short-list growth, footer and viewport changes, late row measurements, and native drag-to-momentum handoffs. #563 #568
33
+ - Fix: Automatic native end following waits for a drag and its momentum to settle, then catches up near the end using the configured threshold.
34
+ - Fix: Keyed scroll targets survive prepends and same-key item updates, cached dataset switches restart initial placement, and fixed-size updates and cache clearing preserve the reader's anchor.
35
+ - Fix: `scrollToOffset` can reach `contentContainerStyle` minimum sizes, horizontal RTL web lists use the scroll owner's direction for offsets, and horizontal RTL scroll-offset conversion keeps numeric `minWidth` in the physical content extent.
36
+ - Fix: When `getFixedItemSize` is set, changing `extraData` invalidates all fixed sizes and repositions the data.
37
+ - Fix: Inline `onMetricsChange` callbacks no longer cause repeated metrics emissions.
38
+ - Fix: End following rearms when a scroll or gesture settles within `maintainScrollAtEndThreshold`, so new content resumes following after `scrollToIndex`/`scrollToOffset` or a small nudge that stays near the end.
39
+ - Fix: When `getFixedItemSize` starts returning sizes for rows that were already measured, their stale measurements are dropped and the fixed sizes are applied, including rows reordered in the same render.
40
+ - Fix: Web lists follow the end in one painted frame when every row grows after a mount or `dataKey` switch, instead of clamping against the stale content extent and catching up a frame late.
41
+ - Fix: On Fabric, the native content extent commits in the same render pass as row positions, so shrinking content cannot briefly leave rows mispositioned.
42
+ - Fix: `scrollMessageToEnd({ closeKeyboard: true })` starts scrolling right away and eases to the closed end in one motion while the keyboard closes, dismissing it once, including back-to-back and mixed animated and non-animated sends.
43
+ - Fix: `AnimatedLegendList` `itemLayoutAnimation` no longer animates rows while scrolling, during list scroll commands, or after layout adjustments, and skips the transition when a container switches items even without `recycleItems`.
44
+ - Fix: Keyboard occlusion is excluded from viewability calculations, horizontal viewability uses the correct start padding including RTL, native content insets are counted only where applied, and `KeyboardAwareLegendList` keeps the static `contentInset` prop out of Android keyboard inset reports so it is not counted twice.
45
+ - Fix: `scrollMessageToEnd({ closeKeyboard: true })` freezes keyboard tracking before the keyboard starts closing, so the list scrolls to the end in one motion instead of first moving back with the closing keyboard. A rejected scroll or a keyboard close that never settles no longer leaves `freeze` set. #557
46
+ - Fix: Animated scrolls that start past the end of the content ease back into range instead of snapping.
47
+ - Perf: With `recycleItems` enabled, reuse warm offscreen rows before empty containers, and skip key extraction for unchanged append prefixes.
48
+
1
49
  ## 3.6.0
2
50
 
3
51
  - Feat: Add `viewPositionFallback` to align oversized items to the start or end of the viewport during initial or programmatic scrolling, while keeping `viewPosition` alignment for items that fit.
package/README.md CHANGED
@@ -26,8 +26,9 @@ Beyond standard `FlatList` capabilities:
26
26
  * `recycleItems`: (boolean) Toggles item component recycling.
27
27
  * `true`: Reuses item components for optimal performance. Be cautious if your item components contain local state, as it might be reused unexpectedly.
28
28
  * `false` (default): Creates new item components every time. Less performant but safer if items have complex internal state.
29
- * `maintainScrollAtEnd`: Keeps the list pinned to the tail when the user is already near the end (within `maintainScrollAtEndThreshold * screen height`). Pass `true` for all triggers, or `{ animated?: boolean, on?: { dataChange?: boolean, layout?: boolean, itemLayout?: boolean } }`; if `on` is omitted, the object form also enables all triggers.
30
- * `maintainVisibleContentPosition`: Keeps visible content steady during size/layout changes while scrolling up or when items resize above the viewport (default). Pass `true` or `{ data: true }` to also anchor during data updates; pass `false` to disable; pass `{ size: false }` to opt out of scroll-time stabilization.
29
+ * `maintainScrollAtEnd`: Keeps the list pinned to the tail when the user is already near the end (within `maintainScrollAtEndThreshold * screen height`). Pass `true` for all triggers, or `{ animated?: boolean, on?: { dataChange?: boolean, dataAppend?: boolean, footerLayout?: boolean, layout?: boolean, itemLayout?: boolean } }`; `dataAppend` follows only additions that preserve existing item keys in order. If `on` is omitted, the object form also enables all triggers.
30
+ * `onUserScroll` (experimental): Runs once per accepted user gesture, including inferred native accessibility scrolling. See the [API JSDoc](https://github.com/LegendApp/legend-list/blob/main/src/types.base.ts#L376-L394) for supported input, gesture grouping, and native commands that require `cancelScroll()`.
31
+ * `maintainVisibleContentPosition`: Keeps visible content steady during size/layout changes while scrolling up or when items resize above the viewport (default). Pass `true` or `{ data: true }` to also anchor during data updates; pass `false` to disable JS size/data stabilization; pass `{ size: false }` to opt out of scroll-time stabilization. Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, and disabled when both are disabled.
31
32
  * `alignItemsAtEnd`: (boolean) Useful for chat UIs, content smaller than the View will be aligned to the bottom of the list.
32
33
 
33
34
  ---
@@ -74,7 +75,7 @@ interface UserData {
74
75
  }
75
76
 
76
77
  const LegendListExample = () => {
77
- // Optional: Ref for accessing list methods (e.g., scrollTo)
78
+ // Optional: Ref for accessing list methods (e.g., scrollToIndex)
78
79
  const listRef = useRef<LegendListRef | null>(null)
79
80
 
80
81
  const data = []
package/animated.d.ts CHANGED
@@ -8,8 +8,8 @@ interface MaintainVisibleContentPositionNormalized<ItemT = any> {
8
8
  shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean;
9
9
  }
10
10
 
11
- type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
12
- type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
11
+ type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "isScrollActive" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
12
+ type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
13
13
  type ListenerTypeValueMap = {
14
14
  activeStickyIndex: number;
15
15
  alignItemsAtEndPadding: number;
@@ -21,6 +21,7 @@ type ListenerTypeValueMap = {
21
21
  extraData: any;
22
22
  footerSize: number;
23
23
  headerSize: number;
24
+ isFollowingEnd: boolean;
24
25
  isAtEnd: boolean;
25
26
  isAtStart: boolean;
26
27
  isNearEnd: boolean;
@@ -34,6 +35,7 @@ type ListenerTypeValueMap = {
34
35
  numContainersPooled: number;
35
36
  otherAxisSize: number;
36
37
  readyToRender: boolean;
38
+ isScrollActive: boolean;
37
39
  scrollAdjust: number;
38
40
  scrollAdjustPending: number;
39
41
  scrollAdjustUserOffset: number;
@@ -199,6 +201,7 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
199
201
  extraData?: any;
200
202
  /**
201
203
  * In case items always have a fixed size, you can provide a function to return it.
204
+ * Change extraData when the fixed sizes returned by this callback change.
202
205
  */
203
206
  getFixedItemSize?: (item: ItemT, index: number, type: TItemType) => number | undefined;
204
207
  /**
@@ -271,6 +274,11 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
271
274
  /**
272
275
  * If true, auto-scrolls to end when new items are added.
273
276
  * Use an options object to opt into specific triggers and control whether that scroll is animated.
277
+ * Setting false pauses maintenance while retaining end-follow intent. Re-enabling resumes
278
+ * that intent even if content grew beyond the threshold; user or list navigation releases it.
279
+ * While paused, isFollowingEnd reports the retained intent.
280
+ * With a native screen reader enabled, unowned accessibility scrolls toward the start
281
+ * release following. Assistive-tech scrolls during library animations are not detected.
274
282
  * @default false
275
283
  */
276
284
  maintainScrollAtEnd?: boolean | MaintainScrollAtEndOptions;
@@ -281,11 +289,13 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
281
289
  maintainScrollAtEndThreshold?: number;
282
290
  /**
283
291
  * Maintains visibility of content.
284
- * - scroll (default: true) stabilizes during size/layout changes while scrolling.
285
- * - data (default: false) stabilizes when the data array changes; passing true also sets the RN maintainVisibleContentPosition prop.
292
+ * - size (default: true) stabilizes during size/layout changes while scrolling.
293
+ * - data (default: false) stabilizes when the data array changes.
286
294
  * - shouldRestorePosition can opt out specific items from data-change anchoring.
287
295
  * - undefined (default) enables scroll stabilization but skips data-change anchoring.
288
- * - true enables both behaviors; false disables both.
296
+ * - true enables both JS behaviors; false disables both.
297
+ * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled,
298
+ * and disabled when both are disabled.
289
299
  */
290
300
  maintainVisibleContentPosition?: boolean | MaintainVisibleContentPositionConfig<ItemT>;
291
301
  /**
@@ -377,8 +387,25 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
377
387
  */
378
388
  onRefresh?: () => void;
379
389
  /**
380
- * Called when the list scrolls.
381
- */
390
+ * Called once per accepted user gesture, in either direction: native drag (including
391
+ * momentum), web wheel/touch movement, non-repeated scroll keydown, or scrollbar drag.
392
+ * Wheel input at a boundary is ignored unless navigation is active. Without wheel
393
+ * momentum metadata, gesture boundaries are inferred from timing, direction and force.
394
+ * iOS status-bar scrolling emits on native onScrollToTop unless accessibility motion
395
+ * already reported it during the animation.
396
+ * With a native screen reader enabled, otherwise unowned scroll motion with stable
397
+ * content/viewport size is inferred as accessibility input (including focus navigation),
398
+ * once per burst; motion toward the start releases end following. Library animations
399
+ * and controller/native-ref command windows mask assistive-tech scrolling.
400
+ * Native commands bypassing the list and its refs (e.g. Reanimated scrollTo worklets)
401
+ * cannot be distinguished from accessibility input while a screen reader is on; call
402
+ * cancelScroll() on the JS thread before that navigation to release end following.
403
+ * Layout/clamp/MVCP events and commands through the list or its refs do not emit it.
404
+ * @experimental Native pointer/wheel-only input without a screen reader is not detected.
405
+ * Web scrollbar detection requires pointer events in a measurable, non-overlay gutter.
406
+ */
407
+ onUserScroll?: () => void;
408
+ /** Called when the list scrolls. */
382
409
  onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
383
410
  /**
384
411
  * Called when scrolling reaches the start within onStartReachedThreshold.
@@ -510,6 +537,8 @@ interface AlwaysRenderConfig {
510
537
  keys?: string[];
511
538
  }
512
539
  interface MaintainScrollAtEndOnOptions {
540
+ /** Follow only additions that preserve every existing item key in order. */
541
+ dataAppend?: boolean;
513
542
  dataChange?: boolean;
514
543
  footerLayout?: boolean;
515
544
  itemLayout?: boolean;
@@ -551,9 +580,13 @@ type LegendListState = {
551
580
  activeStickyIndex: number;
552
581
  contentLength: number;
553
582
  data: readonly any[];
583
+ dataKey?: Key;
554
584
  elementAtIndex: (index: number) => any;
555
585
  end: number;
556
586
  endBuffered: number;
587
+ /** Distance to the physical scroll edge, including the trailing inset. */
588
+ distanceFromEnd: number;
589
+ isFollowingEnd: boolean;
557
590
  isAtEnd: boolean;
558
591
  isAtStart: boolean;
559
592
  isNearEnd: boolean;
@@ -563,7 +596,10 @@ type LegendListState = {
563
596
  isWithinMaintainScrollAtEndThreshold: boolean;
564
597
  getAverageItemSizes: () => Record<string, LegendListAverageItemSize>;
565
598
  indexByKey: (key: string) => number | undefined;
566
- listen: <T extends LegendListListenerType>(listenerType: T, callback: (value: ListenerTypeValueMap[T]) => void) => () => void;
599
+ listen: {
600
+ <T extends Exclude<LegendListListenerType, "isFollowingEnd">>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
601
+ <T extends LegendListListenerType>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
602
+ };
567
603
  listenToPosition: (key: string, callback: (value: number) => void) => () => void;
568
604
  positionAtIndex: (index: number) => number;
569
605
  positionByKey: (key: string) => number | undefined;
@@ -576,6 +612,28 @@ type LegendListState = {
576
612
  startBuffered: number;
577
613
  };
578
614
  type LegendListRef$1 = {
615
+ /** Cancels queued and active scrolling, settles its promise, and stops at the observed offset. */
616
+ cancelScroll(): void;
617
+ /** Scrolls by stable item identity through prepends; resolves without scrolling if the key is missing or removed. */
618
+ scrollToKey(params: {
619
+ key: string;
620
+ animated?: boolean;
621
+ viewOffset?: number;
622
+ viewPosition?: number;
623
+ }): Promise<void>;
624
+ /**
625
+ * Captures an item's key and viewport offset, holding it through layout changes by default.
626
+ * Omit key to use the first visible item; set hold to false to read without side effects.
627
+ * Returns undefined before initial placement, during dataset replacement, or if no item is found.
628
+ * While following at the end, returns the anchor without interrupting end following.
629
+ */
630
+ anchorVisibleContent(options?: {
631
+ key?: string;
632
+ hold?: boolean;
633
+ }): {
634
+ key: string;
635
+ offset: number;
636
+ } | undefined;
579
637
  /**
580
638
  * Clears internal virtualization caches.
581
639
  * @param options - Cache clearing options.
@@ -618,20 +676,40 @@ type LegendListRef$1 = {
618
676
  * @param params - Parameters for scrolling.
619
677
  * @param params.animated - If true, animates the scroll. Default: true.
620
678
  * @param params.index - The index to scroll to.
679
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
680
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
681
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
682
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
621
683
  */
622
684
  scrollIndexIntoView(params: {
623
685
  animated?: boolean | undefined;
624
686
  index: number;
687
+ itemOffset?: number;
688
+ itemLength?: number;
689
+ viewportMargin?: {
690
+ start?: number;
691
+ end?: number;
692
+ };
625
693
  }): Promise<void>;
626
694
  /**
627
- * Scrolls a specific index into view.
695
+ * Scrolls a specific item into view.
628
696
  * @param params - Parameters for scrolling.
629
697
  * @param params.animated - If true, animates the scroll. Default: true.
630
698
  * @param params.item - The item to scroll to.
699
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
700
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
701
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
702
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
631
703
  */
632
704
  scrollItemIntoView(params: {
633
705
  animated?: boolean | undefined;
634
706
  item: any;
707
+ itemOffset?: number;
708
+ itemLength?: number;
709
+ viewportMargin?: {
710
+ start?: number;
711
+ end?: number;
712
+ };
635
713
  }): Promise<void>;
636
714
  /**
637
715
  * Scrolls to the end of the list.
@@ -761,7 +839,9 @@ type LegendListPropsOverrides<ItemT, TItemType extends string | undefined> = Omi
761
839
  };
762
840
  type LegendListProps<ItemT = any, TItemType extends string | undefined = string | undefined> = LegendListPropsOverrides<ItemT, TItemType>;
763
841
  type LegendListRef = Omit<LegendListRef$1, "getAnimatableRef" | "getNativeScrollRef" | "getScrollResponder" | "reportContentInset"> & {
842
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
764
843
  getAnimatableRef(): React.ElementRef<typeof ScrollViewComponent>;
844
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
765
845
  getNativeScrollRef(): React.ElementRef<typeof ScrollViewComponent>;
766
846
  getScrollResponder(): ScrollResponderMixin;
767
847
  reportContentInset(inset?: Partial<Insets$1> | null): void;
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { Key } from 'react';
2
3
  import { ScrollViewComponent, ScrollResponderMixin, Insets as Insets$1 } from 'react-native';
3
4
  import { ScrollEvent, ScrollHandlerProcessed } from 'react-native-reanimated';
4
5
  import { AnimatedLegendListProps } from '@legendapp/list/reanimated';
@@ -9,8 +10,8 @@ interface MaintainVisibleContentPositionNormalized<ItemT = any> {
9
10
  shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean;
10
11
  }
11
12
 
12
- type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
13
- type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
13
+ type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "isScrollActive" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
14
+ type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
14
15
  type ListenerTypeValueMap = {
15
16
  activeStickyIndex: number;
16
17
  alignItemsAtEndPadding: number;
@@ -22,6 +23,7 @@ type ListenerTypeValueMap = {
22
23
  extraData: any;
23
24
  footerSize: number;
24
25
  headerSize: number;
26
+ isFollowingEnd: boolean;
25
27
  isAtEnd: boolean;
26
28
  isAtStart: boolean;
27
29
  isNearEnd: boolean;
@@ -35,6 +37,7 @@ type ListenerTypeValueMap = {
35
37
  numContainersPooled: number;
36
38
  otherAxisSize: number;
37
39
  readyToRender: boolean;
40
+ isScrollActive: boolean;
38
41
  scrollAdjust: number;
39
42
  scrollAdjustPending: number;
40
43
  scrollAdjustUserOffset: number;
@@ -88,9 +91,13 @@ type LegendListState = {
88
91
  activeStickyIndex: number;
89
92
  contentLength: number;
90
93
  data: readonly any[];
94
+ dataKey?: Key;
91
95
  elementAtIndex: (index: number) => any;
92
96
  end: number;
93
97
  endBuffered: number;
98
+ /** Distance to the physical scroll edge, including the trailing inset. */
99
+ distanceFromEnd: number;
100
+ isFollowingEnd: boolean;
94
101
  isAtEnd: boolean;
95
102
  isAtStart: boolean;
96
103
  isNearEnd: boolean;
@@ -100,7 +107,10 @@ type LegendListState = {
100
107
  isWithinMaintainScrollAtEndThreshold: boolean;
101
108
  getAverageItemSizes: () => Record<string, LegendListAverageItemSize>;
102
109
  indexByKey: (key: string) => number | undefined;
103
- listen: <T extends LegendListListenerType>(listenerType: T, callback: (value: ListenerTypeValueMap[T]) => void) => () => void;
110
+ listen: {
111
+ <T extends Exclude<LegendListListenerType, "isFollowingEnd">>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
112
+ <T extends LegendListListenerType>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
113
+ };
104
114
  listenToPosition: (key: string, callback: (value: number) => void) => () => void;
105
115
  positionAtIndex: (index: number) => number;
106
116
  positionByKey: (key: string) => number | undefined;
@@ -113,6 +123,28 @@ type LegendListState = {
113
123
  startBuffered: number;
114
124
  };
115
125
  type LegendListRef$1 = {
126
+ /** Cancels queued and active scrolling, settles its promise, and stops at the observed offset. */
127
+ cancelScroll(): void;
128
+ /** Scrolls by stable item identity through prepends; resolves without scrolling if the key is missing or removed. */
129
+ scrollToKey(params: {
130
+ key: string;
131
+ animated?: boolean;
132
+ viewOffset?: number;
133
+ viewPosition?: number;
134
+ }): Promise<void>;
135
+ /**
136
+ * Captures an item's key and viewport offset, holding it through layout changes by default.
137
+ * Omit key to use the first visible item; set hold to false to read without side effects.
138
+ * Returns undefined before initial placement, during dataset replacement, or if no item is found.
139
+ * While following at the end, returns the anchor without interrupting end following.
140
+ */
141
+ anchorVisibleContent(options?: {
142
+ key?: string;
143
+ hold?: boolean;
144
+ }): {
145
+ key: string;
146
+ offset: number;
147
+ } | undefined;
116
148
  /**
117
149
  * Clears internal virtualization caches.
118
150
  * @param options - Cache clearing options.
@@ -155,20 +187,40 @@ type LegendListRef$1 = {
155
187
  * @param params - Parameters for scrolling.
156
188
  * @param params.animated - If true, animates the scroll. Default: true.
157
189
  * @param params.index - The index to scroll to.
190
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
191
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
192
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
193
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
158
194
  */
159
195
  scrollIndexIntoView(params: {
160
196
  animated?: boolean | undefined;
161
197
  index: number;
198
+ itemOffset?: number;
199
+ itemLength?: number;
200
+ viewportMargin?: {
201
+ start?: number;
202
+ end?: number;
203
+ };
162
204
  }): Promise<void>;
163
205
  /**
164
- * Scrolls a specific index into view.
206
+ * Scrolls a specific item into view.
165
207
  * @param params - Parameters for scrolling.
166
208
  * @param params.animated - If true, animates the scroll. Default: true.
167
209
  * @param params.item - The item to scroll to.
210
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
211
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
212
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
213
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
168
214
  */
169
215
  scrollItemIntoView(params: {
170
216
  animated?: boolean | undefined;
171
217
  item: any;
218
+ itemOffset?: number;
219
+ itemLength?: number;
220
+ viewportMargin?: {
221
+ start?: number;
222
+ end?: number;
223
+ };
172
224
  }): Promise<void>;
173
225
  /**
174
226
  * Scrolls to the end of the list.
@@ -239,7 +291,9 @@ type LegendListRef$1 = {
239
291
  };
240
292
 
241
293
  type LegendListRef = Omit<LegendListRef$1, "getAnimatableRef" | "getNativeScrollRef" | "getScrollResponder" | "reportContentInset"> & {
294
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
242
295
  getAnimatableRef(): React.ElementRef<typeof ScrollViewComponent>;
296
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
243
297
  getNativeScrollRef(): React.ElementRef<typeof ScrollViewComponent>;
244
298
  getScrollResponder(): ScrollResponderMixin;
245
299
  reportContentInset(inset?: Partial<Insets$1> | null): void;
package/keyboard.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { Key } from 'react';
2
3
  import { ScrollViewComponent, ScrollResponderMixin, Insets as Insets$1, ScrollViewProps, View, LayoutChangeEvent } from 'react-native';
3
4
  import { KeyboardChatScrollViewProps } from 'react-native-keyboard-controller';
4
5
  import { SharedValue } from 'react-native-reanimated';
@@ -10,8 +11,8 @@ interface MaintainVisibleContentPositionNormalized<ItemT = any> {
10
11
  shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean;
11
12
  }
12
13
 
13
- type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
14
- type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
14
+ type ListenerType = "activeStickyIndex" | "alignItemsAtEndPadding" | "anchoredEndSpaceSize" | "containerLayoutEpoch" | "debugComputedScroll" | "debugRawScroll" | "extraData" | "footerSize" | "headerSize" | "lastItemKeys" | "lastPositionUpdate" | "maintainVisibleContentPosition" | "numColumns" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "isScrollActive" | "scrollAdjust" | "scrollAdjustPending" | "scrollAdjustUserOffset" | "scrollSize" | "snapToOffsets" | "stylePaddingTop" | "totalSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | `containerColumn${number}` | `containerSpan${number}` | `containerItemData${number}` | `containerItemIndex${number}` | `containerItemKey${number}` | `containerLayoutReady${number}` | `containerPosition${number}` | `containerSticky${number}`;
15
+ type LegendListListenerType = Extract<ListenerType, "activeStickyIndex" | "anchoredEndSpaceSize" | "footerSize" | "headerSize" | "isFollowingEnd" | "isAtEnd" | "isAtStart" | "isNearEnd" | "isNearStart" | "isWithinMaintainScrollAtEndThreshold" | "adaptiveRender" | "lastItemKeys" | "lastPositionUpdate" | "numContainers" | "numContainersPooled" | "otherAxisSize" | "readyToRender" | "snapToOffsets" | "totalSize">;
15
16
  type ListenerTypeValueMap = {
16
17
  activeStickyIndex: number;
17
18
  alignItemsAtEndPadding: number;
@@ -23,6 +24,7 @@ type ListenerTypeValueMap = {
23
24
  extraData: any;
24
25
  footerSize: number;
25
26
  headerSize: number;
27
+ isFollowingEnd: boolean;
26
28
  isAtEnd: boolean;
27
29
  isAtStart: boolean;
28
30
  isNearEnd: boolean;
@@ -36,6 +38,7 @@ type ListenerTypeValueMap = {
36
38
  numContainersPooled: number;
37
39
  otherAxisSize: number;
38
40
  readyToRender: boolean;
41
+ isScrollActive: boolean;
39
42
  scrollAdjust: number;
40
43
  scrollAdjustPending: number;
41
44
  scrollAdjustUserOffset: number;
@@ -106,9 +109,13 @@ type LegendListState = {
106
109
  activeStickyIndex: number;
107
110
  contentLength: number;
108
111
  data: readonly any[];
112
+ dataKey?: Key;
109
113
  elementAtIndex: (index: number) => any;
110
114
  end: number;
111
115
  endBuffered: number;
116
+ /** Distance to the physical scroll edge, including the trailing inset. */
117
+ distanceFromEnd: number;
118
+ isFollowingEnd: boolean;
112
119
  isAtEnd: boolean;
113
120
  isAtStart: boolean;
114
121
  isNearEnd: boolean;
@@ -118,7 +125,10 @@ type LegendListState = {
118
125
  isWithinMaintainScrollAtEndThreshold: boolean;
119
126
  getAverageItemSizes: () => Record<string, LegendListAverageItemSize>;
120
127
  indexByKey: (key: string) => number | undefined;
121
- listen: <T extends LegendListListenerType>(listenerType: T, callback: (value: ListenerTypeValueMap[T]) => void) => () => void;
128
+ listen: {
129
+ <T extends Exclude<LegendListListenerType, "isFollowingEnd">>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
130
+ <T extends LegendListListenerType>(listenerType: T, callback: (value: Pick<ListenerTypeValueMap, LegendListListenerType>[T]) => void): () => void;
131
+ };
122
132
  listenToPosition: (key: string, callback: (value: number) => void) => () => void;
123
133
  positionAtIndex: (index: number) => number;
124
134
  positionByKey: (key: string) => number | undefined;
@@ -131,6 +141,28 @@ type LegendListState = {
131
141
  startBuffered: number;
132
142
  };
133
143
  type LegendListRef$1 = {
144
+ /** Cancels queued and active scrolling, settles its promise, and stops at the observed offset. */
145
+ cancelScroll(): void;
146
+ /** Scrolls by stable item identity through prepends; resolves without scrolling if the key is missing or removed. */
147
+ scrollToKey(params: {
148
+ key: string;
149
+ animated?: boolean;
150
+ viewOffset?: number;
151
+ viewPosition?: number;
152
+ }): Promise<void>;
153
+ /**
154
+ * Captures an item's key and viewport offset, holding it through layout changes by default.
155
+ * Omit key to use the first visible item; set hold to false to read without side effects.
156
+ * Returns undefined before initial placement, during dataset replacement, or if no item is found.
157
+ * While following at the end, returns the anchor without interrupting end following.
158
+ */
159
+ anchorVisibleContent(options?: {
160
+ key?: string;
161
+ hold?: boolean;
162
+ }): {
163
+ key: string;
164
+ offset: number;
165
+ } | undefined;
134
166
  /**
135
167
  * Clears internal virtualization caches.
136
168
  * @param options - Cache clearing options.
@@ -173,20 +205,40 @@ type LegendListRef$1 = {
173
205
  * @param params - Parameters for scrolling.
174
206
  * @param params.animated - If true, animates the scroll. Default: true.
175
207
  * @param params.index - The index to scroll to.
208
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
209
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
210
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
211
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
176
212
  */
177
213
  scrollIndexIntoView(params: {
178
214
  animated?: boolean | undefined;
179
215
  index: number;
216
+ itemOffset?: number;
217
+ itemLength?: number;
218
+ viewportMargin?: {
219
+ start?: number;
220
+ end?: number;
221
+ };
180
222
  }): Promise<void>;
181
223
  /**
182
- * Scrolls a specific index into view.
224
+ * Scrolls a specific item into view.
183
225
  * @param params - Parameters for scrolling.
184
226
  * @param params.animated - If true, animates the scroll. Default: true.
185
227
  * @param params.item - The item to scroll to.
228
+ * @param params.itemOffset - Region start measured from the item's start along the scroll axis. Default: 0 when itemLength is provided.
229
+ * @param params.itemLength - Region length along the scroll axis. Default: 0 when itemOffset is provided. Omit both to reveal the whole item.
230
+ * @param params.viewportMargin - Space to keep clear at the start and end of the viewport along the scroll axis.
231
+ * The region should be within the item. A visible region does not scroll; otherwise its nearest edge aligns inside the margins. A region larger than the available space aligns its start.
186
232
  */
187
233
  scrollItemIntoView(params: {
188
234
  animated?: boolean | undefined;
189
235
  item: any;
236
+ itemOffset?: number;
237
+ itemLength?: number;
238
+ viewportMargin?: {
239
+ start?: number;
240
+ end?: number;
241
+ };
190
242
  }): Promise<void>;
191
243
  /**
192
244
  * Scrolls to the end of the list.
@@ -257,7 +309,9 @@ type LegendListRef$1 = {
257
309
  };
258
310
 
259
311
  type LegendListRef = Omit<LegendListRef$1, "getAnimatableRef" | "getNativeScrollRef" | "getScrollResponder" | "reportContentInset"> & {
312
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
260
313
  getAnimatableRef(): React.ElementRef<typeof ScrollViewComponent>;
314
+ /** Call cancelScroll() before scrolling it directly so end following and list scrolling stop first. */
261
315
  getNativeScrollRef(): React.ElementRef<typeof ScrollViewComponent>;
262
316
  getScrollResponder(): ScrollResponderMixin;
263
317
  reportContentInset(inset?: Partial<Insets$1> | null): void;
@@ -270,9 +324,7 @@ type ScrollMessageToEndOptions = {
270
324
  };
271
325
  type KeyboardScrollToEndListRef = {
272
326
  current: {
273
- scrollToEnd(params?: {
274
- animated?: boolean;
275
- }): Promise<void>;
327
+ scrollToEnd: LegendListRef["scrollToEnd"];
276
328
  } | null;
277
329
  };
278
330
  type UseKeyboardScrollToEndOptions = {