@rific/scroll-view 0.4.3 → 0.6.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/README.md CHANGED
@@ -71,12 +71,13 @@ Screen-level provider that owns header/footer state and scroll position. Must wr
71
71
  |------|------|---------|-------------|
72
72
  | `blur` | `boolean` | system | Enable frosted-glass backdrop on header/footer |
73
73
  | `fixed` | `boolean` | `false` | Pin both header and footer (overrides `headerFixed`/`footerFixed`) |
74
+ | `footerAboveKeyboard` | `boolean` | `false` | With a fixed footer, float it above the keyboard instead of letting the keyboard cover it — see [Keyboard awareness](#keyboard-awareness) |
74
75
  | `headerFixed` | `boolean` | `false` | Pin header; overrides settings default |
75
76
  | `footerFixed` | `boolean` | `false` | Pin footer; overrides settings default |
76
77
  | `snapBack` | `boolean` | `false` | Snap header and footer back when scrolling up |
77
78
  | `snapBackHeader` | `boolean` | — | Override `snapBack` for header only |
78
79
  | `snapBackFooter` | `boolean` | — | Override `snapBack` for footer only |
79
- | `tabBarHeight` | `number` | `60` | Used to compute safe scroll height |
80
+ | `tabBarHeight` | `number` | `0` | Extra bottom inset reserved for a host app's own persistent tab bar (added on top of the safe-area inset, independent of any `ScrollViewFooter`) |
80
81
 
81
82
  ---
82
83
 
@@ -269,6 +270,45 @@ export default function SearchScreen() {
269
270
 
270
271
  ---
271
272
 
273
+ ## Keyboard awareness
274
+
275
+ Pass `keyboardAware` to any scroll component (`ScrollView`, `FlatList`, `SectionList`, `CustomList`) to add the keyboard's height to the bottom content inset, so a focused field near the bottom of the content stays above the keyboard instead of getting hidden behind it.
276
+
277
+ ```tsx
278
+ <ScrollViewProvider>
279
+ <ScrollViewHeader title="Edit profile" />
280
+ <ScrollView keyboardAware>
281
+ <TextInput label="Name" />
282
+ <TextInput label="Bio" multiline />
283
+ </ScrollView>
284
+ </ScrollViewProvider>
285
+ ```
286
+
287
+ By default, a fixed `ScrollViewFooter` doesn't follow the keyboard — it stays pinned to the screen bottom and the keyboard simply covers it. If the footer holds something the user needs while typing (a submit button, say), set `footerAboveKeyboard` on `ScrollViewProvider` so the footer floats above the keyboard instead, reachable without dismissing it first:
288
+
289
+ ```tsx
290
+ <ScrollViewProvider footerFixed footerAboveKeyboard>
291
+ <ScrollViewHeader title="Edit profile" />
292
+ <ScrollView keyboardAware>
293
+ <TextInput label="Name" />
294
+ <TextInput label="Bio" multiline />
295
+ </ScrollView>
296
+ <ScrollViewFooter>
297
+ <Button mode="contained" onPress={handleSubmit}>Save</Button>
298
+ </ScrollViewFooter>
299
+ </ScrollViewProvider>
300
+ ```
301
+
302
+ `footerAboveKeyboard` only has an effect when the footer is fixed (`footerFixed`) — a footer that scrolls away with content has nothing to reserve keyboard space for. Always pair it with `keyboardAware: true` on any scroll component that could have a focused input while it's visible. Content always reserves space for the current footer height regardless of `keyboardAware` (so a fixed footer never overlaps it) — and while floating, that height already includes the keyboard, so a list with `keyboardAware: false` ends up reserving keyboard space anyway, just indirectly through the footer rather than directly. There's no useful configuration where you'd want the footer reachable above the keyboard but *not* want that same list's own content clear of it too, so don't rely on `keyboardAware: false` to opt a list out of this while `footerAboveKeyboard` is on — it won't.
303
+
304
+ Floating is done by growing the footer's own container rather than translating it, so its `BlurView` backdrop always stays sealed against the true physical screen bottom — nothing shows through beneath it, including at the keyboard's rounded top corners.
305
+
306
+ While floating above an open keyboard, the footer also drops its safe-area bottom inset — it's no longer sitting at the physical screen edge, so that padding would just be dead space under its content. As the keyboard closes, the inset ramps back in continuously over the last bit of its travel rather than snapping back once it's fully closed, so the content settles at the safe-area line smoothly instead of dropping to the physical bottom edge and correcting back up.
307
+
308
+ Keyboard awareness is native-only — `useKeyboardInset` (and therefore `keyboardAware`/`footerAboveKeyboard`) always resolves to `0` height on web.
309
+
310
+ ---
311
+
272
312
  ## Hooks
273
313
 
274
314
  ### `useScrollView`
package/dist/index.d.mts CHANGED
@@ -121,6 +121,7 @@ declare const ScrollView: typeof ScrollViewInner;
121
121
 
122
122
  type ScrollViewContextType = {
123
123
  blur: boolean;
124
+ footerAboveKeyboard: boolean;
124
125
  footerHeight: number;
125
126
  footerHeightShared: SharedValue<number>;
126
127
  footerFixed: boolean;
@@ -158,6 +159,7 @@ type ScrollViewHeaderProps = {
158
159
  actionSize?: number;
159
160
  actionStyle?: ViewStyle;
160
161
  backAction?: () => void;
162
+ backActionAccessibilityLabel?: string;
161
163
  backActionFixed?: boolean;
162
164
  caption?: string;
163
165
  centerContent?: ReactNode;
@@ -169,12 +171,13 @@ type ScrollViewHeaderProps = {
169
171
  trailingAction?: ReactNode;
170
172
  trailingActionFixed?: boolean;
171
173
  };
172
- declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset, trailingAction, trailingActionFixed }: ScrollViewHeaderProps) => react.JSX.Element;
174
+ declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset, trailingAction, trailingActionFixed }: ScrollViewHeaderProps) => react.JSX.Element;
173
175
 
174
176
  type ScrollViewProviderProps = {
175
177
  blur?: boolean;
176
178
  children: ReactNode;
177
179
  fixed?: boolean;
180
+ footerAboveKeyboard?: boolean;
178
181
  footerFixed?: boolean;
179
182
  headerFixed?: boolean;
180
183
  snapBack?: boolean;
@@ -182,7 +185,7 @@ type ScrollViewProviderProps = {
182
185
  snapBackHeader?: boolean;
183
186
  tabBarHeight?: number;
184
187
  };
185
- declare const ScrollViewProvider: ({ blur, children, fixed, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
188
+ declare const ScrollViewProvider: ({ blur, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
186
189
 
187
190
  type ScrollViewSettingsProviderProps = {
188
191
  children: ReactNode;
package/dist/index.d.ts CHANGED
@@ -121,6 +121,7 @@ declare const ScrollView: typeof ScrollViewInner;
121
121
 
122
122
  type ScrollViewContextType = {
123
123
  blur: boolean;
124
+ footerAboveKeyboard: boolean;
124
125
  footerHeight: number;
125
126
  footerHeightShared: SharedValue<number>;
126
127
  footerFixed: boolean;
@@ -158,6 +159,7 @@ type ScrollViewHeaderProps = {
158
159
  actionSize?: number;
159
160
  actionStyle?: ViewStyle;
160
161
  backAction?: () => void;
162
+ backActionAccessibilityLabel?: string;
161
163
  backActionFixed?: boolean;
162
164
  caption?: string;
163
165
  centerContent?: ReactNode;
@@ -169,12 +171,13 @@ type ScrollViewHeaderProps = {
169
171
  trailingAction?: ReactNode;
170
172
  trailingActionFixed?: boolean;
171
173
  };
172
- declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset, trailingAction, trailingActionFixed }: ScrollViewHeaderProps) => react.JSX.Element;
174
+ declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset, trailingAction, trailingActionFixed }: ScrollViewHeaderProps) => react.JSX.Element;
173
175
 
174
176
  type ScrollViewProviderProps = {
175
177
  blur?: boolean;
176
178
  children: ReactNode;
177
179
  fixed?: boolean;
180
+ footerAboveKeyboard?: boolean;
178
181
  footerFixed?: boolean;
179
182
  headerFixed?: boolean;
180
183
  snapBack?: boolean;
@@ -182,7 +185,7 @@ type ScrollViewProviderProps = {
182
185
  snapBackHeader?: boolean;
183
186
  tabBarHeight?: number;
184
187
  };
185
- declare const ScrollViewProvider: ({ blur, children, fixed, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
188
+ declare const ScrollViewProvider: ({ blur, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
186
189
 
187
190
  type ScrollViewSettingsProviderProps = {
188
191
  children: ReactNode;
package/dist/index.js CHANGED
@@ -87,6 +87,7 @@ var import_react_native_reanimated3 = require("react-native-reanimated");
87
87
  var import_react = require("react");
88
88
  var ScrollViewContext = (0, import_react.createContext)({
89
89
  blur: true,
90
+ footerAboveKeyboard: false,
90
91
  footerHeight: 0,
91
92
  footerHeightShared: null,
92
93
  footerFixed: false,
@@ -116,7 +117,7 @@ var ScrollViewContext = (0, import_react.createContext)({
116
117
  },
117
118
  snapBackFooterShared: null,
118
119
  snapBackHeaderShared: null,
119
- tabBarHeight: 60
120
+ tabBarHeight: 0
120
121
  });
121
122
 
122
123
  // src/useScrollInit.ts
@@ -424,17 +425,21 @@ var useKeyboardInset = import_react_native4.Platform.OS === "web" ? useKeyboardI
424
425
  var ZERO_INSET = { bottom: 0, top: 0 };
425
426
  var ZERO_OFFSET = { x: 0, y: 0 };
426
427
  var CHIP_SLIDE = 48;
427
- function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
428
+ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
428
429
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
429
430
  const keyboardHeight = useKeyboardInset();
430
- const { footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared } = (0, import_react5.useContext)(ScrollViewContext);
431
+ const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = (0, import_react5.useContext)(ScrollViewContext);
431
432
  const headerFixed = isHorizontal ? true : headerFixedProp ?? contextHeaderFixed;
432
433
  const footerFixed = isHorizontal ? true : footerFixedProp ?? contextFooterFixed;
433
434
  (0, import_react5.useEffect)(() => {
434
435
  pullSearchHeightShared.value = usesContentInset ? pullSearchHeight ?? 0 : 0;
435
436
  }, [pullSearchHeight, pullSearchHeightShared]);
436
- const containerStyle = (0, import_react5.useMemo)(() => headerHeight === null ? [{ flex: 1, opacity: 0 }, style] : [import_react_native5.StyleSheet.absoluteFill, style], [headerHeight, style]);
437
- const insetGeometry = (0, import_react5.useMemo)(() => ({ bottom: (footerFixed ? footerHeight || insets.bottom : insets.bottom) + (keyboardAware ? keyboardHeight : 0), top: headerHeight ?? 0 }), [footerFixed, footerHeight, insets.bottom, headerHeight, keyboardAware, keyboardHeight]);
437
+ const containerStyle = (0, import_react5.useMemo)(() => headerHeight === null ? [{ flex: 1, opacity: hideUntilMeasured ? 0 : 1 }, style] : [import_react_native5.StyleSheet.absoluteFill, style], [headerHeight, hideUntilMeasured, style]);
438
+ const insetGeometry = (0, import_react5.useMemo)(() => {
439
+ const footerReserve = footerFixed ? footerHeight || insets.bottom : insets.bottom;
440
+ const bottom = (!keyboardAware || footerFixed && footerAboveKeyboard ? footerReserve : Math.max(footerReserve, keyboardHeight)) + tabBarHeight;
441
+ return { bottom, top: headerHeight ?? 0 };
442
+ }, [footerAboveKeyboard, footerFixed, footerHeight, insets.bottom, tabBarHeight, headerHeight, keyboardAware, keyboardHeight]);
438
443
  const contentInset = usesContentInset ? insetGeometry : ZERO_INSET;
439
444
  const contentPadding = (0, import_react5.useMemo)(() => usesContentInset || isHorizontal ? null : { paddingBottom: insetGeometry.bottom, paddingTop: insetGeometry.top }, [insetGeometry, isHorizontal]);
440
445
  const [startedWithHeader] = (0, import_react5.useState)(headerHeight !== null);
@@ -460,7 +465,7 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
460
465
  top,
461
466
  transform: [{ translateY: chipHidden.value ? (0, import_react_native_reanimated5.withTiming)(-CHIP_SLIDE) : (0, import_react_native_reanimated5.withTiming)(0) }]
462
467
  };
463
- }, [isHorizontal, headerFixed, headerHeight, insets.top]);
468
+ }, [isHorizontal, headerFixed, headerHeight, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
464
469
  return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false };
465
470
  }
466
471
 
@@ -473,6 +478,10 @@ var CustomListInner = ({ chipProps, chipThreshold, component: List, renderFilter
473
478
  const capturedGeneration = (0, import_react_native_reanimated6.useSharedValue)(-1);
474
479
  (0, import_react6.useLayoutEffect)(() => {
475
480
  const generation = listGeneration.value;
481
+ if (import_react_native6.Platform.OS === "web") {
482
+ capturedGeneration.value = generation;
483
+ return;
484
+ }
476
485
  (0, import_react_native_reanimated6.runOnUI)((gen) => {
477
486
  "worklet";
478
487
  capturedGeneration.value = gen;
@@ -570,7 +579,7 @@ var Dot = ({ activeColor, index, inactiveColor, pageWidth, scrollPosition }) =>
570
579
  const size = DOT_FULL + (DOT_SMALL - DOT_FULL) * t;
571
580
  const backgroundColor = (0, import_react_native_reanimated7.interpolateColor)(t, [0, 1], [activeColor, inactiveColor]);
572
581
  return { backgroundColor, borderRadius: size / 2, height: size, width: size };
573
- });
582
+ }, [scrollPosition, pageWidth, index, activeColor, inactiveColor]);
574
583
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native7.View, { style: styles3.dotContainer, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_reanimated7.default.View, { style: circleStyle }) });
575
584
  };
576
585
  var HorizontalDots = ({ total }) => {
@@ -581,15 +590,18 @@ var HorizontalDots = ({ total }) => {
581
590
  const insetsBottom = insets.bottom;
582
591
  const activeColor = theme.colors.primary;
583
592
  const inactiveColor = theme.colors.outlineVariant;
584
- const pillContainerStyle = (0, import_react_native_reanimated7.useAnimatedStyle)(() => ({
585
- bottom: Math.max(footerHeightShared.value, insetsBottom) + 8
586
- }));
593
+ const pillContainerStyle = (0, import_react_native_reanimated7.useAnimatedStyle)(
594
+ () => ({
595
+ bottom: Math.max(footerHeightShared.value, insetsBottom) + 8
596
+ }),
597
+ [footerHeightShared, insetsBottom]
598
+ );
587
599
  const rowStyle = (0, import_react_native_reanimated7.useAnimatedStyle)(() => {
588
600
  const page = scrollPosition.value / width;
589
601
  const maxOffset = Math.max(0, (total - CLIP_COUNT) * DOT_STEP);
590
602
  const offset = Math.max(0, Math.min((page - (CLIP_COUNT - 1) / 2) * DOT_STEP, maxOffset));
591
603
  return { transform: [{ translateX: -offset }] };
592
- });
604
+ }, [scrollPosition, width, total]);
593
605
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_reanimated7.default.View, { pointerEvents: "none", style: [styles3.pillContainer, pillContainerStyle], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native7.View, { style: styles3.pill, children: [
594
606
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_auto_paper2.BlurView, { blur, style: import_react_native7.StyleSheet.absoluteFill }),
595
607
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native7.View, { style: styles3.dotsClip, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_reanimated7.default.View, { style: [styles3.dotsRow, rowStyle], children: Array.from({ length: total }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Dot, { activeColor, index: i, inactiveColor, pageWidth: width, scrollPosition }, i)) }) })
@@ -896,7 +908,10 @@ var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce,
896
908
  (isHidden, wasHidden) => {
897
909
  if (isHidden && !wasHidden) (0, import_react_native_reanimated9.runOnJS)(blurInput)();
898
910
  },
899
- [barHeight, headerHeight, blurInput]
911
+ // scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
912
+ // for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
913
+ // never re-triggers this reaction (confirmed empirically elsewhere in this package).
914
+ [barHeight, headerHeight, blurInput, scrollPosition]
900
915
  );
901
916
  const handleClear = (0, import_react10.useCallback)(() => {
902
917
  setValue("");
@@ -927,7 +942,7 @@ var PullSearch = (0, import_react10.forwardRef)(function PullSearch2({ debounce,
927
942
  const showY = -h;
928
943
  const hideY = -h + barHeight;
929
944
  return { opacity: (0, import_react_native_reanimated9.interpolate)(scrollPosition.value, [showY, hideY], [1, 0], import_react_native_reanimated9.Extrapolation.CLAMP) };
930
- }, [barHeight, headerHeight]);
945
+ }, [barHeight, headerHeight, scrollPosition]);
931
946
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_reanimated9.default.View, { onLayout: handleLayout, style: [styles5.container, animatedStyle], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper2.Searchbar, { autoCorrect: false, onChangeText: (text) => setValue((text || "").trimStart()), onClearIconPress: handleClear, placeholder, placeholderTextColor: theme.colors.onSurfaceVariant, ref: inputRef, spellCheck: false, style: styles5.input, value }) });
932
947
  });
933
948
  var styles5 = import_react_native9.StyleSheet.create({
@@ -977,6 +992,10 @@ var ScrollViewInner = ({ chipProps, chipThreshold, children, contentContainerSty
977
992
  const capturedGeneration = (0, import_react_native_reanimated10.useSharedValue)(-1);
978
993
  (0, import_react12.useLayoutEffect)(() => {
979
994
  const generation = listGeneration.value;
995
+ if (import_react_native10.Platform.OS === "web") {
996
+ capturedGeneration.value = generation;
997
+ return;
998
+ }
980
999
  (0, import_react_native_reanimated10.runOnUI)((gen) => {
981
1000
  "worklet";
982
1001
  capturedGeneration.value = gen;
@@ -1041,8 +1060,9 @@ var import_react_native_reanimated11 = __toESM(require("react-native-reanimated"
1041
1060
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
1042
1061
  var import_jsx_runtime8 = require("react/jsx-runtime");
1043
1062
  var ScrollViewFooter = ({ children, style }) => {
1044
- const { blur, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = (0, import_react13.useContext)(ScrollViewContext);
1063
+ const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = (0, import_react13.useContext)(ScrollViewContext);
1045
1064
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1065
+ const keyboardHeight = useKeyboardInset();
1046
1066
  (0, import_react13.useEffect)(
1047
1067
  () => () => {
1048
1068
  setFooterHeight(0);
@@ -1062,10 +1082,12 @@ var ScrollViewFooter = ({ children, style }) => {
1062
1082
  const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
1063
1083
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1064
1084
  return { transform: [{ translateY: Math.min(effective, footerHeight) }] };
1065
- }, [footerHeight, footerFixed]);
1066
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native_reanimated11.default.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, footerStyle], children: [
1085
+ }, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset]);
1086
+ const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0;
1087
+ const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom;
1088
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native_reanimated11.default.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { paddingBottom: containerPaddingBottom }, footerStyle], children: [
1067
1089
  footerHeight > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper3.BlurView, { blur, style: import_react_native11.StyleSheet.absoluteFill }),
1068
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native11.View, { style: [styles6.row, { paddingBottom: insets.bottom }, style], children })
1090
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native11.View, { style: [styles6.row, { paddingBottom: rowPaddingBottom }, style], children })
1069
1091
  ] });
1070
1092
  };
1071
1093
  var styles6 = import_react_native11.StyleSheet.create({
@@ -1082,7 +1104,7 @@ var import_react_native_reanimated12 = __toESM(require("react-native-reanimated"
1082
1104
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
1083
1105
  var import_jsx_runtime9 = require("react/jsx-runtime");
1084
1106
  var ActionBg = ({ blur, style }) => blur ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_auto_paper4.BlurView, { blur: true, style: import_react_native12.StyleSheet.absoluteFill }) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [import_react_native12.StyleSheet.absoluteFill, styles7.actionBg, style], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Surface, { style: import_react_native12.StyleSheet.absoluteFill, elevation: 1, children: null }) });
1085
- var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
1107
+ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
1086
1108
  const { blur, headerHeight, headerFixed, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = (0, import_react14.useContext)(ScrollViewContext);
1087
1109
  const { settings } = (0, import_react14.useContext)(ScrollViewSettingsContext);
1088
1110
  const effectiveBackActionFixed = backActionFixed ?? settings.backActionFixed;
@@ -1096,35 +1118,36 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFi
1096
1118
  }) => {
1097
1119
  if (headerHeight !== height) setHeaderHeight(height);
1098
1120
  };
1121
+ const actionMargin = 4;
1122
+ const contentMinHeight = actionSize + 2 * actionMargin;
1123
+ const unmeasuredHeight = top + contentMinHeight;
1099
1124
  const translateStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1100
1125
  if (headerFixed) return { transform: [{ translateY: 0 }] };
1101
1126
  if (snapBackHeaderShared.value) return { transform: [{ translateY: headerOffset.value }] };
1102
1127
  const effective = scrollPosition.value + (headerHeight ?? 0) - pullSearchHeightShared.value;
1103
1128
  if (effective <= 0) return { transform: [{ translateY: 0 }] };
1104
1129
  return { transform: [{ translateY: -effective }] };
1105
- }, [headerFixed, headerHeight]);
1130
+ }, [headerFixed, headerHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1106
1131
  const blurStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1132
+ if (headerHeight === null) return { height: unmeasuredHeight };
1107
1133
  if (!headerHeight) return { height: 0 };
1108
1134
  if (headerFixed) return { height: headerHeight };
1109
- const h = headerHeight ?? 0;
1110
- const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
1111
- return { height: Math.max(h + translateY, top) };
1112
- }, [headerFixed, headerHeight, top]);
1135
+ const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value);
1136
+ return { height: Math.max(headerHeight + translateY, top) };
1137
+ }, [headerFixed, headerHeight, top, unmeasuredHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1113
1138
  const progressStyle = (0, import_react_native_reanimated12.useAnimatedStyle)(() => {
1114
1139
  const h = headerHeight ?? 0;
1115
1140
  if (headerFixed) return { top: h };
1116
1141
  const slide = snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
1117
1142
  return { top: Math.max(h - slide, top) };
1118
- }, [headerFixed, headerHeight, top]);
1119
- const actionMargin = 4;
1120
- const contentMinHeight = actionSize + 2 * actionMargin;
1143
+ }, [headerFixed, headerHeight, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
1121
1144
  const actionTop = top + actionMargin;
1122
1145
  const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
1123
1146
  const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
1124
1147
  const leadingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", left: actionMargin, position: "absolute", top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
1125
1148
  const trailingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", position: "absolute", right: actionMargin, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
1126
1149
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1127
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated12.default.View, { pointerEvents: "none", style: [styles7.blur, blurStyle], children: headerHeight !== null && headerHeight > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_auto_paper4.BlurView, { blur, style: [styles7.blurInner, { height: headerHeight }] }) }),
1150
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated12.default.View, { pointerEvents: "none", style: [styles7.blur, blurStyle], children: headerHeight !== 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_auto_paper4.BlurView, { blur, style: [styles7.blurInner, { height: headerHeight ?? unmeasuredHeight }] }) }),
1128
1151
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native_reanimated12.default.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [headerHeight === null ? styles7.headerInit : styles7.header, translateStyle], children: [
1129
1152
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: [{ paddingTop: top }, style], children: children ?? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native12.View, { style: [styles7.content, { minHeight: contentMinHeight }], children: [
1130
1153
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native12.View, { style: styles7.side }),
@@ -1139,10 +1162,10 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFi
1139
1162
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_reanimated12.default.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
1140
1163
  backAction && (effectiveBackActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native_reanimated12.default.View, { style: leadingStyle, children: [
1141
1164
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
1142
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1165
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1143
1166
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native_reanimated12.default.View, { style: [leadingStyle, translateStyle], children: [
1144
1167
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
1145
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1168
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper3.Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
1146
1169
  ] })),
1147
1170
  trailingAction && (trailingActionFixed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native12.View, { style: trailingStyle, children: [
1148
1171
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionBg, { blur, style: actionStyle }),
@@ -1175,7 +1198,7 @@ var import_react15 = require("react");
1175
1198
  var import_react_native13 = require("react-native");
1176
1199
  var import_react_native_reanimated13 = require("react-native-reanimated");
1177
1200
  var import_jsx_runtime10 = require("react/jsx-runtime");
1178
- var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight = 60 }) => {
1201
+ var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard = false, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight = 0 }) => {
1179
1202
  const { settings } = (0, import_react15.useContext)(ScrollViewSettingsContext);
1180
1203
  const effectiveBlur = (0, import_auto_paper5.useBlur)(blur);
1181
1204
  const effectiveSnapBack = snapBack ?? settings.snapBack;
@@ -1201,10 +1224,11 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFi
1201
1224
  }, [effectiveSnapBack, effectiveSnapBackFooter, effectiveSnapBackHeader, snapBackFooterShared, snapBackHeaderShared]);
1202
1225
  const setHeaderHeight = (0, import_react15.useCallback)(
1203
1226
  (h) => {
1227
+ if (h === 0 && headerHeight) return;
1204
1228
  setHeaderHeightState(h);
1205
1229
  headerHeightShared.value = h ?? 0;
1206
1230
  },
1207
- [headerHeightShared]
1231
+ [headerHeight, headerHeightShared]
1208
1232
  );
1209
1233
  const onListUnmount = (0, import_react15.useCallback)(() => {
1210
1234
  listGeneration.value += 1;
@@ -1229,7 +1253,7 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFi
1229
1253
  const scrollHeight = (0, import_react15.useMemo)(() => import_react_native13.Dimensions.get("window").height - (headerHeight ?? 0) - footerHeight, [headerHeight, footerHeight]);
1230
1254
  const effectiveHeaderFixed = fixed || (headerFixed ?? settings.headerFixed);
1231
1255
  const effectiveFooterFixed = fixed || (footerFixed ?? settings.footerFixed);
1232
- const value = (0, import_react15.useMemo)(() => ({ blur: effectiveBlur, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
1256
+ const value = (0, import_react15.useMemo)(() => ({ blur: effectiveBlur, footerAboveKeyboard, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, footerAboveKeyboard, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
1233
1257
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ScrollViewContext.Provider, { value, children });
1234
1258
  };
1235
1259
 
@@ -1304,7 +1328,12 @@ function useStickyHeaders(headerFixed, enabled) {
1304
1328
  (curr, prev) => {
1305
1329
  if (curr !== prev) (0, import_react_native_reanimated14.runOnJS)(setActiveIndex)(curr);
1306
1330
  },
1307
- [enabled, headerFixed, insetsTop]
1331
+ // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
1332
+ // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
1333
+ // snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
1334
+ // — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
1335
+ // then never again on web, permanently freezing activeIndex/the active sticky section.
1336
+ [enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
1308
1337
  );
1309
1338
  const clipStyle = (0, import_react_native_reanimated14.useAnimatedStyle)(() => {
1310
1339
  const hHeight = headerHeightShared.value;
@@ -1319,7 +1348,7 @@ function useStickyHeaders(headerFixed, enabled) {
1319
1348
  stickyTop = effective <= 0 ? hHeight : Math.max(insetsTop, hHeight - effective);
1320
1349
  }
1321
1350
  return { top: stickyTop };
1322
- }, [enabled, headerFixed, insetsTop]);
1351
+ }, [enabled, headerFixed, insetsTop, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]);
1323
1352
  const overlayStyle = (0, import_react_native_reanimated14.useAnimatedStyle)(() => {
1324
1353
  const hidden = { opacity: 0, top: 0, transform: [{ translateY: 0 }] };
1325
1354
  if (!enabled) return hidden;
@@ -1347,7 +1376,7 @@ function useStickyHeaders(headerFixed, enabled) {
1347
1376
  if (nextScreenY < pushThreshold) translateY = nextScreenY - pushThreshold;
1348
1377
  }
1349
1378
  return { opacity: 1, top: 0, transform: [{ translateY }] };
1350
- }, [enabled, headerFixed, insetsTop]);
1379
+ }, [enabled, headerFixed, insetsTop, contentIndexShared, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared, sectionHeights]);
1351
1380
  const measureHeader = (0, import_react17.useCallback)(
1352
1381
  (index, pageY, height) => {
1353
1382
  const contentY = pageY + scrollPosition.value;
@@ -1376,11 +1405,15 @@ var AnimatedSectionList = import_react_native_reanimated15.default.createAnimate
1376
1405
  var SectionListInner = ({ chipProps, chipThreshold, contentContainerStyle: externalContentContainerStyle, initialNumToRender = 20, maxToRenderPerBatch = 50, renderFilters, footerFixed: footerFixedProp, gesture, headerFixed: headerFixedProp, keyboardAware, ListHeaderComponent: externalListHeaderComponent, onChipPress, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onContentSizeChange, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pullSearchHeight, refreshing, removeClippedSubviews = false, sections, showsVerticalScrollIndicator, style, ref: externalRef, windowSize = 100, ...props }) => {
1377
1406
  const scrollView = (0, import_react18.useRef)(null);
1378
1407
  const { stickySectionHeadersEnabled = true, renderSectionHeader, ...passThroughProps } = props;
1379
- const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, keyboardAware, pullSearchHeight, style });
1408
+ const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured: true, keyboardAware, pullSearchHeight, style });
1380
1409
  const { listGeneration, onListUnmount } = (0, import_react18.useContext)(ScrollViewContext);
1381
1410
  const capturedGeneration = (0, import_react_native_reanimated15.useSharedValue)(-1);
1382
1411
  (0, import_react18.useLayoutEffect)(() => {
1383
1412
  const generation = listGeneration.value;
1413
+ if (import_react_native14.Platform.OS === "web") {
1414
+ capturedGeneration.value = generation;
1415
+ return;
1416
+ }
1384
1417
  (0, import_react_native_reanimated15.runOnUI)((gen) => {
1385
1418
  "worklet";
1386
1419
  capturedGeneration.value = gen;